@firebaseextensions/firestore-bigquery-change-tracker 1.1.31 → 1.1.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,12 +1,13 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const firebase = require("firebase-admin");
4
+ const firestore_1 = require("firebase-admin/firestore");
4
5
  if (!firebase.apps.length) {
5
6
  firebase.initializeApp();
6
7
  firebase.firestore().settings({ ignoreUndefinedProperties: true });
7
8
  }
8
9
  exports.default = async (rows, config, e) => {
9
- const db = firebase.firestore();
10
+ const db = (0, firestore_1.getFirestore)();
10
11
  const batchArray = [db.batch()];
11
12
  let operationCounter = 0;
12
13
  let batchIndex = 0;
@@ -13,6 +13,7 @@ const logs = require("../logs");
13
13
  const partitioning_1 = require("./partitioning");
14
14
  const clustering_1 = require("./clustering");
15
15
  const checkUpdates_1 = require("./checkUpdates");
16
+ const utils_1 = require("./utils");
16
17
  var schema_2 = require("./schema");
17
18
  Object.defineProperty(exports, "RawChangelogSchema", { enumerable: true, get: function () { return schema_2.RawChangelogSchema; } });
18
19
  Object.defineProperty(exports, "RawChangelogViewSchema", { enumerable: true, get: function () { return schema_2.RawChangelogViewSchema; } });
@@ -129,26 +130,10 @@ class FirestoreBigQueryEventHistoryTracker {
129
130
  * A half a second delay is added per check while the function
130
131
  * continually re-checks until the referenced dataset and table become available.
131
132
  */
132
- async waitForInitialization() {
133
- return new Promise((resolve) => {
134
- let handle = setInterval(async () => {
135
- try {
136
- const dataset = this.bigqueryDataset();
137
- const changelogName = this.rawChangeLogTableName();
138
- const table = dataset.table(changelogName);
139
- const [datasetExists] = await dataset.exists();
140
- const [tableExists] = await table.exists();
141
- if (datasetExists && tableExists) {
142
- clearInterval(handle);
143
- return resolve(table);
144
- }
145
- }
146
- catch (ex) {
147
- clearInterval(handle);
148
- logs.failedToInitializeWait(ex.message);
149
- }
150
- }, 5000);
151
- });
133
+ async _waitForInitialization() {
134
+ const dataset = this.bigqueryDataset();
135
+ const changelogName = this.rawChangeLogTableName();
136
+ return (0, utils_1.waitForInitialization)({ dataset, changelogName });
152
137
  }
153
138
  /**
154
139
  * Inserts rows of data into the BigQuery raw change log table.
@@ -192,14 +177,34 @@ class FirestoreBigQueryEventHistoryTracker {
192
177
  if (this._initialized) {
193
178
  return;
194
179
  }
195
- await this.initializeDataset();
196
- await this.initializeRawChangeLogTable();
197
- await this.initializeLatestView();
180
+ try {
181
+ await this.initializeDataset();
182
+ }
183
+ catch (error) {
184
+ const message = (0, utils_1.parseErrorMessage)(error, "initializing dataset");
185
+ throw new Error(`Error initializing dataset: ${message}`);
186
+ }
187
+ try {
188
+ await this.initializeRawChangeLogTable();
189
+ }
190
+ catch (error) {
191
+ const message = (0, utils_1.parseErrorMessage)(error, "initializing raw change log table");
192
+ throw new Error(`Error initializing raw change log table: ${message}`);
193
+ }
194
+ try {
195
+ await this.initializeLatestView();
196
+ }
197
+ catch (error) {
198
+ const message = (0, utils_1.parseErrorMessage)(error, "initializing latest view");
199
+ throw new Error(`Error initializing latest view: ${message}`);
200
+ }
201
+ await this._waitForInitialization();
198
202
  this._initialized = true;
199
203
  }
200
- catch (ex) {
201
- await this.waitForInitialization();
202
- this._initialized = true;
204
+ catch (error) {
205
+ const message = (0, utils_1.parseErrorMessage)(error, "initializing BigQuery resources");
206
+ console.error("Error initializing BigQuery resources: ", message);
207
+ throw error;
203
208
  }
204
209
  }
205
210
  /**
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Partitioning = void 0;
4
4
  const firebase = require("firebase-admin");
5
5
  const logs = require("../logs");
6
+ const functions = require("firebase-functions");
6
7
  const schema_1 = require("./schema");
7
8
  const bigquery_1 = require("@google-cloud/bigquery");
8
9
  const types_1 = require("../types");
@@ -87,41 +88,36 @@ class Partitioning {
87
88
  return !!metadata.schema;
88
89
  }
89
90
  hasValidTableReference() {
90
- logs.invalidTableReference();
91
+ if (!this.table) {
92
+ logs.invalidTableReference();
93
+ }
91
94
  return !!this.table;
92
95
  }
93
96
  async isTablePartitioned() {
94
- if (!this.table)
95
- return Promise.resolve(false);
96
- // No table provided, cannot evaluate
97
- if (this.table.exists()) {
98
- logs.cannotPartitionExistingTable(this.table);
99
- return Promise.resolve(false);
100
- }
101
- /*** No table exists, return */
102
97
  const [tableExists] = await this.table.exists();
103
- if (!tableExists)
104
- return Promise.resolve(false);
105
- /* Check if partition metadata already exists */
98
+ if (!this.table || !tableExists)
99
+ return false;
100
+ /* Return true if partition metadata already exists */
106
101
  const [metadata] = await this.table.getMetadata();
107
- if (!!metadata.timePartitioning)
108
- return Promise.resolve(true);
102
+ if (metadata.timePartitioning) {
103
+ logs.cannotPartitionExistingTable(this.table);
104
+ return true;
105
+ }
109
106
  /** Find schema fields **/
110
107
  const schemaFields = await this.metaDataSchemaFields();
111
- /** No Schema exists, return */
108
+ /** Return false if no schema exists */
112
109
  if (!schemaFields)
113
- return Promise.resolve(false);
110
+ return false;
114
111
  /* Return false if time partition field not found */
115
112
  return schemaFields.some((column) => column.name === this.config.timePartitioningField);
116
113
  }
117
114
  async isValidPartitionForExistingTable() {
115
+ /** Return false if partition type option has not been set */
116
+ if (!this.isPartitioningEnabled())
117
+ return false;
118
+ /* Return false if table is already partitioned */
118
119
  const isPartitioned = await this.isTablePartitioned();
119
120
  if (isPartitioned)
120
- return Promise.resolve(false);
121
- return this.hasValidCustomPartitionConfig();
122
- }
123
- isValidPartitionForNewTable() {
124
- if (!this.isPartitioningEnabled())
125
121
  return false;
126
122
  return this.hasValidCustomPartitionConfig();
127
123
  }
@@ -181,57 +177,71 @@ class Partitioning {
181
177
  const { timePartitioningField } = this.config;
182
178
  return fields.map(($) => $.name).includes(timePartitioningField);
183
179
  }
180
+ async shouldAddPartitioningToSchema() {
181
+ if (!this.isPartitioningEnabled()) {
182
+ return { proceed: false, message: "Partitioning not enabled" };
183
+ }
184
+ if (!this.hasValidTableReference()) {
185
+ return { proceed: false, message: "Invalid table reference" };
186
+ }
187
+ if (!this.hasValidCustomPartitionConfig()) {
188
+ return { proceed: false, message: "Invalid partition config" };
189
+ }
190
+ if (!this.hasValidTimePartitionType()) {
191
+ return { proceed: false, message: "Invalid partition type" };
192
+ }
193
+ if (!this.hasValidTimePartitionOption()) {
194
+ return { proceed: false, message: "Invalid partition option" };
195
+ }
196
+ if (this.hasHourAndDatePartitionConfig()) {
197
+ return {
198
+ proceed: false,
199
+ message: "Invalid partitioning and field type combination",
200
+ };
201
+ }
202
+ if (this.customFieldExists()) {
203
+ return { proceed: false, message: "Field already exists on schema" };
204
+ }
205
+ if (await this.isTablePartitioned()) {
206
+ return { proceed: false, message: "Table is already partitioned" };
207
+ }
208
+ if (!this.config.timePartitioningField) {
209
+ return { proceed: false, message: "Partition field not provided" };
210
+ }
211
+ return { proceed: true, message: "" };
212
+ }
184
213
  async addPartitioningToSchema(fields = []) {
185
- /** check if class has valid table reference */
186
- if (!this.hasValidTableReference())
187
- return;
188
- /** return if table is already partitioned **/
189
- if (await this.isTablePartitioned())
190
- return;
191
- /** return if an invalid partition type has been requested**/
192
- if (!this.hasValidTimePartitionType())
193
- return;
194
- /** Return if invalid partitioning and field type combination */
195
- if (this.hasHourAndDatePartitionConfig())
196
- return;
197
- /** return if an invalid partition type has been requested**/
198
- if (!this.hasValidCustomPartitionConfig())
199
- return;
200
- /** return if an invalid partition type has been requested**/
201
- if (!this.hasValidCustomPartitionConfig())
202
- return;
203
- /** update fields with new schema option ** */
204
- if (!this.hasValidTimePartitionOption())
205
- return;
206
- /* Check if partition field has been provided */
207
- if (!this.config.timePartitioningField)
208
- return;
209
- // if (await !this.hasExistingSchema) return Promise.resolve();
210
- // Field already exists on schema, skip
211
- if (this.customFieldExists(fields))
214
+ const { proceed, message } = await this.shouldAddPartitioningToSchema();
215
+ if (!proceed) {
216
+ functions.logger.warn(`Did not add partitioning to schema: ${message}`);
212
217
  return;
218
+ }
219
+ // Add new partitioning field
213
220
  fields.push((0, schema_1.getNewPartitionField)(this.config));
214
- /** log successful addition of partition column */
215
- logs.addPartitionFieldColumn(this.table.id, this.config.timePartitioningField);
216
- return;
221
+ functions.logger.log(`Added new partition field: ${this.config.timePartitioningField} to table ID: ${this.table.id}`);
217
222
  }
218
223
  async updateTableMetadata(options) {
219
- /** return if table is already partitioned **/
224
+ /** Return if partition type option has not been set */
225
+ if (!this.isPartitioningEnabled())
226
+ return;
227
+ /** Return if class has invalid table reference */
228
+ if (!this.hasValidTableReference())
229
+ return;
230
+ /** Return if table is already partitioned **/
220
231
  if (await this.isTablePartitioned())
221
232
  return;
222
- /** return if an invalid partition type has been requested**/
233
+ /** Return if an invalid partition type has been requested**/
234
+ if (!this.hasValidCustomPartitionConfig())
235
+ return;
236
+ /** Return if an invalid partition type has been requested**/
223
237
  if (!this.hasValidTimePartitionType())
224
238
  return;
225
- /** update fields with new schema option ** */
239
+ /** Update fields with new schema option ** */
226
240
  if (!this.hasValidTimePartitionOption())
227
241
  return;
228
242
  /** Return if invalid partitioning and field type combination */
229
243
  if (this.hasHourAndDatePartitionConfig())
230
244
  return;
231
- /** return if an invalid partition type has been requested**/
232
- if (!this.hasValidCustomPartitionConfig())
233
- return;
234
- // if (await !this.hasExistingSchema) return Promise.resolve();
235
245
  if (this.config.timePartitioning) {
236
246
  options.timePartitioning = { type: this.config.timePartitioning };
237
247
  }
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseErrorMessage = exports.waitForInitialization = void 0;
4
+ const logs = require("../logs");
5
+ /**
6
+ * Periodically checks for the existence of a dataset and table until both are found or a maximum number of attempts is reached.
7
+ * @param {WaitForInitializationParams} params - Parameters for initialization including the dataset and the table name to check.
8
+ * @param {number} maxAttempts - Maximum number of attempts before giving up (defaults to 12).
9
+ * @returns {Promise<Table>} A promise that resolves with the Table if it exists, or rejects if it doesn't exist after maxAttempts or an error occurs.
10
+ * @throws {Error} Throws an error if the dataset or table cannot be verified to exist after multiple attempts or if an unexpected error occurs.
11
+ */
12
+ async function waitForInitialization({ dataset, changelogName }, maxAttempts = 12) {
13
+ return new Promise((resolve, reject) => {
14
+ let attempts = 0;
15
+ let handle = setInterval(async () => {
16
+ try {
17
+ const [datasetExists] = await dataset.exists();
18
+ const table = dataset.table(changelogName);
19
+ const [tableExists] = await table.exists();
20
+ if (datasetExists && tableExists) {
21
+ clearInterval(handle);
22
+ resolve(table);
23
+ }
24
+ else {
25
+ attempts++;
26
+ if (attempts >= maxAttempts) {
27
+ clearInterval(handle);
28
+ reject(new Error("Initialization timed out. Dataset or table could not be verified to exist after multiple attempts."));
29
+ }
30
+ }
31
+ }
32
+ catch (error) {
33
+ clearInterval(handle);
34
+ const message = error instanceof Error
35
+ ? error.message
36
+ : "An unexpected error occurred";
37
+ logs.failedToInitializeWait(message);
38
+ reject(new Error(message));
39
+ }
40
+ }, 500);
41
+ });
42
+ }
43
+ exports.waitForInitialization = waitForInitialization;
44
+ function parseErrorMessage(error, process = "") {
45
+ let message;
46
+ if (error instanceof Error) {
47
+ // Standard error handling
48
+ message = error.message;
49
+ }
50
+ else if (typeof error === "object" &&
51
+ error !== null &&
52
+ "message" in error) {
53
+ // Handling errors from APIs or other sources that are not native Error objects
54
+ message = error.message;
55
+ }
56
+ else {
57
+ // Fallback for when error is neither an Error object nor an expected structured object
58
+ message = `An unexpected error occurred${process ? ` during ${process}` : ""}.`;
59
+ }
60
+ return message;
61
+ }
62
+ exports.parseErrorMessage = parseErrorMessage;
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const utils_1 = require("./utils");
4
+ const logs = require("../logs");
5
+ jest.mock("@google-cloud/bigquery");
6
+ jest.mock("../../logs");
7
+ const dataset = {
8
+ exists: jest.fn(),
9
+ table: jest.fn(),
10
+ };
11
+ const table = {
12
+ exists: jest.fn(),
13
+ };
14
+ const changelogName = "testTable";
15
+ describe("waitForInitialization", () => {
16
+ beforeEach(() => {
17
+ jest.clearAllMocks();
18
+ dataset.table.mockReturnValue(table);
19
+ });
20
+ test("should successfully find the dataset and table", async () => {
21
+ dataset.exists.mockResolvedValue([true]);
22
+ table.exists.mockResolvedValue([true]);
23
+ const result = await (0, utils_1.waitForInitialization)({
24
+ dataset: dataset,
25
+ changelogName,
26
+ });
27
+ expect(result).toBe(table);
28
+ expect(dataset.exists).toHaveBeenCalledTimes(1);
29
+ expect(table.exists).toHaveBeenCalledTimes(1);
30
+ });
31
+ test("should fail after max attempts if table does not exist", async () => {
32
+ dataset.exists.mockResolvedValue([true]);
33
+ table.exists.mockResolvedValue([false]);
34
+ await expect((0, utils_1.waitForInitialization)({ dataset: dataset, changelogName }, 3)).rejects.toThrow("Initialization timed out. Dataset or table could not be verified to exist after multiple attempts.");
35
+ expect(dataset.exists).toHaveBeenCalledTimes(3);
36
+ expect(table.exists).toHaveBeenCalledTimes(3);
37
+ });
38
+ test("should handle and throw an error if dataset.exists throws", async () => {
39
+ const error = new Error("Access denied");
40
+ dataset.exists.mockRejectedValue(error);
41
+ await expect((0, utils_1.waitForInitialization)({
42
+ dataset: dataset,
43
+ changelogName,
44
+ })).rejects.toThrow("Access denied");
45
+ expect(logs.failedToInitializeWait).toHaveBeenCalledWith(error.message);
46
+ });
47
+ test("should handle and throw an error if table.exists throws", async () => {
48
+ dataset.exists.mockResolvedValue([true]);
49
+ const error = new Error("Table error");
50
+ table.exists.mockRejectedValue(error);
51
+ await expect((0, utils_1.waitForInitialization)({
52
+ dataset: dataset,
53
+ changelogName,
54
+ })).rejects.toThrow("Table error");
55
+ expect(logs.failedToInitializeWait).toHaveBeenCalledWith(error.message);
56
+ });
57
+ test("should handle unexpected error types gracefully", async () => {
58
+ dataset.exists.mockRejectedValue("String error");
59
+ await expect((0, utils_1.waitForInitialization)({
60
+ dataset: dataset,
61
+ changelogName,
62
+ })).rejects.toThrow("An unexpected error occurred");
63
+ expect(logs.failedToInitializeWait).toHaveBeenCalledWith("An unexpected error occurred");
64
+ });
65
+ });
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "url": "github.com/firebase/extensions.git",
6
6
  "directory": "firestore-bigquery-export/firestore-bigquery-change-tracker"
7
7
  },
8
- "version": "1.1.31",
8
+ "version": "1.1.33",
9
9
  "description": "Core change-tracker library for Cloud Firestore Collection BigQuery Exports",
10
10
  "main": "./lib/index.js",
11
11
  "scripts": {
@@ -23,10 +23,10 @@
23
23
  "author": "Jan Wyszynski <wyszynski@google.com>",
24
24
  "license": "Apache-2.0",
25
25
  "dependencies": {
26
- "@google-cloud/bigquery": "^4.7.0",
27
- "@google-cloud/resource-manager": "^3.0.0",
28
- "firebase-admin": "^11.4.1",
29
- "firebase-functions": "^3.13.2",
26
+ "@google-cloud/bigquery": "^7.6.0",
27
+ "@google-cloud/resource-manager": "^5.1.0",
28
+ "firebase-admin": "^12.0.0",
29
+ "firebase-functions": "^4.9.0",
30
30
  "generate-schema": "^2.6.0",
31
31
  "inquirer": "^6.4.0",
32
32
  "lodash": "^4.17.14",
@@ -36,15 +36,17 @@
36
36
  },
37
37
  "devDependencies": {
38
38
  "@types/chai": "^4.1.6",
39
- "@types/jest": "^24.0.18",
39
+ "@types/jest": "29.5.0",
40
40
  "@types/node": "14.18.34",
41
41
  "@types/traverse": "^0.6.32",
42
42
  "chai": "^4.2.0",
43
- "jest": "^24.9.0",
44
43
  "nyc": "^14.0.0",
45
44
  "rimraf": "^2.6.3",
46
- "ts-jest": "^24.1.0",
47
- "ts-node": "^7.0.1",
48
- "typescript": "^4.9.4"
45
+ "typescript": "^4.9.4",
46
+ "jest": "29.5.0",
47
+ "jest-environment-node": "29.5.0",
48
+ "mocked-env": "^1.3.2",
49
+ "ts-jest": "29.1.2",
50
+ "jest-config": "29.5.0"
49
51
  }
50
52
  }