TWebFirestoreClientDataset Component
Introduction
The component TWebFirestoreClientDataset
makes it easy for a Delphi TMS Web Application to create and use database tables (called collections) on Google Cloud Firestore noSQL
database by a familiar syntax of using ClientDataSet
. It also allows a seamless integration of the Firestore
data collections with data-aware components like TWebDBGrid
. All the database operations can be done in the standard Delphi way through the TWebFirestoreClientDataset
component. All you need to do is specify the Firestore
properties and add the field definitions either in design time or in code in a standard Delphi syntax. Then connect a DataSource
and Data components to it and make the dataset active.
Your first web application using TWebFirestoreClientDataset
Set up your Firestore project in the Firebase console
Follow these steps:
1. Navigate to https://console.firebase.google.com/?pli=1 and sign up for Firebase
if not already done
2. Create a new project in Firebase
or select an existing project
3. In the left menu, select Database
4. Create a Firestore
database. Choose the options “Start in test mode” and let the region be
default
5. Don’t create a collection as our ClientDataSet
component will create it if it doesn’t exist
6. Click on the tab “Rules”
above and change the rules to allow only authenticated users to
access the database:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if request.auth != null;
}
}
}
Authentication
in left menu and select Sign-in
method as Google
. Enable it. Note the authorized domain with firebaseapp.com
. For example, test-15a3d.firebaseapp.com
. This will be our AuthDomain
property to be used later. If your TMS web application will run on localhost, make sure localhost is added to the list. If your TMS web application will run on a remote webserver, make sure the domain name is added to the list.
- Click on the Settings Gear Icon next to Project Overview on the left. Note the Project
ID
andWeb API Key values
. These will be our propertiesProjectId
andApiKey
to be used later.
Create a TMS WEB application
Create a standard TMS WEB Application in the Delphi IDE by choosing File
, New
, Other
, TMS WEB Application
. A new web form is created.
Enable the Firestore
JavaScript libraries for your project. From the project context menu in the
IDE, select “Manage JavaScript libraries” and select Google Firestore
Set up the TWebFirestoreClientDataset component
Go to the Tool Palette
and select the TWebFirestoreClientDataset
component from the “TMS
Data Access” section and drop it on the web form.
Specify the Component Properties
Set up the properties either in code or in the Object Inspector
as given below:
Property | Description |
---|---|
ApiKey | as obtained above in step 8 above. |
AuthDomain | as obtained above in step 7 above. |
ProjectId | as obtained above in step 8 above. |
CollectionName | select a name of the collection that you want to use |
KeyFieldName | specify the name of the key field |
AutoGenerateKeys | set to True |
SignInRequired | set to True as we set up this requirement in authentication rules above |
Create the Fields or Properties of each object in the Object
Store
The DataSet field definitions need to be set up either in code or in the Object Inspector
by rightclicking on the “Fields Editor”.
Select the fields in the Object Inspector
Follow these steps:
1) Set up your Google App
in the Google Developers
Console
(https://console.cloud.google.com/projectselector2/apis/dashboard?pli=1&supportedpurview=project)
1a. Go to “Credentials”
→ “Create Credentials”
“Create OAuth client ID”
1b. Select “Web Application”
, enter the Authorized URL
: http://127.0.0.1:8888 and click “Create”
1c. The Client ID
and Client Secret
values are displayed
1d. Go to “Dashboard”
and enable the required API(s)
2) Right-click the TWebFirestoreClientDataset
and select “Fetch Fields”
3) Enter the Client ID
, Client Secret
and CallbackURL
values from step 1. Note that the CollectionName
and ProjectID
are retrieved automatically from the CollectionName
and ProjectID
properties.
4) Click the “Fetch” button and follow the authentication instructions. If the process is successfull, a dialog with the list of available fields is displayed.
5) Right-click the TWebFirestoreClientDataset
and select “Fields Edito
- Select the required fields
Create the Fields in code Here is an example of adding the field definitions in code in the OnCreate event. In the Object Inspector, double-click on OnCreate event of the Web Form. This creates an event handler procedure WebFormCreate. Type the following code in it that sets up the fields and then makes the DataSet active.
myCloudClientDataSet.FieldDefs.Clear;
myCloudClientDataSet.FieldDefs.Add('_ID', ftString);
myCloudClientDataSet.FieldDefs.Add('note',ftString);
myCloudClientDataSet.FieldDefs.Add('date',ftDate);
myCloudClientDataSet.Active := True
Add Data Components that connect to the DataSet
Now select and drop a TWebDataSource
, TWebDBGrid
and TWebDBNavigator
component on
the Web Form
.
Set up the DataSource
and Data
components
Set the DataSource’s
DataSet property to WebFirestoreClientDataset1
. Then set the
DataSource
property of the grid and navigator to point to TWebDataSource1
.
Set up the Columns
of the DBGrid
Do that by clicking on the Columns property of the DBGrid
.
Set up a New Record
event
Since we will be adding New Records with the DB Navigator
, we need to set up the default
values of the record. For this, we set up an OnNewRecord
event procedure for the Client Data
Set in the Object Inspector
and type the following code in it.
procedure TForm1...NewRecord(DataSet: TDataSet);
begin
DataSet.FieldByName('note').AsString := 'New Note';
DataSet.FieldByName('date').AsDateTime := Today;
end;
Run the Web Application
Now you can build and run the application. When you run it in a browser that is not
logged in to Google already
, the component automatically asks you to login by using
your Google credentials
. The DB Grid will appear empty as there are no records. Try
adding new records with the Navigator
and see how it works.
Todo List Demo
Please find this demo in the folder Demos
. This Demo
connects the component to a Tasks
table
to show you the Tasks
with their status, description and dates.
Additional features in this Demo
Add
, Update
, Delete
through separate data aware controls and buttons
The Demo
allows you to perform add, update,delete operations through datbase field editor
controls and buttons instead of through the Navigator
.
Sorting on columns
Warning: We are using Firestore
service side Sort
Order for this feature just to demonstrate them. But in practice, column sorting should rather be implemented by using local sorting features of the ClientDataSet
as described later.
We want to be able to sort on any column of the DB Grid by clicking on the header of the column. So we need to be able to read all the records in the order of that field. For this, we need to add a Sort Field Definition
specifying the field to be sorted on. This is done in the event procedure GridTasksFixedCellClick
.
fireStoreClientDataSet.ClearSortFieldDefs;
fireStoreClientDataSet.AddSortFieldDef(LIndex, gridTasks.Columns[ACol].SortIn
dicator = siAscending);
fireStoreClientDataSet.Refresh;
The first parameter to AddSortFieldDef
call is the field name and the second parameter is a boolean flag that is true for ascending order and false for descending order. The Demo
uses its own logic to pass this information and then Refreshes (reloads) the data in the desired order.
Local Sorting recommended
Although the column sorting above was implemented using Firestore
features to demo them, in practice, this should be done by local sorting. This also prevents problems with Firestore
filters if you are using them.
Here is a quick hint on how to do local sorting. To sort descending on due_date field, do this:
fireStoreCDS.Indexes.Add('byDate',
'due_date',[ixDescending]);
fireStoreCDS.ActiveIndex := 'byDate';
Here, 'byDate'
is any name you give to this index. To sort ascending, remove the ixDescending
flag. You will find an example in the Advanced TodoList Demo
.
Updating, inserting and deleting data
This Demo also shows an example of connecting Data components like CheckBox
or a Memo
to the database so that those fields can be edited in the current record. After editing, a call to
Update from the update button takes care of committing the changes to the cloud database.
Similary, the Demo has examples of Inserting a new record and Deleting the current record by
respective calls.
Troubleshooting
Normally, you will see any exceptions raised in a red alert message at the bottom of the web page. You can also look at the Browser Console
for error messages. For any debugging, if you need to browse or edit the actual collection on the Cloud
, you can do that in Firestore
console. Note that individual records or objects under a Collection are called Documents in Firestore
terminology.
Filtering records at Firestore
If the collection contains a large number of records, you may want to limit the records obtained from the server. The following features are available for this purpose.
Naming of procedures and mapping to Firestore Filter functions
Note that all the functions below start with the prefix AddService
to indicate that the filtering occurs on the service/server side. Also, each function maps to a particular kind of filter on the Firestore side, for example AddServiceFilterCondition
maps to a "where" filter on Firestore. This is important to understand so that you can refer to proper Firestore documentation to look at filtering examples, their limitations and errors.
Filters may require use of Firestore Sorting!
Filters may require to use a Sort on the field being used in the filter. This is done by the calls ClearSortFieldDefs
and AddSortFieldDef
as indicated in descriptions of filters below. But if you are using them for other purposes, for example, for column click sorting, better not do that and use local sorting as described in the previous section. Because any current sort order is going to interfere with filter results.
Filtering methods available at Firestore level
AddServiceFilterCount
method
Maps to: Firestore
"limit" type filter
Use this to specify a limit condition. You can limit the number of records obtained by using this filter. Setting a filter activates it on next Refresh or when you next make the dataset active.
Example:
Usage note: Note that if you are using a sort condition as defined by aAddSortFieldDef
specification, the count will be done in that sort order. This type of filter can be used along with
AddServiceFilterRange that akways works in the current sort order.
AddServiceFilterCondition
method
Maps to: Firestore
"where" type filter
Use this method to specify a where condition. Setting a filter activates it on next Refresh
or
when you next make the dataset active.
Important: If you are using a Sort Order by using a AddSortFieldDef
call, it must be on the
same field that you are using in this filter.
Examples:
- Get records where field "status" has the value "New"
-
Use more than once to specify multiple conditions ANDed but for the same field.
-
For an OR condition, use the "in" operator. For example, to get records where field "status" has the value "New" or "Pending"
Warning:Date/Time
fields require special code SinceDate/Time
values are stored as strings on the Firestore side, you need to pass values properly. This is described in the section 4.6 "Special considerations forDate/Time
fields."
Limitations of this filter that maps to where on Firestore
The Where filter feature in FireStore
can not be used in all possible ways that SQL
allows. For
example, you can add more than one where filters, provided they are on the same field and if a
Sort Order is being used, the Sort Order must be on the same field.
Usage note: It's not possible to describe all possible rules and usage of Firestore
"where" filter
in this document. For more details, please see the Firestore document "Querying and filtering
data" (search Google on this) and refer to the section on "where" clauses.
AddServiceFilterRange
method
Maps to: Firestore
filters startAt
, startAfter
, endAt
, endBefore
Use this method to specify a Firestore
"start" and/or "end" condition on a value that refers to the
current sort field set by AddSortFieldDef
call. Setting a range filter activates it on next Refresh or
when you next make the dataset active.
Important: The value passed works on the current sort field. So you must have defined a sort
order by AddSortFieldDef
call.
Example:
Suppose you have defined the sort on the "age" field by AddSortFieldDef
Now you want to start at age 18
and end at age 65
. You will use 2
filters.
fireStoreCDS.AddServiceFilterCondition(frStartAt, 18);
fireStoreCDS.AddServiceFilterCondition(frEndAt, 65);
Firestore
side, you need to pass values
properly. This is described in the section 4.6
"Special considerations for Date/Time fields."
AddServiceFilterContinueAfterLast
When you use the filters above such that all the records are not obtained, for example, you used
AddServiceFilterCount to get only 50
records. How do you get the next 50
records? Add this
filter and Refresh. You will get next set of records.
Using this method appropriately will allow you to step forward through a set of records. You may
need to use ClearServiceFilters
sometimes, for instance, if you are using a start or end
condition to specify new conditions. On the other hand, using it with just the limit condition
AddServiceFilterCount
may not require a use of ClearServiceFilters
before using it as there is
no starting or ending condition.
If there are no more records, you will get an empty dataset.
ClearServiceFilters
Clears all filters added so that all the records are obtained from the server. Clearing takes effect on next Refresh or when you next make the dataset active.
Special consideration for Date/Time fields
When you specify field definition as TDateTimeField or TDateField, the component stores them
as RFC3399
strings in Firestore. An RFC3339
string looks like this:
AddServiceFilterCondition
or
AddServiceRangeFilter
call, you need to be able to pass such a string. For that purpose, you
need to use the function DateTimeToRFC3339
from DateUtils unit.
So for example, you will be calling a filer function as this:
This is especially tricky if you are using aTDateField
and when storing values in Firestore, care is not taken to make the time part Zeros. For example, the first record for this date might have the date field value as "2019-10-12T07:20:50.52Z"
and you want to start the range on the date 2019-10-12.
If you call AddServiceFilterRange
with frStartAt
and value as "2019-10-12"
it won't find that record and you get an empty list of records. Further, even if you use the value as DateTimeToRFC3339(aDate)
with that date, it won't work unless your date has the exact time in the string.
What is the solution in this case? When storing a Delphi TDateTime
value in your Delphi code,
always use Trunc on the datetime variable so that time part becomes Zero.
DateTimeToRFC3339(aDelphiDate)
where aDelphiDate
has the same date.
To summarize, depending on whether you use only date values or datetime values in your fields, your App has to take care to store only date part with Trunc
or full date time string. Further, you have to send a similar value with or without the time part when using filters for them to work properly.
Firestore
timestamp field: Firestore also has a data type of timestamp. In case you want to connect to existing data in Firestore
that has a timestamp field, please contact us. We have a pending modification to support the timestamp field of Firestore
that will be released in due course.
Firestore Filtering Demo
A demo is available that shows use of the above filtering methods. You will find this demo under
Demo\Services\Firestore.There are 2 parts to this demo, an import utility that imports 500
JSON
objects to a Firestore
collection as a prerequisite for the demo and then the Demo itself that
filters the collection when obtaining data.
Preparing for the Filter Demo: Build and Run the Import Utility
In the Demo folders, you will find a project ImportTestData
. Please follow these steps:
- Open the project TMSWeb_ImportTestData
- Build and Run the project
- Enter values for API Key, AuthDomain and ProjectID if they are not automatically filled by your previous usage of any Firestore demo.
- Click on Import Test Data.
This should import 500
objects from the JSON file in a new collection called SalesData
. You can
verify that in the Firestore Console
. Also, in case you want to recreate this collection due to any
reruns etc, you can delete the colleciton in Firestore
console and import again.
Side note: How to customize the Import Utility
to create collections from other JSON files
The import utility demonstrates the use of Class method AddServiceObjects
of the component.
It basically loads the JSON into a ClientDataSet
and then uses its JSON records array to
directly add objects at the server.
To develop another import utility to import other JSON files to Firestore collections, you can
make a copy of this project and then search for CUSTOMIZE
comments in the source and
change them according to your new requirements.
KNOWN PROBLEM IN JSON LOADING FROM URI: All data types are properly identified
except Date/Time fields. So according to how many such fields are there and their names, you
need to take care of fixing date/time fields as the Web Core URI
Loading code does not identify
them properly. Please see the code on how the fields were fixed by using a utility function.
Running the Filters Demo
Steps:
-
Open the project TMSWeb_FirestoreFilters.
-
If you didn't change the Collection name when importing, just Build the project. Otherwise, please search for CUSTOMIZE comment and use the same collection name here to which you imported the data above.
-
Now run the project.
-
Click on the Filters items one by one and see how they work.
-
To look at how the actual filters are used in code, please see the procedure setupExampleFilter.
New Async methods for code-based processing
In traditional Delphi code, you might use code like the following to process a ClientDataSet
.
aDataset.Open;
aDataset.Insert;
....change field values
aDataSet.Post;
...get the generated ID of new
...record to use in some code
...
This is not going to work for a Firestore ClientDataSet
because the operations are
asynchronous. So when the Open finishes, the dataset may not be in open state and the Insert
will get an error. Similarly, when the Post after Insert finishes, there is no guarantee that the
generated ID
of the new record is ready for use somewhere else.
Some workarounds can be coded in the dataset events like AfterOpen
that ensures that the
dataset is open. But it's not as convenient as the code above.
New Async methods
To deal with such processing code, we now provide Async
methods that allow you to code the
same solution but in a different way.
Here is some sample code using the new Async
functions provided for the purpose.
OpenAsync
fireStoreClientDataSet.OpenAsync(
procedure(success: Boolean; errorName, errorMsg: String)
begin
if not success then
begin
..handle error case
end
else
begin
.. further processing on success
... inserts, updates, etc
end;
end);
PostAsync after Insert
Similarly, if you were to do an Insert and obtain the generated ID for the record in the Firestore collection, you will use this kind of code.
fireStoreClientDataSet.Insert;
... set field values as required
fireStoreClientDataSet.PostAsync(
procedure(success: Boolean; data: JSValue; errorName, errorMsg: String)
begin
if not success then
begin
..handle error case
end
else
begin
.. data parameter is the ID
.. generated by the Firestore
end;
end);
Here is an example of modify.
fireStoreClientDataSet.Edit;
... set field values as required
fireStoreClientDataSet.PostAsync(
procedure(success: Boolean; data: JSValue; errorName, errorMsg: String)
begin
if not success then
begin
..handle error case
end
else
begin
.. data parameter is the the
.. JSON data updated
end;
end);
DeleteAsync and CloseAsync
Similarly, there are DeleteAsync and CloseAsync methods that return a success or failure to the passed response procedure as in case of OpenAsync.
So when it comes to processing the dataset in code, you can use the above methods with the kind of code suggested to check for errors and success before proceeding.
Processing Loops
It might be tricky to make processing loops this way that process all the records till EOF
using
Next but it's certainly possible. Several possible designs are possible by either using
anonymous response functions with recursion or by using object methods instead of an
anonymous response procedures.
Batch Inserts with AddServiceObjects
If you need to insert a large number of records in the Firestore
collection, you could write a
processing loop as described above. But that is complicated and would be slow if you waited for
previous insert to finish before inserting the next record. On the other hand, if you decided to fire
many inserts at once, the speed might improve but there are complications of finding when they
finish and whether there were any errors.
To deal with such use cases, we have added a Class Method AddServiceObjects
that you can
use to insert upto 500
records in a JSON Array at once directly to the Firestore collection. Since
this is a class method, you are supposed to use it by prefixing with class name
TFirestoreClientDataset
. You don't need to open any dataset locally as it directly inserts at the
server end.
Please see the ImportTestData
project described under Firestore
Filters above for an example
of how it uses this method to import a JSON file into a firestore collection.
Sign In Authentication Summary and Alternatives
Google Sign-In
method, simple to use
Here is how we set up user authentication in the Todo List demo above.
- In step 6 of the setup, we set up a Security Rule in Firebase console that allows only Signed In users to access the database.
- In step 7 of the setup, we enabled only Google Sign-In method for authentication. Here we also noted the values of ApiKey, AuthDomain and ProjectId to be used.
- After specifying the above 3 properties, we also switched ON the property SignInRequired of the component.
These are the only steps necessary if you want to secure your database so that it can be
accessed only those users who can Login to Google
.
Advantage of Google Sign-In
The advantage of Google SignIn
is that you don't have to make any Login
form, SignUp
form or
handle the situations where the user wants to change or reset his password. The component
takes care of making the correct calls without having any special user interface and Google
takes care of all the user interface and other services.
Other Sign-In alternatives
You will see many more Sign-In
methods in Firebase
console. The component does not support
them yet with the exception of Email/Password
method that has been implemented now as
described next.
Allowing all users (remove authentication)
Before we see the Email/Password Sign-In
option, you might wonder how to allow all users,
logged in or anonymous to access your database in case you need to do that for some reason?
For example, when you are developing and testing database logic and don't want any Login
complications.
To do that, in the Firebase
console, change the security rule described in section 2.1
such that
there is no "if" condition. For example, here is the changed security rule to allow "ALL"
access to
the database.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write;
}
}
}
Email/Password Sign-In method
If you enable this method in Firebase
console then the previous steps are same as far as setting
up the Security Rule
and switching ON
of the property SignInRequired
of the component.
In addition, you need to take care of the following in your App code:
Decide if you want to support both Google Sign-In and Email/Password
methods
In this case, your code will need to have your own user interface to let the user select either of
the above. If the user selects Google Sign-In
, you just need to switch ON
the flag
SignInRequired
of the component and make it active or else use the OpenAsync method
described earlier if you want to know about the success or failure.
Signing in with Email/Password
In this case, your code will need to have your own user interface to ask the user for the Email
and Password and an additional Signup flag depending on whether the user wants to sign up.
Then your code will call SignInAsync
method of the component, passing it the email, password
and the Signup
flag. You will know the success or failure of the call by the Callback
function
passed. Here is an example of this call. This is quite similar to OpenAsync call described earlier
except that this includes new parameters before the callback.
fireStoreClientDataSet.SignInAsync(
aUserEmail, aPassword,
false, // Signup flag
procedure(success: Boolean; errorName,
errorMsg: String)
begin
if not success then
begin
showmessage(errorMsg);
exit;
end;
... Open success actions like
... disabling buttons, etc.
end
);
Signup
parameter is passed as true, Firebase
will attempt to create a new user.
The component is smart enough to identify if the user is already logged in to avoid that step internally. On the other hand, if another user is logged in, it forces a new login.
Viewing the list of users in Firebase console
If you go to Firebase
console, you can click on Users menu to see the list of users who signed
up for your App
. You can disable one or more of these users by console's action menu. If you
have also enabled Google Sign-In
method then those users will also appear in this list.
What if the user has forgotten the password?
Your code can give this interface option to the user and if he indicates a "Forgot password"
action, call the method SendPasswordResetEmail
of the component. Here is an example code:
fireStoreClientDataSet.SendPasswordResetEmail(
aUserEmail,
procedure(success: Boolean; errorName,
errorMsg: String)
begin
if not success then
begin
showmessage(errorMsg);
exit;
end;
... Success actions like
... asking the user to check email
... and follow the instructions
end
);
User specific data (multi-tenant)
So far, our design allows the users to see all the records of the collection. The collection can be
secured by the Sign
In methods used above but all Signed In users will see all the records in the
collection. How do we implement user specific data so that a logged in user is able to create
and see only his or her records?
UseridFilter
Before signing in or making the ClientDataSet
active, you need to make the property
UseridFilter active as given below.
Once you set the UseridFilter
active, the Component takes care of using the id of the Signed-In
user internally in the following operations.
-
While creating or updating an object, it forces a property (column) that stores the Userid of the Signed-In user.
-
While getting the list of objects, it filters the list by the above column so that the list only contains objects that have the Userid of the Signed-In user.
Setting the above property functionally completes the requirement of storing and getting user
specific data. But that's not enough as far securing the data in Firestore
is concerned. For that,
you need to modify the security rule.
New Security Rule
What if a knowledgeable malicious user who has the Login permissions for your App
, tries to
use Firestore API
directly and after a login, tries to get a list of all objects, even those belonging to other users? To prevent this, you will need to modify the Security Rule described earlier in Section 2.1.
Here is the new security rule that you need to set in Firestore
console for this project.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow create: if request.auth != null;
allow read, write: if request.auth != null && request.auth.uid ==
resource.data.uid;
}
}
}
Signed-In
user. This check is not needed for a new
record and hence the allow for create operation only checks for a Signed-In
user access.
UseridFieldName
The default UseridFilter
feature uses a field or property name of 'uid' for the records read and
written by the Signed-In
user.
What if you already have existing data having such a field storing the uid but with a different field name? Or, may be, you want to use a different field name instead of 'uid?'
In such a case, you can specify the field name by assigning it to the property UseridFieldName
.
For example,
How to find the Signed-In
Status
In order to give the best experience to the user, a web app should be able to find out if a user is already signed-in to Firebase.
There are 2
alternatives to finding and taking action on a Signed-In status.
SubscribeToSignInEvents method
This method requires that you have already set the Firebase related properties, namely
, ApiKey
, AuthDomain
and ProjectId
.
When you call this method, the component
keeps informing you of a SignIn
change by the OnSignInChange
event till you call it again with
an Off parameter.
Your app can take proper actions in the OnSignInChange
event, for example, hiding a Login
panel and showing a panel that should come up after SignIn
.
First time, this event occurs immediately as soon as you call the Subscribe method. However, if you want to take a once-only action based on the SignedIn status, it's not possible to do that in this asynchronously occuring event. For that purpose, you need to use the second method descibed below.
GetSignedInUserAsync
method
This method also requires that you have already set the Firebase
related properties, namely
, ApiKey
, AuthDomain
and ProjectId
. Once you do that, you can find out if a user is already signed-in. Here is some sample code:
pascal
fireStoreClientDataSet.GetSignedInUserAsync(
procedure(isSignIn: Boolean;
UserName: String; UserEmail: String)
begin
if isSignIn then
begin
... some code...
end;
end
An app may use both the above methods--the event to do always-do type actions on a SignIn
change and the method GetSignedInUserAsync
to do once-only after SignIn type of actions.
Advanced Demo to show features for multi-tenant
You will find another TodoList
Advanced Demo that demonstrates all the features described in the Section 6
for Sign-In
features and the User Specific Data. Please see the folder Demo\Services\Firestore to find this demo.
TWebFirestoreClientDataset reference
Below is a list of the most important properties and methods of TWebFirestoreClientDataset
component.
Properties of TWebFirestoreClientDataset
Property | Description |
---|---|
Active | Set this to True to activate the DataSet . Field definitions must be present along with other properties described below. |
ApiKey | Get from the “Project settings” section of Firebase console as described earlier |
AuthDomain | Get from the Authentication section of Firebase console as described earlier |
CollectionName | Specify a collection name to connect to in Firestore |
KeyFieldName | Set the name of the primary key field |
AutoGenerateKeys | Recommended to set to True to let Firestore generate keys for new records |
ProjectId | Get from the “Project settings” section of Firebase console as described earlier |
SignInRequired | Set to True if only authenticated users are allowed access as per the Rules set up for the database. In this case, the component automatically tries to login on the first access. |
UseridFilter | Set to ufActive if you want the component to automatically force a uid field so that each logged in user can only see his or her own records. The default is ufInactive |
UseridFieldName | Set a field name if you don't want the component to use the default field name of 'uid' for this feature. You might need this, for example, if you have existing data that already has a field with a different name having the same uid value |
OnError | This is an event property that notifies the application of any errors from Firestore . The event can be set up at design time in Object Inspector by double-clicking on it. If the Application does not subscribe to this event, an Exception is raised on such errors. If subscribed, the application can then decide what to do. For example, show error, raise exception or take some corrective action. Note that hard errors (Delphi Exceptions) are not passed in this event. Rather, they cause an Exception that appears in a red alert. But in any case, all errors are always logged to the browser console. |
Methods of TWebFirestoreClientDataset
Only the methods specific to Firestore are listed below. Other methods from the base DataSet
classes are used in the standard way.
Method | Description |
---|---|
Refresh | procedure Refresh(Force: Boolean=False); Refresh reloads all the objects from the database. If AddSortFieldDef has been used to set up sorting definitions, the objects are loaded in the order specified. In addition, the current record pointer is restored after the Reload which is convenient for the user interface of the web application. Refresh is internally postponed till all the pending updates started asynchronously are finished. The Force parameter ignores the pending updates and forces a reload. |
AddSortFieldDef and ClearSortFieldDefs | Use AddSortFieldDef to add one or more sort definitions for loading the data. Before using a series of these calls, you must clear all sort definitions by calling ClearSortFieldDefs . procedure AddSortFieldDef(aField: String; isAscending: Boolean)); where - aField - the field name for the sorting order - isAscending - Set True for ascending order. |
AddServiceFilterCount | Maps to: "limit" filter type in Firestore Limit the number of records coming from the Firestore collection. Setting a filter activates it on next Refresh or when you next make the dataset active. procedure AddServiceFilterCount(numRecords: Integer); |
AddServiceFilterCondition | Maps to: "where" filter type in Firestore Adding one or more such filters is another way to limit the number of records coming from the Firestore collection. Setting a filter activates it on next Refresh or when you next make the dataset active. procedure AddServiceFilterCondition(aField: String; anOperator: String; aValue: JSValue); where - aField - the field name - anOperator - can be a comparison operator like '>='. Another operator 'in' is also available for look up of a value in an array of values. See an example in section 4 above. Special rules govern use of operators like ''. See Limitations note below. - aValue - is a value depending on the field type.Note: If the Field is a Date/Time field, the value needs to be passed by special code.Limitations : The Where feature in FireStore can not be used in all possible ways that SQL allows. For example, you can add more than one where filters, provided they are on the same field and if a Sort Order is being used, the Sort Order must be on the same field. Futher, in case of '' operator, the Sort Order must not be on the same field. For more details, please see Firestore documentation on filtering. |
AddServiceFilterRange | Maps to: "start" and "end" type filters in Firestore Adding one or more such filters is another way to limit the number of records coming from the Firestore collection. Setting a filter activates it on next Refresh or when you next make the dataset active. Further, this works only on the current sort field. The value passed refers to the current sort field set by AddSortFieldDef call. TFireStoreRangeFilterType = (frStartAt, frEndAt, frStartAfter, frEndBefore); procedure AddServiceFilterRange( rangeType: TFireStoreRangeFilterType; aValue: JSValue ); where - rangeType - specifies the type of filter by the enum type given above. - aValue - the value for the range. It refers to the value of current sort field set by AddSortFieldDef call. Note: You nust have defined the current sort field by using the method AddSortFieldDef. Further, if the current sort field is a Date/Time field, the value needs to be passed by special code. |
AddServiceFilterContinueAfterLast | This gives you a way to get records beyond the current last record obtained. For example, if you first obtained only 30 records by AddServiceFilterCount(30) . Next time, call this method to add this filter. Then each time you call Refresh , you will get next 30 records and when they finish, you will get an empty dataset. |
ClearServiceFilters | Clears all filters added so that all the records are obtained from the server. Clearing takes effect on next Refresh or when you next make the dataset active. procedure ClearFilters; |
Async methods
These methods allow you to do processing of dataset in code where you can wait for the outcome of the previous async operation before doing the next.
Methods | Description |
---|---|
OpenAsync | TFirestoreOpenAsyncResult = reference to procedure(success: Boolean; errorName, errorMsg: String); procedure OpenAsync(response: TFirestoreOpenAsyncResult); Where the response procedure gets a success flag along with error parameters. |
CloseAsync | TFirestoreCloseAsyncResult = reference to Procedure; procedure CloseAsync(response: TFirestoreCloseAsyncResult); Where the response procedure just indicates end of close without any parameters. |
PostAsync | TFirestorePostAsyncResult = reference to procedure(success: Boolean; data: JSValue; errorName, errorMsg: String); procedure PostAsync(response: TFirestorePostAsyncResult); Where the response procedure gets a success flag along with error parameters. In addition, there is a data parameter that returns the generated ID for a PostAsync after Insert and the whole JSON data object in case of PostAsync after Edit. |
DeleteAsync | procedure DeleteAsync(response: TFirestorePostAsyncResult); where the response procedures is same as for PostAsync and the data returned is the JSON object deleted. |
AddServiceObjects | class procedure AddServiceObjects( anApiKey, anAuthDomain, aProjectId, aCollectionName: String; dataObjects: TJSArray; responseEvent: TFirestoreBatchCommitResultEvent); where - The parameters anApiKey, anAuthDomain, aProjectId, aCollectionName are same as the properties by similar name described for FirestoreClientDataSet. - dataObjects is the JSON array containing the objects to be passed. Maximum 500 objects are allowed at a time. - responseEvent is the procedure that gets the completion event. The response event procedure has the following format, giving a success flag or error details. TFirestoreBatchCommitResultEvent = reference to procedure(success: Boolean; errorName, errorMsg: String); |
Sign-In related methods
If SignInRequired
is ON then Google Sign-In
is automatically tried when the ClientDataSet
is made active or OpenAsync
is used.
Method | Description |
---|---|
Signout | Use this method to Sign Out of Firebase. You need to Close the dataset before calling it. |
SignInAsync | If Sign-In method Email/Password is enabled in Firebase Console then you need to use this method to Sign-In. procedure SignInAsync( aUserEmail, aPassword: String; IsSignUp: Boolean; responseEvent: TFirestoreOpenAsyncResult); where - IsSignup is True if a new user is to be created with the given Email and Password - responseEvent is the procedure that gets the success or failure result The response event procedure has the same format described in OpenAsync method above. |
SendPasswordResetEmail | Use this method to let Firebase send a Reset Password link to the user. procedure SendPasswordResetEmail(aUserEmail: String; responseEvent: TFirestoreOpenAsyncResult); The response event procedure has the same format described in OpenAsync method above. |
SubscribeToSignInEvents | procedure SubscribeToSignInEvents(doSubscribe: Boolean); Use this method to get notifications on any Sign-In change by the event OnSignInChange. The event can be used to take special action if a user is detected as already signed-in. The event signature is: TFirestoreSignInChangeEvent = procedure(isSignIn: Boolean; UserName: String; UserEmail: String) of object; When IsSignIn is ON, the UserEmail parameter contain valid data of the signed-in user. Note that the first time this event occurs as soon as you call the subscribe method. |
GetSignedInUserAsync | Use this method to find out if a user is signed-in and the Email for the user.TFirestoreGetSignedInAsyncResult = reference to procedure(isSignIn: Boolean; UserName: String; UserEmail: String); procedure GetSignedInUserAsync(responseEvent: TFirestoreGetSignedInAsyncResult); |
Tips, tricks, troubleshooting notes
We will be adding items in this section based on user support queries from the customers.
Error
processing
If you do some operations like Open by using the new Async
methods, you will get to know if errors occurred in the immediate Response
function. So please use them whenever you can. For example, instead of setting active or Open, it is better to use OpenAsync
or SignInAsync
. Any other errors occuring during Firestore operations will raise an exception. As a developer, you can probably identify them or can use the Console
Log to find if errors occurred. But for the benefit of the End User
, it is recommended that you use the OnError event of the component to get notified of errors and display them to the user with or without modification as per your own interface design.