@firebaseextensions/firestore-bigquery-change-tracker 1.1.38 → 1.1.39
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/clustering.js +42 -10
- package/lib/bigquery/index.d.ts +5 -1
- package/lib/bigquery/index.js +21 -71
- package/lib/bigquery/initializeLatestMaterializedView.d.ts +17 -0
- package/lib/bigquery/initializeLatestMaterializedView.js +71 -0
- package/lib/bigquery/initializeLatestView.d.ts +21 -0
- package/lib/bigquery/initializeLatestView.js +94 -0
- package/lib/bigquery/snapshot.d.ts +24 -2
- package/lib/bigquery/snapshot.js +172 -58
- package/lib/bigquery/utils.d.ts +2 -1
- package/lib/bigquery/utils.js +8 -2
- package/lib/bigquery/utils.test.d.ts +1 -0
- package/lib/bigquery/utils.test.js +65 -0
- package/lib/logs.d.ts +1 -0
- package/lib/logs.js +5 -1
- package/package.json +7 -6
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.Clustering = void 0;
|
|
4
4
|
const logs = require("../logs");
|
|
5
|
+
const VALID_CLUSTERING_TYPES = [
|
|
6
|
+
"BIGNUMERIC",
|
|
7
|
+
"BOOL",
|
|
8
|
+
"DATE",
|
|
9
|
+
"DATETIME",
|
|
10
|
+
"GEOGRAPHY",
|
|
11
|
+
"INT64",
|
|
12
|
+
"NUMERIC",
|
|
13
|
+
"RANGE",
|
|
14
|
+
"STRING",
|
|
15
|
+
"TIMESTAMP",
|
|
16
|
+
];
|
|
5
17
|
class Clustering {
|
|
6
18
|
constructor(config, table, schema) {
|
|
7
19
|
this.updateCluster = async (metaData) => {
|
|
@@ -33,17 +45,37 @@ class Clustering {
|
|
|
33
45
|
}
|
|
34
46
|
async hasInvalidFields(metaData) {
|
|
35
47
|
const { clustering = [] } = this.config;
|
|
36
|
-
if (!clustering)
|
|
37
|
-
return
|
|
38
|
-
const fieldNames = metaData
|
|
39
|
-
? metaData.schema.fields.map(($) => $.name)
|
|
40
|
-
: [];
|
|
41
|
-
const invalidFields = clustering.filter(($) => !fieldNames.includes($));
|
|
42
|
-
if (invalidFields.length) {
|
|
43
|
-
logs.invalidClustering(invalidFields.join(","));
|
|
44
|
-
return Promise.resolve(true);
|
|
48
|
+
if (!clustering) {
|
|
49
|
+
return false;
|
|
45
50
|
}
|
|
46
|
-
|
|
51
|
+
if (!clustering.length) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
if (!metaData?.schema.fields.length) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
const fields = metaData.schema.fields;
|
|
58
|
+
const fieldNameToType = new Map(fields.map((field) => [field.name, field.type]));
|
|
59
|
+
// First check if all clustering fields exist in the schema
|
|
60
|
+
const nonExistentFields = clustering.filter((fieldName) => !fieldNameToType.has(fieldName));
|
|
61
|
+
if (nonExistentFields.length) {
|
|
62
|
+
logs.invalidClustering(nonExistentFields.join(","));
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
// Then check for invalid types among existing clustering fields
|
|
66
|
+
const invalidFieldTypes = clustering
|
|
67
|
+
.map((fieldName) => ({
|
|
68
|
+
fieldName,
|
|
69
|
+
type: fieldNameToType.get(fieldName),
|
|
70
|
+
}))
|
|
71
|
+
.filter(({ type }) => !VALID_CLUSTERING_TYPES.includes(type));
|
|
72
|
+
if (invalidFieldTypes.length) {
|
|
73
|
+
logs.invalidClusteringTypes(invalidFieldTypes
|
|
74
|
+
.map(({ fieldName, type }) => `${fieldName} (${type})`)
|
|
75
|
+
.join(", "));
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
47
79
|
}
|
|
48
80
|
}
|
|
49
81
|
exports.Clustering = Clustering;
|
package/lib/bigquery/index.d.ts
CHANGED
|
@@ -18,6 +18,10 @@ export interface FirestoreBigQueryEventHistoryTrackerConfig {
|
|
|
18
18
|
useNewSnapshotQuerySyntax?: boolean;
|
|
19
19
|
skipInit?: boolean;
|
|
20
20
|
kmsKeyName?: string | undefined;
|
|
21
|
+
useMaterializedView?: boolean;
|
|
22
|
+
useIncrementalMaterializedView?: boolean;
|
|
23
|
+
maxStaleness?: string;
|
|
24
|
+
refreshIntervalMinutes?: number;
|
|
21
25
|
}
|
|
22
26
|
/**
|
|
23
27
|
* An FirestoreEventHistoryTracker that exports data to BigQuery.
|
|
@@ -70,7 +74,7 @@ export declare class FirestoreBigQueryEventHistoryTracker implements FirestoreEv
|
|
|
70
74
|
* Creates the latest snapshot view, which returns only latest operations
|
|
71
75
|
* of all existing documents over the raw change log table.
|
|
72
76
|
*/
|
|
73
|
-
private
|
|
77
|
+
private _initializeLatestView;
|
|
74
78
|
bigqueryDataset(): bigquery.Dataset;
|
|
75
79
|
private rawChangeLogTableName;
|
|
76
80
|
private rawLatestView;
|
package/lib/bigquery/index.js
CHANGED
|
@@ -6,7 +6,6 @@ const firestore_1 = require("firebase-admin/firestore");
|
|
|
6
6
|
const traverse = require("traverse");
|
|
7
7
|
const node_fetch_1 = require("node-fetch");
|
|
8
8
|
const schema_1 = require("./schema");
|
|
9
|
-
const snapshot_1 = require("./snapshot");
|
|
10
9
|
const handleFailedTransactions_1 = require("./handleFailedTransactions");
|
|
11
10
|
const tracker_1 = require("../tracker");
|
|
12
11
|
const logs = require("../logs");
|
|
@@ -14,6 +13,7 @@ const partitioning_1 = require("./partitioning");
|
|
|
14
13
|
const clustering_1 = require("./clustering");
|
|
15
14
|
const checkUpdates_1 = require("./checkUpdates");
|
|
16
15
|
const utils_1 = require("./utils");
|
|
16
|
+
const initializeLatestView_1 = require("./initializeLatestView");
|
|
17
17
|
var schema_2 = require("./schema");
|
|
18
18
|
Object.defineProperty(exports, "RawChangelogSchema", { enumerable: true, get: function () { return schema_2.RawChangelogSchema; } });
|
|
19
19
|
Object.defineProperty(exports, "RawChangelogViewSchema", { enumerable: true, get: function () { return schema_2.RawChangelogViewSchema; } });
|
|
@@ -133,7 +133,15 @@ class FirestoreBigQueryEventHistoryTracker {
|
|
|
133
133
|
async _waitForInitialization() {
|
|
134
134
|
const dataset = this.bigqueryDataset();
|
|
135
135
|
const changelogName = this.rawChangeLogTableName();
|
|
136
|
-
|
|
136
|
+
let materializedViewName;
|
|
137
|
+
if (this.config.useMaterializedView) {
|
|
138
|
+
materializedViewName = this.rawLatestView();
|
|
139
|
+
}
|
|
140
|
+
return (0, utils_1.waitForInitialization)({
|
|
141
|
+
dataset,
|
|
142
|
+
changelogName,
|
|
143
|
+
materializedViewName,
|
|
144
|
+
});
|
|
137
145
|
}
|
|
138
146
|
/**
|
|
139
147
|
* Inserts rows of data into the BigQuery raw change log table.
|
|
@@ -192,7 +200,7 @@ class FirestoreBigQueryEventHistoryTracker {
|
|
|
192
200
|
throw new Error(`Error initializing raw change log table: ${message}`);
|
|
193
201
|
}
|
|
194
202
|
try {
|
|
195
|
-
await this.
|
|
203
|
+
await this._initializeLatestView();
|
|
196
204
|
}
|
|
197
205
|
catch (error) {
|
|
198
206
|
const message = (0, utils_1.parseErrorMessage)(error, "initializing latest view");
|
|
@@ -310,77 +318,19 @@ class FirestoreBigQueryEventHistoryTracker {
|
|
|
310
318
|
* Creates the latest snapshot view, which returns only latest operations
|
|
311
319
|
* of all existing documents over the raw change log table.
|
|
312
320
|
*/
|
|
313
|
-
async
|
|
321
|
+
async _initializeLatestView() {
|
|
314
322
|
const dataset = this.bigqueryDataset();
|
|
315
323
|
const view = dataset.table(this.rawLatestView());
|
|
316
324
|
const [viewExists] = await view.exists();
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
const columnNames = fields.map((field) => field.name);
|
|
327
|
-
const documentIdColExists = columnNames.includes("document_id");
|
|
328
|
-
const pathParamsColExists = columnNames.includes("path_params");
|
|
329
|
-
const oldDataColExists = columnNames.includes("old_data");
|
|
330
|
-
/** If new view or opt-in to new query syntax **/
|
|
331
|
-
const updateView = (0, checkUpdates_1.viewRequiresUpdate)({
|
|
332
|
-
metadata,
|
|
333
|
-
config: this.config,
|
|
334
|
-
documentIdColExists,
|
|
335
|
-
pathParamsColExists,
|
|
336
|
-
oldDataColExists,
|
|
337
|
-
});
|
|
338
|
-
if (updateView) {
|
|
339
|
-
metadata.view = (0, snapshot_1.latestConsistentSnapshotView)({
|
|
340
|
-
datasetId: this.config.datasetId,
|
|
341
|
-
tableName: this.rawChangeLogTableName(),
|
|
342
|
-
schema,
|
|
343
|
-
useLegacyQuery: !this.config.useNewSnapshotQuerySyntax,
|
|
344
|
-
});
|
|
345
|
-
if (!documentIdColExists) {
|
|
346
|
-
logs.addNewColumn(this.rawLatestView(), schema_1.documentIdField.name);
|
|
347
|
-
}
|
|
348
|
-
await view.setMetadata(metadata);
|
|
349
|
-
logs.updatingMetadata(this.rawLatestView(), {
|
|
350
|
-
config: this.config,
|
|
351
|
-
documentIdColExists,
|
|
352
|
-
pathParamsColExists,
|
|
353
|
-
oldDataColExists,
|
|
354
|
-
});
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
else {
|
|
358
|
-
const schema = { fields: [...schema_1.RawChangelogViewSchema.fields] };
|
|
359
|
-
if (this.config.wildcardIds) {
|
|
360
|
-
schema.fields.push(schema_1.documentPathParams);
|
|
361
|
-
}
|
|
362
|
-
const latestSnapshot = (0, snapshot_1.latestConsistentSnapshotView)({
|
|
363
|
-
datasetId: this.config.datasetId,
|
|
364
|
-
tableName: this.rawChangeLogTableName(),
|
|
365
|
-
schema,
|
|
366
|
-
bqProjectId: this.bq.projectId,
|
|
367
|
-
useLegacyQuery: !this.config.useNewSnapshotQuerySyntax,
|
|
368
|
-
});
|
|
369
|
-
logs.bigQueryViewCreating(this.rawLatestView(), latestSnapshot.query);
|
|
370
|
-
const options = {
|
|
371
|
-
friendlyName: this.rawLatestView(),
|
|
372
|
-
view: latestSnapshot,
|
|
373
|
-
};
|
|
374
|
-
try {
|
|
375
|
-
await view.create(options);
|
|
376
|
-
await view.setMetadata({ schema: schema_1.RawChangelogViewSchema });
|
|
377
|
-
logs.bigQueryViewCreated(this.rawLatestView());
|
|
378
|
-
}
|
|
379
|
-
catch (ex) {
|
|
380
|
-
logs.tableCreationError(this.rawLatestView(), ex.message);
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
return view;
|
|
325
|
+
return await (0, initializeLatestView_1.initializeLatestView)({
|
|
326
|
+
bq: this.bq,
|
|
327
|
+
changeTrackerConfig: this.config,
|
|
328
|
+
dataset,
|
|
329
|
+
view,
|
|
330
|
+
viewExists,
|
|
331
|
+
rawChangeLogTableName: this.rawChangeLogTableName(),
|
|
332
|
+
rawLatestViewName: this.rawLatestView(),
|
|
333
|
+
});
|
|
384
334
|
}
|
|
385
335
|
bigqueryDataset() {
|
|
386
336
|
return this.bq.dataset(this.config.datasetId, {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { BigQuery, Table } from "@google-cloud/bigquery";
|
|
2
|
+
import { FirestoreBigQueryEventHistoryTrackerConfig } from ".";
|
|
3
|
+
interface InitializeLatestMaterializedViewOptions {
|
|
4
|
+
bq: BigQuery;
|
|
5
|
+
changeTrackerConfig: FirestoreBigQueryEventHistoryTrackerConfig;
|
|
6
|
+
view: Table;
|
|
7
|
+
viewExists: boolean;
|
|
8
|
+
rawChangeLogTableName: string;
|
|
9
|
+
rawLatestViewName: string;
|
|
10
|
+
schema?: any;
|
|
11
|
+
}
|
|
12
|
+
export declare function shouldRecreateMaterializedView(view: Table, config: FirestoreBigQueryEventHistoryTrackerConfig, source: string): Promise<boolean>;
|
|
13
|
+
/**
|
|
14
|
+
* Creates the latest materialized view.
|
|
15
|
+
*/
|
|
16
|
+
export declare function initializeLatestMaterializedView({ bq, changeTrackerConfig: config, view, viewExists, rawChangeLogTableName, rawLatestViewName, schema, }: InitializeLatestMaterializedViewOptions): Promise<Table>;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.initializeLatestMaterializedView = exports.shouldRecreateMaterializedView = void 0;
|
|
4
|
+
const logs = require("../logs");
|
|
5
|
+
const snapshot_1 = require("./snapshot");
|
|
6
|
+
const firebase_functions_1 = require("firebase-functions");
|
|
7
|
+
const sqlFormatter = require("sql-formatter");
|
|
8
|
+
async function shouldRecreateMaterializedView(view, config, source) {
|
|
9
|
+
const [viewMetadata] = await view.getMetadata();
|
|
10
|
+
const isIncremental = !viewMetadata.materializedView
|
|
11
|
+
?.allowNonIncrementalDefinition;
|
|
12
|
+
const incrementalMatch = isIncremental === !!config.useIncrementalMaterializedView;
|
|
13
|
+
const viewQuery = viewMetadata.materializedView?.query || "";
|
|
14
|
+
const queryMatch = sqlFormatter.format(viewQuery) === sqlFormatter.format(source);
|
|
15
|
+
return !queryMatch || !incrementalMatch;
|
|
16
|
+
}
|
|
17
|
+
exports.shouldRecreateMaterializedView = shouldRecreateMaterializedView;
|
|
18
|
+
/**
|
|
19
|
+
* Creates the latest materialized view.
|
|
20
|
+
*/
|
|
21
|
+
async function initializeLatestMaterializedView({ bq, changeTrackerConfig: config, view, viewExists, rawChangeLogTableName, rawLatestViewName, schema, }) {
|
|
22
|
+
try {
|
|
23
|
+
const { query, source } = config.useIncrementalMaterializedView
|
|
24
|
+
? (0, snapshot_1.buildMaterializedViewQuery)({
|
|
25
|
+
projectId: bq.projectId,
|
|
26
|
+
datasetId: config.datasetId,
|
|
27
|
+
tableName: rawChangeLogTableName,
|
|
28
|
+
rawLatestViewName,
|
|
29
|
+
schema,
|
|
30
|
+
})
|
|
31
|
+
: (0, snapshot_1.buildNonIncrementalMaterializedViewQuery)({
|
|
32
|
+
projectId: bq.projectId,
|
|
33
|
+
datasetId: config.datasetId,
|
|
34
|
+
tableName: rawChangeLogTableName,
|
|
35
|
+
maxStaleness: config.maxStaleness,
|
|
36
|
+
refreshIntervalMinutes: config.refreshIntervalMinutes,
|
|
37
|
+
rawLatestViewName,
|
|
38
|
+
enableRefresh: true,
|
|
39
|
+
schema,
|
|
40
|
+
});
|
|
41
|
+
const desiredQuery = sqlFormatter.format(query);
|
|
42
|
+
if (viewExists) {
|
|
43
|
+
const shouldRecreate = await shouldRecreateMaterializedView(view, config, source);
|
|
44
|
+
if (!shouldRecreate) {
|
|
45
|
+
firebase_functions_1.logger.warn(`Materialized view requested, but a view with matching configuration exists. Skipping creation.`);
|
|
46
|
+
return view;
|
|
47
|
+
}
|
|
48
|
+
firebase_functions_1.logger.warn(`Configuration mismatch detected for ${rawLatestViewName} ` +
|
|
49
|
+
`Recreating view...`);
|
|
50
|
+
await view.delete();
|
|
51
|
+
return await initializeLatestMaterializedView({
|
|
52
|
+
bq,
|
|
53
|
+
changeTrackerConfig: config,
|
|
54
|
+
view,
|
|
55
|
+
viewExists: false,
|
|
56
|
+
rawChangeLogTableName,
|
|
57
|
+
rawLatestViewName,
|
|
58
|
+
schema,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
logs.bigQueryViewCreating(rawLatestViewName, desiredQuery);
|
|
62
|
+
await bq.query(desiredQuery);
|
|
63
|
+
logs.bigQueryViewCreated(rawLatestViewName);
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
logs.tableCreationError(rawLatestViewName, error.message);
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
return view;
|
|
70
|
+
}
|
|
71
|
+
exports.initializeLatestMaterializedView = initializeLatestMaterializedView;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { BigQuery, Dataset, Table } from "@google-cloud/bigquery";
|
|
2
|
+
import { FirestoreBigQueryEventHistoryTrackerConfig } from ".";
|
|
3
|
+
interface InitializeLatestViewOptions {
|
|
4
|
+
bq: BigQuery;
|
|
5
|
+
changeTrackerConfig: FirestoreBigQueryEventHistoryTrackerConfig;
|
|
6
|
+
dataset: Dataset;
|
|
7
|
+
view: Table;
|
|
8
|
+
viewExists: boolean;
|
|
9
|
+
rawChangeLogTableName: string;
|
|
10
|
+
rawLatestViewName: string;
|
|
11
|
+
useMaterializedView?: boolean;
|
|
12
|
+
useIncrementalMaterializedView?: boolean;
|
|
13
|
+
useLegacyQuery?: boolean;
|
|
14
|
+
refreshIntervalMinutes?: number;
|
|
15
|
+
maxStaleness?: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Creates the latest snapshot view or materialized view.
|
|
19
|
+
*/
|
|
20
|
+
export declare function initializeLatestView({ changeTrackerConfig: config, dataset, view, viewExists, rawChangeLogTableName, rawLatestViewName, bq, }: InitializeLatestViewOptions): Promise<Table>;
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.initializeLatestView = void 0;
|
|
4
|
+
const schema_1 = require("./schema");
|
|
5
|
+
const logs = require("../logs");
|
|
6
|
+
const snapshot_1 = require("./snapshot");
|
|
7
|
+
const checkUpdates_1 = require("./checkUpdates");
|
|
8
|
+
const initializeLatestMaterializedView_1 = require("./initializeLatestMaterializedView");
|
|
9
|
+
/**
|
|
10
|
+
* Creates the latest snapshot view or materialized view.
|
|
11
|
+
*/
|
|
12
|
+
async function initializeLatestView({ changeTrackerConfig: config, dataset, view, viewExists, rawChangeLogTableName, rawLatestViewName, bq, }) {
|
|
13
|
+
if (config.useMaterializedView) {
|
|
14
|
+
const schema = { fields: [...schema_1.RawChangelogViewSchema.fields] };
|
|
15
|
+
if (config.wildcardIds) {
|
|
16
|
+
schema.fields.push(schema_1.documentPathParams);
|
|
17
|
+
}
|
|
18
|
+
return (0, initializeLatestMaterializedView_1.initializeLatestMaterializedView)({
|
|
19
|
+
bq,
|
|
20
|
+
changeTrackerConfig: config,
|
|
21
|
+
view,
|
|
22
|
+
viewExists,
|
|
23
|
+
rawChangeLogTableName,
|
|
24
|
+
rawLatestViewName,
|
|
25
|
+
schema,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
const schema = schema_1.RawChangelogViewSchema;
|
|
29
|
+
if (viewExists) {
|
|
30
|
+
logs.bigQueryViewAlreadyExists(view.id, dataset.id);
|
|
31
|
+
const [metadata] = await view.getMetadata();
|
|
32
|
+
const fields = (metadata.schema ? metadata.schema.fields : []);
|
|
33
|
+
if (config.wildcardIds) {
|
|
34
|
+
schema.fields.push(schema_1.documentPathParams);
|
|
35
|
+
}
|
|
36
|
+
const columnNames = fields.map((field) => field.name);
|
|
37
|
+
const documentIdColExists = columnNames.includes("document_id");
|
|
38
|
+
const pathParamsColExists = columnNames.includes("path_params");
|
|
39
|
+
const oldDataColExists = columnNames.includes("old_data");
|
|
40
|
+
const updateView = (0, checkUpdates_1.viewRequiresUpdate)({
|
|
41
|
+
metadata,
|
|
42
|
+
config,
|
|
43
|
+
documentIdColExists,
|
|
44
|
+
pathParamsColExists,
|
|
45
|
+
oldDataColExists,
|
|
46
|
+
});
|
|
47
|
+
if (updateView) {
|
|
48
|
+
metadata.view = (0, snapshot_1.latestConsistentSnapshotView)({
|
|
49
|
+
datasetId: config.datasetId,
|
|
50
|
+
tableName: rawChangeLogTableName,
|
|
51
|
+
schema,
|
|
52
|
+
useLegacyQuery: !config.useNewSnapshotQuerySyntax,
|
|
53
|
+
});
|
|
54
|
+
if (!documentIdColExists) {
|
|
55
|
+
logs.addNewColumn(rawLatestViewName, schema_1.documentIdField.name);
|
|
56
|
+
}
|
|
57
|
+
await view.setMetadata(metadata);
|
|
58
|
+
logs.updatingMetadata(rawLatestViewName, {
|
|
59
|
+
config,
|
|
60
|
+
documentIdColExists,
|
|
61
|
+
pathParamsColExists,
|
|
62
|
+
oldDataColExists,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
const schema = { fields: [...schema_1.RawChangelogViewSchema.fields] };
|
|
68
|
+
if (config.wildcardIds) {
|
|
69
|
+
schema.fields.push(schema_1.documentPathParams);
|
|
70
|
+
}
|
|
71
|
+
const latestSnapshot = (0, snapshot_1.latestConsistentSnapshotView)({
|
|
72
|
+
datasetId: config.datasetId,
|
|
73
|
+
tableName: rawChangeLogTableName,
|
|
74
|
+
schema,
|
|
75
|
+
bqProjectId: bq.projectId,
|
|
76
|
+
useLegacyQuery: !config.useNewSnapshotQuerySyntax,
|
|
77
|
+
});
|
|
78
|
+
logs.bigQueryViewCreating(rawLatestViewName, latestSnapshot.query);
|
|
79
|
+
const options = {
|
|
80
|
+
friendlyName: rawLatestViewName,
|
|
81
|
+
view: latestSnapshot,
|
|
82
|
+
};
|
|
83
|
+
try {
|
|
84
|
+
await view.create(options);
|
|
85
|
+
await view.setMetadata({ schema: schema_1.RawChangelogViewSchema });
|
|
86
|
+
logs.bigQueryViewCreated(rawLatestViewName);
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
logs.tableCreationError(rawLatestViewName, error.message);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return view;
|
|
93
|
+
}
|
|
94
|
+
exports.initializeLatestView = initializeLatestView;
|
|
@@ -9,7 +9,7 @@ export declare const latestConsistentSnapshotView: ({ datasetId, tableName, sche
|
|
|
9
9
|
query: string;
|
|
10
10
|
useLegacySql: boolean;
|
|
11
11
|
};
|
|
12
|
-
interface
|
|
12
|
+
interface BuildLatestSnapshotViewQueryOptions {
|
|
13
13
|
datasetId: string;
|
|
14
14
|
tableName: string;
|
|
15
15
|
timestampColumnName: string;
|
|
@@ -17,5 +17,27 @@ interface buildLatestSnapshotViewQueryOptions {
|
|
|
17
17
|
bqProjectId?: string;
|
|
18
18
|
useLegacyQuery?: boolean;
|
|
19
19
|
}
|
|
20
|
-
export declare function buildLatestSnapshotViewQuery({ datasetId, tableName, timestampColumnName, groupByColumns, bqProjectId, useLegacyQuery, }:
|
|
20
|
+
export declare function buildLatestSnapshotViewQuery({ datasetId, tableName, timestampColumnName, groupByColumns, bqProjectId, useLegacyQuery, }: BuildLatestSnapshotViewQueryOptions): string;
|
|
21
|
+
interface MaterializedViewOptions {
|
|
22
|
+
projectId: string;
|
|
23
|
+
datasetId: string;
|
|
24
|
+
tableName: string;
|
|
25
|
+
rawLatestViewName: string;
|
|
26
|
+
schema: any;
|
|
27
|
+
refreshIntervalMinutes?: number;
|
|
28
|
+
maxStaleness?: string;
|
|
29
|
+
}
|
|
30
|
+
interface NonIncrementalMaterializedViewOptions extends MaterializedViewOptions {
|
|
31
|
+
enableRefresh?: boolean;
|
|
32
|
+
}
|
|
33
|
+
export declare function buildMaterializedViewQuery({ projectId, datasetId, tableName, rawLatestViewName, schema, refreshIntervalMinutes, maxStaleness, }: NonIncrementalMaterializedViewOptions): {
|
|
34
|
+
target: string;
|
|
35
|
+
source: string;
|
|
36
|
+
query: string;
|
|
37
|
+
};
|
|
38
|
+
export declare function buildNonIncrementalMaterializedViewQuery({ projectId, datasetId, tableName, rawLatestViewName, schema, refreshIntervalMinutes, maxStaleness, enableRefresh, }: NonIncrementalMaterializedViewOptions): {
|
|
39
|
+
target: string;
|
|
40
|
+
source: string;
|
|
41
|
+
query: string;
|
|
42
|
+
};
|
|
21
43
|
export {};
|
package/lib/bigquery/snapshot.js
CHANGED
|
@@ -15,18 +15,17 @@
|
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
17
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
-
exports.buildLatestSnapshotViewQuery = exports.latestConsistentSnapshotView = void 0;
|
|
18
|
+
exports.buildNonIncrementalMaterializedViewQuery = exports.buildMaterializedViewQuery = exports.buildLatestSnapshotViewQuery = exports.latestConsistentSnapshotView = void 0;
|
|
19
19
|
const sqlFormatter = require("sql-formatter");
|
|
20
20
|
const schema_1 = require("./schema");
|
|
21
21
|
const excludeFields = ["document_name", "document_id"];
|
|
22
|
+
const nonGroupFields = ["event_id", "data", "old_data"];
|
|
22
23
|
const latestConsistentSnapshotView = ({ datasetId, tableName, schema, bqProjectId, useLegacyQuery = false, }) => ({
|
|
23
24
|
query: buildLatestSnapshotViewQuery({
|
|
24
25
|
datasetId,
|
|
25
26
|
tableName,
|
|
26
27
|
timestampColumnName: schema_1.timestampField.name,
|
|
27
|
-
groupByColumns: schema
|
|
28
|
-
.map((field) => field.name)
|
|
29
|
-
.filter((name) => excludeFields.indexOf(name) === -1),
|
|
28
|
+
groupByColumns: extractGroupByColumns(schema),
|
|
30
29
|
bqProjectId,
|
|
31
30
|
useLegacyQuery,
|
|
32
31
|
}),
|
|
@@ -34,72 +33,187 @@ const latestConsistentSnapshotView = ({ datasetId, tableName, schema, bqProjectI
|
|
|
34
33
|
});
|
|
35
34
|
exports.latestConsistentSnapshotView = latestConsistentSnapshotView;
|
|
36
35
|
function buildLatestSnapshotViewQuery({ datasetId, tableName, timestampColumnName, groupByColumns, bqProjectId, useLegacyQuery = true, }) {
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
validateInputs({ datasetId, tableName, timestampColumnName, groupByColumns });
|
|
37
|
+
const projectId = bqProjectId || process.env.PROJECT_ID;
|
|
38
|
+
return useLegacyQuery
|
|
39
|
+
? buildLegacyQuery(projectId, datasetId, tableName, timestampColumnName, groupByColumns)
|
|
40
|
+
: buildStandardQuery(projectId, datasetId, tableName, timestampColumnName, groupByColumns);
|
|
41
|
+
}
|
|
42
|
+
exports.buildLatestSnapshotViewQuery = buildLatestSnapshotViewQuery;
|
|
43
|
+
function extractGroupByColumns(schema) {
|
|
44
|
+
return schema["fields"]
|
|
45
|
+
.map((field) => field.name)
|
|
46
|
+
.filter((name) => !excludeFields.includes(name));
|
|
47
|
+
}
|
|
48
|
+
function validateInputs({ datasetId, tableName, timestampColumnName, groupByColumns, }) {
|
|
49
|
+
if (!datasetId || !tableName || !timestampColumnName) {
|
|
50
|
+
throw new Error("Missing required query parameters!");
|
|
39
51
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
throw Error(`Found empty group by column!`);
|
|
43
|
-
}
|
|
52
|
+
if (groupByColumns.some((columnName) => !columnName)) {
|
|
53
|
+
throw new Error("Group by columns must not contain empty values!");
|
|
44
54
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
${groupByColumns.join(",")}
|
|
55
|
-
FROM (
|
|
55
|
+
}
|
|
56
|
+
function buildLegacyQuery(projectId, datasetId, tableName, timestampColumnName, groupByColumns) {
|
|
57
|
+
return sqlFormatter.format(`
|
|
58
|
+
-- Retrieves the latest document change events for all live documents.
|
|
59
|
+
-- timestamp: The Firestore timestamp at which the event took place.
|
|
60
|
+
-- operation: One of INSERT, UPDATE, DELETE, IMPORT.
|
|
61
|
+
-- event_id: The id of the event that triggered the cloud function mirrored the event.
|
|
62
|
+
-- data: A raw JSON payload of the current state of the document.
|
|
63
|
+
-- document_id: The document id as defined in the Firestore database
|
|
56
64
|
SELECT
|
|
57
65
|
document_name,
|
|
58
|
-
document_id
|
|
59
|
-
${groupByColumns
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
};
|
|
84
|
-
const query = sqlFormatter.format(` -- Retrieves the latest document change events for all live documents.
|
|
66
|
+
document_id${groupByColumns.length > 0 ? `,` : ``}
|
|
67
|
+
${groupByColumns.join(",")}
|
|
68
|
+
FROM (
|
|
69
|
+
SELECT
|
|
70
|
+
document_name,
|
|
71
|
+
document_id,
|
|
72
|
+
${groupByColumns
|
|
73
|
+
.map((columnName) => `FIRST_VALUE(${columnName}) OVER (
|
|
74
|
+
PARTITION BY document_name
|
|
75
|
+
ORDER BY ${timestampColumnName} DESC
|
|
76
|
+
) AS ${columnName}`)
|
|
77
|
+
.join(",")}${groupByColumns.length > 0 ? "," : ""}
|
|
78
|
+
FIRST_VALUE(operation) OVER (
|
|
79
|
+
PARTITION BY document_name
|
|
80
|
+
ORDER BY ${timestampColumnName} DESC
|
|
81
|
+
) = "DELETE" AS is_deleted
|
|
82
|
+
FROM \`${projectId}.${datasetId}.${tableName}\`
|
|
83
|
+
ORDER BY document_name, ${timestampColumnName} DESC
|
|
84
|
+
)
|
|
85
|
+
WHERE NOT is_deleted
|
|
86
|
+
GROUP BY document_name, document_id${groupByColumns.length > 0 ? ", " : ""}${groupByColumns.join(",")}`);
|
|
87
|
+
}
|
|
88
|
+
function buildStandardQuery(projectId, datasetId, tableName, timestampColumnName, groupByColumns) {
|
|
89
|
+
return sqlFormatter.format(`
|
|
90
|
+
-- Retrieves the latest document change events for all live documents.
|
|
85
91
|
-- timestamp: The Firestore timestamp at which the event took place.
|
|
86
92
|
-- operation: One of INSERT, UPDATE, DELETE, IMPORT.
|
|
87
93
|
-- event_id: The id of the event that triggered the cloud function mirrored the event.
|
|
88
94
|
-- data: A raw JSON payload of the current state of the document.
|
|
89
95
|
-- document_id: The document id as defined in the Firestore database
|
|
90
96
|
WITH latest AS (
|
|
91
|
-
SELECT
|
|
92
|
-
FROM \`${
|
|
97
|
+
SELECT MAX(${timestampColumnName}) AS latest_timestamp, document_name
|
|
98
|
+
FROM \`${projectId}.${datasetId}.${tableName}\`
|
|
93
99
|
GROUP BY document_name
|
|
94
100
|
)
|
|
95
101
|
SELECT
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
${groupByColumns
|
|
99
|
-
|
|
100
|
-
|
|
102
|
+
t.document_name,
|
|
103
|
+
document_id${groupByColumns.length > 0 ? "," : ""}
|
|
104
|
+
${groupByColumns
|
|
105
|
+
.map((field) => nonGroupFields.includes(field)
|
|
106
|
+
? `ANY_VALUE(${field}) AS ${field}`
|
|
107
|
+
: `${field} AS ${field}`)
|
|
108
|
+
.join(",")}
|
|
109
|
+
FROM \`${projectId}.${datasetId}.${tableName}\` AS t
|
|
110
|
+
JOIN latest ON (
|
|
111
|
+
t.document_name = latest.document_name AND
|
|
112
|
+
IFNULL(t.${timestampColumnName}, TIMESTAMP("1970-01-01 00:00:00+00")) =
|
|
113
|
+
IFNULL(latest.latest_timestamp, TIMESTAMP("1970-01-01 00:00:00+00"))
|
|
114
|
+
)
|
|
101
115
|
WHERE operation != "DELETE"
|
|
102
|
-
GROUP BY document_name, document_id${groupByColumns.length > 0 ?
|
|
103
|
-
|
|
116
|
+
GROUP BY document_name, document_id${groupByColumns.length > 0 ? ", " : ""}${groupByColumns
|
|
117
|
+
.filter((field) => !nonGroupFields.includes(field))
|
|
118
|
+
.join(",")}`);
|
|
104
119
|
}
|
|
105
|
-
|
|
120
|
+
// Helper function to extract fields from schema
|
|
121
|
+
function extractFieldsFromSchema(schema) {
|
|
122
|
+
if (!schema || !schema.fields) {
|
|
123
|
+
throw new Error("Invalid schema: must contain fields array");
|
|
124
|
+
}
|
|
125
|
+
return schema.fields.map((field) => field.name);
|
|
126
|
+
}
|
|
127
|
+
function buildMaterializedViewQuery({ projectId, datasetId, tableName, rawLatestViewName, schema, refreshIntervalMinutes, maxStaleness, }) {
|
|
128
|
+
// Build the options string
|
|
129
|
+
const options = [];
|
|
130
|
+
if (refreshIntervalMinutes !== undefined) {
|
|
131
|
+
options.push(`refresh_interval_minutes = ${refreshIntervalMinutes}`);
|
|
132
|
+
}
|
|
133
|
+
if (maxStaleness) {
|
|
134
|
+
options.push(`max_staleness = ${maxStaleness}`);
|
|
135
|
+
}
|
|
136
|
+
const optionsString = options.length > 0
|
|
137
|
+
? `OPTIONS (
|
|
138
|
+
${options.join(",\n ")}
|
|
139
|
+
)`
|
|
140
|
+
: "";
|
|
141
|
+
// Extract fields from schema
|
|
142
|
+
const fields = extractFieldsFromSchema(schema);
|
|
143
|
+
// Build the aggregated fields for the CTE
|
|
144
|
+
const aggregatedFields = fields
|
|
145
|
+
.map((fieldName) => {
|
|
146
|
+
if (fieldName === "document_name") {
|
|
147
|
+
return " document_name";
|
|
148
|
+
}
|
|
149
|
+
if (fieldName === "timestamp") {
|
|
150
|
+
return " MAX(timestamp) AS timestamp";
|
|
151
|
+
}
|
|
152
|
+
return ` MAX_BY(${fieldName}, timestamp) AS ${fieldName}`;
|
|
153
|
+
})
|
|
154
|
+
.join(",\n ");
|
|
155
|
+
const target = `CREATE MATERIALIZED VIEW \`${projectId}.${datasetId}.${rawLatestViewName}\` ${optionsString}`;
|
|
156
|
+
const source = `
|
|
157
|
+
WITH latests AS (
|
|
158
|
+
SELECT
|
|
159
|
+
${aggregatedFields}
|
|
160
|
+
FROM \`${projectId}.${datasetId}.${tableName}\`
|
|
161
|
+
GROUP BY document_name
|
|
162
|
+
)
|
|
163
|
+
SELECT *
|
|
164
|
+
FROM latests
|
|
165
|
+
`;
|
|
166
|
+
// Combine all parts with options before AS
|
|
167
|
+
const fullQuery = sqlFormatter.format(`${target} AS (${source})`);
|
|
168
|
+
return { target, source, query: fullQuery };
|
|
169
|
+
}
|
|
170
|
+
exports.buildMaterializedViewQuery = buildMaterializedViewQuery;
|
|
171
|
+
function buildNonIncrementalMaterializedViewQuery({ projectId, datasetId, tableName, rawLatestViewName, schema, refreshIntervalMinutes, maxStaleness, enableRefresh = true, }) {
|
|
172
|
+
// Build the options string
|
|
173
|
+
const options = [];
|
|
174
|
+
options.push("allow_non_incremental_definition = true");
|
|
175
|
+
if (enableRefresh !== undefined) {
|
|
176
|
+
options.push(`enable_refresh = ${enableRefresh}`);
|
|
177
|
+
}
|
|
178
|
+
if (refreshIntervalMinutes !== undefined) {
|
|
179
|
+
options.push(`refresh_interval_minutes = ${refreshIntervalMinutes}`);
|
|
180
|
+
}
|
|
181
|
+
if (maxStaleness) {
|
|
182
|
+
options.push(`max_staleness = ${maxStaleness}`);
|
|
183
|
+
}
|
|
184
|
+
const optionsString = options.length > 0
|
|
185
|
+
? `OPTIONS (
|
|
186
|
+
${options.join(",\n ")}
|
|
187
|
+
)`
|
|
188
|
+
: "";
|
|
189
|
+
// Extract fields from schema
|
|
190
|
+
const fields = extractFieldsFromSchema(schema);
|
|
191
|
+
// Build the aggregated fields for the CTE
|
|
192
|
+
const aggregatedFields = fields
|
|
193
|
+
.map((fieldName) => {
|
|
194
|
+
if (fieldName === "document_name") {
|
|
195
|
+
return " document_name";
|
|
196
|
+
}
|
|
197
|
+
if (fieldName === "timestamp") {
|
|
198
|
+
return " MAX(timestamp) AS timestamp";
|
|
199
|
+
}
|
|
200
|
+
return ` MAX_BY(${fieldName}, timestamp) AS ${fieldName}`;
|
|
201
|
+
})
|
|
202
|
+
.join(",\n ");
|
|
203
|
+
const target = `CREATE MATERIALIZED VIEW \`${projectId}.${datasetId}.${rawLatestViewName}\` ${optionsString}`;
|
|
204
|
+
const source = `
|
|
205
|
+
WITH latests AS (
|
|
206
|
+
SELECT
|
|
207
|
+
${aggregatedFields}
|
|
208
|
+
FROM \`${projectId}.${datasetId}.${tableName}\`
|
|
209
|
+
GROUP BY document_name
|
|
210
|
+
)
|
|
211
|
+
SELECT *
|
|
212
|
+
FROM latests
|
|
213
|
+
WHERE operation != "DELETE"
|
|
214
|
+
`;
|
|
215
|
+
// Combine all parts with options before AS
|
|
216
|
+
const fullQuery = sqlFormatter.format(`${target} AS (${source})`);
|
|
217
|
+
return { target, source, query: fullQuery };
|
|
218
|
+
}
|
|
219
|
+
exports.buildNonIncrementalMaterializedViewQuery = buildNonIncrementalMaterializedViewQuery;
|
package/lib/bigquery/utils.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { Dataset, Table } from "@google-cloud/bigquery";
|
|
|
2
2
|
interface WaitForInitializationParams {
|
|
3
3
|
dataset: Dataset;
|
|
4
4
|
changelogName: string;
|
|
5
|
+
materializedViewName?: string;
|
|
5
6
|
}
|
|
6
7
|
/**
|
|
7
8
|
* Periodically checks for the existence of a dataset and table until both are found or a maximum number of attempts is reached.
|
|
@@ -10,6 +11,6 @@ interface WaitForInitializationParams {
|
|
|
10
11
|
* @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.
|
|
11
12
|
* @throws {Error} Throws an error if the dataset or table cannot be verified to exist after multiple attempts or if an unexpected error occurs.
|
|
12
13
|
*/
|
|
13
|
-
export declare function waitForInitialization({ dataset, changelogName }: WaitForInitializationParams, maxAttempts?: number): Promise<Table>;
|
|
14
|
+
export declare function waitForInitialization({ dataset, changelogName, materializedViewName }: WaitForInitializationParams, maxAttempts?: number): Promise<Table>;
|
|
14
15
|
export declare function parseErrorMessage(error: unknown, process?: string): string;
|
|
15
16
|
export {};
|
package/lib/bigquery/utils.js
CHANGED
|
@@ -9,7 +9,7 @@ const logs = require("../logs");
|
|
|
9
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
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
11
|
*/
|
|
12
|
-
async function waitForInitialization({ dataset, changelogName }, maxAttempts = 12) {
|
|
12
|
+
async function waitForInitialization({ dataset, changelogName, materializedViewName }, maxAttempts = 12) {
|
|
13
13
|
return new Promise((resolve, reject) => {
|
|
14
14
|
let attempts = 0;
|
|
15
15
|
let handle = setInterval(async () => {
|
|
@@ -17,7 +17,13 @@ async function waitForInitialization({ dataset, changelogName }, maxAttempts = 1
|
|
|
17
17
|
const [datasetExists] = await dataset.exists();
|
|
18
18
|
const table = dataset.table(changelogName);
|
|
19
19
|
const [tableExists] = await table.exists();
|
|
20
|
-
|
|
20
|
+
let waitingForMaterializedView = false;
|
|
21
|
+
if (materializedViewName) {
|
|
22
|
+
const materializedView = dataset.table(materializedViewName);
|
|
23
|
+
const [materializedViewExists] = await materializedView.exists();
|
|
24
|
+
waitingForMaterializedView = !materializedViewExists;
|
|
25
|
+
}
|
|
26
|
+
if (datasetExists && tableExists && !waitingForMaterializedView) {
|
|
21
27
|
clearInterval(handle);
|
|
22
28
|
resolve(table);
|
|
23
29
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -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/lib/logs.d.ts
CHANGED
|
@@ -50,6 +50,7 @@ export declare const cannotPartitionExistingTable: (table: Table) => void;
|
|
|
50
50
|
export declare function invalidProjectIdWarning(bqProjectId: string): void;
|
|
51
51
|
export declare function invalidTableReference(): void;
|
|
52
52
|
export declare function hourAndDatePartitioningWarning(): void;
|
|
53
|
+
export declare function invalidClusteringTypes(fields: string): void;
|
|
53
54
|
export declare function invalidClustering(fields: string): void;
|
|
54
55
|
export declare const tableCreationError: (table: any, message: any) => void;
|
|
55
56
|
export declare const failedToInitializeWait: (message: any) => void;
|
package/lib/logs.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
17
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
-
exports.updatingMetadata = exports.failedToInitializeWait = exports.tableCreationError = exports.invalidClustering = exports.hourAndDatePartitioningWarning = exports.invalidTableReference = exports.invalidProjectIdWarning = exports.cannotPartitionExistingTable = exports.removedClustering = exports.updatedClustering = exports.bigQueryTableInsertErrors = exports.firestoreTimePartitioningParametersWarning = exports.firestoreTimePartitionFieldError = exports.addPartitionFieldColumn = exports.addNewColumn = exports.timestampMissingValue = exports.error = exports.dataTypeInvalid = exports.dataInserting = exports.dataInsertRetried = exports.dataInserted = exports.complete = exports.bigQueryViewValidating = exports.bigQueryViewValidated = exports.bigQueryViewUpToDate = exports.bigQueryViewUpdating = exports.bigQueryViewUpdated = exports.bigQueryViewAlreadyExists = exports.bigQueryViewCreating = exports.bigQueryViewCreated = exports.bigQueryUserDefinedFunctionCreated = exports.bigQueryUserDefinedFunctionCreating = exports.bigQueryTableValidating = exports.bigQueryTableValidated = exports.bigQueryTableUpToDate = exports.bigQueryTableUpdating = exports.bigQueryTableUpdated = exports.bigQueryTableCreating = exports.bigQueryTableCreated = exports.bigQueryTableAlreadyExists = exports.bigQuerySchemaViewCreated = exports.bigQueryLatestSnapshotViewQueryCreated = exports.bigQueryErrorRecordingDocumentChange = exports.bigQueryDatasetExists = exports.bigQueryDatasetCreating = exports.bigQueryDatasetCreated = exports.arrayFieldInvalid = void 0;
|
|
18
|
+
exports.updatingMetadata = exports.failedToInitializeWait = exports.tableCreationError = exports.invalidClustering = exports.invalidClusteringTypes = exports.hourAndDatePartitioningWarning = exports.invalidTableReference = exports.invalidProjectIdWarning = exports.cannotPartitionExistingTable = exports.removedClustering = exports.updatedClustering = exports.bigQueryTableInsertErrors = exports.firestoreTimePartitioningParametersWarning = exports.firestoreTimePartitionFieldError = exports.addPartitionFieldColumn = exports.addNewColumn = exports.timestampMissingValue = exports.error = exports.dataTypeInvalid = exports.dataInserting = exports.dataInsertRetried = exports.dataInserted = exports.complete = exports.bigQueryViewValidating = exports.bigQueryViewValidated = exports.bigQueryViewUpToDate = exports.bigQueryViewUpdating = exports.bigQueryViewUpdated = exports.bigQueryViewAlreadyExists = exports.bigQueryViewCreating = exports.bigQueryViewCreated = exports.bigQueryUserDefinedFunctionCreated = exports.bigQueryUserDefinedFunctionCreating = exports.bigQueryTableValidating = exports.bigQueryTableValidated = exports.bigQueryTableUpToDate = exports.bigQueryTableUpdating = exports.bigQueryTableUpdated = exports.bigQueryTableCreating = exports.bigQueryTableCreated = exports.bigQueryTableAlreadyExists = exports.bigQuerySchemaViewCreated = exports.bigQueryLatestSnapshotViewQueryCreated = exports.bigQueryErrorRecordingDocumentChange = exports.bigQueryDatasetExists = exports.bigQueryDatasetCreating = exports.bigQueryDatasetCreated = exports.arrayFieldInvalid = void 0;
|
|
19
19
|
const firebase_functions_1 = require("firebase-functions");
|
|
20
20
|
const arrayFieldInvalid = (fieldName) => {
|
|
21
21
|
firebase_functions_1.logger.warn(`Array field '${fieldName}' does not contain an array, skipping`);
|
|
@@ -201,6 +201,10 @@ function hourAndDatePartitioningWarning() {
|
|
|
201
201
|
firebase_functions_1.logger.warn(`Cannot partition table with hour partitioning and Date. For DATE columns, the partitions can have daily, monthly, or yearly granularity. Skipping partitioning`);
|
|
202
202
|
}
|
|
203
203
|
exports.hourAndDatePartitioningWarning = hourAndDatePartitioningWarning;
|
|
204
|
+
function invalidClusteringTypes(fields) {
|
|
205
|
+
firebase_functions_1.logger.warn(`Unable to add clustering, field(s) ${fields} have invalid types.`);
|
|
206
|
+
}
|
|
207
|
+
exports.invalidClusteringTypes = invalidClusteringTypes;
|
|
204
208
|
function invalidClustering(fields) {
|
|
205
209
|
firebase_functions_1.logger.warn(`Unable to add clustering, field(s) ${fields} do not exist on the expected table`);
|
|
206
210
|
}
|
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.39",
|
|
9
9
|
"description": "Core change-tracker library for Cloud Firestore Collection BigQuery Exports",
|
|
10
10
|
"main": "./lib/index.js",
|
|
11
11
|
"scripts": {
|
|
@@ -38,17 +38,18 @@
|
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@types/chai": "^4.1.6",
|
|
41
|
-
"@types/jest": "29.5.
|
|
41
|
+
"@types/jest": "^29.5.14",
|
|
42
42
|
"@types/node": "14.18.34",
|
|
43
43
|
"@types/traverse": "^0.6.32",
|
|
44
44
|
"chai": "^4.2.0",
|
|
45
|
-
"nyc": "^14.0.0",
|
|
46
|
-
"rimraf": "^2.6.3",
|
|
47
|
-
"typescript": "^4.9.4",
|
|
48
45
|
"jest": "29.5.0",
|
|
46
|
+
"jest-config": "29.5.0",
|
|
49
47
|
"jest-environment-node": "29.5.0",
|
|
48
|
+
"jest-summarizing-reporter": "^1.1.4",
|
|
50
49
|
"mocked-env": "^1.3.2",
|
|
50
|
+
"nyc": "^17.1.0",
|
|
51
|
+
"rimraf": "^2.6.3",
|
|
51
52
|
"ts-jest": "29.1.2",
|
|
52
|
-
"
|
|
53
|
+
"typescript": "^4.9.4"
|
|
53
54
|
}
|
|
54
55
|
}
|