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.
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
.
- Add full transaction support
- Added WriteBatch functionality.
- Added
isEmpty
getter to check if theBatchWrite
has any mutations. - Converted private fields into TS 3.8 private fields
- Added GetBatch functionality.
- Added additional query operands:
!=
,not-in
,is-nan
,is-not-null
,is-not-nan
. - Aliased
contains
andconstainsany
toarray-contains
andarray-contains-any
respectively to match the Firestore JS SDK.
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.
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,
- Open the Google Service Accounts page by clicking here.
- Select your Firestore project, and then click "Create Service Account."
- For your service account's role, choose
Datastore > Cloud Datastore Owner
. - Check the "Furnish a new private key" box and select JSON as your key type.
- 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. - [Bonus] It is considered best practice to make use of the Properties Service to store this sensitive information.
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);
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);
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);
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.
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.
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.
const batch = firestore.batch();
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.
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.
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.
batch.delete(doc);
This operation does not fail if document does not exist.
batch.commit(true);
If the commit operation fails, it throws an exception.
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;
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"]);
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)
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.
-
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:
- Migrate your script to use V8, or
- Use the last Rhino version of this library (v26).
- v27: Library rewritten with Typescript and Prettier.
- Query function names have been capitalized (
Select
,Where
,OrderBy
,Limit
,Offset
,Range
). - All functions return
Document
orDocument[]
types directly from Firebase. Usedocument.obj
to extract the raw object. - Undo breaking change from v23.
document.createTime
anddocument.updateTime
will remain as timestamped strings. Howeverdocument.created
,document.updated
, anddocument.read
are Date objects.
- Query function names have been capitalized (
- 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 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.