@firebaseextensions/firestore-bigquery-change-tracker 1.1.32 → 1.1.34

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.
@@ -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");
@@ -93,28 +94,31 @@ class Partitioning {
93
94
  return !!this.table;
94
95
  }
95
96
  async isTablePartitioned() {
97
+ const [tableExists] = await this.table.exists();
98
+ if (!this.table || !tableExists)
99
+ return false;
96
100
  /* Return true if partition metadata already exists */
97
101
  const [metadata] = await this.table.getMetadata();
98
- if (!!metadata.timePartitioning) {
102
+ if (metadata.timePartitioning) {
99
103
  logs.cannotPartitionExistingTable(this.table);
100
- return Promise.resolve(true);
104
+ return true;
101
105
  }
102
106
  /** Find schema fields **/
103
107
  const schemaFields = await this.metaDataSchemaFields();
104
108
  /** Return false if no schema exists */
105
109
  if (!schemaFields)
106
- return Promise.resolve(false);
110
+ return false;
107
111
  /* Return false if time partition field not found */
108
112
  return schemaFields.some((column) => column.name === this.config.timePartitioningField);
109
113
  }
110
114
  async isValidPartitionForExistingTable() {
111
115
  /** Return false if partition type option has not been set */
112
116
  if (!this.isPartitioningEnabled())
113
- return Promise.resolve(false);
117
+ return false;
114
118
  /* Return false if table is already partitioned */
115
119
  const isPartitioned = await this.isTablePartitioned();
116
120
  if (isPartitioned)
117
- return Promise.resolve(false);
121
+ return false;
118
122
  return this.hasValidCustomPartitionConfig();
119
123
  }
120
124
  convertDateValue(fieldValue) {
@@ -168,44 +172,53 @@ class Partitioning {
168
172
  return {};
169
173
  }
170
174
  customFieldExists(fields = []) {
171
- if (!fields.length)
172
- return false;
175
+ /** Extract the time partioning field name */
173
176
  const { timePartitioningField } = this.config;
177
+ /** Return based the field already exist */
174
178
  return fields.map(($) => $.name).includes(timePartitioningField);
175
179
  }
180
+ async shouldAddPartitioningToSchema(fields) {
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(fields)) {
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
+ }
176
213
  async addPartitioningToSchema(fields = []) {
177
- /** Return if partition type option has not been set */
178
- if (!this.isPartitioningEnabled())
214
+ const { proceed, message } = await this.shouldAddPartitioningToSchema(fields);
215
+ if (!proceed) {
216
+ functions.logger.warn(`Did not add partitioning to schema: ${message}`);
179
217
  return;
180
- /** Return if class has invalid table reference */
181
- if (!this.hasValidTableReference())
182
- return;
183
- /** Return if table is already partitioned **/
184
- if (await this.isTablePartitioned())
185
- return;
186
- /** Return if partition config is invalid */
187
- if (!this.hasValidCustomPartitionConfig())
188
- return;
189
- /** Return if an invalid partition type has been requested */
190
- if (!this.hasValidTimePartitionType())
191
- return;
192
- /** Return if an invalid partition option has been requested */
193
- if (!this.hasValidTimePartitionOption())
194
- return;
195
- /** Return if invalid partitioning and field type combination */
196
- if (this.hasHourAndDatePartitionConfig())
197
- return;
198
- /** Return if partition field has not been provided */
199
- if (!this.config.timePartitioningField)
200
- return;
201
- /** Return if field already exists on schema */
202
- if (this.customFieldExists(fields))
203
- return;
204
- /** Add new partitioning field **/
218
+ }
219
+ // Add new partitioning field
205
220
  fields.push((0, schema_1.getNewPartitionField)(this.config));
206
- /** Log successful addition of partition column */
207
- logs.addPartitionFieldColumn(this.table.id, this.config.timePartitioningField);
208
- return;
221
+ functions.logger.log(`Added new partition field: ${this.config.timePartitioningField} to table ID: ${this.table.id}`);
209
222
  }
210
223
  async updateTableMetadata(options) {
211
224
  /** Return if partition type option has not been set */
@@ -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;
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.32",
8
+ "version": "1.1.34",
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.11.1",
29
- "firebase-functions": "^4.6.0",
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
  }