Skip to content

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;
    }
  }
} 
7. Click on 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.

  1. Click on the Settings Gear Icon next to Project Overview on the left. Note the Project ID and Web API Key values. These will be our properties ProjectId and ApiKey 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

  1. 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:

fireStoreCDS.AddServiceFilterCount(100);
Usage note: Note that if you are using a sort condition as defined by a AddSortFieldDef 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:

  1. Get records where field "status" has the value "New"
    fireStoreCDS.AddServiceFilterCondition('status', '==', 'New');
    
  2. Use more than once to specify multiple conditions ANDed but for the same field.

    fireStoreCDS.AddServiceFilterCondition('age', '>', 18);
    fireStoreCDS.AddServiceFilterCondition('age', '<', 65);
    

  3. For an OR condition, use the "in" operator. For example, to get records where field "status" has the value "New" or "Pending"

    fireStoreCDS.AddServiceFilterCondition('status', 'in', TJSArray.New('New',
    'Pending'));
    
    Warning: Date/Time fields require special code Since Date/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 for Date/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

fireStoreCDS.ClearSortFieldDefs;
fireStoreCDS.AddSortFieldDef("age", true);

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);
Warning: Date/Time fields require special code Since Date/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 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:

//RFC3339 format date time string
2019-10-12T07:20:50.52Z
In order to pass a field value for such a field to be used in a 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:

fireStoreCDS.AddServiceFilterRange(
 frStartAfter,
 DateTimeToRFC3339(aDelphiDateTime)
 );
This is especially tricky if you are using a TDateField 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.

 // correct way to store only dates
 CDS.FieldByName('date').AsDateTime := Trunc(aDelphiDateTime);
Then the filter will work with the value 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:

  1. Open the project TMSWeb_ImportTestData
  2. Build and Run the project
  3. Enter values for API Key, AuthDomain and ProjectID if they are not automatically filled by your previous usage of any Firestore demo.
  4. 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:

  1. Open the project TMSWeb_FirestoreFilters.

  2. 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.

  3. Now run the project.

  4. Click on the Filters items one by one and see how they work.

  5. 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);
PostAsync after Edit

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.

  1. 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.
  2. 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.
  3. 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
 );
If the 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
 );
Firebase sends an email message to the user with a password reset link that allows the user to select another password. You can view and modify the template of the message in Firebase console.

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.

 fireStoreClientDataSet.UseridFilter := ufActive;
Obviously, you would do this for a new collection as far as possible because nothing can be done about the existing records of an existing collection.

Once you set the UseridFilter active, the Component takes care of using the id of the Signed-In user internally in the following operations.

  1. While creating or updating an object, it forces a property (column) that stores the Userid of the Signed-In user.

  2. 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;
 }
 }
}
The new allow statement for read/write protects any data in which the uid property added by the component does not match the uid of the 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,

 // See CAUTION note below
 fireStoreClientDataSet.UseridFieldName := 'userid';
CAUTION: But if you have applied a security rule as described above, please be sure to change that to use the correct field name.

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);

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.