diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index c5cbc5540..92394b1e4 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -11,7 +11,7 @@ jobs: matrix: node: [8, 10, 12, 13] steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 - uses: actions/setup-node@v1 with: node-version: ${{ matrix.node }} @@ -30,7 +30,7 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 - uses: actions/setup-node@v1 with: node-version: 12 @@ -39,7 +39,7 @@ jobs: docs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 - uses: actions/setup-node@v1 with: node-version: 12 @@ -48,7 +48,7 @@ jobs: coverage: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 - uses: actions/setup-node@v1 with: node-version: 13 diff --git a/CHANGELOG.md b/CHANGELOG.md index e50e0775b..136e1da88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,21 @@ [1]: https://www.npmjs.com/package/@google-cloud/firestore?activeTab=versions +### [3.7.3](https://www.github.com/googleapis/nodejs-firestore/compare/v3.7.2...v3.7.3) (2020-03-31) + + +### Bug Fixes + +* support array of references for IN queries ([#993](https://www.github.com/googleapis/nodejs-firestore/issues/993)) ([a6d8fe0](https://www.github.com/googleapis/nodejs-firestore/commit/a6d8fe061fcfe0fde7a4fa023b2ec454e2adb432)) + +### [3.7.2](https://www.github.com/googleapis/nodejs-firestore/compare/v3.7.1...v3.7.2) (2020-03-25) + + +### Bug Fixes + +* fix flaky contention test ([#979](https://www.github.com/googleapis/nodejs-firestore/issues/979)) ([f294998](https://www.github.com/googleapis/nodejs-firestore/commit/f294998daab77a0a51c81265945e28eec34db186)) +* fix: use Random Number from `crypto` to generate AutoId ([05b3363](https://www.github.com/googleapis/nodejs-firestore/commit/ce6ea390f2fffcbe796ba1c5b040ee02452e287a)) + ### [3.7.1](https://www.github.com/googleapis/nodejs-firestore/compare/v3.7.0...v3.7.1) (2020-03-16) diff --git a/README.md b/README.md index e2381d368..ba729e8a8 100644 --- a/README.md +++ b/README.md @@ -113,6 +113,27 @@ has instructions for running the samples. The [Cloud Firestore Node.js Client API Reference][client-docs] documentation also contains samples. +## Supported Node.js Versions + +Our client libraries follow the [Node.js release schedule](https://nodejs.org/en/about/releases/). +Libraries are compatible with all current _active_ and _maintenance_ versions of +Node.js. + +Client libraries targetting some end-of-life versions of Node.js are available, and +can be installed via npm [dist-tags](https://docs.npmjs.com/cli/dist-tag). +The dist-tags follow the naming convention `legacy-(version)`. + +_Legacy Node.js versions are supported as a best effort:_ + +* Legacy versions will not be tested in continuous integration. +* Some security patches may not be able to be backported. +* Dependencies will not be kept up-to-date, and features will not be backported. + +#### Legacy tags available + +* `legacy-8`: install client libraries from this dist-tag for versions + compatible with Node.js 8. + ## Versioning This library follows [Semantic Versioning](http://semver.org/). diff --git a/dev/src/document.ts b/dev/src/document.ts index 7bb020398..6ce7ccb99 100644 --- a/dev/src/document.ts +++ b/dev/src/document.ts @@ -505,26 +505,6 @@ export class DocumentSnapshot { export class QueryDocumentSnapshot extends DocumentSnapshot< T > { - /** - * @hideconstructor - * - * @param ref The reference to the document. - * @param fieldsProto The fields of the Firestore `Document` Protobuf backing - * this document. - * @param readTime The time when this snapshot was read. - * @param createTime The time when the document was created. - * @param updateTime The time when the document was last updated. - */ - constructor( - ref: DocumentReference, - fieldsProto: ApiMapValue, - readTime: Timestamp, - createTime: Timestamp, - updateTime: Timestamp - ) { - super(ref, fieldsProto, readTime, createTime, updateTime); - } - /** * The time the document was created. * diff --git a/dev/src/reference.ts b/dev/src/reference.ts index 40f60f522..7d46f0d16 100644 --- a/dev/src/reference.ts +++ b/dev/src/reference.ts @@ -1240,7 +1240,23 @@ export class Query { const path = FieldPath.fromArgument(fieldPath); if (FieldPath.documentId().isEqual(path)) { - value = this.validateReference(value); + if (opStr === 'array-contains' || opStr === 'array-contains-any') { + throw new Error( + `Invalid Query. You can't perform '${opStr}' ` + + 'queries on FieldPath.documentId().' + ); + } + + if (opStr === 'in') { + if (!Array.isArray(value) || value.length === 0) { + throw new Error( + `Invalid Query. A non-empty array is required for '${opStr}' filters.` + ); + } + value = value.map(el => this.validateReference(el)); + } else { + value = this.validateReference(value); + } } const fieldFilter = new FieldFilter( @@ -1595,7 +1611,7 @@ export class Query { } else { throw new Error( 'The corresponding value for FieldPath.documentId() must be a ' + - 'string or a DocumentReference.' + `string or a DocumentReference, but was "${val}".` ); } diff --git a/dev/src/util.ts b/dev/src/util.ts index d7a690ce7..d04253699 100644 --- a/dev/src/util.ts +++ b/dev/src/util.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import {randomBytes} from 'crypto'; import {GoogleError, ServiceConfig, Status} from 'google-gax'; import {DocumentData} from './types'; @@ -52,8 +53,17 @@ export function autoId(): string { const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; let autoId = ''; - for (let i = 0; i < 20; i++) { - autoId += chars.charAt(Math.floor(Math.random() * chars.length)); + while (autoId.length < 20) { + const bytes = randomBytes(40); + bytes.forEach(b => { + // Length of `chars` is 62. We only take bytes between 0 and 62*4-1 + // (both inclusive). The value is then evenly mapped to indices of `char` + // via a modulo operation. + const maxValue = 62 * 4 - 1; + if (autoId.length < 20 && b <= maxValue) { + autoId += chars.charAt(b % 62); + } + }); } return autoId; } diff --git a/dev/src/v1/firestore_admin_client.ts b/dev/src/v1/firestore_admin_client.ts index 0787ec706..96c5f19f6 100644 --- a/dev/src/v1/firestore_admin_client.ts +++ b/dev/src/v1/firestore_admin_client.ts @@ -1,4 +1,4 @@ -// Copyright 2019 Google LLC +// Copyright 2020 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -42,7 +42,12 @@ const version = require('../../../package.json').version; * @memberof v1 */ export class FirestoreAdminClient { - private _descriptors: Descriptors = {page: {}, stream: {}, longrunning: {}}; + private _descriptors: Descriptors = { + page: {}, + stream: {}, + longrunning: {}, + batching: {}, + }; private _innerApiCalls: {[name: string]: Function}; private _pathTemplates: {[name: string]: gax.PathTemplate}; private _terminated = false; @@ -307,7 +312,8 @@ export class FirestoreAdminClient { if (this._terminated) { return Promise.reject('The client has already been closed.'); } - return stub[methodName].apply(stub, args); + const func = stub[methodName]; + return func.apply(stub, args); }, (err: Error | null | undefined) => () => { throw err; diff --git a/dev/src/v1/firestore_client.ts b/dev/src/v1/firestore_client.ts index 5a6339187..4a0630f95 100644 --- a/dev/src/v1/firestore_client.ts +++ b/dev/src/v1/firestore_client.ts @@ -1,4 +1,4 @@ -// Copyright 2019 Google LLC +// Copyright 2020 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -47,7 +47,12 @@ const version = require('../../../package.json').version; * @memberof v1 */ export class FirestoreClient { - private _descriptors: Descriptors = {page: {}, stream: {}, longrunning: {}}; + private _descriptors: Descriptors = { + page: {}, + stream: {}, + longrunning: {}, + batching: {}, + }; private _innerApiCalls: {[name: string]: Function}; private _terminated = false; private _opts: ClientOptions; @@ -250,7 +255,8 @@ export class FirestoreClient { if (this._terminated) { return Promise.reject('The client has already been closed.'); } - return stub[methodName].apply(stub, args); + const func = stub[methodName]; + return func.apply(stub, args); }, (err: Error | null | undefined) => () => { throw err; diff --git a/dev/src/v1beta1/firestore_client.ts b/dev/src/v1beta1/firestore_client.ts index 938b1e5db..b330e4e56 100644 --- a/dev/src/v1beta1/firestore_client.ts +++ b/dev/src/v1beta1/firestore_client.ts @@ -1,4 +1,4 @@ -// Copyright 2019 Google LLC +// Copyright 2020 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -55,7 +55,12 @@ const version = require('../../../package.json').version; * @memberof v1beta1 */ export class FirestoreClient { - private _descriptors: Descriptors = {page: {}, stream: {}, longrunning: {}}; + private _descriptors: Descriptors = { + page: {}, + stream: {}, + longrunning: {}, + batching: {}, + }; private _innerApiCalls: {[name: string]: Function}; private _terminated = false; private _opts: ClientOptions; @@ -258,7 +263,8 @@ export class FirestoreClient { if (this._terminated) { return Promise.reject('The client has already been closed.'); } - return stub[methodName].apply(stub, args); + const func = stub[methodName]; + return func.apply(stub, args); }, (err: Error | null | undefined) => () => { throw err; diff --git a/dev/synth.metadata b/dev/synth.metadata index 22062172c..fb2024a6f 100644 --- a/dev/synth.metadata +++ b/dev/synth.metadata @@ -1,19 +1,19 @@ { - "updateTime": "2020-03-05T23:07:35.336516Z", + "updateTime": "2020-03-22T11:28:16.621427Z", "sources": [ { "git": { "name": "googleapis", "remote": "https://github.com/googleapis/googleapis.git", - "sha": "f0b581b5bdf803e45201ecdb3688b60e381628a8", - "internalRef": "299181282" + "sha": "0be7105dc52590fa9a24e784052298ae37ce53aa", + "internalRef": "302154871" } }, { - "template": { - "name": "node_library", - "origin": "synthtool.gcp", - "version": "2020.2.4" + "git": { + "name": "synthtool", + "remote": "https://github.com/googleapis/synthtool.git", + "sha": "7e98e1609c91082f4eeb63b530c6468aefd18cfd" } } ], diff --git a/dev/system-test/firestore.ts b/dev/system-test/firestore.ts index 02ba04186..b5e6c4bb6 100644 --- a/dev/system-test/firestore.ts +++ b/dev/system-test/firestore.ts @@ -18,6 +18,7 @@ import * as chaiAsPromised from 'chai-as-promised'; import { CollectionReference, DocumentData, + DocumentReference, DocumentSnapshot, FieldPath, FieldValue, @@ -1079,9 +1080,17 @@ describe('Query class', () => { }); }; - function addDocs(...data: DocumentData[]): Promise { + async function addDocs( + ...docs: DocumentData[] + ): Promise { let id = 0; // Guarantees consistent ordering for the first documents - return Promise.all(data.map(d => randomCol.doc('' + id++).set(d))); + const refs: DocumentReference[] = []; + for (const doc of docs) { + const ref = randomCol.doc('doc' + id++); + await ref.set(doc); + refs.push(ref); + } + return refs; } function expectDocs(result: QuerySnapshot, ...data: DocumentData[]) { @@ -1171,7 +1180,7 @@ describe('Query class', () => { }); }); - it('supports in', async () => { + it('supports "in"', async () => { await addDocs( {zip: 98101}, {zip: 91102}, @@ -1184,6 +1193,14 @@ describe('Query class', () => { expectDocs(res, {zip: 98101}, {zip: 98103}); }); + it('supports "in" with document ID array', async () => { + const refs = await addDocs({count: 1}, {count: 2}, {count: 3}); + const res = await randomCol + .where(FieldPath.documentId(), 'in', [refs[0].id, refs[1]]) + .get(); + expectDocs(res, {count: 1}, {count: 2}); + }); + it('supports array-contains-any', async () => { await addDocs( {array: [42]}, diff --git a/dev/test/gapic-firestore-v1.ts b/dev/test/gapic-firestore-v1.ts index 6e5ba0e20..7e884a8d2 100644 --- a/dev/test/gapic-firestore-v1.ts +++ b/dev/test/gapic-firestore-v1.ts @@ -1,4 +1,4 @@ -// Copyright 2019 Google LLC +// Copyright 2020 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/dev/test/gapic-firestore-v1beta1.ts b/dev/test/gapic-firestore-v1beta1.ts index 61472d1dc..bd1169dbc 100644 --- a/dev/test/gapic-firestore-v1beta1.ts +++ b/dev/test/gapic-firestore-v1beta1.ts @@ -1,4 +1,4 @@ -// Copyright 2019 Google LLC +// Copyright 2020 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/dev/test/gapic-firestore_admin-v1.ts b/dev/test/gapic-firestore_admin-v1.ts index c4f322a73..179089643 100644 --- a/dev/test/gapic-firestore_admin-v1.ts +++ b/dev/test/gapic-firestore_admin-v1.ts @@ -1,4 +1,4 @@ -// Copyright 2019 Google LLC +// Copyright 2020 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/dev/test/query.ts b/dev/test/query.ts index 12f6a84e3..33cdf4b72 100644 --- a/dev/test/query.ts +++ b/dev/test/query.ts @@ -847,6 +847,77 @@ describe('where() interface', () => { }); }); + it('supports reference array for IN queries', () => { + const overrides: ApiOverride = { + runQuery: request => { + queryEquals( + request, + fieldFilters('__name__', 'IN', { + arrayValue: { + values: [ + { + referenceValue: `projects/${PROJECT_ID}/databases/(default)/documents/collectionId/foo`, + }, + { + referenceValue: `projects/${PROJECT_ID}/databases/(default)/documents/collectionId/bar`, + }, + ], + }, + }) + ); + + return stream(); + }, + }; + + return createInstance(overrides).then(firestore => { + const collection = firestore.collection('collectionId'); + const query = collection.where(FieldPath.documentId(), 'in', [ + 'foo', + collection.doc('bar'), + ]); + return query.get(); + }); + }); + + it('validates references for IN queries', () => { + const query = firestore.collection('collectionId'); + + expect(() => { + query.where(FieldPath.documentId(), 'in', ['foo', 42]); + }).to.throw( + 'The corresponding value for FieldPath.documentId() must be a string or a DocumentReference, but was "42".' + ); + + expect(() => { + query.where(FieldPath.documentId(), 'in', 42); + }).to.throw( + "Invalid Query. A non-empty array is required for 'in' filters." + ); + + expect(() => { + query.where(FieldPath.documentId(), 'in', []); + }).to.throw( + "Invalid Query. A non-empty array is required for 'in' filters." + ); + }); + + it('validates query operator for FieldPath.document()', () => { + const query = firestore.collection('collectionId'); + + expect(() => { + query.where(FieldPath.documentId(), 'array-contains', query.doc()); + }).to.throw( + "Invalid Query. You can't perform 'array-contains' queries on FieldPath.documentId()." + ); + + expect(() => { + query.where(FieldPath.documentId(), 'array-contains-any', query.doc()); + }).to.throw( + "Invalid Query. You can't perform 'array-contains-any' queries on FieldPath.documentId()." + ); + }); + it('rejects custom objects for field paths', () => { expect(() => { let query: Query = firestore.collection('collectionId'); @@ -1511,7 +1582,7 @@ describe('startAt() interface', () => { expect(() => { query.orderBy(FieldPath.documentId()).startAt(42); }).to.throw( - 'The corresponding value for FieldPath.documentId() must be a string or a DocumentReference.' + 'The corresponding value for FieldPath.documentId() must be a string or a DocumentReference, but was "42".' ); expect(() => { diff --git a/package.json b/package.json index 129d92386..feed6f49d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@google-cloud/firestore", "description": "Firestore Client Library for Node.js", - "version": "3.7.1", + "version": "3.7.3", "license": "Apache-2.0", "author": "Google Inc.", "engines": { diff --git a/samples/package.json b/samples/package.json index d2e8e617e..238dae4b3 100644 --- a/samples/package.json +++ b/samples/package.json @@ -11,7 +11,7 @@ "test": "mocha --timeout 600000" }, "dependencies": { - "@google-cloud/firestore": "^3.7.1" + "@google-cloud/firestore": "^3.7.3" }, "devDependencies": { "chai": "^4.2.0",