From b35c2246c8b4a84f34799a09d19ca384d5cf6574 Mon Sep 17 00:00:00 2001 From: Sunny Patel Date: Mon, 4 May 2020 11:45:40 -0500 Subject: [PATCH] Allow API version selection (defaults to "v1beta1"). 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. --- .github/README.md | 18 +++++++++++++----- Firestore.js | 35 +++++++++++++++++++++++------------ FirestoreDocument.js | 16 ++++++++++++++++ Query.js | 24 ++++++++++++++---------- Read.js | 29 +++++++++++++++++++++++------ Util.js | 5 ++--- package.json | 4 +++- 7 files changed, 94 insertions(+), 37 deletions(-) diff --git a/.github/README.md b/.github/README.md index 8eea08e..d2c827a 100644 --- a/.github/README.md +++ b/.github/README.md @@ -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: @@ -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. diff --git a/Firestore.js b/Firestore.js index da587c7..028933f 100644 --- a/Firestore.js +++ b/Firestore.js @@ -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) } /** @@ -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. @@ -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 } /** @@ -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 @@ -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 diff --git a/FirestoreDocument.js b/FirestoreDocument.js index 3895d55..a9029ad 100644 --- a/FirestoreDocument.js +++ b/FirestoreDocument.js @@ -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) { diff --git a/Query.js b/Query.js index 82a125b..90a4571 100644 --- a/Query.js +++ b/Query.js @@ -7,14 +7,14 @@ * * @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', @@ -22,21 +22,25 @@ var FirestoreQuery_ = function (from, callback) { '<=': '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' @@ -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': { @@ -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 */ @@ -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 diff --git a/Read.js b/Read.js index 28d3878..02c679f 100644 --- a/Read.js +++ b/Read.js @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 * @@ -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 diff --git a/Util.js b/Util.js index 3650c88..dd8c043 100644 --- a/Util.js +++ b/Util.js @@ -12,7 +12,7 @@ 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 */ @@ -20,7 +20,6 @@ function isInt_ (n) { return n % 1 === 0 } - /** * Check if a value is a valid number. * @@ -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) diff --git a/package.json b/package.json index f0b0d19..d83c3c4 100644 --- a/package.json +++ b/package.json @@ -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", @@ -21,6 +21,7 @@ "getDocumentFromPath_", "getDocumentIds_", "getDocument_", + "getDocuments_", "isInt_", "isNumberNaN_", "isNumeric_", @@ -28,6 +29,7 @@ "regexBinary_", "regexDatePrecision_", "regexPath_", + "unwrapBatchDocuments_", "unwrapDocumentFields_", "updateDocument_", "wrapValue_"