@firebase-function-kits/bigquery-firestore-export 0.0.2-rc.0 → 0.0.2-rc.2
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/CHANGELOG.md +1 -1
- package/README.md +138 -4
- package/lib/config.d.ts.map +1 -1
- package/lib/config.js +128 -9
- package/lib/config.js.map +1 -1
- package/lib/dts.d.ts +17 -2
- package/lib/dts.d.ts.map +1 -1
- package/lib/dts.js +25 -8
- package/lib/dts.js.map +1 -1
- package/lib/export-config.d.ts +0 -3
- package/lib/export-config.d.ts.map +1 -1
- package/lib/export-config.js +0 -1
- package/lib/export-config.js.map +1 -1
- package/lib/handlers.d.ts.map +1 -1
- package/lib/handlers.js +26 -4
- package/lib/handlers.js.map +1 -1
- package/lib/helper.d.ts.map +1 -1
- package/lib/helper.js +1 -0
- package/lib/helper.js.map +1 -1
- package/lib/index.js +1 -1
- package/lib/index.js.map +1 -1
- package/lib/logs.d.ts +4 -0
- package/lib/logs.d.ts.map +1 -1
- package/lib/logs.js +18 -0
- package/lib/logs.js.map +1 -1
- package/lib/metadata.d.ts.map +1 -1
- package/npm-shrinkwrap.json +3841 -0
- package/package.json +2 -5
- package/src/config.ts +142 -9
- package/src/dts.ts +27 -9
- package/src/export-config.ts +0 -4
- package/src/handlers.ts +36 -9
- package/src/helper.ts +1 -0
- package/src/index.ts +1 -1
- package/src/logs.ts +28 -0
- package/tests/config.test.ts +54 -2
- package/tests/dts-transfer-config.test.ts +400 -0
- package/tests/dts.test.ts +18 -5
- package/tests/export-config.test.ts +0 -4
- package/tests/handlers.test.ts +113 -5
- package/tests/helper-values.test.ts +168 -0
- package/tests/helper.test.ts +65 -3
- package/tests/notification-topic.test.ts +151 -0
- package/tests/run-results.test.ts +373 -0
- package/tsconfig.tsbuildinfo +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@firebase-function-kits/bigquery-firestore-export",
|
|
3
|
-
"version": "0.0.2-rc.
|
|
3
|
+
"version": "0.0.2-rc.2",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/firebase/extensions.git",
|
|
@@ -37,10 +37,7 @@
|
|
|
37
37
|
"@google-cloud/bigquery-data-transfer": "^5.0.1",
|
|
38
38
|
"@google-cloud/pubsub": "^4.11.0",
|
|
39
39
|
"firebase-admin": "^14.1.0",
|
|
40
|
-
"firebase-functions": "7.3.2"
|
|
41
|
-
},
|
|
42
|
-
"devDependencies": {
|
|
43
|
-
"vitest": "^4.1.10"
|
|
40
|
+
"firebase-functions": "^7.3.2"
|
|
44
41
|
},
|
|
45
42
|
"overrides": {
|
|
46
43
|
"uuid@9.0.1": "11.1.1"
|
package/src/config.ts
CHANGED
|
@@ -32,26 +32,159 @@ const instanceId = defineString("INSTANCE_ID");
|
|
|
32
32
|
const params = {
|
|
33
33
|
instanceId,
|
|
34
34
|
bigqueryDatasetLocation: defineString("BIGQUERY_DATASET_LOCATION", {
|
|
35
|
+
label: "BigQuery Dataset Location",
|
|
36
|
+
description:
|
|
37
|
+
"What is the location of the BigQuery dataset referenced in the query?",
|
|
38
|
+
|
|
35
39
|
default: "US",
|
|
40
|
+
input: select({
|
|
41
|
+
"Columbus, Ohio (us-east5)": "us-east5",
|
|
42
|
+
"Iowa (us-central1)": "us-central1",
|
|
43
|
+
"Las Vegas (us-west4)": "us-west4",
|
|
44
|
+
"Los Angeles (us-west2)": "us-west2",
|
|
45
|
+
"Montréal (northamerica-northeast1)": "northamerica-northeast1",
|
|
46
|
+
"Northern Virginia (us-east4)": "us-east4",
|
|
47
|
+
"Oregon (us-west1)": "us-west1",
|
|
48
|
+
"Salt Lake City (us-west3)": "us-west3",
|
|
49
|
+
"São Paulo (southamerica-east1)": "southamerica-east1",
|
|
50
|
+
"Santiago (southamerica-west1)": "southamerica-west1",
|
|
51
|
+
"South Carolina (us-east1)": "us-east1",
|
|
52
|
+
"Toronto (northamerica-northeast2)": "northamerica-northeast2",
|
|
53
|
+
"Delhi (asia-south2)": "asia-south2",
|
|
54
|
+
"Hong Kong (asia-east2)": "asia-east2",
|
|
55
|
+
"Jakarta (asia-southeast2)": "asia-southeast2",
|
|
56
|
+
"Melbourne (australia-southeast2)": "australia-southeast2",
|
|
57
|
+
"Mumbai (asia-south1)": "asia-south1",
|
|
58
|
+
"Osaka (asia-northeast2)": "asia-northeast2",
|
|
59
|
+
"Seoul (asia-northeast3)": "asia-northeast3",
|
|
60
|
+
"Singapore (asia-southeast1)": "asia-southeast1",
|
|
61
|
+
"Sydney (australia-southeast1)": "australia-southeast1",
|
|
62
|
+
"Taiwan (asia-east1)": "asia-east1",
|
|
63
|
+
"Tokyo (asia-northeast1)": "asia-northeast1",
|
|
64
|
+
"Belgium (europe-west1)": "europe-west1",
|
|
65
|
+
"Finland (europe-north1)": "europe-north1",
|
|
66
|
+
"Frankfurt (europe-west3)": "europe-west3",
|
|
67
|
+
"London (europe-west2)": "europe-west2",
|
|
68
|
+
"Madrid (europe-southwest1)": "europe-southwest1",
|
|
69
|
+
"Milan (europe-west8)": "europe-west8",
|
|
70
|
+
"Netherlands (europe-west4)": "europe-west4",
|
|
71
|
+
"Paris (europe-west9)": "europe-west9",
|
|
72
|
+
"Warsaw (europe-central2)": "europe-central2",
|
|
73
|
+
"Zürich (europe-west6)": "europe-west6",
|
|
74
|
+
"US Multi-Region (US)": "US",
|
|
75
|
+
"EU Mutli-Region (EU)": "EU",
|
|
76
|
+
}),
|
|
36
77
|
}),
|
|
37
78
|
transferConfigName: defineString("TRANSFER_CONFIG_NAME", { default: "" }),
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
79
|
+
pubSubTopic: defineString("PUB_SUB_TOPIC", {
|
|
80
|
+
label: "Pub/Sub Topic",
|
|
81
|
+
description:
|
|
82
|
+
"Which Pub/Sub topic should receive BigQuery Data Transfer completion notifications? Leave the default unless you are migrating from the bigquery-firestore-export extension, whose topic is named ext-<instance id>-processMessages. Pointing this at the extension's topic keeps the existing scheduled query's notification settings untouched.",
|
|
83
|
+
|
|
84
|
+
default: expr`kit-${instanceId}-processMessages`,
|
|
85
|
+
input: {
|
|
86
|
+
text: {
|
|
87
|
+
nonEmpty: true,
|
|
88
|
+
example: "ext-my-instance-processMessages",
|
|
89
|
+
validationRegex: /^(?!goog)[a-zA-Z][a-zA-Z0-9\-_.~+%]{2,254}$/,
|
|
90
|
+
validationErrorMessage:
|
|
91
|
+
"Must be a Pub/Sub topic ID, not a full projects/<project>/topics/<topic> resource name. IDs are 3 to 255 characters, start with a letter, may contain letters, numbers and - _ . ~ + %, and cannot start with goog.",
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
}),
|
|
95
|
+
datasetId: defineString("DATASET_ID", {
|
|
96
|
+
label: "Dataset ID",
|
|
97
|
+
description:
|
|
98
|
+
"What's the BigQuery destination dataset you'd like to use? Each transfer run will write to a table in this destination dataset.",
|
|
99
|
+
input: {
|
|
100
|
+
text: {
|
|
101
|
+
nonEmpty: true,
|
|
102
|
+
example: "customer_data",
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
}),
|
|
106
|
+
tableName: defineString("TABLE_NAME", {
|
|
107
|
+
label: "Destination Table Name",
|
|
108
|
+
description:
|
|
109
|
+
"What's the destination table name prefix you'd like to use? Each transfer run will write to the table with this name, postfixed with the runtime.",
|
|
110
|
+
input: {
|
|
111
|
+
text: {
|
|
112
|
+
nonEmpty: true,
|
|
113
|
+
example: "transactions",
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
}),
|
|
117
|
+
queryString: defineString("QUERY_STRING", {
|
|
118
|
+
label: "Query String",
|
|
119
|
+
description: "What's the BQ query you'd like to execute?",
|
|
120
|
+
input: {
|
|
121
|
+
text: {
|
|
122
|
+
nonEmpty: true,
|
|
123
|
+
|
|
124
|
+
example: "SELECT * from <PROJECT_ID>.customer_data.transactions",
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
}),
|
|
128
|
+
displayName: defineString("DISPLAY_NAME", {
|
|
129
|
+
label: "Display Name",
|
|
130
|
+
description: "What display name would you like to use?",
|
|
131
|
+
input: {
|
|
132
|
+
text: {
|
|
133
|
+
nonEmpty: true,
|
|
134
|
+
example: "Daily Rollup - Customer Transactions",
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
}),
|
|
138
|
+
partitioningField: defineString("PARTITIONING_FIELD", {
|
|
139
|
+
label: "Partitioning Field",
|
|
140
|
+
description:
|
|
141
|
+
"What's the partitioning field on the destination table ID? Leave empty if not using a partitioning field.",
|
|
142
|
+
default: "",
|
|
143
|
+
input: { text: { example: "timestamp" } },
|
|
144
|
+
}),
|
|
145
|
+
schedule: defineString("SCHEDULE", {
|
|
146
|
+
label: "Schedule",
|
|
147
|
+
description:
|
|
148
|
+
"What's the execution schedule you'd like to use for this query?",
|
|
149
|
+
input: {
|
|
150
|
+
text: {
|
|
151
|
+
nonEmpty: true,
|
|
152
|
+
example: "every 15 minutes",
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
}),
|
|
44
156
|
firestoreCollection: defineString("COLLECTION_PATH", {
|
|
157
|
+
label: "Firestore Collection",
|
|
158
|
+
description:
|
|
159
|
+
"What's the top-level Firestore Collection to store transfer configs, run metadata, and query output?",
|
|
160
|
+
|
|
45
161
|
default: "transferConfigs",
|
|
162
|
+
input: {
|
|
163
|
+
text: {
|
|
164
|
+
example: "transferConfigs",
|
|
165
|
+
|
|
166
|
+
validationRegex: /^[^\/]+(\/[^\/]+\/[^\/]+)*$/,
|
|
167
|
+
validationErrorMessage: "Must be a valid Cloud Firestore Collection",
|
|
168
|
+
},
|
|
169
|
+
},
|
|
46
170
|
}),
|
|
47
171
|
logLevel: defineString("LOG_LEVEL", {
|
|
172
|
+
label: "Log Level",
|
|
173
|
+
description: "What's the log level you'd like to use for this extension?",
|
|
174
|
+
|
|
48
175
|
default: "info",
|
|
49
|
-
input: select(
|
|
176
|
+
input: select({
|
|
177
|
+
DEBUG: "debug",
|
|
178
|
+
INFO: "info",
|
|
179
|
+
WARN: "warn",
|
|
180
|
+
ERROR: "error",
|
|
181
|
+
SILENT: "silent",
|
|
182
|
+
}),
|
|
50
183
|
}),
|
|
51
184
|
};
|
|
52
185
|
|
|
53
186
|
export const CONFIG_EXPRESSIONS: DeployTimeOptions = {
|
|
54
|
-
pubSubTopic:
|
|
187
|
+
pubSubTopic: params.pubSubTopic,
|
|
55
188
|
};
|
|
56
189
|
|
|
57
190
|
function optional(value: string): string | undefined {
|
|
@@ -81,7 +214,7 @@ export function configFromEnv(): BigqueryFirestoreExportConfig {
|
|
|
81
214
|
displayName: params.displayName.value(),
|
|
82
215
|
partitioningField: optional(params.partitioningField.value()),
|
|
83
216
|
schedule: params.schedule.value(),
|
|
84
|
-
pubSubTopic:
|
|
217
|
+
pubSubTopic: params.pubSubTopic.value(),
|
|
85
218
|
firestoreCollection: params.firestoreCollection.value(),
|
|
86
219
|
logLevel: normalizeLogLevel(params.logLevel.value()),
|
|
87
220
|
};
|
package/src/dts.ts
CHANGED
|
@@ -61,14 +61,35 @@ function transferConfigFields(config: TransferConfig) {
|
|
|
61
61
|
return fields;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Full resource name of the topic DTS publishes run notifications to.
|
|
66
|
+
*
|
|
67
|
+
* PUB_SUB_TOPIC is validated as a bare topic ID when prompted, but values from
|
|
68
|
+
* a dotenv file reach us unvalidated, and both the Pub/Sub client and the
|
|
69
|
+
* trigger accept a full resource name, so accept one here too.
|
|
70
|
+
*/
|
|
71
|
+
export function notificationTopicName(
|
|
72
|
+
config: ResolvedBigqueryFirestoreExportConfig
|
|
73
|
+
): string {
|
|
74
|
+
return config.pubSubTopic.startsWith("projects/")
|
|
75
|
+
? config.pubSubTopic
|
|
76
|
+
: `projects/${config.projectId}/topics/${config.pubSubTopic}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
64
79
|
function stringField(value: string | undefined): { stringValue: string } {
|
|
65
80
|
return { stringValue: value ?? "" };
|
|
66
81
|
}
|
|
67
82
|
|
|
68
|
-
/**
|
|
83
|
+
/**
|
|
84
|
+
* Creates the protobuf-shaped request used for a scheduled query.
|
|
85
|
+
*
|
|
86
|
+
* The query runs as whichever identity creates it, which is this function. To
|
|
87
|
+
* name a different account, `serviceAccountName` goes on the request rather
|
|
88
|
+
* than on the transfer config, and the caller needs `actAs` on the account it
|
|
89
|
+
* names, including its own.
|
|
90
|
+
*/
|
|
69
91
|
export function createTransferConfigRequest(
|
|
70
|
-
config: ResolvedBigqueryFirestoreExportConfig
|
|
71
|
-
serviceAccountEmail?: string
|
|
92
|
+
config: ResolvedBigqueryFirestoreExportConfig
|
|
72
93
|
): bigqueryDataTransfer.protos.google.cloud.bigquery.datatransfer.v1.ICreateTransferConfigRequest {
|
|
73
94
|
return {
|
|
74
95
|
parent: `projects/${config.projectId}`,
|
|
@@ -87,10 +108,7 @@ export function createTransferConfigRequest(
|
|
|
87
108
|
},
|
|
88
109
|
},
|
|
89
110
|
schedule: config.schedule,
|
|
90
|
-
notificationPubsubTopic:
|
|
91
|
-
...(serviceAccountEmail
|
|
92
|
-
? { serviceAccountName: serviceAccountEmail }
|
|
93
|
-
: {}),
|
|
111
|
+
notificationPubsubTopic: notificationTopicName(config),
|
|
94
112
|
},
|
|
95
113
|
};
|
|
96
114
|
}
|
|
@@ -120,7 +138,7 @@ export async function createTransferConfig(
|
|
|
120
138
|
): Promise<TransferConfig> {
|
|
121
139
|
logs.createTransferConfig();
|
|
122
140
|
const [created] = await client.createTransferConfig(
|
|
123
|
-
createTransferConfigRequest(config
|
|
141
|
+
createTransferConfigRequest(config)
|
|
124
142
|
);
|
|
125
143
|
if (!created.name) {
|
|
126
144
|
throw new Error("BigQuery API returned a transfer config without a name");
|
|
@@ -181,7 +199,7 @@ export async function constructUpdateTransferConfigRequest(
|
|
|
181
199
|
updatedConfig.schedule = config.schedule;
|
|
182
200
|
}
|
|
183
201
|
|
|
184
|
-
const expectedTopic =
|
|
202
|
+
const expectedTopic = notificationTopicName(config);
|
|
185
203
|
if (expectedTopic !== transferConfig.notificationPubsubTopic) {
|
|
186
204
|
updateMask.push("notification_pubsub_topic");
|
|
187
205
|
updatedConfig.notificationPubsubTopic = expectedTopic;
|
package/src/export-config.ts
CHANGED
|
@@ -45,8 +45,6 @@ export interface BigqueryFirestoreExportConfig {
|
|
|
45
45
|
pubSubTopic?: string;
|
|
46
46
|
/** Root Firestore collection for configs and run output. */
|
|
47
47
|
firestoreCollection?: string;
|
|
48
|
-
/** Runtime identity used when creating a DTS config. */
|
|
49
|
-
serviceAccount?: string;
|
|
50
48
|
/** Log verbosity. Defaults to `info`. */
|
|
51
49
|
logLevel?: LogLevel;
|
|
52
50
|
}
|
|
@@ -65,7 +63,6 @@ export interface ResolvedBigqueryFirestoreExportConfig {
|
|
|
65
63
|
schedule: string;
|
|
66
64
|
pubSubTopic: string;
|
|
67
65
|
firestoreCollection: string;
|
|
68
|
-
serviceAccount?: string;
|
|
69
66
|
logLevel: LogLevel;
|
|
70
67
|
}
|
|
71
68
|
|
|
@@ -117,7 +114,6 @@ export function resolveConfig(
|
|
|
117
114
|
optional(config.pubSubTopic) ?? `kit-${instanceId}-processMessages`,
|
|
118
115
|
firestoreCollection:
|
|
119
116
|
optional(config.firestoreCollection) ?? "transferConfigs",
|
|
120
|
-
serviceAccount: optional(config.serviceAccount),
|
|
121
117
|
logLevel,
|
|
122
118
|
};
|
|
123
119
|
}
|
package/src/handlers.ts
CHANGED
|
@@ -23,6 +23,8 @@ import {
|
|
|
23
23
|
createTransferConfig,
|
|
24
24
|
type DataTransferClient,
|
|
25
25
|
getTransferConfig,
|
|
26
|
+
notificationTopicName,
|
|
27
|
+
PARTITIONING_FIELD_REMOVAL_ERROR_PREFIX,
|
|
26
28
|
updateTransferConfig,
|
|
27
29
|
} from "./dts";
|
|
28
30
|
import type { ResolvedBigqueryFirestoreExportConfig } from "./export-config";
|
|
@@ -64,6 +66,13 @@ async function ensureNotificationTopic(ctx: HandlerContext): Promise<void> {
|
|
|
64
66
|
}
|
|
65
67
|
}
|
|
66
68
|
|
|
69
|
+
function isPartitioningFieldRemovalError(err: unknown): err is Error {
|
|
70
|
+
return (
|
|
71
|
+
err instanceof Error &&
|
|
72
|
+
err.message.includes(PARTITIONING_FIELD_REMOVAL_ERROR_PREFIX)
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
67
76
|
async function storeTransferConfig(
|
|
68
77
|
ctx: HandlerContext,
|
|
69
78
|
transferConfig: Awaited<ReturnType<typeof getTransferConfig>>
|
|
@@ -105,8 +114,19 @@ export async function handleUpsertTransferConfig(
|
|
|
105
114
|
ctx.config.transferConfigName
|
|
106
115
|
);
|
|
107
116
|
if (!linked) {
|
|
108
|
-
|
|
109
|
-
|
|
117
|
+
// Only a redeploy with a corrected TRANSFER_CONFIG_NAME can resolve this,
|
|
118
|
+
// so retrying the task cannot help.
|
|
119
|
+
logs.linkedTransferConfigMissing(ctx.config.transferConfigName);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
// A linked config is adopted as-is, so a topic mismatch is the user's to
|
|
123
|
+
// resolve: rewriting it would repoint a config this deployment did not create.
|
|
124
|
+
const expectedTopic = notificationTopicName(ctx.config);
|
|
125
|
+
if (linked.notificationPubsubTopic !== expectedTopic) {
|
|
126
|
+
logs.linkedTopicMismatch(
|
|
127
|
+
ctx.config.transferConfigName,
|
|
128
|
+
linked.notificationPubsubTopic,
|
|
129
|
+
expectedTopic
|
|
110
130
|
);
|
|
111
131
|
}
|
|
112
132
|
await storeTransferConfig(ctx, linked);
|
|
@@ -128,14 +148,21 @@ export async function handleUpsertTransferConfig(
|
|
|
128
148
|
const transferConfigName = existing.docs[0].data().name;
|
|
129
149
|
if (typeof transferConfigName !== "string" || !transferConfigName) {
|
|
130
150
|
throw new Error(
|
|
131
|
-
`Existing transfer config document in ${ctx.config.firestoreCollection} is missing required 'name' field.`
|
|
151
|
+
`Existing transfer config document in ${ctx.config.firestoreCollection} is missing required 'name' field. Delete the document so a new scheduled query is created, then redeploy.`
|
|
132
152
|
);
|
|
133
153
|
}
|
|
134
154
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
155
|
+
try {
|
|
156
|
+
const updated = await updateTransferConfig(
|
|
157
|
+
ctx.dataTransfer,
|
|
158
|
+
transferConfigName,
|
|
159
|
+
ctx.config
|
|
160
|
+
);
|
|
161
|
+
await storeTransferConfig(ctx, updated);
|
|
162
|
+
} catch (err) {
|
|
163
|
+
if (!isPartitioningFieldRemovalError(err)) throw err;
|
|
164
|
+
// The guard rejects the update while building the request, so nothing was
|
|
165
|
+
// sent and a retry hits the same guard.
|
|
166
|
+
logs.partitioningFieldRemovalAborted(err.message);
|
|
167
|
+
}
|
|
141
168
|
}
|
package/src/helper.ts
CHANGED
|
@@ -227,6 +227,7 @@ export async function writeRunResultsToFirestore(
|
|
|
227
227
|
message.json.destinationDatasetId,
|
|
228
228
|
tableName
|
|
229
229
|
);
|
|
230
|
+
logs.writeRunResultsToFirestore(runId);
|
|
230
231
|
const collection = db.collection(
|
|
231
232
|
`${config.firestoreCollection}/${transferConfigId}/runs/${runId}/output`
|
|
232
233
|
);
|
package/src/index.ts
CHANGED
|
@@ -111,7 +111,7 @@ function getContext(): HandlerContext {
|
|
|
111
111
|
export const processMessages = onMessagePublished<TransferRunPayload>(
|
|
112
112
|
{
|
|
113
113
|
topic: CONFIG_EXPRESSIONS.pubSubTopic,
|
|
114
|
-
retry:
|
|
114
|
+
retry: false,
|
|
115
115
|
},
|
|
116
116
|
(event) => handleMessagePublished(event, getContext())
|
|
117
117
|
);
|
package/src/logs.ts
CHANGED
|
@@ -70,6 +70,10 @@ export function bigqueryQueryFailed(
|
|
|
70
70
|
});
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
export function writeRunResultsToFirestore(runId: string): void {
|
|
74
|
+
logger.debug("Writing BigQuery results to Firestore", { runId });
|
|
75
|
+
}
|
|
76
|
+
|
|
73
77
|
export function runResultsWrittenToFirestore(
|
|
74
78
|
runId: string,
|
|
75
79
|
successCount: number,
|
|
@@ -140,6 +144,30 @@ export function partitioningFieldRemovalAttempted(
|
|
|
140
144
|
});
|
|
141
145
|
}
|
|
142
146
|
|
|
147
|
+
export function linkedTransferConfigMissing(name: string): void {
|
|
148
|
+
logger.error(
|
|
149
|
+
"The scheduled query named by TRANSFER_CONFIG_NAME does not exist, so nothing was linked. Set it to a scheduled query that exists in this project and redeploy, or clear it to have this deployment create its own.",
|
|
150
|
+
{ name }
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// The reason carries the remediation, and an Error passed as structured data
|
|
155
|
+
// serialises to {}, so it goes in the message.
|
|
156
|
+
export function partitioningFieldRemovalAborted(reason: string): void {
|
|
157
|
+
logger.error(`Stopped without updating the scheduled query. ${reason}`);
|
|
158
|
+
}
|
|
159
|
+
|
|
143
160
|
export function topicCreated(name: string): void {
|
|
144
161
|
logger.info("Created Pub/Sub topic for transfer notifications", { name });
|
|
145
162
|
}
|
|
163
|
+
|
|
164
|
+
export function linkedTopicMismatch(
|
|
165
|
+
name: string,
|
|
166
|
+
linkedTopic: string | null | undefined,
|
|
167
|
+
expectedTopic: string
|
|
168
|
+
): void {
|
|
169
|
+
logger.warn(
|
|
170
|
+
"Linked transfer config notifies a different Pub/Sub topic, so its runs will not reach this kit. Set PUB_SUB_TOPIC to the linked topic, or point the transfer config at the configured one.",
|
|
171
|
+
{ name, linkedTopic: linkedTopic ?? "", expectedTopic }
|
|
172
|
+
);
|
|
173
|
+
}
|
package/tests/config.test.ts
CHANGED
|
@@ -23,12 +23,41 @@ afterEach(() => {
|
|
|
23
23
|
});
|
|
24
24
|
|
|
25
25
|
describe("CONFIG_EXPRESSIONS", () => {
|
|
26
|
-
test("
|
|
26
|
+
test("binds the trigger to the Pub/Sub topic parameter", () => {
|
|
27
27
|
expect(CONFIG_EXPRESSIONS.pubSubTopic).toBeInstanceOf(Expression);
|
|
28
28
|
expect((CONFIG_EXPRESSIONS.pubSubTopic as Expression<string>).toCEL()).toBe(
|
|
29
|
-
"
|
|
29
|
+
"{{ params.PUB_SUB_TOPIC }}"
|
|
30
30
|
);
|
|
31
31
|
});
|
|
32
|
+
|
|
33
|
+
test("defaults the topic parameter to the instance-namespaced kit topic", () => {
|
|
34
|
+
const spec = (
|
|
35
|
+
CONFIG_EXPRESSIONS.pubSubTopic as unknown as {
|
|
36
|
+
toSpec: () => { default?: string };
|
|
37
|
+
}
|
|
38
|
+
).toSpec();
|
|
39
|
+
expect(spec.default).toBe("kit-{{ params.INSTANCE_ID }}-processMessages");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("accepts a topic ID but rejects a full resource name", () => {
|
|
43
|
+
const spec = (
|
|
44
|
+
CONFIG_EXPRESSIONS.pubSubTopic as unknown as {
|
|
45
|
+
toSpec: () => { input?: { text?: { validationRegex?: string } } };
|
|
46
|
+
}
|
|
47
|
+
).toSpec();
|
|
48
|
+
const pattern = spec.input?.text?.validationRegex;
|
|
49
|
+
expect(pattern).toBeTypeOf("string");
|
|
50
|
+
const validate = (value: string) =>
|
|
51
|
+
new RegExp(pattern as string).test(value);
|
|
52
|
+
|
|
53
|
+
expect(validate("ext-users-export-processMessages")).toBe(true);
|
|
54
|
+
expect(validate("kit-users-export-processMessages")).toBe(true);
|
|
55
|
+
expect(
|
|
56
|
+
validate("projects/test-project/topics/ext-users-export-processMessages")
|
|
57
|
+
).toBe(false);
|
|
58
|
+
expect(validate("")).toBe(false);
|
|
59
|
+
expect(validate("goog-reserved-prefix")).toBe(false);
|
|
60
|
+
});
|
|
32
61
|
});
|
|
33
62
|
|
|
34
63
|
describe("configFromEnv", () => {
|
|
@@ -47,6 +76,8 @@ describe("configFromEnv", () => {
|
|
|
47
76
|
vi.stubEnv("COLLECTION_PATH", "transferConfigs");
|
|
48
77
|
vi.stubEnv("LOG_LEVEL", "info");
|
|
49
78
|
|
|
79
|
+
vi.stubEnv("PUB_SUB_TOPIC", "kit-users-export-processMessages");
|
|
80
|
+
|
|
50
81
|
expect(configFromEnv()).toMatchObject({
|
|
51
82
|
projectId: "test-project",
|
|
52
83
|
instanceId: "users-export",
|
|
@@ -58,4 +89,25 @@ describe("configFromEnv", () => {
|
|
|
58
89
|
logLevel: "info",
|
|
59
90
|
});
|
|
60
91
|
});
|
|
92
|
+
|
|
93
|
+
test("passes through a topic pointing at the extension's own topic", () => {
|
|
94
|
+
vi.stubEnv(
|
|
95
|
+
"FIREBASE_CONFIG",
|
|
96
|
+
JSON.stringify({ projectId: "test-project" })
|
|
97
|
+
);
|
|
98
|
+
vi.stubEnv("INSTANCE_ID", "users-export");
|
|
99
|
+
vi.stubEnv("BIGQUERY_DATASET_LOCATION", "EU");
|
|
100
|
+
vi.stubEnv("DATASET_ID", "analytics");
|
|
101
|
+
vi.stubEnv("TABLE_NAME", "users");
|
|
102
|
+
vi.stubEnv("QUERY_STRING", "SELECT * FROM source.users");
|
|
103
|
+
vi.stubEnv("DISPLAY_NAME", "Users export");
|
|
104
|
+
vi.stubEnv("SCHEDULE", "every 24 hours");
|
|
105
|
+
vi.stubEnv("COLLECTION_PATH", "transferConfigs");
|
|
106
|
+
vi.stubEnv("LOG_LEVEL", "info");
|
|
107
|
+
vi.stubEnv("PUB_SUB_TOPIC", "ext-users-export-processMessages");
|
|
108
|
+
|
|
109
|
+
expect(configFromEnv().pubSubTopic).toBe(
|
|
110
|
+
"ext-users-export-processMessages"
|
|
111
|
+
);
|
|
112
|
+
});
|
|
61
113
|
});
|