@firebase-function-kits/bigquery-firestore-export 0.0.2-rc.1 → 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/README.md +23 -9
- package/lib/config.d.ts.map +1 -1
- package/lib/config.js +15 -2
- package/lib/config.js.map +1 -1
- package/lib/dts.d.ts +8 -0
- package/lib/dts.d.ts.map +1 -1
- package/lib/dts.js +15 -2
- package/lib/dts.js.map +1 -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.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 +7 -1215
- package/package.json +2 -5
- package/src/config.ts +18 -2
- package/src/dts.ts +17 -2
- package/src/handlers.ts +36 -9
- package/src/helper.ts +1 -0
- 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 +17 -0
- 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
|
@@ -76,6 +76,22 @@ const params = {
|
|
|
76
76
|
}),
|
|
77
77
|
}),
|
|
78
78
|
transferConfigName: defineString("TRANSFER_CONFIG_NAME", { default: "" }),
|
|
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
|
+
}),
|
|
79
95
|
datasetId: defineString("DATASET_ID", {
|
|
80
96
|
label: "Dataset ID",
|
|
81
97
|
description:
|
|
@@ -168,7 +184,7 @@ const params = {
|
|
|
168
184
|
};
|
|
169
185
|
|
|
170
186
|
export const CONFIG_EXPRESSIONS: DeployTimeOptions = {
|
|
171
|
-
pubSubTopic:
|
|
187
|
+
pubSubTopic: params.pubSubTopic,
|
|
172
188
|
};
|
|
173
189
|
|
|
174
190
|
function optional(value: string): string | undefined {
|
|
@@ -198,7 +214,7 @@ export function configFromEnv(): BigqueryFirestoreExportConfig {
|
|
|
198
214
|
displayName: params.displayName.value(),
|
|
199
215
|
partitioningField: optional(params.partitioningField.value()),
|
|
200
216
|
schedule: params.schedule.value(),
|
|
201
|
-
pubSubTopic:
|
|
217
|
+
pubSubTopic: params.pubSubTopic.value(),
|
|
202
218
|
firestoreCollection: params.firestoreCollection.value(),
|
|
203
219
|
logLevel: normalizeLogLevel(params.logLevel.value()),
|
|
204
220
|
};
|
package/src/dts.ts
CHANGED
|
@@ -61,6 +61,21 @@ 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
|
}
|
|
@@ -93,7 +108,7 @@ export function createTransferConfigRequest(
|
|
|
93
108
|
},
|
|
94
109
|
},
|
|
95
110
|
schedule: config.schedule,
|
|
96
|
-
notificationPubsubTopic:
|
|
111
|
+
notificationPubsubTopic: notificationTopicName(config),
|
|
97
112
|
},
|
|
98
113
|
};
|
|
99
114
|
}
|
|
@@ -184,7 +199,7 @@ export async function constructUpdateTransferConfigRequest(
|
|
|
184
199
|
updatedConfig.schedule = config.schedule;
|
|
185
200
|
}
|
|
186
201
|
|
|
187
|
-
const expectedTopic =
|
|
202
|
+
const expectedTopic = notificationTopicName(config);
|
|
188
203
|
if (expectedTopic !== transferConfig.notificationPubsubTopic) {
|
|
189
204
|
updateMask.push("notification_pubsub_topic");
|
|
190
205
|
updatedConfig.notificationPubsubTopic = expectedTopic;
|
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/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
|
});
|