From 69bd69a3bc3d49e82a56c7cf14e0e4885d9af9cb Mon Sep 17 00:00:00 2001 From: Sebastian Schmidt Date: Thu, 9 Jan 2020 10:45:36 -0800 Subject: [PATCH] refactor: use typed GAPIC methods (#849) --- dev/conformance/runner.ts | 44 ++++----- dev/src/index.ts | 118 +++++++++++------------ dev/src/reference.ts | 14 ++- dev/src/transaction.ts | 2 +- dev/src/types.ts | 62 ++++++++++++- dev/src/watch.ts | 2 +- dev/src/write-batch.ts | 8 +- dev/test/collection.ts | 9 +- dev/test/document.ts | 165 ++++++++++++++++----------------- dev/test/fake-certificate.json | 8 -- dev/test/field-value.ts | 17 ++-- dev/test/index.ts | 33 +++---- dev/test/query.ts | 4 +- dev/test/transaction.ts | 45 +++++---- dev/test/util/helpers.ts | 100 +++++--------------- dev/test/watch.ts | 8 +- dev/test/write-batch.ts | 17 ++-- package.json | 3 +- 18 files changed, 334 insertions(+), 325 deletions(-) delete mode 100644 dev/test/fake-certificate.json diff --git a/dev/conformance/runner.ts b/dev/conformance/runner.ts index 41811ee47..0c8f6019e 100644 --- a/dev/conformance/runner.ts +++ b/dev/conformance/runner.ts @@ -15,7 +15,6 @@ const duplexify = require('duplexify'); import {expect} from 'chai'; -import {CallOptions} from 'google-gax'; import * as path from 'path'; import * as protobufjs from 'protobufjs'; import * as through2 from 'through2'; @@ -36,10 +35,12 @@ import { import {fieldsFromJson} from '../src/convert'; import {DocumentChangeType} from '../src/document-change'; import {QualifiedResourcePath} from '../src/path'; +import {UnaryMethod} from '../src/types'; import {isObject} from '../src/util'; import { ApiOverride, createInstance as createInstanceHelper, + response, } from '../test/util/helpers'; import api = proto.google.firestore.v1; @@ -248,32 +249,23 @@ const convertProto = { }; /** Request handler for _commit. */ -function commitHandler(spec: ConformanceProto) { - return ( - request: api.ICommitRequest, - options: CallOptions, - callback: ( - err: Error | null | undefined, - resp?: api.ICommitResponse - ) => void - ) => { - try { - const actualCommit = COMMIT_REQUEST_TYPE.fromObject(request); - const expectedCommit = COMMIT_REQUEST_TYPE.fromObject(spec.request); - expect(actualCommit).to.deep.equal(expectedCommit); - const res: api.IWriteResponse = { - commitTime: {}, - writeResults: [], - }; - for (let i = 1; i <= request.writes!.length; ++i) { - res.writeResults!.push({ - updateTime: {}, - }); - } - callback(null, res); - } catch (err) { - callback(err); +function commitHandler( + spec: ConformanceProto +): UnaryMethod { + return request => { + const actualCommit = COMMIT_REQUEST_TYPE.fromObject(request); + const expectedCommit = COMMIT_REQUEST_TYPE.fromObject(spec.request); + expect(actualCommit).to.deep.equal(expectedCommit); + const res: api.ICommitResponse = { + commitTime: {}, + writeResults: [], + }; + for (let i = 1; i <= request.writes!.length; ++i) { + res.writeResults!.push({ + updateTime: {}, + }); } + return response(res); }; } diff --git a/dev/src/index.ts b/dev/src/index.ts index c765b30b5..2d5f92fe8 100644 --- a/dev/src/index.ts +++ b/dev/src/index.ts @@ -41,7 +41,15 @@ import {DocumentReference} from './reference'; import {Serializer} from './serializer'; import {Timestamp} from './timestamp'; import {parseGetAllArguments, Transaction} from './transaction'; -import {ApiMapValue, GapicClient, ReadOptions, Settings} from './types'; +import { + ApiMapValue, + FirestoreStreamingMethod, + FirestoreUnaryMethod, + GapicClient, + ReadOptions, + Settings, + UnaryMethod, +} from './types'; import {Deferred, isPermanentRpcError, requestTag} from './util'; import { validateBoolean, @@ -392,7 +400,7 @@ export class Firestore { this._settings.maxIdleChannels === undefined ? DEFAULT_MAX_IDLE_CHANNELS : this._settings.maxIdleChannels; - this._clientPool = new ClientPool( + this._clientPool = new ClientPool( MAX_CONCURRENT_REQUESTS_PER_CLIENT, maxIdleChannels, /* clientFactory= */ () => { @@ -409,7 +417,7 @@ export class Firestore { logger('Firestore', null, 'Initialized Firestore GAPIC Client'); return client; }, - /* clientDestructor= */ (client: GapicClient) => client.close() + /* clientDestructor= */ client => client.close() ); logger('Firestore', null, 'Initialized Firestore'); @@ -956,7 +964,7 @@ export class Firestore { const self = this; return self - .requestStream('batchGetDocuments', 'unidirectional', request, requestTag) + .requestStream('batchGetDocuments', request, requestTag) .then(stream => { return new Promise((resolve, reject) => { stream @@ -1056,29 +1064,25 @@ export class Firestore { this._settingsFrozen = true; if (this._projectId === undefined) { - this._projectId = await this._clientPool.run(requestTag, gapicClient => { - return new Promise((resolve, reject) => { - gapicClient.getProjectId((err: Error, projectId: string) => { - if (err) { - logger( - 'Firestore._detectProjectId', - null, - 'Failed to detect project ID: %s', - err - ); - reject(err); - } else { - logger( - 'Firestore._detectProjectId', - null, - 'Detected project ID: %s', - projectId - ); - resolve(projectId); - } - }); - }); - }); + try { + this._projectId = await this._clientPool.run(requestTag, gapicClient => + gapicClient.getProjectId() + ); + logger( + 'Firestore.initializeIfNeeded', + null, + 'Detected project ID: %s', + this._projectId + ); + } catch (err) { + logger( + 'Firestore.initializeIfNeeded', + null, + 'Failed to detect project ID: %s', + err + ); + return Promise.reject(err); + } } } @@ -1263,7 +1267,7 @@ export class Firestore { /** * A funnel for all non-streaming API requests, assigning a project ID where - * necessary within the request options. + * necessary within the request options. * * @private * @param methodName Name of the Veneer API endpoint that takes a request @@ -1272,32 +1276,32 @@ export class Firestore { * @param requestTag A unique client-assigned identifier for this request. * @returns A Promise with the request result. */ - request(methodName: string, request: {}, requestTag: string): Promise { + request( + methodName: FirestoreUnaryMethod, + request: Req, + requestTag: string + ): Promise { const callOptions = this.createCallOptions(); - return this._clientPool.run(requestTag, gapicClient => { - return new Promise((resolve, reject) => { + return this._clientPool.run(requestTag, async gapicClient => { + try { logger('Firestore.request', requestTag, 'Sending request: %j', request); - gapicClient[methodName]( - request, - callOptions, - (err: GoogleError, result: T) => { - if (err) { - logger('Firestore.request', requestTag, 'Received error:', err); - reject(err); - } else { - logger( - 'Firestore.request', - requestTag, - 'Received response: %j', - result - ); - this._lastSuccessfulRequest = new Date().getTime(); - resolve(result); - } - } + const [result] = await (gapicClient[methodName] as UnaryMethod< + Req, + Resp + >)(request, callOptions); + logger( + 'Firestore.request', + requestTag, + 'Received response: %j', + result ); - }); + this._lastSuccessfulRequest = new Date().getTime(); + return result; + } catch (err) { + logger('Firestore.request', requestTag, 'Received error:', err); + return Promise.reject(err); + } }); } @@ -1311,19 +1315,18 @@ export class Firestore { * @private * @param methodName Name of the streaming Veneer API endpoint that * takes a request and GAX options. - * @param mode Whether this a unidirectional or bidirectional call. * @param request The Protobuf request to send. * @param requestTag A unique client-assigned identifier for this request. * @returns A Promise with the resulting read-only stream. */ requestStream( - methodName: string, - mode: 'unidirectional' | 'bidirectional', + methodName: FirestoreStreamingMethod, request: {}, requestTag: string ): Promise { const callOptions = this.createCallOptions(); + const bidrectional = methodName === 'listen'; const result = new Deferred(); this._clientPool.run(requestTag, gapicClient => { @@ -1339,10 +1342,9 @@ export class Firestore { 'Sending request: %j', request ); - const stream: Duplex = - mode === 'unidirectional' - ? gapicClient[methodName](request, callOptions) - : gapicClient[methodName](callOptions); + const stream = bidrectional + ? gapicClient[methodName](callOptions) + : gapicClient[methodName](request, callOptions); const logStream = through2.obj(function(this, chunk, enc, callback) { logger( 'Firestore.requestStream', @@ -1362,7 +1364,7 @@ export class Firestore { const resultStream = await this._initializeStream( stream, requestTag, - mode === 'bidirectional' ? request : undefined + bidrectional ? request : undefined ); resultStream.on('end', () => stream.end()); diff --git a/dev/src/reference.ts b/dev/src/reference.ts index 048622b0e..442db77db 100644 --- a/dev/src/reference.ts +++ b/dev/src/reference.ts @@ -293,7 +293,11 @@ export class DocumentReference implements Serializable { pageSize: Math.pow(2, 16) - 1, }; return this._firestore - .request('listCollectionIds', request, tag) + .request( + 'listCollectionIds', + request, + tag + ) .then(collectionIds => { const collections: CollectionReference[] = []; @@ -1829,7 +1833,7 @@ export class Query { this.firestore.initializeIfNeeded(tag).then(() => { const request = this.toProto(transactionId); this._firestore - .requestStream('runQuery', 'unidirectional', request, tag) + .requestStream('runQuery', request, tag) .then(backendStream => { backendStream.on('error', err => { logger( @@ -2060,7 +2064,11 @@ export class CollectionReference extends Query { }; return this.firestore - .request('listDocuments', request, tag) + .request( + 'listDocuments', + request, + tag + ) .then(documents => { // Note that the backend already orders these documents by name, // so we do not need to manually sort them. diff --git a/dev/src/transaction.ts b/dev/src/transaction.ts index 7f20ec910..ebefaeb86 100644 --- a/dev/src/transaction.ts +++ b/dev/src/transaction.ts @@ -363,7 +363,7 @@ export class Transaction { } return this._firestore - .request( + .request( 'beginTransaction', request, this._requestTag diff --git a/dev/src/types.ts b/dev/src/types.ts index cca4fdc0d..a82b68349 100644 --- a/dev/src/types.ts +++ b/dev/src/types.ts @@ -14,6 +14,9 @@ * limitations under the License. */ +import {CallOptions} from 'google-gax'; +import {Duplex} from 'stream'; + import {google} from '../protos/firestore_v1_proto_api'; import {FieldPath} from './path'; import {Timestamp} from './timestamp'; @@ -27,9 +30,62 @@ export interface ApiMapValue { [k: string]: google.firestore.v1.IValue; } -// We don't have type information for the JavaScript GapicClient. -// tslint:disable-next-line:no-any -export type GapicClient = any; +/** + * The subset of methods we use from FirestoreClient. + * + * We don't depend on the actual Gapic client to avoid loading the GAX stack at + * module initialization time. + */ +export interface GapicClient { + getProjectId(): Promise; + beginTransaction( + request: api.IBeginTransactionRequest, + options?: CallOptions + ): Promise<[api.IBeginTransactionResponse, unknown, unknown]>; + commit( + request: api.ICommitRequest, + options?: CallOptions + ): Promise<[api.ICommitResponse, unknown, unknown]>; + rollback( + request: api.IRollbackRequest, + options?: CallOptions + ): Promise<[google.protobuf.IEmpty, unknown, unknown]>; + batchGetDocuments( + request?: api.IBatchGetDocumentsRequest, + options?: CallOptions + ): Duplex; + runQuery(request?: api.IRunQueryRequest, options?: CallOptions): Duplex; + listDocuments( + request: api.IListDocumentsRequest, + options?: CallOptions + ): Promise<[api.IDocument[], unknown, unknown]>; + listCollectionIds( + request: api.IListCollectionIdsRequest, + options?: CallOptions + ): Promise<[string[], unknown, unknown]>; + listen(options?: CallOptions): Duplex; + close(): Promise; +} + +/** Request/response methods used in the Firestore SDK. */ +export type FirestoreUnaryMethod = + | 'listDocuments' + | 'listCollectionIds' + | 'rollback' + | 'beginTransaction' + | 'commit'; + +/** Streaming methods used in the Firestore SDK. */ +export type FirestoreStreamingMethod = + | 'listen' + | 'runQuery' + | 'batchGetDocuments'; + +/** Type signature for the unary methods in the GAPIC layer. */ +export type UnaryMethod = ( + request: Req, + callOptions: CallOptions +) => Promise<[Resp, unknown, unknown]>; // We don't have type information for the npm package // `functional-red-black-tree`. diff --git a/dev/src/watch.ts b/dev/src/watch.ts index 6771399fa..4a6ef2a16 100644 --- a/dev/src/watch.ts +++ b/dev/src/watch.ts @@ -390,7 +390,7 @@ abstract class Watch { // Note that we need to call the internal _listen API to pass additional // header values in readWriteStream. return this.firestore - .requestStream('listen', 'bidirectional', request, this.requestTag) + .requestStream('listen', request, this.requestTag) .then(backendStream => { if (!this.isActive) { logger( diff --git a/dev/src/write-batch.ts b/dev/src/write-batch.ts index 19028f55c..1f3bf5103 100644 --- a/dev/src/write-batch.ts +++ b/dev/src/write-batch.ts @@ -534,7 +534,6 @@ export class WriteBatch { await this._firestore.initializeIfNeeded(tag); const database = this._firestore.formattedName; - const request: api.ICommitRequest = {database}; // On GCF, we periodically force transactional commits to allow for // request retries in case GCF closes our backend connection. @@ -542,9 +541,9 @@ export class WriteBatch { if (!explicitTransaction && this._shouldCreateTransaction()) { logger('WriteBatch.commit', tag, 'Using transaction for commit'); return this._firestore - .request( + .request( 'beginTransaction', - request, + {database}, tag ) .then(resp => { @@ -552,6 +551,7 @@ export class WriteBatch { }); } + const request: api.ICommitRequest = {database}; const writes = this._ops.map(op => op()); request.writes = []; @@ -586,7 +586,7 @@ export class WriteBatch { } return this._firestore - .request('commit', request, tag) + .request('commit', request, tag) .then(resp => { const writeResults: WriteResult[] = []; diff --git a/dev/test/collection.ts b/dev/test/collection.ts index 96e2a3f64..6bf8b00b2 100644 --- a/dev/test/collection.ts +++ b/dev/test/collection.ts @@ -21,6 +21,7 @@ import { DATABASE_ROOT, document, InvalidApiUsage, + response, verifyInstance, } from './util/helpers'; @@ -84,7 +85,7 @@ describe('Collection interface', () => { it('has add() method', () => { const overrides: ApiOverride = { - commit: (request, options, callback) => { + commit: request => { // Verify that the document name uses an auto-generated id. const docIdRe = /^projects\/test-project\/databases\/\(default\)\/documents\/collectionId\/[a-zA-Z0-9]{20}$/; expect(request.writes![0].update!.name).to.match(docIdRe); @@ -105,7 +106,7 @@ describe('Collection interface', () => { ], }); - callback(null, { + return response({ commitTime: { nanos: 0, seconds: 0, @@ -137,7 +138,7 @@ describe('Collection interface', () => { it('has list() method', () => { const overrides: ApiOverride = { - listDocuments: (request, options, callback) => { + listDocuments: (request, options) => { expect(request).to.deep.eq({ parent: `${DATABASE_ROOT}/documents/a/b`, collectionId: 'c', @@ -146,7 +147,7 @@ describe('Collection interface', () => { mask: {fieldPaths: []}, }); - callback(null, [document('first'), document('second')]); + return response([document('first'), document('second')]); }, }; diff --git a/dev/test/document.ts b/dev/test/document.ts index 6facb2029..43c764a4d 100644 --- a/dev/test/document.ts +++ b/dev/test/document.ts @@ -13,8 +13,8 @@ // limitations under the License. import {expect} from 'chai'; +import {GoogleError, Status} from 'google-gax'; -import * as proto from '../protos/firestore_v1_proto_api'; import { DocumentReference, FieldPath, @@ -34,6 +34,7 @@ import { missing, remove, requestEquals, + response, retrieve, serverTimestamp, set, @@ -44,8 +45,6 @@ import { writeResult, } from './util/helpers'; -import {GoogleError, Status} from 'google-gax'; - const PROJECT_ID = 'test-project'; const INVALID_ARGUMENTS_TO_UPDATE = new RegExp( @@ -116,7 +115,7 @@ describe('serialize document', () => { it('serializes to Protobuf JS', () => { const overrides: ApiOverride = { - commit: (request, options, callback) => { + commit: request => { requestEquals( request, set({ @@ -125,7 +124,7 @@ describe('serialize document', () => { }), }) ); - callback(null, writeResult(1)); + return response(writeResult(1)); }, }; @@ -199,7 +198,7 @@ describe('serialize document', () => { it('serializes large numbers into doubles', () => { const overrides: ApiOverride = { - commit: (request, options, callback) => { + commit: request => { requestEquals( request, set({ @@ -208,7 +207,7 @@ describe('serialize document', () => { }), }) ); - callback(null, writeResult(1)); + return response(writeResult(1)); }, }; @@ -222,7 +221,7 @@ describe('serialize document', () => { it('serializes date before 1970', () => { const overrides: ApiOverride = { - commit: (request, options, callback) => { + commit: request => { requestEquals( request, set({ @@ -234,7 +233,7 @@ describe('serialize document', () => { }), }) ); - callback(null, writeResult(1)); + return response(writeResult(1)); }, }; @@ -247,14 +246,14 @@ describe('serialize document', () => { it('serializes unicode keys', () => { const overrides: ApiOverride = { - commit: (request, options, callback) => { + commit: request => { requestEquals( request, set({ document: document('documentId', '😀', '😜'), }) ); - callback(null, writeResult(1)); + return response(writeResult(1)); }, }; @@ -267,7 +266,7 @@ describe('serialize document', () => { it('accepts both blob formats', () => { const overrides: ApiOverride = { - commit: (request, options, callback) => { + commit: request => { requestEquals( request, set({ @@ -282,7 +281,7 @@ describe('serialize document', () => { ), }) ); - callback(null, writeResult(1)); + return response(writeResult(1)); }, }; @@ -296,14 +295,14 @@ describe('serialize document', () => { it('supports NaN and Infinity', () => { const overrides: ApiOverride = { - commit: (request, options, callback) => { + commit: request => { const fields = request.writes![0].update!.fields!; expect(fields.nanValue.doubleValue).to.be.a('number'); expect(fields.nanValue.doubleValue).to.be.NaN; expect(fields.posInfinity.doubleValue).to.equal(Infinity); expect(fields.negInfinity.doubleValue).to.equal(-Infinity); - callback(null, writeResult(1)); + return response(writeResult(1)); }, }; @@ -365,7 +364,7 @@ describe('serialize document', () => { it('is able to write a document reference with cycles', () => { const overrides: ApiOverride = { - commit: (request, options, callback) => { + commit: request => { requestEquals( request, set({ @@ -374,7 +373,7 @@ describe('serialize document', () => { }), }) ); - callback(null, writeResult(1)); + return response(writeResult(1)); }, }; @@ -718,10 +717,10 @@ describe('delete document', () => { it('generates proto', () => { const overrides: ApiOverride = { - commit: (request, options, callback) => { + commit: request => { requestEquals(request, remove('documentId')); - callback(null, writeResult(1)); + return response(writeResult(1)); }, }; @@ -732,10 +731,10 @@ describe('delete document', () => { it('returns update time', () => { const overrides: ApiOverride = { - commit: (request, options, callback) => { + commit: request => { requestEquals(request, remove('documentId')); - callback(null, { + return response({ commitTime: { nanos: 123000000, seconds: 479978400, @@ -758,7 +757,7 @@ describe('delete document', () => { it('with last update time precondition', () => { const overrides: ApiOverride = { - commit: (request, options, callback) => { + commit: request => { requestEquals( request, remove('documentId', { @@ -769,7 +768,7 @@ describe('delete document', () => { }) ); - callback(null, writeResult(1)); + return response(writeResult(1)); }, }; @@ -837,14 +836,14 @@ describe('set document', () => { it('supports empty map', () => { const overrides: ApiOverride = { - commit: (request, options, callback) => { + commit: request => { requestEquals( request, set({ document: document('documentId'), }) ); - callback(null, writeResult(1)); + return response(writeResult(1)); }, }; @@ -855,7 +854,7 @@ describe('set document', () => { it('supports nested empty map', () => { const overrides: ApiOverride = { - commit: (request, options, callback) => { + commit: request => { requestEquals( request, set({ @@ -864,7 +863,7 @@ describe('set document', () => { }), }) ); - callback(null, writeResult(1)); + return response(writeResult(1)); }, }; @@ -875,14 +874,14 @@ describe('set document', () => { it('skips merges with just field transform', () => { const overrides: ApiOverride = { - commit: (request, options, callback) => { + commit: request => { requestEquals( request, set({ transforms: [serverTimestamp('a'), serverTimestamp('b.c')], }) ); - callback(null, writeResult(1)); + return response(writeResult(1)); }, }; @@ -899,7 +898,7 @@ describe('set document', () => { it('sends empty non-merge write even with just field transform', () => { const overrides: ApiOverride = { - commit: (request, options, callback) => { + commit: request => { requestEquals( request, set({ @@ -907,7 +906,7 @@ describe('set document', () => { transforms: [serverTimestamp('a'), serverTimestamp('b.c')], }) ); - callback(null, writeResult(2)); + return response(writeResult(2)); }, }; @@ -921,7 +920,7 @@ describe('set document', () => { it('supports document merges', () => { const overrides: ApiOverride = { - commit: (request, options, callback) => { + commit: request => { requestEquals( request, set({ @@ -937,7 +936,7 @@ describe('set document', () => { mask: updateMask('a', 'c.d', 'f'), }) ); - callback(null, writeResult(1)); + return response(writeResult(1)); }, }; @@ -950,7 +949,7 @@ describe('set document', () => { it('supports document merges with field mask', () => { const overrides: ApiOverride = { - commit: (request, options, callback) => { + commit: request => { requestEquals( request, set({ @@ -982,7 +981,7 @@ describe('set document', () => { mask: updateMask('a', 'b', 'd.e', 'f'), }) ); - callback(null, writeResult(1)); + return response(writeResult(1)); }, }; @@ -1003,7 +1002,7 @@ describe('set document', () => { it('supports document merges with empty field mask', () => { const overrides: ApiOverride = { - commit: (request, options, callback) => { + commit: request => { requestEquals( request, set({ @@ -1011,7 +1010,7 @@ describe('set document', () => { mask: updateMask(), }) ); - callback(null, writeResult(1)); + return response(writeResult(1)); }, }; @@ -1027,7 +1026,7 @@ describe('set document', () => { it('supports document merges with field mask and empty maps', () => { const overrides: ApiOverride = { - commit: (request, options, callback) => { + commit: request => { requestEquals( request, set({ @@ -1057,7 +1056,7 @@ describe('set document', () => { mask: updateMask('a', 'c.d'), }) ); - callback(null, writeResult(1)); + return response(writeResult(1)); }, }; @@ -1074,7 +1073,7 @@ describe('set document', () => { it('supports document merges with field mask and field transform', () => { const overrides: ApiOverride = { - commit: (request, options, callback) => { + commit: request => { requestEquals( request, set({ @@ -1087,7 +1086,7 @@ describe('set document', () => { ], }) ); - callback(null, writeResult(2)); + return response(writeResult(2)); }, }; @@ -1111,7 +1110,7 @@ describe('set document', () => { it('supports empty merge', () => { const overrides: ApiOverride = { - commit: (request, options, callback) => { + commit: request => { requestEquals( request, set({ @@ -1119,7 +1118,7 @@ describe('set document', () => { mask: updateMask(), }) ); - callback(null, writeResult(1)); + return response(writeResult(1)); }, }; @@ -1130,7 +1129,7 @@ describe('set document', () => { it('supports nested empty merge', () => { const overrides: ApiOverride = { - commit: (request, options, callback) => { + commit: request => { requestEquals( request, set({ @@ -1140,7 +1139,7 @@ describe('set document', () => { mask: updateMask('a'), }) ); - callback(null, writeResult(1)); + return response(writeResult(1)); }, }; @@ -1156,14 +1155,14 @@ describe('set document', () => { it("doesn't split on dots", () => { const overrides: ApiOverride = { - commit: (request, options, callback) => { + commit: request => { requestEquals( request, set({ document: document('documentId', 'a.b', 'c'), }) ); - callback(null, writeResult(1)); + return response(writeResult(1)); }, }; @@ -1270,9 +1269,9 @@ describe('create document', () => { it('creates document', () => { const overrides: ApiOverride = { - commit: (request, options, callback) => { + commit: request => { requestEquals(request, create({document: document('documentId')})); - callback(null, writeResult(1)); + return response(writeResult(1)); }, }; @@ -1283,10 +1282,10 @@ describe('create document', () => { it('returns update time', () => { const overrides: ApiOverride = { - commit: (request, options, callback) => { + commit: request => { requestEquals(request, create({document: document('documentId')})); - callback(null, { + return response({ commitTime: { nanos: 0, seconds: 0, @@ -1316,7 +1315,7 @@ describe('create document', () => { it('supports field transform', () => { const overrides: ApiOverride = { - commit: (request, options, callback) => { + commit: request => { requestEquals( request, create({ @@ -1326,7 +1325,7 @@ describe('create document', () => { ], }) ); - callback(null, writeResult(1)); + return response(writeResult(1)); }, }; @@ -1340,7 +1339,7 @@ describe('create document', () => { it('supports nested empty map', () => { const overrides: ApiOverride = { - commit: (request, options, callback) => { + commit: request => { requestEquals( request, create({ @@ -1355,7 +1354,7 @@ describe('create document', () => { }), }) ); - callback(null, writeResult(1)); + return response(writeResult(1)); }, }; @@ -1394,7 +1393,7 @@ describe('update document', () => { it('generates proto', () => { const overrides: ApiOverride = { - commit: (request, options, callback) => { + commit: request => { requestEquals( request, update({ @@ -1402,7 +1401,7 @@ describe('update document', () => { mask: updateMask('foo'), }) ); - callback(null, writeResult(1)); + return response(writeResult(1)); }, }; @@ -1413,7 +1412,7 @@ describe('update document', () => { it('supports nested field transform', () => { const overrides: ApiOverride = { - commit: (request, options, callback) => { + commit: request => { requestEquals( request, update({ @@ -1424,7 +1423,7 @@ describe('update document', () => { mask: updateMask('a', 'foo'), }) ); - callback(null, writeResult(2)); + return response(writeResult(2)); }, }; @@ -1439,9 +1438,9 @@ describe('update document', () => { it('skips write for single field transform', () => { const overrides: ApiOverride = { - commit: (request, options, callback) => { + commit: request => { requestEquals(request, update({transforms: [serverTimestamp('a')]})); - callback(null, writeResult(1)); + return response(writeResult(1)); }, }; @@ -1454,7 +1453,7 @@ describe('update document', () => { it('supports nested empty map', () => { const overrides: ApiOverride = { - commit: (request, options, callback) => { + commit: request => { requestEquals( request, update({ @@ -1464,7 +1463,7 @@ describe('update document', () => { mask: updateMask('a'), }) ); - callback(null, writeResult(1)); + return response(writeResult(1)); }, }; @@ -1475,12 +1474,12 @@ describe('update document', () => { it('supports nested delete', () => { const overrides: ApiOverride = { - commit: (request, options, callback) => { + commit: request => { requestEquals( request, update({document: document('documentId'), mask: updateMask('a.b')}) ); - callback(null, writeResult(1)); + return response(writeResult(1)); }, }; @@ -1493,7 +1492,7 @@ describe('update document', () => { it('returns update time', () => { const overrides: ApiOverride = { - commit: (request, options, callback) => { + commit: request => { requestEquals( request, update({ @@ -1501,7 +1500,7 @@ describe('update document', () => { mask: updateMask('foo'), }) ); - callback(null, { + return response({ commitTime: { nanos: 0, seconds: 0, @@ -1531,7 +1530,7 @@ describe('update document', () => { it('with last update time precondition', () => { const overrides: ApiOverride = { - commit: (request, options, callback) => { + commit: request => { requestEquals( request, update({ @@ -1545,7 +1544,7 @@ describe('update document', () => { }, }) ); - callback(null, writeResult(1)); + return response(writeResult(1)); }, }; @@ -1615,7 +1614,7 @@ describe('update document', () => { it('with top-level document', () => { const overrides: ApiOverride = { - commit: (request, options, callback) => { + commit: request => { requestEquals( request, update({ @@ -1623,7 +1622,7 @@ describe('update document', () => { mask: updateMask('foo'), }) ); - callback(null, writeResult(1)); + return response(writeResult(1)); }, }; @@ -1636,7 +1635,7 @@ describe('update document', () => { it('with nested document', () => { const overrides: ApiOverride = { - commit: (request, options, callback) => { + commit: request => { requestEquals( request, update({ @@ -1672,7 +1671,7 @@ describe('update document', () => { mask: updateMask('a.b.c', 'foo.bar'), }) ); - callback(null, writeResult(1)); + return response(writeResult(1)); }, }; @@ -1691,7 +1690,7 @@ describe('update document', () => { it('with two nested fields ', () => { const overrides: ApiOverride = { - commit: (request, options, callback) => { + commit: request => { requestEquals( request, update({ @@ -1719,7 +1718,7 @@ describe('update document', () => { ), }) ); - callback(null, writeResult(1)); + return response(writeResult(1)); }, }; @@ -1749,7 +1748,7 @@ describe('update document', () => { it('with nested field and document transform ', () => { const overrides: ApiOverride = { - commit: (request, options, callback) => { + commit: request => { requestEquals( request, update({ @@ -1785,7 +1784,7 @@ describe('update document', () => { ), }) ); - callback(null, writeResult(1)); + return response(writeResult(1)); }, }; @@ -1801,7 +1800,7 @@ describe('update document', () => { it('with field with dot ', () => { const overrides: ApiOverride = { - commit: (request, options, callback) => { + commit: request => { requestEquals( request, update({ @@ -1809,7 +1808,7 @@ describe('update document', () => { mask: updateMask('`a.b`'), }) ); - callback(null, writeResult(1)); + return response(writeResult(1)); }, }; return createInstance(overrides).then(firestore => { @@ -1960,7 +1959,7 @@ describe('update document', () => { it('with field delete', () => { const overrides: ApiOverride = { - commit: (request, options, callback) => { + commit: request => { requestEquals( request, update({ @@ -1968,7 +1967,7 @@ describe('update document', () => { mask: updateMask('bar', 'foo'), }) ); - callback(null, writeResult(1)); + return response(writeResult(1)); }, }; @@ -1984,13 +1983,13 @@ describe('update document', () => { describe('listCollections() method', () => { it('sorts results', () => { const overrides: ApiOverride = { - listCollectionIds: (request, options, callback) => { + listCollectionIds: request => { expect(request).to.deep.eq({ parent: `projects/${PROJECT_ID}/databases/(default)/documents/coll/doc`, pageSize: 65535, }); - callback(null, ['second', 'first']); + return response(['second', 'first']); }, }; diff --git a/dev/test/fake-certificate.json b/dev/test/fake-certificate.json deleted file mode 100644 index c72de1984..000000000 --- a/dev/test/fake-certificate.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "type": "service_account", - "project_id": "...", - "private_key_id": "...", - "private_key": "...", - "client_email": "...", - "client_id": "..." -} diff --git a/dev/test/field-value.ts b/dev/test/field-value.ts index 973e3309f..2228a1138 100644 --- a/dev/test/field-value.ts +++ b/dev/test/field-value.ts @@ -23,6 +23,7 @@ import { incrementTransform, InvalidApiUsage, requestEquals, + response, serverTimestamp, set, writeResult, @@ -96,7 +97,7 @@ describe('FieldValue.arrayUnion()', () => { it('can be used with set()', () => { const overrides: ApiOverride = { - commit: (request, options, callback) => { + commit: request => { const expectedRequest = set({ document: document('documentId', 'foo', 'bar'), transforms: [ @@ -107,7 +108,7 @@ describe('FieldValue.arrayUnion()', () => { requestEquals(request, expectedRequest); - callback(null, writeResult(2)); + return response(writeResult(2)); }, }; @@ -152,7 +153,7 @@ describe('FieldValue.increment()', () => { it('can be used with set()', () => { const overrides: ApiOverride = { - commit: (request, options, callback) => { + commit: request => { const expectedRequest = set({ document: document('documentId', 'foo', 'bar'), transforms: [ @@ -161,7 +162,7 @@ describe('FieldValue.increment()', () => { ], }); requestEquals(request, expectedRequest); - callback(null, writeResult(2)); + return response(writeResult(2)); }, }; @@ -194,7 +195,7 @@ describe('FieldValue.arrayRemove()', () => { it('can be used with set()', () => { const overrides: ApiOverride = { - commit: (request, options, callback) => { + commit: request => { const expectedRequest = set({ document: document('documentId', 'foo', 'bar'), transforms: [ @@ -204,7 +205,7 @@ describe('FieldValue.arrayRemove()', () => { }); requestEquals(request, expectedRequest); - callback(null, writeResult(2)); + return response(writeResult(2)); }, }; @@ -232,14 +233,14 @@ describe('FieldValue.serverTimestamp()', () => { it('can be used with set()', () => { const overrides: ApiOverride = { - commit: (request, options, callback) => { + commit: request => { const expectedRequest = set({ document: document('documentId', 'foo', 'bar'), transforms: [serverTimestamp('field'), serverTimestamp('map.field')], }); requestEquals(request, expectedRequest); - callback(null, writeResult(2)); + return response(writeResult(2)); }, }; diff --git a/dev/test/index.ts b/dev/test/index.ts index 0ab27c308..73e1bb2af 100644 --- a/dev/test/index.ts +++ b/dev/test/index.ts @@ -15,7 +15,7 @@ import {expect, use} from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; import * as extend from 'extend'; -import * as gax from 'google-gax'; +import {GoogleError, GrpcClient, Status} from 'google-gax'; import {google} from '../protos/firestore_v1_proto_api'; @@ -31,23 +31,22 @@ import { found, InvalidApiUsage, missing, + response, stream, verifyInstance, } from './util/helpers'; import api = google.firestore.v1; -import {Status} from 'google-gax'; use(chaiAsPromised); -const {grpc} = new gax.GrpcClient({}); +const {grpc} = new GrpcClient({}); const PROJECT_ID = 'test-project'; const DATABASE_ROOT = `projects/${PROJECT_ID}/databases/(default)`; const DEFAULT_SETTINGS = { projectId: PROJECT_ID, sslCreds: grpc.credentials.createInsecure(), - keyFilename: __dirname + '/fake-certificate.json', }; // Change the argument to 'console.log' to enable debug output. @@ -516,9 +515,7 @@ describe('instantiation', () => { it('uses project id from gapic client', async () => { return createInstance( { - getProjectId: callback => { - callback(null, 'foo'); - }, + getProjectId: () => Promise.resolve('foo'), }, {projectId: undefined} ).then(async firestore => { @@ -533,7 +530,6 @@ describe('instantiation', () => { it('uses project ID from settings()', () => { const firestore = new Firestore.Firestore({ sslCreds: grpc.credentials.createInsecure(), - keyFilename: './test/fake-certificate.json', }); firestore.settings({projectId: PROJECT_ID}); @@ -546,9 +542,7 @@ describe('instantiation', () => { it('handles error from project ID detection', () => { return createInstance( { - getProjectId: callback => { - callback(new Error('Injected Error'), null); - }, + getProjectId: () => Promise.reject(new Error('Injected Error')), }, {projectId: undefined} ).then(firestore => { @@ -609,11 +603,11 @@ describe('instantiation', () => { describe('serializer', () => { it('supports all types', () => { const overrides: ApiOverride = { - commit: (request, options, callback) => { + commit: request => { expect(allSupportedTypesProtobufJs.fields).to.deep.eq( request.writes![0].update!.fields ); - callback(null, { + return response({ commitTime: {}, writeResults: [ { @@ -662,7 +656,6 @@ describe('snapshot_() method', () => { firestore = new Firestore.Firestore({ projectId: PROJECT_ID, sslCreds: grpc.credentials.createInsecure(), - keyFilename: './test/fake-certificate.json', }); }); @@ -882,13 +875,13 @@ describe('collection() method', () => { describe('listCollections() method', () => { it('returns collections', () => { const overrides: ApiOverride = { - listCollectionIds: (request, options, callback) => { + listCollectionIds: request => { expect(request).to.deep.eq({ parent: `projects/${PROJECT_ID}/databases/(default)/documents`, pageSize: 65535, }); - callback(null, ['first', 'second']); + return response(['first', 'second']); }, }; @@ -1075,10 +1068,10 @@ describe('getAll() method', () => { const overrides: ApiOverride = { batchGetDocuments: request => { - const errorCode = Number(request.documents![0].split('/').pop()); + const errorCode = Number(request!.documents![0].split('/').pop()); actualErrorAttempts[errorCode] = (actualErrorAttempts[errorCode] || 0) + 1; - const error = new gax.GoogleError('Expected exception'); + const error = new GoogleError('Expected exception'); error.code = errorCode; return stream(error); }, @@ -1171,7 +1164,7 @@ describe('getAll() method', () => { it('accepts same document multiple times', () => { const overrides: ApiOverride = { batchGetDocuments: request => { - expect(request.documents!.length).to.equal(2); + expect(request!.documents!.length).to.equal(2); return stream(found('a'), found('b')); }, }; @@ -1193,7 +1186,7 @@ describe('getAll() method', () => { it('applies field mask', () => { const overrides: ApiOverride = { batchGetDocuments: request => { - expect(request.mask!.fieldPaths).to.have.members([ + expect(request!.mask!.fieldPaths).to.have.members([ 'foo.bar', '`foo.bar`', ]); diff --git a/dev/test/query.ts b/dev/test/query.ts index 6aa4fda80..f2c546c78 100644 --- a/dev/test/query.ts +++ b/dev/test/query.ts @@ -254,9 +254,11 @@ function endAt( } function queryEquals( - actual: api.IRunQueryRequest, + actual: api.IRunQueryRequest | undefined, ...protoComponents: api.IStructuredQuery[] ) { + expect(actual).to.not.be.undefined; + const query: api.IRunQueryRequest = { parent: DATABASE_ROOT + '/documents', structuredQuery: { diff --git a/dev/test/transaction.ts b/dev/test/transaction.ts index 0e202261c..b4dba86a8 100644 --- a/dev/test/transaction.ts +++ b/dev/test/transaction.ts @@ -16,12 +16,18 @@ import {expect, use} from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; import * as extend from 'extend'; import {GoogleError, Status} from 'google-gax'; +import {Duplex} from 'stream'; import * as through2 from 'through2'; import * as proto from '../protos/firestore_v1_proto_api'; import * as Firestore from '../src'; import {DocumentReference, FieldPath, Transaction} from '../src'; -import {ApiOverride, createInstance, InvalidApiUsage} from './util/helpers'; +import { + ApiOverride, + createInstance, + InvalidApiUsage, + response, +} from './util/helpers'; import api = proto.google.firestore.v1; @@ -58,7 +64,7 @@ interface TransactionStep { | api.IRunQueryRequest; error?: Error; response?: api.ICommitResponse | api.IBeginTransactionResponse; - stream?: NodeJS.ReadableStream; + stream?: Duplex; } function commit( @@ -267,26 +273,35 @@ function runTransaction( ...expectedRequests: TransactionStep[] ) { const overrides: ApiOverride = { - beginTransaction: (actual, _, callback) => { + beginTransaction: actual => { const request = expectedRequests.shift()!; expect(request.type).to.equal('begin'); expect(actual).to.deep.eq(request.request); - callback( - request.error, - request.response as api.IBeginTransactionResponse - ); + if (request.error) { + return Promise.reject(request.error); + } else { + return response(request.response as api.IBeginTransactionResponse); + } }, - commit: (actual, _, callback) => { + commit: actual => { const request = expectedRequests.shift()!; expect(request.type).to.equal('commit'); expect(actual).to.deep.eq(request.request); - callback(request.error, request.response as api.ICommitResponse); + if (request.error) { + return Promise.reject(request.error); + } else { + return response(request.response as api.ICommitResponse); + } }, - rollback: (actual, _, callback) => { + rollback: actual => { const request = expectedRequests.shift()!; expect(request.type).to.equal('rollback'); expect(actual).to.deep.eq(request.request); - callback(request.error); + if (request.error) { + return Promise.reject(request.error); + } else { + return response({}); + } }, batchGetDocuments: actual => { const request = expectedRequests.shift()!; @@ -347,9 +362,7 @@ describe('successful transactions', () => { describe('failed transactions', () => { it('requires update function', () => { const overrides: ApiOverride = { - beginTransaction: () => { - expect.fail(); - }, + beginTransaction: () => Promise.reject(), }; return createInstance(overrides).then(firestore => { @@ -361,9 +374,7 @@ describe('failed transactions', () => { it('requires valid retry number', () => { const overrides: ApiOverride = { - beginTransaction: () => { - expect.fail(); - }, + beginTransaction: () => Promise.reject(), }; return createInstance(overrides).then(firestore => { diff --git a/dev/test/util/helpers.ts b/dev/test/util/helpers.ts index df050d683..614e30db2 100644 --- a/dev/test/util/helpers.ts +++ b/dev/test/util/helpers.ts @@ -14,7 +14,8 @@ import {expect} from 'chai'; import * as extend from 'extend'; -import {CallOptions, GrpcClient} from 'google-gax'; +import {GrpcClient} from 'google-gax'; +import {Duplex} from 'stream'; import * as through2 from 'through2'; import * as proto from '../../protos/firestore_v1_proto_api'; @@ -40,94 +41,35 @@ export const DOCUMENT_NAME = `${COLLECTION_ROOT}/documentId`; // tslint:disable-next-line:no-any export type InvalidApiUsage = any; -/** - * Interface that defines the request handlers used by Firestore. - */ -export interface ApiOverride { - beginTransaction?: ( - request: api.IBeginTransactionRequest, - options: CallOptions, - callback: (err?: Error | null, resp?: api.IBeginTransactionResponse) => void - ) => void; - commit?: ( - request: api.ICommitRequest, - options: CallOptions, - callback: (err?: Error | null, resp?: api.ICommitResponse) => void - ) => void; - rollback?: ( - request: api.IRollbackRequest, - options: CallOptions, - callback: (err?: Error | null, resp?: void) => void - ) => void; - listCollectionIds?: ( - request: api.IListCollectionIdsRequest, - options: CallOptions, - callback: (err?: Error | null, resp?: string[]) => void - ) => void; - listDocuments?: ( - request: api.IListDocumentsRequest, - options: CallOptions, - callback: (err?: Error | null, resp?: api.IDocument[]) => void - ) => void; - batchGetDocuments?: ( - request: api.IBatchGetDocumentsRequest - ) => NodeJS.ReadableStream; - runQuery?: (request: api.IRunQueryRequest) => NodeJS.ReadableStream; - listen?: () => NodeJS.ReadWriteStream; - getProjectId?: ( - callback: (err?: Error | null, projectId?: string | null) => void - ) => void; -} +/** Defines the request handlers used by Firestore. */ +export type ApiOverride = Partial; /** * Creates a new Firestore instance for testing. Request handlers can be * overridden by providing `apiOverrides`. * - * @param {ApiOverride} apiOverrides An object with the request handlers to - * override. - * @param {Object} firestoreSettings Firestore Settings to configure the client. - * @return {Promise} A Promise that resolves with the new Firestore - * client. + * @param apiOverrides An object with request handlers to override. + * @param firestoreSettings Firestore Settings to configure the client. + * @return A Promise that resolves with the new Firestore client. */ export function createInstance( apiOverrides?: ApiOverride, firestoreSettings?: {} ): Promise { - const initializationOptions = Object.assign( - { - projectId: PROJECT_ID, - sslCreds: SSL_CREDENTIALS, - keyFilename: __dirname + '/../fake-certificate.json', - }, - firestoreSettings - ); + const initializationOptions = { + ...{projectId: PROJECT_ID, sslCreds: SSL_CREDENTIALS}, + ...firestoreSettings, + }; const firestore = new Firestore(); firestore.settings(initializationOptions); - const clientPool = new ClientPool( + firestore['_clientPool'] = new ClientPool( /* concurrentRequestLimit= */ 1, /* maxIdleClients= */ 0, - () => { - const gapicClient: GapicClient = new v1(initializationOptions); - if (apiOverrides) { - Object.keys(apiOverrides).forEach(override => { - const apiOverride = (apiOverrides as {[k: string]: unknown})[ - override - ]; - if (override !== 'getProjectId') { - gapicClient._innerApiCalls[override] = apiOverride; - } else { - gapicClient[override] = apiOverride; - } - }); - } - return gapicClient; - } + () => ({...new v1(initializationOptions), ...apiOverrides}) ); - firestore['_clientPool'] = clientPool; - return Promise.resolve(firestore); } @@ -357,7 +299,12 @@ export function writeResult(count: number): api.IWriteResponse { return response; } -export function requestEquals(actual: object, expected: object): void { +export function requestEquals( + actual: object | undefined, + expected: object +): void { + expect(actual).to.not.be.undefined; + // 'extend' removes undefined fields in the request object. The backend // ignores these fields, but we need to manually strip them before we compare // the expected and the actual request. @@ -366,9 +313,7 @@ export function requestEquals(actual: object, expected: object): void { expect(actual).to.deep.eq(proto); } -export function stream( - ...elements: Array -): NodeJS.ReadableStream { +export function stream(...elements: Array): Duplex { const stream = through2.obj(); setImmediate(() => { @@ -384,3 +329,8 @@ export function stream( return stream; } + +/** Creates a response as formatted by the GAPIC request methods. */ +export function response(result: T): Promise<[T, unknown, unknown]> { + return Promise.resolve([result, undefined, undefined]); +} diff --git a/dev/test/watch.ts b/dev/test/watch.ts index 5975ee666..efdd3f6c9 100644 --- a/dev/test/watch.ts +++ b/dev/test/watch.ts @@ -17,7 +17,7 @@ const duplexify = require('duplexify'); import {expect} from 'chai'; import * as extend from 'extend'; import {GoogleError, Status} from 'google-gax'; -import {Transform} from 'stream'; +import {Duplex, Transform} from 'stream'; import * as through2 from 'through2'; import {google} from '../protos/firestore_v1_proto_api'; @@ -244,14 +244,14 @@ class StreamHelper { private readonly deferredListener = new DeferredListener< api.IListenRequest >(); - private backendStream: NodeJS.ReadWriteStream | null = null; + private backendStream: Duplex | null = null; streamCount = 0; // The number of streams that the client has requested readStream: Transform | null = null; writeStream: Transform | null = null; /** Returns the GAPIC callback to use with this stream helper. */ - getListenCallback(): () => NodeJS.ReadWriteStream { + getListenCallback(): () => Duplex { return () => { // Create a mock backend whose stream we can return. ++this.streamCount; @@ -676,7 +676,7 @@ describe('Query watch', () => { }; /** The GAPIC callback that executes the listen. */ - let listenCallback: () => NodeJS.ReadWriteStream; + let listenCallback: () => Duplex; beforeEach(() => { // We are intentionally skipping the delays to ensure fast test execution. diff --git a/dev/test/write-batch.ts b/dev/test/write-batch.ts index ef71e68f3..1b125347f 100644 --- a/dev/test/write-batch.ts +++ b/dev/test/write-batch.ts @@ -26,6 +26,7 @@ import { ApiOverride, createInstance, InvalidApiUsage, + response, verifyInstance, } from './util/helpers'; @@ -187,7 +188,7 @@ describe('batch support', () => { beforeEach(() => { const overrides: ApiOverride = { - commit: (request, options, callback) => { + commit: request => { expect(request).to.deep.eq({ database: `projects/${PROJECT_ID}/databases/(default)`, writes: [ @@ -238,7 +239,7 @@ describe('batch support', () => { }, ], }); - callback(null, { + return response({ commitTime: { nanos: 0, seconds: 0, @@ -368,8 +369,8 @@ describe('batch support', () => { it('can return same write result', () => { const overrides: ApiOverride = { - commit: (request, options, callback) => { - callback(null, { + commit: request => { + return response({ commitTime: { nanos: 0, seconds: 0, @@ -411,13 +412,13 @@ describe('batch support', () => { let commitCalled = 0; const overrides: ApiOverride = { - beginTransaction: (actual, options, callback) => { + beginTransaction: () => { ++beginCalled; - callback(null, {transaction: Buffer.from('foo')}); + return response({transaction: Buffer.from('foo')}); }, - commit: (request, options, callback) => { + commit: () => { ++commitCalled; - callback(null, { + return response({ commitTime: { nanos: 0, seconds: 0, diff --git a/package.json b/package.json index efc96eead..4825af8a8 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,8 @@ "test": "npm run test-only && npm run conformance", "lint": "gts check", "clean": "gts clean", - "compile": "tsc -p . && cp -r dev/protos build && cp -r dev/test/fake-certificate.json build/test/fake-certificate.json && cp dev/src/v1beta1/*.json build/src/v1beta1/ && cp dev/src/v1/*.json build/src/v1/ && cp dev/conformance/test-definition.proto build/conformance && cp dev/conformance/test-suite.binproto build/conformance", + "compile": "tsc -p .", + "postcompile": "cp -r dev/protos build && cp dev/src/v1beta1/*.json build/src/v1beta1/ && cp dev/src/v1/*.json build/src/v1/ && cp dev/conformance/test-definition.proto build/conformance && cp dev/conformance/test-suite.binproto build/conformance", "fix": "gts fix", "prepare": "npm run compile", "docs-test": "linkinator docs",