From 020aeeec32fd0887a3345150326a3f2a0a6d7edd Mon Sep 17 00:00:00 2001 From: Dominik Radziszowski Date: Tue, 26 Sep 2023 09:25:48 +0200 Subject: [PATCH 1/2] Support for multiple databases in one GCP project --- Auth.ts | 2 +- Firestore.ts | 39 ++++++++++++++++++++++++++++----------- Request.ts | 33 +++++++++++++++++++++++++++++++-- Tests.ts | 8 ++++---- 4 files changed, 64 insertions(+), 18 deletions(-) diff --git a/Auth.ts b/Auth.ts index cc8fc52..dae65fe 100644 --- a/Auth.ts +++ b/Auth.ts @@ -31,7 +31,7 @@ class Auth { * @returns {string} The generated access token string */ get accessToken(): string { - const request = new Request(this.authUrl, '', this.options_).post(); + const request = Request.authRequest(this.authUrl, this.options_).post(); return request.access_token; } diff --git a/Firestore.ts b/Firestore.ts index 7fcae10..f82f00b 100644 --- a/Firestore.ts +++ b/Firestore.ts @@ -7,6 +7,8 @@ class Firestore implements FirestoreRead, FirestoreWrite, FirestoreDelete { auth: Auth; basePath: string; baseUrl: string; + projectId: string; + databaseName: string; /** * Constructor @@ -15,12 +17,15 @@ class Firestore implements FirestoreRead, FirestoreWrite, FirestoreDelete { * @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") + * @param {string} databaseName [Optional] database name * @return {Firestore} an authenticated interface with a Firestore project (constructor) */ - constructor(email: string, key: string, projectId: string, apiVersion: Version = 'v1') { + constructor(email: string, key: string, projectId: string, apiVersion: Version = 'v1', databaseName = '(default)') { // The authentication token used for accessing Firestore this.auth = new Auth(email, key); - this.basePath = `projects/${projectId}/databases/(default)/documents/`; + this.projectId = projectId; + this.databaseName = databaseName; + this.basePath = `projects/${projectId}/databases/${databaseName}/documents/`; this.baseUrl = `https://firestore.googleapis.com/${apiVersion}/${this.basePath}`; } @@ -38,7 +43,7 @@ class Firestore implements FirestoreRead, FirestoreWrite, FirestoreDelete { * @return {object} the document object */ getDocument(path: string): Document { - const request = new Request(this.baseUrl, this.authToken); + const request = Request.dbRequest(this.baseUrl, this.authToken, this.projectId, this.databaseName); return this.getDocument_(path, request); } getDocument_ = FirestoreRead.prototype.getDocument_; @@ -55,7 +60,12 @@ class Firestore implements FirestoreRead, FirestoreWrite, FirestoreDelete { if (!ids) { docs = this.query(path).Execute() as Document[]; } else { - const request = new Request(this.baseUrl.replace('/documents/', '/documents:batchGet/'), this.authToken); + const request = Request.dbRequest( + this.baseUrl.replace('/documents/', '/documents:batchGet/'), + this.authToken, + this.projectId, + this.databaseName + ); docs = this.getDocuments_(this.basePath + path, request, ids); } return docs; @@ -69,7 +79,7 @@ class Firestore implements FirestoreRead, FirestoreWrite, FirestoreDelete { * @return {object} an array of IDs of the documents in the collection */ getDocumentIds(path: string): string[] { - const request = new Request(this.baseUrl, this.authToken); + const request = Request.dbRequest(this.baseUrl, this.authToken, this.projectId, this.databaseName); return this.getDocumentIds_(path, request); } getDocumentIds_ = FirestoreRead.prototype.getDocumentIds_; @@ -82,7 +92,7 @@ class Firestore implements FirestoreRead, FirestoreWrite, FirestoreDelete { * @return {object} the Document object written to Firestore */ createDocument(path: string, fields?: Record): Document { - const request = new Request(this.baseUrl, this.authToken); + const request = Request.dbRequest(this.baseUrl, this.authToken, this.projectId, this.databaseName); return this.createDocument_(path, fields || {}, request); } @@ -99,7 +109,7 @@ class Firestore implements FirestoreRead, FirestoreWrite, FirestoreDelete { * @return {object} the Document object written to Firestore */ updateDocument(path: string, fields: Record, mask?: boolean | string[]): Document { - const request = new Request(this.baseUrl, this.authToken); + const request = Request.dbRequest(this.baseUrl, this.authToken, this.projectId, this.databaseName); return this.updateDocument_(path, fields, request, mask); } @@ -113,7 +123,7 @@ class Firestore implements FirestoreRead, FirestoreWrite, FirestoreDelete { * @return {object} the JSON response from the DELETE request */ deleteDocument(path: string): FirestoreAPI.Empty { - const request = new Request(this.baseUrl, this.authToken); + const request = Request.dbRequest(this.baseUrl, this.authToken, this.projectId, this.databaseName); return this.deleteDocument_(path, request); } @@ -127,7 +137,7 @@ class Firestore implements FirestoreRead, FirestoreWrite, FirestoreDelete { * @return {object} the JSON response from the GET request */ query(path: string): Query { - const request = new Request(this.baseUrl, this.authToken); + const request = Request.dbRequest(this.baseUrl, this.authToken, this.projectId, this.databaseName); return this.query_(path, request); } query_ = FirestoreRead.prototype.query_; @@ -142,8 +152,15 @@ type Version = 'v1' | 'v1beta1' | 'v1beta2'; * @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") + * @param {string} databaseName [Optional] database name * @return {Firestore} an authenticated interface with a Firestore project (function) */ -function getFirestore(email: string, key: string, projectId: string, apiVersion: Version = 'v1'): Firestore { - return new Firestore(email, key, projectId, apiVersion); +function getFirestore( + email: string, + key: string, + projectId: string, + apiVersion: Version = 'v1', + databaseName = '(default)' +): Firestore { + return new Firestore(email, key, projectId, apiVersion, databaseName); } diff --git a/Request.ts b/Request.ts index 5eb1887..449b6e8 100644 --- a/Request.ts +++ b/Request.ts @@ -6,25 +6,54 @@ class Request { url: string; authToken: string; queryString: string; + projectId: string; + databaseName: string; options: RequestOptions; nextPageToken?: string | null; documents?: any[]; fields?: Record; + /** + * @param url the base url to utilize + * @param authToken authorization token to make requests + * @param projectId id od the GCP project + * @param databaseName naem otf the databaseName + */ + static dbRequest(url: string, authToken: string, projectId: string, databaseName = '(default)') { + return new Request(url, authToken, projectId, databaseName); + } + + /** + * @param url the base url to utilize + * @param options set of options to utilize over the default headers + */ + static authRequest(url: string, options: RequestOptions) { + return new Request(url, '', '', '', options); + } + /** * @param url the base url to utilize * @param authToken authorization token to make requests * @param options [Optional] set of options to utilize over the default headers */ - constructor(url: string, authToken?: string, options?: RequestOptions) { + constructor( + url: string, + authToken?: string, + projectId?: string, + databaseName = '(default)', + options?: RequestOptions + ) { this.url = url; this.queryString = ''; this.authToken = authToken || ''; + this.projectId = projectId || ''; + this.databaseName = databaseName || '(default)'; if (!this.authToken) options = options || {}; // Set default header options if none are passed in this.options = options || { headers: { + 'x-goog-request-params': `project_id=${projectId}&database_id=${databaseName}`, 'content-type': 'application/json', 'Authorization': 'Bearer ' + this.authToken, }, @@ -139,7 +168,7 @@ class Request { * @return {Request} A copy of this object */ clone(): Request { - return new Request(this.url, this.authToken, this.options); + return new Request(this.url, this.authToken, this.projectId, this.databaseName, this.options); } /** diff --git a/Tests.ts b/Tests.ts index d30aa58..7bea27b 100644 --- a/Tests.ts +++ b/Tests.ts @@ -30,7 +30,7 @@ class Tests implements TestManager { this.pass.push('Test_Get_Firestore'); } catch (e) { // On failure, fail the remaining tests without execution - this.fail.set('Test_Get_Firestore', e); + this.fail.set('Test_Get_Firestore', e); const err = new Error('Test Initialization Error'); err.stack = 'See Test_Get_Firestore Error'; for (const func of funcs) { @@ -73,7 +73,7 @@ class Tests implements TestManager { // eslint-disable-next-line no-ex-assign e = err; } - this.fail.set(func, e); + this.fail.set(func, e); } } } @@ -112,7 +112,7 @@ class Tests implements TestManager { this.db.createDocument(path); GSUnit.fail('Duplicate document without error'); } catch (e) { - if (e.message !== `Document already exists: ${this.db.basePath}${path}`) { + if ((e).message !== `Document already exists: ${this.db.basePath}${path}`) { throw e; } } @@ -219,7 +219,7 @@ class Tests implements TestManager { this.db.getDocument(path); GSUnit.fail('Missing document without error'); } catch (e) { - if (e.message !== `Document "${this.db.basePath}${path}" not found.`) { + if ((e).message !== `Document "${this.db.basePath}${path}" not found.`) { throw e; } } From 9d7e7c4e75ad31b379d5f615052c69664e6426f2 Mon Sep 17 00:00:00 2001 From: radzisz Date: Tue, 26 Sep 2023 09:34:04 +0200 Subject: [PATCH 2/2] Update README.md --- .github/README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/README.md b/.github/README.md index 41c65ed..e394e73 100644 --- a/.github/README.md +++ b/.github/README.md @@ -162,6 +162,14 @@ const documents2_3_4_5 = firestore.query("FirstCollection").Limit(4).Offset(2).E const documents3_4_5_6 = firestore.query("FirstCollection").Range(3, 7).Execute(); ``` +#### Support for multiple databases in one GCP project +By default, GCP crates only one "(default)" database; however, it is possible to have multiple databases under different names in one GCP project. +You can access a non-default database using its name while initializing the library. + +```javascript +const firestore = FirestoreApp.getFirestore(email, key, projected, "v1", yourDatabaseName); +``` + See other library methods and details [in the wiki](../../../wiki). ### Frequently Asked Questions