@firebaseextensions/firestore-bigquery-change-tracker 1.1.42 → 2.0.0
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/checkUpdates.d.ts +3 -3
- package/lib/bigquery/checkUpdates.js +3 -1
- package/lib/bigquery/clustering.d.ts +3 -3
- package/lib/bigquery/handleFailedTransactions.d.ts +2 -2
- package/lib/bigquery/index.d.ts +6 -27
- package/lib/bigquery/index.js +6 -2
- package/lib/bigquery/initializeLatestMaterializedView.d.ts +3 -3
- package/lib/bigquery/initializeLatestView.d.ts +2 -2
- package/lib/bigquery/schema.d.ts +0 -7
- package/lib/bigquery/schema.js +1 -12
- package/lib/bigquery/types.d.ts +26 -0
- package/lib/bigquery/types.js +2 -0
- package/package.json +4 -6
- package/lib/bigquery/partitioning.d.ts +0 -29
- package/lib/bigquery/partitioning.js +0 -277
- package/lib/bigquery/utils.test.d.ts +0 -1
- package/lib/bigquery/utils.test.js +0 -65
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Table, TableMetadata } from "@google-cloud/bigquery/build/src/table";
|
|
2
|
-
import {
|
|
2
|
+
import { Config } from ".";
|
|
3
3
|
interface TableRequiresUpdateOptions {
|
|
4
4
|
table: Table;
|
|
5
|
-
config:
|
|
5
|
+
config: Config;
|
|
6
6
|
documentIdColExists: boolean;
|
|
7
7
|
pathParamsColExists: boolean;
|
|
8
8
|
oldDataColExists: boolean;
|
|
@@ -10,7 +10,7 @@ interface TableRequiresUpdateOptions {
|
|
|
10
10
|
export declare function tableRequiresUpdate({ table, config, documentIdColExists, pathParamsColExists, oldDataColExists, }: TableRequiresUpdateOptions): Promise<boolean>;
|
|
11
11
|
interface ViewRequiresUpdateOptions {
|
|
12
12
|
metadata?: TableMetadata;
|
|
13
|
-
config:
|
|
13
|
+
config: Config;
|
|
14
14
|
documentIdColExists: boolean;
|
|
15
15
|
pathParamsColExists: boolean;
|
|
16
16
|
oldDataColExists: boolean;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.viewRequiresUpdate = exports.tableRequiresUpdate = void 0;
|
|
4
4
|
const partitioning_1 = require("./partitioning");
|
|
5
|
+
const config_1 = require("./partitioning/config");
|
|
5
6
|
async function tableRequiresUpdate({ table, config, documentIdColExists, pathParamsColExists, oldDataColExists, }) {
|
|
6
7
|
/* Setup checks */
|
|
7
8
|
const { metadata } = table;
|
|
@@ -20,7 +21,8 @@ async function tableRequiresUpdate({ table, config, documentIdColExists, pathPar
|
|
|
20
21
|
if (!oldDataColExists)
|
|
21
22
|
return true;
|
|
22
23
|
/** Check partitioning */
|
|
23
|
-
const
|
|
24
|
+
const partitioningConfig = new config_1.PartitioningConfig(config.partitioning);
|
|
25
|
+
const partitioning = new partitioning_1.Partitioning(partitioningConfig, table);
|
|
24
26
|
const isValidPartition = await partitioning.isValidPartitionForExistingTable();
|
|
25
27
|
if (isValidPartition)
|
|
26
28
|
return true;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Config } from ".";
|
|
2
2
|
import * as bigquery from "@google-cloud/bigquery";
|
|
3
3
|
import { TableMetadata } from "@google-cloud/bigquery";
|
|
4
4
|
export declare class Clustering {
|
|
5
|
-
config:
|
|
5
|
+
config: Config;
|
|
6
6
|
table: bigquery.Table;
|
|
7
7
|
schema: object;
|
|
8
|
-
constructor(config:
|
|
8
|
+
constructor(config: Config, table?: bigquery.Table, schema?: object);
|
|
9
9
|
hasValidTableReference(): boolean;
|
|
10
10
|
private hasInvalidFields;
|
|
11
11
|
private updateCluster;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
declare const _default: (rows: any[], config:
|
|
1
|
+
import { Config } from ".";
|
|
2
|
+
declare const _default: (rows: any[], config: Config, e: Error) => Promise<void>;
|
|
3
3
|
export default _default;
|
package/lib/bigquery/index.d.ts
CHANGED
|
@@ -1,31 +1,9 @@
|
|
|
1
1
|
import * as bigquery from "@google-cloud/bigquery";
|
|
2
2
|
import { FirestoreEventHistoryTracker, FirestoreDocumentChangeEvent } from "../tracker";
|
|
3
|
-
import { LogLevel } from "../logger";
|
|
4
3
|
export { RawChangelogSchema, RawChangelogViewSchema } from "./schema";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
firestoreInstanceId?: string;
|
|
9
|
-
datasetLocation?: string | undefined;
|
|
10
|
-
transformFunction?: string | undefined;
|
|
11
|
-
timePartitioning?: string | undefined;
|
|
12
|
-
timePartitioningField?: string | undefined;
|
|
13
|
-
timePartitioningFieldType?: string | undefined;
|
|
14
|
-
timePartitioningFirestoreField?: string | undefined;
|
|
15
|
-
clustering: string[] | null;
|
|
16
|
-
databaseId?: string | undefined;
|
|
17
|
-
wildcardIds?: boolean;
|
|
18
|
-
bqProjectId?: string | undefined;
|
|
19
|
-
backupTableId?: string | undefined;
|
|
20
|
-
useNewSnapshotQuerySyntax?: boolean;
|
|
21
|
-
skipInit?: boolean;
|
|
22
|
-
kmsKeyName?: string | undefined;
|
|
23
|
-
useMaterializedView?: boolean;
|
|
24
|
-
useIncrementalMaterializedView?: boolean;
|
|
25
|
-
maxStaleness?: string;
|
|
26
|
-
refreshIntervalMinutes?: number;
|
|
27
|
-
logLevel?: LogLevel | string;
|
|
28
|
-
}
|
|
4
|
+
import type { Config } from "./types";
|
|
5
|
+
import { PartitioningConfig } from "./partitioning/config";
|
|
6
|
+
export type { Config } from "./types";
|
|
29
7
|
/**
|
|
30
8
|
* An FirestoreEventHistoryTracker that exports data to BigQuery.
|
|
31
9
|
*
|
|
@@ -36,10 +14,11 @@ export interface FirestoreBigQueryEventHistoryTrackerConfig {
|
|
|
36
14
|
* If any subsequent data export fails, it will attempt to reinitialize.
|
|
37
15
|
*/
|
|
38
16
|
export declare class FirestoreBigQueryEventHistoryTracker implements FirestoreEventHistoryTracker {
|
|
39
|
-
config:
|
|
17
|
+
config: Config;
|
|
40
18
|
bq: bigquery.BigQuery;
|
|
41
19
|
_initialized: boolean;
|
|
42
|
-
|
|
20
|
+
partitioningConfig: PartitioningConfig;
|
|
21
|
+
constructor(config: Config);
|
|
43
22
|
record(events: FirestoreDocumentChangeEvent[]): Promise<void>;
|
|
44
23
|
private transformRows;
|
|
45
24
|
serializeData(eventData: any): any;
|
package/lib/bigquery/index.js
CHANGED
|
@@ -33,6 +33,7 @@ const logger_1 = require("../logger");
|
|
|
33
33
|
var schema_2 = require("./schema");
|
|
34
34
|
Object.defineProperty(exports, "RawChangelogSchema", { enumerable: true, get: function () { return schema_2.RawChangelogSchema; } });
|
|
35
35
|
Object.defineProperty(exports, "RawChangelogViewSchema", { enumerable: true, get: function () { return schema_2.RawChangelogViewSchema; } });
|
|
36
|
+
const config_1 = require("./partitioning/config");
|
|
36
37
|
/**
|
|
37
38
|
* An FirestoreEventHistoryTracker that exports data to BigQuery.
|
|
38
39
|
*
|
|
@@ -48,6 +49,7 @@ class FirestoreBigQueryEventHistoryTracker {
|
|
|
48
49
|
this._initialized = false;
|
|
49
50
|
this.bq = new bigquery.BigQuery();
|
|
50
51
|
this.bq.projectId = config.bqProjectId || process.env.PROJECT_ID;
|
|
52
|
+
this.partitioningConfig = new config_1.PartitioningConfig(this.config.partitioning);
|
|
51
53
|
if (!this.config.datasetLocation) {
|
|
52
54
|
this.config.datasetLocation = "us";
|
|
53
55
|
}
|
|
@@ -59,7 +61,7 @@ class FirestoreBigQueryEventHistoryTracker {
|
|
|
59
61
|
if (!this.config.skipInit) {
|
|
60
62
|
await this.initialize();
|
|
61
63
|
}
|
|
62
|
-
const partitionHandler = new partitioning_1.Partitioning(this.
|
|
64
|
+
const partitionHandler = new partitioning_1.Partitioning(this.partitioningConfig);
|
|
63
65
|
const rows = events.map((event) => {
|
|
64
66
|
const partitionValue = partitionHandler.getPartitionValue(event);
|
|
65
67
|
const { documentId, ...pathParams } = event.pathParams || {};
|
|
@@ -251,6 +253,7 @@ class FirestoreBigQueryEventHistoryTracker {
|
|
|
251
253
|
}
|
|
252
254
|
catch (ex) {
|
|
253
255
|
logs.tableCreationError(this.config.datasetId, ex.message);
|
|
256
|
+
throw ex;
|
|
254
257
|
}
|
|
255
258
|
}
|
|
256
259
|
return dataset;
|
|
@@ -263,7 +266,7 @@ class FirestoreBigQueryEventHistoryTracker {
|
|
|
263
266
|
const dataset = this.bigqueryDataset();
|
|
264
267
|
const table = dataset.table(changelogName);
|
|
265
268
|
const [tableExists] = await table.exists();
|
|
266
|
-
const partitioning = new partitioning_1.Partitioning(this.
|
|
269
|
+
const partitioning = new partitioning_1.Partitioning(this.partitioningConfig, table);
|
|
267
270
|
const clustering = new clustering_1.Clustering(this.config, table);
|
|
268
271
|
if (tableExists) {
|
|
269
272
|
logs.bigQueryTableAlreadyExists(table.id, dataset.id);
|
|
@@ -329,6 +332,7 @@ class FirestoreBigQueryEventHistoryTracker {
|
|
|
329
332
|
}
|
|
330
333
|
catch (ex) {
|
|
331
334
|
logs.tableCreationError(changelogName, ex.message);
|
|
335
|
+
throw ex;
|
|
332
336
|
}
|
|
333
337
|
}
|
|
334
338
|
return table;
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { BigQuery, Table } from "@google-cloud/bigquery";
|
|
2
|
-
import {
|
|
2
|
+
import { Config } from ".";
|
|
3
3
|
interface InitializeLatestMaterializedViewOptions {
|
|
4
4
|
bq: BigQuery;
|
|
5
|
-
changeTrackerConfig:
|
|
5
|
+
changeTrackerConfig: Config;
|
|
6
6
|
view: Table;
|
|
7
7
|
viewExists: boolean;
|
|
8
8
|
rawChangeLogTableName: string;
|
|
9
9
|
rawLatestViewName: string;
|
|
10
10
|
schema?: any;
|
|
11
11
|
}
|
|
12
|
-
export declare function shouldRecreateMaterializedView(view: Table, config:
|
|
12
|
+
export declare function shouldRecreateMaterializedView(view: Table, config: Config, source: string): Promise<boolean>;
|
|
13
13
|
/**
|
|
14
14
|
* Creates the latest materialized view.
|
|
15
15
|
*/
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { BigQuery, Dataset, Table } from "@google-cloud/bigquery";
|
|
2
|
-
import {
|
|
2
|
+
import { Config } from ".";
|
|
3
3
|
interface InitializeLatestViewOptions {
|
|
4
4
|
bq: BigQuery;
|
|
5
|
-
changeTrackerConfig:
|
|
5
|
+
changeTrackerConfig: Config;
|
|
6
6
|
dataset: Dataset;
|
|
7
7
|
view: Table;
|
|
8
8
|
viewExists: boolean;
|
package/lib/bigquery/schema.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { FirestoreBigQueryEventHistoryTrackerConfig } from ".";
|
|
2
1
|
export type BigQueryFieldMode = "NULLABLE" | "REPEATED" | "REQUIRED";
|
|
3
2
|
export type BigQueryFieldType = "BOOLEAN" | "NUMERIC" | "RECORD" | "STRING" | "TIMESTAMP";
|
|
4
3
|
export type BigQueryField = {
|
|
@@ -48,9 +47,3 @@ export declare const RawChangelogSchema: {
|
|
|
48
47
|
description: string;
|
|
49
48
|
}[];
|
|
50
49
|
};
|
|
51
|
-
export declare const getNewPartitionField: (config: FirestoreBigQueryEventHistoryTrackerConfig) => {
|
|
52
|
-
name: string;
|
|
53
|
-
mode: string;
|
|
54
|
-
type: string;
|
|
55
|
-
description: string;
|
|
56
|
-
};
|
package/lib/bigquery/schema.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
17
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
-
exports.
|
|
18
|
+
exports.RawChangelogSchema = exports.RawChangelogViewSchema = exports.oldDataField = exports.documentPathParams = exports.documentIdField = exports.longitudeField = exports.latitudeField = exports.timestampField = exports.operationField = exports.eventIdField = exports.documentNameField = exports.dataField = void 0;
|
|
19
19
|
const bigQueryField = (name, type, mode, fields) => ({
|
|
20
20
|
fields,
|
|
21
21
|
mode: mode || "NULLABLE",
|
|
@@ -135,14 +135,3 @@ exports.RawChangelogSchema = {
|
|
|
135
135
|
exports.documentIdField,
|
|
136
136
|
],
|
|
137
137
|
};
|
|
138
|
-
// Helper function for Partitioned Changelogs field
|
|
139
|
-
const getNewPartitionField = (config) => {
|
|
140
|
-
const { timePartitioningField, timePartitioningFieldType } = config;
|
|
141
|
-
return {
|
|
142
|
-
name: timePartitioningField,
|
|
143
|
-
mode: "NULLABLE",
|
|
144
|
-
type: timePartitioningFieldType,
|
|
145
|
-
description: "The document TimePartition partition field selected by user",
|
|
146
|
-
};
|
|
147
|
-
};
|
|
148
|
-
exports.getNewPartitionField = getNewPartitionField;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { PartitioningStrategy } from "./partitioning/config";
|
|
2
|
+
/**
|
|
3
|
+
* Base configuration for all variants. Includes all parameters
|
|
4
|
+
* that are not dependent on the partitioning strategy.
|
|
5
|
+
*/
|
|
6
|
+
export interface Config {
|
|
7
|
+
datasetId: string;
|
|
8
|
+
tableId: string;
|
|
9
|
+
firestoreInstanceId?: string;
|
|
10
|
+
datasetLocation?: string;
|
|
11
|
+
transformFunction?: string;
|
|
12
|
+
partitioning?: PartitioningStrategy;
|
|
13
|
+
clustering?: string[] | null;
|
|
14
|
+
databaseId?: string;
|
|
15
|
+
wildcardIds?: boolean;
|
|
16
|
+
bqProjectId?: string;
|
|
17
|
+
backupTableId?: string;
|
|
18
|
+
useNewSnapshotQuerySyntax?: boolean;
|
|
19
|
+
skipInit?: boolean;
|
|
20
|
+
kmsKeyName?: string;
|
|
21
|
+
useMaterializedView?: boolean;
|
|
22
|
+
useIncrementalMaterializedView?: boolean;
|
|
23
|
+
maxStaleness?: string;
|
|
24
|
+
refreshIntervalMinutes?: number;
|
|
25
|
+
logLevel?: "debug" | "info" | "warn" | "error" | "silent";
|
|
26
|
+
}
|
package/package.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"name": "@firebaseextensions/firestore-bigquery-change-tracker",
|
|
3
3
|
"repository": {
|
|
4
4
|
"type": "git",
|
|
5
|
-
"url": "github.com/firebase/extensions.git",
|
|
5
|
+
"url": "git+https://github.com/firebase/extensions.git",
|
|
6
6
|
"directory": "firestore-bigquery-export/firestore-bigquery-change-tracker"
|
|
7
7
|
},
|
|
8
|
-
"version": "
|
|
8
|
+
"version": "2.0.0",
|
|
9
9
|
"description": "Core change-tracker library for Cloud Firestore Collection BigQuery Exports",
|
|
10
10
|
"main": "./lib/index.js",
|
|
11
11
|
"scripts": {
|
|
@@ -29,9 +29,6 @@
|
|
|
29
29
|
"@google-cloud/resource-manager": "^5.1.0",
|
|
30
30
|
"firebase-admin": "^13.2.0",
|
|
31
31
|
"firebase-functions": "^6.3.2",
|
|
32
|
-
"generate-schema": "^2.6.0",
|
|
33
|
-
"inquirer": "^6.4.0",
|
|
34
|
-
"lodash": "^4.17.14",
|
|
35
32
|
"node-fetch": "^2.6.1",
|
|
36
33
|
"sql-formatter": "^2.3.3",
|
|
37
34
|
"traverse": "^0.6.6"
|
|
@@ -50,6 +47,7 @@
|
|
|
50
47
|
"nyc": "^17.1.0",
|
|
51
48
|
"rimraf": "^2.6.3",
|
|
52
49
|
"ts-jest": "29.1.2",
|
|
53
|
-
"typescript": "^4.9.4"
|
|
50
|
+
"typescript": "^4.9.4",
|
|
51
|
+
"wait-for-expect": "^3.0.2"
|
|
54
52
|
}
|
|
55
53
|
}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { FirestoreBigQueryEventHistoryTrackerConfig } from ".";
|
|
2
|
-
import { FirestoreDocumentChangeEvent } from "..";
|
|
3
|
-
import * as bigquery from "@google-cloud/bigquery";
|
|
4
|
-
export declare class Partitioning {
|
|
5
|
-
config: FirestoreBigQueryEventHistoryTrackerConfig;
|
|
6
|
-
table: bigquery.Table;
|
|
7
|
-
schema: object;
|
|
8
|
-
constructor(config: FirestoreBigQueryEventHistoryTrackerConfig, table?: bigquery.Table, schema?: object);
|
|
9
|
-
private isPartitioningEnabled;
|
|
10
|
-
private isValidPartitionTypeString;
|
|
11
|
-
private metaDataSchemaFields;
|
|
12
|
-
private isValidPartitionTypeDate;
|
|
13
|
-
private hasHourAndDatePartitionConfig;
|
|
14
|
-
private hasValidCustomPartitionConfig;
|
|
15
|
-
private hasValidTimePartitionOption;
|
|
16
|
-
private hasValidTimePartitionType;
|
|
17
|
-
hasExistingSchema(): Promise<boolean>;
|
|
18
|
-
hasValidTableReference(): boolean;
|
|
19
|
-
private isTablePartitioned;
|
|
20
|
-
isValidPartitionForExistingTable(): Promise<boolean>;
|
|
21
|
-
convertDateValue(fieldValue: Date): string;
|
|
22
|
-
getPartitionValue(event: FirestoreDocumentChangeEvent): {
|
|
23
|
-
[x: string]: any;
|
|
24
|
-
};
|
|
25
|
-
customFieldExists(fields?: any[]): boolean;
|
|
26
|
-
private shouldAddPartitioningToSchema;
|
|
27
|
-
addPartitioningToSchema(fields?: any[]): Promise<void>;
|
|
28
|
-
updateTableMetadata(options: bigquery.TableMetadata): Promise<void>;
|
|
29
|
-
}
|
|
@@ -1,277 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.Partitioning = void 0;
|
|
4
|
-
const __1 = require("..");
|
|
5
|
-
const firebase = require("firebase-admin");
|
|
6
|
-
const logs = require("../logs");
|
|
7
|
-
const functions = require("firebase-functions");
|
|
8
|
-
const schema_1 = require("./schema");
|
|
9
|
-
const bigquery_1 = require("@google-cloud/bigquery");
|
|
10
|
-
const types_1 = require("../types");
|
|
11
|
-
class Partitioning {
|
|
12
|
-
constructor(config, table, schema) {
|
|
13
|
-
this.config = config;
|
|
14
|
-
this.table = table;
|
|
15
|
-
this.schema = schema;
|
|
16
|
-
}
|
|
17
|
-
isPartitioningEnabled() {
|
|
18
|
-
const { timePartitioning } = this.config;
|
|
19
|
-
return !!timePartitioning;
|
|
20
|
-
}
|
|
21
|
-
isValidPartitionTypeString(value) {
|
|
22
|
-
return typeof value === "string";
|
|
23
|
-
}
|
|
24
|
-
async metaDataSchemaFields() {
|
|
25
|
-
let metadata;
|
|
26
|
-
try {
|
|
27
|
-
[metadata] = await this.table.getMetadata();
|
|
28
|
-
}
|
|
29
|
-
catch {
|
|
30
|
-
console.log("No metadata found");
|
|
31
|
-
return null;
|
|
32
|
-
}
|
|
33
|
-
/** Return null if no valid schema on table **/
|
|
34
|
-
if (!metadata.schema)
|
|
35
|
-
return null;
|
|
36
|
-
return metadata.schema.fields;
|
|
37
|
-
}
|
|
38
|
-
isValidPartitionTypeDate(value) {
|
|
39
|
-
/* Check if valid timestamp value from sdk */
|
|
40
|
-
// if (value instanceof firebase.firestore.Timestamp) return true;
|
|
41
|
-
if (isTimestampLike(value))
|
|
42
|
-
return true;
|
|
43
|
-
/* Check if valid date/timstemap, expedted result from production */
|
|
44
|
-
if (value && value.toDate && value.toDate())
|
|
45
|
-
return true;
|
|
46
|
-
/* Check if valid date/time value from the console, expected result from testing locally */
|
|
47
|
-
return Object.prototype.toString.call(value) === "[object Date]";
|
|
48
|
-
}
|
|
49
|
-
hasHourAndDatePartitionConfig() {
|
|
50
|
-
if (this.config.timePartitioning === "HOUR" &&
|
|
51
|
-
this.config.timePartitioningFieldType === "DATE") {
|
|
52
|
-
logs.hourAndDatePartitioningWarning();
|
|
53
|
-
return true;
|
|
54
|
-
}
|
|
55
|
-
return false;
|
|
56
|
-
}
|
|
57
|
-
hasValidCustomPartitionConfig() {
|
|
58
|
-
/* Return false if partition type option has not been set*/
|
|
59
|
-
if (!this.isPartitioningEnabled())
|
|
60
|
-
return false;
|
|
61
|
-
const { timePartitioningField, timePartitioningFieldType, timePartitioningFirestoreField, } = this.config;
|
|
62
|
-
const hasNoCustomOptions = !timePartitioningField &&
|
|
63
|
-
!timePartitioningFieldType &&
|
|
64
|
-
!timePartitioningFirestoreField;
|
|
65
|
-
/* No custom config has been set, use partition value option only */
|
|
66
|
-
if (hasNoCustomOptions)
|
|
67
|
-
return true;
|
|
68
|
-
/* check if all valid combinations have been provided*/
|
|
69
|
-
const hasOnlyTimestamp = timePartitioningField === "timestamp" &&
|
|
70
|
-
!timePartitioningFieldType &&
|
|
71
|
-
!timePartitioningFirestoreField;
|
|
72
|
-
return (hasOnlyTimestamp ||
|
|
73
|
-
(!!timePartitioningField &&
|
|
74
|
-
!!timePartitioningFieldType &&
|
|
75
|
-
!!timePartitioningFirestoreField));
|
|
76
|
-
}
|
|
77
|
-
hasValidTimePartitionOption() {
|
|
78
|
-
const { timePartitioning } = this.config;
|
|
79
|
-
return ["HOUR", "DAY", "MONTH", "YEAR"].includes(timePartitioning);
|
|
80
|
-
}
|
|
81
|
-
hasValidTimePartitionType() {
|
|
82
|
-
const { timePartitioningFieldType } = this.config;
|
|
83
|
-
if (!timePartitioningFieldType || timePartitioningFieldType === undefined)
|
|
84
|
-
return true;
|
|
85
|
-
return ["TIMESTAMP", "DATE", "DATETIME"].includes(timePartitioningFieldType);
|
|
86
|
-
}
|
|
87
|
-
async hasExistingSchema() {
|
|
88
|
-
const [metadata] = await this.table.getMetadata();
|
|
89
|
-
return !!metadata.schema;
|
|
90
|
-
}
|
|
91
|
-
hasValidTableReference() {
|
|
92
|
-
if (!this.table) {
|
|
93
|
-
logs.invalidTableReference();
|
|
94
|
-
}
|
|
95
|
-
return !!this.table;
|
|
96
|
-
}
|
|
97
|
-
async isTablePartitioned() {
|
|
98
|
-
const [tableExists] = await this.table.exists();
|
|
99
|
-
if (!this.table || !tableExists)
|
|
100
|
-
return false;
|
|
101
|
-
/* Return true if partition metadata already exists */
|
|
102
|
-
const [metadata] = await this.table.getMetadata();
|
|
103
|
-
if (metadata.timePartitioning) {
|
|
104
|
-
logs.cannotPartitionExistingTable(this.table);
|
|
105
|
-
return true;
|
|
106
|
-
}
|
|
107
|
-
/** Find schema fields **/
|
|
108
|
-
const schemaFields = await this.metaDataSchemaFields();
|
|
109
|
-
/** Return false if no schema exists */
|
|
110
|
-
if (!schemaFields)
|
|
111
|
-
return false;
|
|
112
|
-
/* Return false if time partition field not found */
|
|
113
|
-
return schemaFields.some((column) => column.name === this.config.timePartitioningField);
|
|
114
|
-
}
|
|
115
|
-
async isValidPartitionForExistingTable() {
|
|
116
|
-
/** Return false if partition type option has not been set */
|
|
117
|
-
if (!this.isPartitioningEnabled())
|
|
118
|
-
return false;
|
|
119
|
-
/* Return false if table is already partitioned */
|
|
120
|
-
const isPartitioned = await this.isTablePartitioned();
|
|
121
|
-
if (isPartitioned)
|
|
122
|
-
return false;
|
|
123
|
-
return this.hasValidCustomPartitionConfig();
|
|
124
|
-
}
|
|
125
|
-
convertDateValue(fieldValue) {
|
|
126
|
-
const { timePartitioningFieldType } = this.config;
|
|
127
|
-
/* Return as Datetime value */
|
|
128
|
-
if (timePartitioningFieldType === types_1.PartitionFieldType.DATETIME) {
|
|
129
|
-
return bigquery_1.BigQuery.datetime(fieldValue.toISOString()).value;
|
|
130
|
-
}
|
|
131
|
-
/* Return as Date value */
|
|
132
|
-
if (timePartitioningFieldType === types_1.PartitionFieldType.DATE) {
|
|
133
|
-
return bigquery_1.BigQuery.date(fieldValue.toISOString().substring(0, 10)).value;
|
|
134
|
-
}
|
|
135
|
-
/* Return as Timestamp */
|
|
136
|
-
return bigquery_1.BigQuery.timestamp(fieldValue).value;
|
|
137
|
-
}
|
|
138
|
-
/*
|
|
139
|
-
Extracts a valid Partition field from the Document Change Event.
|
|
140
|
-
Matches result based on a pre-defined Firestore field matching the event data object.
|
|
141
|
-
Return an empty object if no field name or value provided.
|
|
142
|
-
Returns empty object if not a string or timestamp (or result of serializing a timestamp)
|
|
143
|
-
Logs warning if not a valid datatype
|
|
144
|
-
Delete changes events have no data, return early as cannot partition on empty data.
|
|
145
|
-
**/
|
|
146
|
-
getPartitionValue(event) {
|
|
147
|
-
// When old data is disabled and the operation is delete
|
|
148
|
-
// the data and old data will be null
|
|
149
|
-
if (event.data == null && event.oldData == null)
|
|
150
|
-
return {};
|
|
151
|
-
const firestoreFieldName = this.config.timePartitioningFirestoreField;
|
|
152
|
-
const fieldName = this.config.timePartitioningField;
|
|
153
|
-
const fieldValue = event.operation === __1.ChangeType.DELETE
|
|
154
|
-
? event.oldData[firestoreFieldName]
|
|
155
|
-
: event.data[firestoreFieldName];
|
|
156
|
-
if (!fieldName || !fieldValue) {
|
|
157
|
-
return {};
|
|
158
|
-
}
|
|
159
|
-
if (this.isValidPartitionTypeString(fieldValue)) {
|
|
160
|
-
return { [fieldName]: fieldValue };
|
|
161
|
-
}
|
|
162
|
-
if (this.isValidPartitionTypeDate(fieldValue)) {
|
|
163
|
-
/* Return converted console value */
|
|
164
|
-
if (isTimestampLike(fieldValue)) {
|
|
165
|
-
const convertedTimestampFieldValue = convertToTimestamp(fieldValue);
|
|
166
|
-
return {
|
|
167
|
-
[fieldName]: this.convertDateValue(convertedTimestampFieldValue.toDate()),
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
if (fieldValue.toDate) {
|
|
171
|
-
return { [fieldName]: this.convertDateValue(fieldValue.toDate()) };
|
|
172
|
-
}
|
|
173
|
-
/* Return standard date value */
|
|
174
|
-
return { [fieldName]: fieldValue };
|
|
175
|
-
}
|
|
176
|
-
logs.firestoreTimePartitionFieldError(event.documentName, fieldName, firestoreFieldName, fieldValue);
|
|
177
|
-
return {};
|
|
178
|
-
}
|
|
179
|
-
customFieldExists(fields = []) {
|
|
180
|
-
/** Extract the time partioning field name */
|
|
181
|
-
const { timePartitioningField } = this.config;
|
|
182
|
-
/** Return based the field already exist */
|
|
183
|
-
return fields.map(($) => $.name).includes(timePartitioningField);
|
|
184
|
-
}
|
|
185
|
-
async shouldAddPartitioningToSchema(fields) {
|
|
186
|
-
if (!this.isPartitioningEnabled()) {
|
|
187
|
-
return { proceed: false, message: "Partitioning not enabled" };
|
|
188
|
-
}
|
|
189
|
-
if (!this.hasValidTableReference()) {
|
|
190
|
-
return { proceed: false, message: "Invalid table reference" };
|
|
191
|
-
}
|
|
192
|
-
if (!this.hasValidCustomPartitionConfig()) {
|
|
193
|
-
return { proceed: false, message: "Invalid partition config" };
|
|
194
|
-
}
|
|
195
|
-
if (!this.hasValidTimePartitionType()) {
|
|
196
|
-
return { proceed: false, message: "Invalid partition type" };
|
|
197
|
-
}
|
|
198
|
-
if (!this.hasValidTimePartitionOption()) {
|
|
199
|
-
return { proceed: false, message: "Invalid partition option" };
|
|
200
|
-
}
|
|
201
|
-
if (this.hasHourAndDatePartitionConfig()) {
|
|
202
|
-
return {
|
|
203
|
-
proceed: false,
|
|
204
|
-
message: "Invalid partitioning and field type combination",
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
|
-
if (this.customFieldExists(fields)) {
|
|
208
|
-
return { proceed: false, message: "Field already exists on schema" };
|
|
209
|
-
}
|
|
210
|
-
if (await this.isTablePartitioned()) {
|
|
211
|
-
return { proceed: false, message: "Table is already partitioned" };
|
|
212
|
-
}
|
|
213
|
-
if (!this.config.timePartitioningField) {
|
|
214
|
-
return { proceed: false, message: "Partition field not provided" };
|
|
215
|
-
}
|
|
216
|
-
return { proceed: true, message: "" };
|
|
217
|
-
}
|
|
218
|
-
async addPartitioningToSchema(fields = []) {
|
|
219
|
-
const { proceed, message } = await this.shouldAddPartitioningToSchema(fields);
|
|
220
|
-
if (!proceed) {
|
|
221
|
-
functions.logger.warn(`Did not add partitioning to schema: ${message}`);
|
|
222
|
-
return;
|
|
223
|
-
}
|
|
224
|
-
// Add new partitioning field
|
|
225
|
-
fields.push((0, schema_1.getNewPartitionField)(this.config));
|
|
226
|
-
functions.logger.log(`Added new partition field: ${this.config.timePartitioningField} to table ID: ${this.table.id}`);
|
|
227
|
-
}
|
|
228
|
-
async updateTableMetadata(options) {
|
|
229
|
-
/** Return if partition type option has not been set */
|
|
230
|
-
if (!this.isPartitioningEnabled())
|
|
231
|
-
return;
|
|
232
|
-
/** Return if class has invalid table reference */
|
|
233
|
-
if (!this.hasValidTableReference())
|
|
234
|
-
return;
|
|
235
|
-
/** Return if table is already partitioned **/
|
|
236
|
-
if (await this.isTablePartitioned())
|
|
237
|
-
return;
|
|
238
|
-
/** Return if an invalid partition type has been requested**/
|
|
239
|
-
if (!this.hasValidCustomPartitionConfig())
|
|
240
|
-
return;
|
|
241
|
-
/** Return if an invalid partition type has been requested**/
|
|
242
|
-
if (!this.hasValidTimePartitionType())
|
|
243
|
-
return;
|
|
244
|
-
/** Update fields with new schema option ** */
|
|
245
|
-
if (!this.hasValidTimePartitionOption())
|
|
246
|
-
return;
|
|
247
|
-
/** Return if invalid partitioning and field type combination */
|
|
248
|
-
if (this.hasHourAndDatePartitionConfig())
|
|
249
|
-
return;
|
|
250
|
-
if (this.config.timePartitioning) {
|
|
251
|
-
options.timePartitioning = { type: this.config.timePartitioning };
|
|
252
|
-
}
|
|
253
|
-
//TODO: Add check for skipping adding views partition field, this is not a feature that can be added .
|
|
254
|
-
if (this.config.timePartitioningField) {
|
|
255
|
-
options.timePartitioning = {
|
|
256
|
-
...options.timePartitioning,
|
|
257
|
-
field: this.config.timePartitioningField,
|
|
258
|
-
};
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
exports.Partitioning = Partitioning;
|
|
263
|
-
const isTimestampLike = (value) => {
|
|
264
|
-
if (value instanceof firebase.firestore.Timestamp)
|
|
265
|
-
return true;
|
|
266
|
-
return (typeof value === "object" &&
|
|
267
|
-
value !== null &&
|
|
268
|
-
"_seconds" in value &&
|
|
269
|
-
typeof value["_seconds"] === "number" &&
|
|
270
|
-
"_nanoseconds" in value &&
|
|
271
|
-
typeof value["_nanoseconds"] === "number");
|
|
272
|
-
};
|
|
273
|
-
const convertToTimestamp = (value) => {
|
|
274
|
-
if (value instanceof firebase.firestore.Timestamp)
|
|
275
|
-
return value;
|
|
276
|
-
return new firebase.firestore.Timestamp(value._seconds, value._nanoseconds);
|
|
277
|
-
};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,65 +0,0 @@
|
|
|
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
|
-
});
|