@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.
- package/lib/bigquery/index.js +31 -26
- package/lib/bigquery/partitioning.js +50 -37
- package/lib/bigquery/utils.js +62 -0
- package/package.json +12 -10
package/lib/bigquery/index.js
CHANGED
|
@@ -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
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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 (
|
|
201
|
-
|
|
202
|
-
|
|
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 (
|
|
102
|
+
if (metadata.timePartitioning) {
|
|
99
103
|
logs.cannotPartitionExistingTable(this.table);
|
|
100
|
-
return
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
178
|
-
if (!
|
|
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
|
-
|
|
181
|
-
|
|
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
|
-
|
|
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.
|
|
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": "^
|
|
27
|
-
"@google-cloud/resource-manager": "^
|
|
28
|
-
"firebase-admin": "^
|
|
29
|
-
"firebase-functions": "^4.
|
|
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": "
|
|
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
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
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
|
}
|