Skip to content

Commit

Permalink
Allow API version selection (defaults to "v1beta1").
Browse files Browse the repository at this point in the history
Add "IN" and "Contains_Any" Query field operators (usable only with "v1" API).
Expand Firestore.getDocuments to get specific named documents.
Update README link and minor documentation.
  • Loading branch information
LaughDonor committed May 4, 2020
1 parent 09738e4 commit b35c224
Show file tree
Hide file tree
Showing 7 changed files with 94 additions and 37 deletions.
18 changes: 13 additions & 5 deletions .github/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ To make a service account,
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.

#### Create a test document in Firestore from your script
Now, with your service account client email address `email`, private key `key`, and project ID `projectId`, we will authenticate with Firestore to get our `Firestore` object. To do this, get the `Firestore` object from the library:
Now, with your service account client email address `email`, private key `key`, project ID `projectId`, and Firestore API version, we will authenticate with Firestore to get our `Firestore` object. To do this, get the `Firestore` object from the library:

```javascript
var firestore = FirestoreApp.getFirestore(email, key, projectId);
var firestore = FirestoreApp.getFirestore(email, key, projectId, "v1");
```

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:
Expand Down Expand Up @@ -71,20 +71,28 @@ You can also retrieve all documents within a collection by using the `getDocumen
const allDocuments = firestore.getDocuments("FirstCollection")
```

If more specific queries need to be performed, you can use the `query` function followed by an `execute` invocation to get that data:
You can also get specific documents by providing an array of document names

```javascript
const someDocuments = firestore.getDocuments("FirstCollection", ["Doc1", "Doc2", "Doc3"])
```


If more specific queries need to be performed, you can use the `query` function followed by an `execute` invocation to get that data:

```javascript
const allDocumentsWithTest = firestore.query("FirstCollection").where("name", "==", "Test!").execute()
```

See other library methods and details [in the wiki](https://github.com/grahamearley/FirestoreGoogleAppsScript/wiki/Firestore-Method-Documentation).
See other library methods and details [in the wiki](https://github.com/grahamearley/FirestoreGoogleAppsScript/wiki/).

### Breaking Changes
* 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! This library is a work in progress. See [here](https://github.com/grahamearley/FirestoreGoogleAppsScript/blob/master/CONTRIBUTING.md) for more information on contributing.
Contributions are welcome — send a pull request! This library is a work in progress. See [here](https://github.com/grahamearley/FirestoreGoogleAppsScript/blob/master/.github/CONTRIBUTING.md) 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](https://github.com/google/clasp) for directions on using `clasp` to develop App Scripts locally.

Expand Down
35 changes: 23 additions & 12 deletions Firestore.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@
* @param {string} email the user email address (for authentication)
* @param {string} key the user private key (for authentication)
* @param {string} projectId the Firestore project ID
* @param {string} apiVersion [Optional] The Firestore API Version ("v1beta1", "v1beta2", or "v1")
* @return {object} an authenticated interface with a Firestore project
*/
function getFirestore (email, key, projectId) {
return new Firestore(email, key, projectId)
function getFirestore (email, key, projectId, apiVersion) {
return new Firestore(email, key, projectId, apiVersion)
}

/**
Expand All @@ -19,14 +20,18 @@ function getFirestore (email, key, projectId) {
* @param {string} email the user email address (for authentication)
* @param {string} key the user private key (for authentication)
* @param {string} projectId the Firestore project ID
* @param {string} apiVersion [Optional] The Firestore API Version ("v1beta1", "v1beta2", or "v1"). Defaults to "v1beta1"
* @return {object} an authenticated interface with a Firestore project
*/
var Firestore = function (email, key, projectId) {
var Firestore = function (email, key, projectId, apiVersion) {
if (!apiVersion) { apiVersion = 'v1beta1' }

/**
* The authentication token used for accessing Firestore.
*/
* The authentication token used for accessing Firestore.
*/
const authToken = getAuthToken_(email, key, 'https://oauth2.googleapis.com/token')
const baseUrl = 'https://firestore.googleapis.com/v1beta1/projects/' + projectId + '/databases/(default)/documents/'
const basePath = 'projects/' + projectId + '/databases/(default)/documents/'
const baseUrl = 'https://firestore.googleapis.com/' + apiVersion + '/' + basePath

/**
* Get a document.
Expand All @@ -43,10 +48,18 @@ var Firestore = function (email, key, projectId) {
* Get a list of all documents in a collection.
*
* @param {string} path the path to the collection
* @param {array} ids [Optional] String array of document names to filter. Missing documents will not be included.
* @return {object} an array of the documents in the collection
*/
this.getDocuments = function (path) {
return this.query(path).execute()
this.getDocuments = function (path, ids) {
var docs
if (!ids) {
docs = this.query(path).execute()
} else {
const request = new FirestoreRequest_(baseUrl.replace('/documents/', '/documents:batchGet/'), authToken)
docs = getDocuments_(basePath + path, request, ids)
}
return docs
}

/**
Expand Down Expand Up @@ -75,8 +88,7 @@ var Firestore = function (email, key, projectId) {
/**
* Update/patch a document at the given path with new fields.
*
* @param {string} path the path of the document to update.
* If document name not provided, a random ID will be generated.
* @param {string} path the path of the document to update. If document name not provided, a random ID will be generated.
* @param {object} fields the document's new fields
* @param {boolean} mask if true, the update will use a mask
* @return {object} the Document object written to Firestore
Expand All @@ -87,8 +99,7 @@ var Firestore = function (email, key, projectId) {
}

/**
* Run a query against the Firestore Database and
* return an all the documents that match the query.
* Run a query against the Firestore Database and return an all the documents that match the query.
* Must call .execute() to send the request.
*
* @param {string} path to query
Expand Down
16 changes: 16 additions & 0 deletions FirestoreDocument.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,22 @@ function unwrapDocumentFields_ (docResponse) {
return docResponse
}

/**
* Unwrap the given array of batch documents.
*
* @private
* @param docsResponse the document response
* @return the array of documents, with unwrapped fields
*/
function unwrapBatchDocuments_ (docsResponse) {
docsResponse = docsResponse.filter(function (docItem) { return docItem.found }) // Remove missing entries
return docsResponse.map(function (docItem) {
const doc = unwrapDocumentFields_(docItem.found)
doc.readTime = unwrapDate_(docItem.readTime)
return doc
})
}

function wrapValue_ (value) {
const type = typeof (value)
switch (type) {
Expand Down
24 changes: 14 additions & 10 deletions Query.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,36 +7,40 @@
*
* @constructor
* @private
* @see {@link https://firebase.google.com/docs/firestore/reference/rest/v1beta1/StructuredQuery Firestore Structured Query}
* @see {@link https://firebase.google.com/docs/firestore/reference/rest/v1/StructuredQuery Firestore Structured Query}
* @param {string} from the base collection to query
* @param {queryCallback} callback the function that is executed with the internally compiled query
*/
var FirestoreQuery_ = function (from, callback) {
const this_ = this

// @see {@link https://firebase.google.com/docs/firestore/reference/rest/v1beta1/StructuredQuery#Operator_1 FieldFilter Operator}
// @see {@link https://firebase.google.com/docs/firestore/reference/rest/v1/StructuredQuery#Operator_1 FieldFilter Operator}
const fieldOps = {
'==': 'EQUAL',
'===': 'EQUAL',
'<': 'LESS_THAN',
'<=': 'LESS_THAN_OR_EQUAL',
'>': 'GREATER_THAN',
'>=': 'GREATER_THAN_OR_EQUAL',
'contains': 'ARRAY_CONTAINS'
'contains': 'ARRAY_CONTAINS',
'containsany': 'ARRAY_CONTAINS_ANY',
'in': 'IN'
}

// @see {@link https://firebase.google.com/docs/firestore/reference/rest/v1beta1/StructuredQuery#Operator_2 FieldFilter Operator}
// @see {@link https://firebase.google.com/docs/firestore/reference/rest/v1/StructuredQuery#Operator_2 FieldFilter Operator}
const unaryOps = {
'nan': 'IS_NAN',
'null': 'IS_NULL'
}

// @see {@link https://firebase.google.com/docs/firestore/reference/rest/v1beta1/StructuredQuery#FieldReference Field Reference}
// @see {@link https://firebase.google.com/docs/firestore/reference/rest/v1/StructuredQuery#FieldReference Field Reference}
const fieldRef = function (field) {
return { 'fieldPath': field }
}
const filter = function (field, operator, value) {
// @see {@link https://firebase.google.com/docs/firestore/reference/rest/v1beta1/StructuredQuery#FieldFilter Field Filter}
operator = operator.toLowerCase().replace('_', '')

// @see {@link https://firebase.google.com/docs/firestore/reference/rest/v1/StructuredQuery#FieldFilter Field Filter}
if (operator in fieldOps) {
if (value == null) { // Covers null and undefined values
operator = 'null'
Expand All @@ -53,7 +57,7 @@ var FirestoreQuery_ = function (from, callback) {
}
}

// @see {@link https://firebase.google.com/docs/firestore/reference/rest/v1beta1/StructuredQuery#UnaryFilter Unary Filter}
// @see {@link https://firebase.google.com/docs/firestore/reference/rest/v1/StructuredQuery#UnaryFilter Unary Filter}
if (operator.toLowerCase() in unaryOps) {
return {
'unaryFilter': {
Expand All @@ -72,9 +76,9 @@ var FirestoreQuery_ = function (from, callback) {

/**
* Select Query which can narrow which fields to return.
* Can be repeated if multiple fields are needed in the response.
* Can be repeated if multiple fields are needed in the response.
*
* @see {@link https://firebase.google.com/docs/firestore/reference/rest/v1beta1/StructuredQuery#Projection Select}
* @see {@link https://firebase.google.com/docs/firestore/reference/rest/v1/StructuredQuery#Projection Select}
* @param {string} field The field to narrow down (if empty, returns name of document)
* @returns {object} this query object for chaining
*/
Expand Down Expand Up @@ -123,7 +127,7 @@ var FirestoreQuery_ = function (from, callback) {
* Orders the Query results based on a field and specific direction.
* Can be repeated if additional ordering is needed.
*
* @see {@link https://firebase.google.com/docs/firestore/reference/rest/v1beta1/StructuredQuery#Projection Select}
* @see {@link https://firebase.google.com/docs/firestore/reference/rest/v1/StructuredQuery#Projection Select}
* @param {string} field The field to order by.
* @param {string} dir The direction to order the field by. Should be one of "asc" or "desc". Defaults to Ascending.
* @returns {object} this query object for chaining
Expand Down
29 changes: 23 additions & 6 deletions Read.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "_" }] */
/* eslint quote-props: ["error", "always"] */

/**
* Get the Firestore document or collection at a given path.
* If the collection contains enough IDs to return a paginated result,
* this method only returns the first page.
* If the collection contains enough IDs to return a paginated result, this method only returns the first page.
*
* @private
* @param {string} path the path to the document or collection to get
Expand All @@ -16,7 +16,7 @@ function get_ (path, request) {

/**
* Get a page of results from the given path.
* If null pageToken is supplied, returns first page.
* If null pageToken is supplied, returns first page.
*
* @private
* @param {string} path the path to the document or collection to get
Expand All @@ -33,7 +33,7 @@ function getPage_ (path, pageToken, request) {

/**
* Get a list of the JSON responses received for getting documents from a collection.
* The items returned by this function are formatted as Firestore documents (with types).
* The items returned by this function are formatted as Firestore documents (with types).
*
* @private
* @param {string} path the path to the collection
Expand All @@ -57,7 +57,7 @@ function getDocumentResponsesFromCollection_ (path, request) {

/**
* Get a list of all IDs of the documents in a collection.
* Works with nested collections.
* Works with nested collections.
*
* @private
* @param {string} path the path to the collection
Expand Down Expand Up @@ -88,6 +88,23 @@ function getDocument_ (path, request) {
}
return unwrapDocumentFields_(doc)
}

/**
* Get documents with given IDs.
*
* @private
* @see {@link https://firebase.google.com/docs/firestore/reference/rest/v1beta1/projects.databases.documents/batchGet Firestore Documents BatchGet}
* @param {string} path the path to the document
* @param {string} request the Firestore Request object to manipulate
* @param {array} ids String array of document names
* @return {object} an object mapping the document's fields to their values
*/
function getDocuments_ (path, request, ids) {
const idPaths = ids.map(function (doc) { return path + '/' + doc }) // Format to absolute paths (relative to API endpoint)
const documents = request.post(null, { 'documents': idPaths })
return unwrapBatchDocuments_(documents)
}

/**
* Set up a Query to receive data from a collection
*
Expand All @@ -101,7 +118,7 @@ function query_ (path, request) {
const callback = function (query) {
// Send request to innermost document with given query
const responseObj = request.post(grouped[0] + ':runQuery', {
structuredQuery: query
'structuredQuery': query
})

// Filter out results without documents and unwrap document fields
Expand Down
5 changes: 2 additions & 3 deletions Util.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,14 @@ var regexDatePrecision_ = /(\.\d{3})\d+/
/**
* Checks if a number is an integer.
*
* @private
* @private
* @param {value} n value to check
* @returns {boolean} true if value can be coerced into an integer, false otherwise
*/
function isInt_ (n) {
return n % 1 === 0
}


/**
* Check if a value is a valid number.
*
Expand Down Expand Up @@ -103,7 +102,7 @@ function getCollectionFromPath_ (path) {
*
* @private
* @param {string} path Document path
* @returns {object} Document object
* @returns {object} Document object
*/
function getDocumentFromPath_ (path) {
return getColDocFromPath_(path, true)
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "firestore_google-apps-script",
"version": "23",
"version": "24",
"description": "A Google Apps Script library for accessing Google Cloud Firestore",
"homepage": "https://github.com/grahamearley/FirestoreGoogleAppsScript",
"bugs": "https://github.com/grahamearley/FirestoreGoogleAppsScript/issues",
Expand All @@ -21,13 +21,15 @@
"getDocumentFromPath_",
"getDocumentIds_",
"getDocument_",
"getDocuments_",
"isInt_",
"isNumberNaN_",
"isNumeric_",
"query_",
"regexBinary_",
"regexDatePrecision_",
"regexPath_",
"unwrapBatchDocuments_",
"unwrapDocumentFields_",
"updateDocument_",
"wrapValue_"
Expand Down

0 comments on commit b35c224

Please sign in to comment.