Skip to content

abkarino/FirestoreGoogleAppsScript

 
 

Repository files navigation

Firestore for Google Apps Scripts

GitHub release (latest by date) Google Apps Script TypeScript clasp code style: prettier GitHub pull requests GitHub issues Tests

A Google Apps Script library for accessing Google Cloud Firestore.

This library allows a user (or service account) to authenticate with Firestore and edit their Firestore database within a Google Apps Script.

Read how this project was started here.

As of v27, this project has been updated to use the GAS V8 runtime with Typescript! This introduces a number of breaking changes. Scripts utilizing the old Rhino runtime must use v26.

Fork details

In this fork, I added BatchWrite functionality both atomic and non-atomic options. The original library id is 1VUSl4b1r1eoNcRWotZM3e87ygkxvXltOgyDZhixqncz9lQ3MjfT1iKFw.

Since this is a new deployment on GAS, the versioning is reset to v1.

Future work

  • Add full transaction support

Changelog

V1

  • Added WriteBatch functionality.

V2

  • Added isEmpty getter to check if the BatchWrite has any mutations.
  • Converted private fields into TS 3.8 private fields

V3

  • Added GetBatch functionality.

V4

  • Added additional query operands: !=, not-in, is-nan, is-not-null, is-not-nan.
  • Aliased contains and constainsany to array-contains and array-contains-any respectively to match the Firestore JS SDK.

Installation

In the Google online script editor, select the Resources menu item and choose Libraries.... In the "Add a library" input box, enter 1bdxX-1xBtYdDgROfBvJgKkAwRJTbgcTkFE99sVClnMQHQoBE2DLrYiH4 and click "Add." Choose the most recent version number.

Quick start

Creating a service account

The easiest way to use this library is to create a Google Service Account for your application and give it read/write access to your datastore. Giving a service account access to your datastore is like giving access to a user's account, but this account is strictly used by your script, not by a person.

If you don't already have a Firestore project you want to use, create one at the Firebase admin console.

To make a service account,

  1. Open the Google Service Accounts page by clicking here.
  2. Select your Firestore project, and then click "Create Service Account."
  3. For your service account's role, choose Datastore > Cloud Datastore Owner.
  4. Check the "Furnish a new private key" box and select JSON as your key type.
  5. When you press "Create," your browser will download a .json file with your private key (private_key), service account email (client_email), and project ID (project_id). Copy these values into your Google Apps Script — you'll need them to authenticate with Firestore.
  6. [Bonus] It is considered best practice to make use of the Properties Service to store this sensitive information.

Configurating Firestore instance from your script

Now, with your service account client email address email, private key key, project ID projectId, we will authenticate with Firestore to get our Firestore object. To do this, get the Firestore object from the library:

const firestore = FirestoreApp.getFirestore(email, key, projectId);
Configuration Template

Here's a quick template to get you started (by replacing email and key with your values):

const email = 'projectname-12345@appspot.gserviceaccount.com';
const key = '-----BEGIN PRIVATE KEY-----\nPrivateKeyLine1\nPrivateKeyLine2\nPrivateKeyLineN\n-----END PRIVATE KEY-----';
const projectId = 'projectname-12345'
const firestore = FirestoreApp.getFirestore(email, key, projectId);

Alternatively, using Properties Service once data is already stored in the service with "client_email", "private_key", and "project_id" property names:

const props = PropertiesService.getUserProperties(); // Or .getScriptProperties() if stored in Script Properties
const [email, key, projectId] = [props.getProperty('client_email'), props.getProperty('private_key'), props.getProperty('project_id')];
const firestore = FirestoreApp.getFirestore(email, key, projectId);
Creating Documents

Using this Firestore instance, we will create a Firestore document with a field name with value test!. Let's encode this as a JSON object:

const data = {
  "name": "test!"
}

We can choose to create a document in collection called "FirstCollection" without a name (Firestore will generate one):

firestore.createDocument("FirstCollection", data);

Alternatively, we can create the document in the "FirstCollection" collection called "FirstDocument":

firestore.createDocument("FirstCollection/FirstDocument", data);
Updating Documents

To update (overwrite) the document at this location, we can use the updateDocument function:

firestore.updateDocument("FirstCollection/FirstDocument", data);

To update only specific fields of a document at this location, we can set the mask parameter to true:

firestore.updateDocument("FirstCollection/FirstDocument", data, true);

Or alternatiavely, we can set the mask parameter to an array of field names:

firestore.updateDocument("FirstCollection/FirstDocument", data, ["field1", "field2", "fieldN"]);

this is useful for this:

If the document exists on the server and has fields not referenced in the mask, they are left unchanged. Fields referenced in the mask, but not present in the input document (the data in our example), are deleted from the document on the server.

Deleting Documents

To delete a document at this location, we can use the deleteDocument function:

firestore.deleteDocument("FirstCollection/FirstDocument");

Note: This cannot handle deleting collections or subcollections, only individual documents.

Batch Write

To do multiple operations in one request, you can utilize WritePatch api. It supports two modes of executing the writes, atomic and non-atomic. In atomic mode, it behaves like a transaction, it fails if any of the requested writes fails and changes are rolled-back. In non-atomic mode, the changes can happen out of order and each write would fail independently of the remaining writes.

The api contains two restrictions:

  • You cannot write more than once to the same document.
  • You can have at most 500 write operations.
To create a batch write:
const batch = firestore.batch();
To add a document:

At the moment of writing, the WriteBatch does not contain an add operation. The reason for that is to ensure the same functionality as Firestore JS SDK. However, we may add it later for ease of use. To circumvent this limitation, you would use set operation with a unique document id.

const doc = `collectionName/${firestore.newId()}`; 
batch.set(doc, docData);

However, this approach will NOT fail if the document already exists. This limitation would be an incentive to adding a create method that utilizes preconditions.exists to ensure failure if document already exist.

To set a document:
batch.set(doc, docData); // this would overwrite the document if exists, create if not
batch.set(doc, docData, {merge: true}); // this would update/merge the document if exists, create if not
batch.set(doc, docData, {mergeFields: ['field1', 'field2']}); // this allows to pass a write mask, only these fields would be set. If a field exists in the mask but not in data, it would be deleted from the document 

merge and mergeFields are mutually exclusive, if both are provided, merge takes precedence.

To update a document:
batch.update(doc, docData);

Another option for update:

batch.update(doc, 'field1', field1Data);
batch.update(doc, 'field1', field1Data, 'field2', field2Data, ...);

This variant is added for mere compatibility with the Firestore JS SDK. The update operation fails, if the document does not exit.

To delete a document:
batch.delete(doc);

This operation does not fail if document does not exist.

To execute a batch write atomically:
batch.commit(true);

If the commit operation fails, it throws an exception.

To execute a batch write independently, either pass false or undefined:
const results = batch.commit();

or

const results = batch.commit(false);

The result of the commit operation is an array with either true or error message per each write operation. The order of the result array is guaranteed to have the same order as the operations in the WriteBatch.

You cannot commit a WriteBatch more than once. The commit method will throw an exception if there are no write operations. You can use isEmpty getter, to check if the batch contains any writes.

const isEmpty = batch.isEmpty;
Getting Documents

You can retrieve documents by calling the getDocument function:

const documentWithMetadata = firestore.getDocument("FirstCollection/FirstDocument");

You can also retrieve all documents within a collection by using the getDocuments function:

const allDocuments = firestore.getDocuments("FirstCollection");

You can also get specific documents by providing an array of document names

const someDocuments = firestore.getDocuments("FirstCollection", ["Doc1", "Doc2", "Doc3"]);
Getting Document Properties

You can access various properties of documents from Firestore:

const doc          = firestore.getDocument("My Collection/My Document");
const originalData = doc.obj      // Original database object (your stored data)
const readTime     = doc.read     // Date Object of the Read time from database
const updateTime   = doc.updated  // Date Object of the Updated time from database
const createdTime  = doc.created  // Date Object of the Created time from database
const name         = doc.name     // Full document path (projects/projName/databases/(default)/documents/My Collection/My Document)
const path         = doc.path     // Local document path (My Collection/My Document)
Getting Documents (Advanced method using Query)

If more specific queries need to be performed, you can use the query function followed by an .Execute() invocation to get that data:

const allDocumentsWithTest = firestore.query("FirstCollection").Where("name", "==", "Test!").Execute();

The Where function can take other operators too: ==, <, <=, >, >=, !=, array-contains, array-contains-any, in and not-in. (contains and containsany are also supported as aliases for array-contains and array-contains-any respectively and are left for backward compatibility.)

There are a set of unary operands that can be used with Where as well: is-nan, is-null, is-not-nan, is-not-null. Those operands are not supported by the Firestore JS SDK.

Queries looking for null values can also be given:

const allDocumentsNullNames = firestore.query("FirstCollection").Where("name", 'is-null').Execute();
const allDocumentsNullNames = firestore.query("FirstCollection").Where("name", null).Execute();

The latter is implicitly converted to null as a string, and it is not recommended for use but left for backward compatibility.

Query results can be ordered:

const allDocumentsNameAsc = firestore.query("FirstCollection").OrderBy("name").Execute();
const allDocumentsNameDesc = firestore.query("FirstCollection").OrderBy("name", "desc").Execute();

To limit, offset, or just select a range of results:

const documents2_3_4_5 = firestore.query("FirstCollection").Limit(4).Offset(2).Execute();
const documents3_4_5_6 = firestore.query("FirstCollection").Range(3, 7).Execute();

See other library methods and details in the wiki.

Frequently Asked Questions

  • I'm getting the following error:

    Missing ; before statement. at [unknown function](Auth:12)

    This is because this library has been updated to utilize the new V8 Engine, and classes are not supported in the Rhino Engine. You can either:

    1. Migrate your script to use V8, or
    2. Use the last Rhino version of this library (v26).

Breaking Changes

  • v27: Library rewritten with Typescript and Prettier.
    • Query function names have been capitalized (Select, Where, OrderBy, Limit, Offset, Range).
    • All functions return Document or Document[] types directly from Firebase. Use document.obj to extract the raw object.
    • Undo breaking change from v23. document.createTime and document.updateTime will remain as timestamped strings. However document.created, document.updated, and document.read are Date objects.
  • v23: When retrieving documents the createTime and updateTime document properties are JS Date objects and not Timestamp Strings.
  • v16: Removed: createDocumentWithId(documentId, path, fields)

    Utilize createDocument(path + '/' + documentId, fields) instead to create a document with a specific ID.

Contributions

Contributions are welcome — send a pull request! See here for more information on contributing.

After cloning this repository, you can push it to your own private copy of this Google Apps Script project to test it yourself. See here for directions on using clasp to develop App Scripts locally. Install all packages from package.json with a bare npm install.

If you want to view the source code directly on Google Apps Script, where you can make a copy for yourself to edit, click here.