@firebase-function-kits/bigquery-firestore-export 0.0.1 → 0.0.2-rc.1

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.
Files changed (63) hide show
  1. package/CHANGELOG.md +1 -0
  2. package/README.md +308 -0
  3. package/lib/config.d.ts +5 -0
  4. package/lib/config.d.ts.map +1 -0
  5. package/lib/config.js +181 -0
  6. package/lib/config.js.map +1 -0
  7. package/lib/dts.d.ts +21 -0
  8. package/lib/dts.d.ts.map +1 -0
  9. package/lib/dts.js +193 -0
  10. package/lib/dts.js.map +1 -0
  11. package/lib/export-config.d.ts +55 -0
  12. package/lib/export-config.d.ts.map +1 -0
  13. package/lib/export-config.js +55 -0
  14. package/lib/export-config.js.map +1 -0
  15. package/lib/handlers.d.ts +22 -0
  16. package/lib/handlers.d.ts.map +1 -0
  17. package/lib/handlers.js +125 -0
  18. package/lib/handlers.js.map +1 -0
  19. package/lib/helper.d.ts +37 -0
  20. package/lib/helper.d.ts.map +1 -0
  21. package/lib/helper.js +219 -0
  22. package/lib/helper.js.map +1 -0
  23. package/lib/index.d.ts +7 -0
  24. package/lib/index.d.ts.map +1 -0
  25. package/lib/index.js +137 -0
  26. package/lib/index.js.map +1 -0
  27. package/lib/lib.d.ts +11 -0
  28. package/lib/lib.d.ts.map +1 -0
  29. package/lib/lib.js +47 -0
  30. package/lib/lib.js.map +1 -0
  31. package/lib/logs.d.ts +21 -0
  32. package/lib/logs.d.ts.map +1 -0
  33. package/lib/logs.js +122 -0
  34. package/lib/logs.js.map +1 -0
  35. package/lib/metadata.d.ts +6 -0
  36. package/lib/metadata.d.ts.map +1 -0
  37. package/lib/metadata.js +34 -0
  38. package/lib/metadata.js.map +1 -0
  39. package/lib/types.d.ts +44 -0
  40. package/lib/types.d.ts.map +1 -0
  41. package/lib/types.js +18 -0
  42. package/lib/types.js.map +1 -0
  43. package/npm-shrinkwrap.json +5049 -0
  44. package/package.json +40 -4
  45. package/src/config.ts +205 -0
  46. package/src/dts.ts +217 -0
  47. package/src/export-config.ts +119 -0
  48. package/src/handlers.ts +141 -0
  49. package/src/helper.ts +320 -0
  50. package/src/index.ts +126 -0
  51. package/src/lib.ts +69 -0
  52. package/src/logs.ts +145 -0
  53. package/src/metadata.ts +31 -0
  54. package/src/types.ts +100 -0
  55. package/tests/config.test.ts +61 -0
  56. package/tests/dts.test.ts +120 -0
  57. package/tests/export-config.test.ts +74 -0
  58. package/tests/handlers.test.ts +170 -0
  59. package/tests/helper.test.ts +74 -0
  60. package/tests/lib.test.ts +43 -0
  61. package/tsconfig.json +18 -0
  62. package/tsconfig.tsbuildinfo +1 -0
  63. package/index.js +0 -0
package/package.json CHANGED
@@ -1,12 +1,48 @@
1
1
  {
2
2
  "name": "@firebase-function-kits/bigquery-firestore-export",
3
- "version": "0.0.1",
3
+ "version": "0.0.2-rc.1",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "https://github.com/firebase/extensions.git",
7
+ "directory": "kits/bigquery-firestore-export"
8
+ },
9
+ "private": false,
4
10
  "description": "Reverse-sync rows from BigQuery into Firestore",
5
11
  "license": "Apache-2.0",
6
- "main": "index.js",
7
- "private": false,
8
- "repository": "firebase/extensions",
12
+ "main": "lib/index.js",
13
+ "types": "lib/index.d.ts",
14
+ "exports": {
15
+ ".": {
16
+ "types": "./lib/index.d.ts",
17
+ "default": "./lib/index.js"
18
+ },
19
+ "./lib": {
20
+ "types": "./lib/lib.d.ts",
21
+ "default": "./lib/lib.js"
22
+ }
23
+ },
9
24
  "engines": {
10
25
  "node": ">=22"
26
+ },
27
+ "scripts": {
28
+ "build": "tsc -b",
29
+ "clean": "tsc -b --clean",
30
+ "test": "vitest run",
31
+ "deploy": "pnpm build && firebase deploy --only functions",
32
+ "serve": "firebase emulators:start --only functions"
33
+ },
34
+ "dependencies": {
35
+ "@firebase/app": "^0.16.0",
36
+ "@google-cloud/bigquery": "^8.3.1",
37
+ "@google-cloud/bigquery-data-transfer": "^5.0.1",
38
+ "@google-cloud/pubsub": "^4.11.0",
39
+ "firebase-admin": "^14.1.0",
40
+ "firebase-functions": "7.3.2"
41
+ },
42
+ "devDependencies": {
43
+ "vitest": "^4.1.10"
44
+ },
45
+ "overrides": {
46
+ "uuid@9.0.1": "11.1.1"
11
47
  }
12
48
  }
package/src/config.ts ADDED
@@ -0,0 +1,205 @@
1
+ /*
2
+ * Copyright 2026 Google LLC
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * https://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import {
18
+ defineString,
19
+ expr,
20
+ projectID,
21
+ select,
22
+ } from "firebase-functions/params";
23
+ import type {
24
+ BigqueryFirestoreExportConfig,
25
+ DeployTimeOptions,
26
+ LogLevel,
27
+ } from "./export-config";
28
+
29
+ const LOG_LEVEL_OPTIONS = ["debug", "info", "warn", "error", "silent"] as const;
30
+ const instanceId = defineString("INSTANCE_ID");
31
+
32
+ const params = {
33
+ instanceId,
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
+
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
+ }),
77
+ }),
78
+ transferConfigName: defineString("TRANSFER_CONFIG_NAME", { default: "" }),
79
+ datasetId: defineString("DATASET_ID", {
80
+ label: "Dataset ID",
81
+ description:
82
+ "What's the BigQuery destination dataset you'd like to use? Each transfer run will write to a table in this destination dataset.",
83
+ input: {
84
+ text: {
85
+ nonEmpty: true,
86
+ example: "customer_data",
87
+ },
88
+ },
89
+ }),
90
+ tableName: defineString("TABLE_NAME", {
91
+ label: "Destination Table Name",
92
+ description:
93
+ "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.",
94
+ input: {
95
+ text: {
96
+ nonEmpty: true,
97
+ example: "transactions",
98
+ },
99
+ },
100
+ }),
101
+ queryString: defineString("QUERY_STRING", {
102
+ label: "Query String",
103
+ description: "What's the BQ query you'd like to execute?",
104
+ input: {
105
+ text: {
106
+ nonEmpty: true,
107
+
108
+ example: "SELECT * from <PROJECT_ID>.customer_data.transactions",
109
+ },
110
+ },
111
+ }),
112
+ displayName: defineString("DISPLAY_NAME", {
113
+ label: "Display Name",
114
+ description: "What display name would you like to use?",
115
+ input: {
116
+ text: {
117
+ nonEmpty: true,
118
+ example: "Daily Rollup - Customer Transactions",
119
+ },
120
+ },
121
+ }),
122
+ partitioningField: defineString("PARTITIONING_FIELD", {
123
+ label: "Partitioning Field",
124
+ description:
125
+ "What's the partitioning field on the destination table ID? Leave empty if not using a partitioning field.",
126
+ default: "",
127
+ input: { text: { example: "timestamp" } },
128
+ }),
129
+ schedule: defineString("SCHEDULE", {
130
+ label: "Schedule",
131
+ description:
132
+ "What's the execution schedule you'd like to use for this query?",
133
+ input: {
134
+ text: {
135
+ nonEmpty: true,
136
+ example: "every 15 minutes",
137
+ },
138
+ },
139
+ }),
140
+ firestoreCollection: defineString("COLLECTION_PATH", {
141
+ label: "Firestore Collection",
142
+ description:
143
+ "What's the top-level Firestore Collection to store transfer configs, run metadata, and query output?",
144
+
145
+ default: "transferConfigs",
146
+ input: {
147
+ text: {
148
+ example: "transferConfigs",
149
+
150
+ validationRegex: /^[^\/]+(\/[^\/]+\/[^\/]+)*$/,
151
+ validationErrorMessage: "Must be a valid Cloud Firestore Collection",
152
+ },
153
+ },
154
+ }),
155
+ logLevel: defineString("LOG_LEVEL", {
156
+ label: "Log Level",
157
+ description: "What's the log level you'd like to use for this extension?",
158
+
159
+ default: "info",
160
+ input: select({
161
+ DEBUG: "debug",
162
+ INFO: "info",
163
+ WARN: "warn",
164
+ ERROR: "error",
165
+ SILENT: "silent",
166
+ }),
167
+ }),
168
+ };
169
+
170
+ export const CONFIG_EXPRESSIONS: DeployTimeOptions = {
171
+ pubSubTopic: expr`kit-${instanceId}-processMessages`,
172
+ };
173
+
174
+ function optional(value: string): string | undefined {
175
+ const normalized = value.trim();
176
+ return normalized.length > 0 ? normalized : undefined;
177
+ }
178
+
179
+ function normalizeLogLevel(value: string): LogLevel {
180
+ const normalized = value.toLowerCase();
181
+ return LOG_LEVEL_OPTIONS.includes(normalized as LogLevel)
182
+ ? (normalized as LogLevel)
183
+ : "info";
184
+ }
185
+
186
+ /** Reads runtime values from Firebase deploy-time parameters. */
187
+ export function configFromEnv(): BigqueryFirestoreExportConfig {
188
+ const resolvedInstanceId = params.instanceId.value();
189
+
190
+ return {
191
+ bigqueryDatasetLocation: params.bigqueryDatasetLocation.value(),
192
+ projectId: projectID.value(),
193
+ instanceId: resolvedInstanceId,
194
+ transferConfigName: optional(params.transferConfigName.value()),
195
+ datasetId: params.datasetId.value(),
196
+ tableName: params.tableName.value(),
197
+ queryString: params.queryString.value(),
198
+ displayName: params.displayName.value(),
199
+ partitioningField: optional(params.partitioningField.value()),
200
+ schedule: params.schedule.value(),
201
+ pubSubTopic: `kit-${resolvedInstanceId}-processMessages`,
202
+ firestoreCollection: params.firestoreCollection.value(),
203
+ logLevel: normalizeLogLevel(params.logLevel.value()),
204
+ };
205
+ }
package/src/dts.ts ADDED
@@ -0,0 +1,217 @@
1
+ /*
2
+ * Copyright 2025 Google LLC
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * https://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import * as bigqueryDataTransfer from "@google-cloud/bigquery-data-transfer";
18
+ import type { ResolvedBigqueryFirestoreExportConfig } from "./export-config";
19
+ import * as logs from "./logs";
20
+
21
+ export type DataTransferClient =
22
+ bigqueryDataTransfer.v1.DataTransferServiceClient;
23
+ export type TransferConfig =
24
+ bigqueryDataTransfer.protos.google.cloud.bigquery.datatransfer.v1.ITransferConfig;
25
+
26
+ const GRPC_NOT_FOUND = 5;
27
+ const PACKAGE_PARTITIONING_ERROR =
28
+ "Cannot remove partitioning_field from an existing transfer config";
29
+
30
+ export const PARTITIONING_FIELD_REMOVAL_ERROR_PREFIX =
31
+ PACKAGE_PARTITIONING_ERROR;
32
+ export const PARTITIONING_FIELD_REMOVAL_ERROR = `${PACKAGE_PARTITIONING_ERROR}. The BigQuery Data Transfer API does not support clearing this parameter once it has been set. To change partitioning, create a new transfer config with the desired setting.`;
33
+
34
+ function isNotFoundError(err: unknown): boolean {
35
+ return (
36
+ typeof err === "object" &&
37
+ err !== null &&
38
+ "code" in err &&
39
+ err.code === GRPC_NOT_FOUND
40
+ );
41
+ }
42
+
43
+ function transferConfigFields(config: TransferConfig) {
44
+ const fields = config.params?.fields;
45
+ if (!fields) {
46
+ throw new Error(
47
+ "Transfer config has invalid structure: missing params.fields"
48
+ );
49
+ }
50
+ if (!fields.query) {
51
+ throw new Error(
52
+ "Transfer config has invalid structure: missing params.fields.query"
53
+ );
54
+ }
55
+ if (!fields.destination_table_name_template) {
56
+ throw new Error(
57
+ "Transfer config has invalid structure: missing params.fields.destination_table_name_template"
58
+ );
59
+ }
60
+
61
+ return fields;
62
+ }
63
+
64
+ function stringField(value: string | undefined): { stringValue: string } {
65
+ return { stringValue: value ?? "" };
66
+ }
67
+
68
+ /**
69
+ * Creates the protobuf-shaped request used for a scheduled query.
70
+ *
71
+ * The query runs as whichever identity creates it, which is this function. To
72
+ * name a different account, `serviceAccountName` goes on the request rather
73
+ * than on the transfer config, and the caller needs `actAs` on the account it
74
+ * names, including its own.
75
+ */
76
+ export function createTransferConfigRequest(
77
+ config: ResolvedBigqueryFirestoreExportConfig
78
+ ): bigqueryDataTransfer.protos.google.cloud.bigquery.datatransfer.v1.ICreateTransferConfigRequest {
79
+ return {
80
+ parent: `projects/${config.projectId}`,
81
+ transferConfig: {
82
+ destinationDatasetId: config.datasetId,
83
+ displayName: config.displayName,
84
+ dataSourceId: "scheduled_query",
85
+ params: {
86
+ fields: {
87
+ query: stringField(config.queryString),
88
+ destination_table_name_template: stringField(
89
+ `${config.tableName}_{run_time|"%H%M%S"}`
90
+ ),
91
+ write_disposition: stringField("WRITE_TRUNCATE"),
92
+ partitioning_field: stringField(config.partitioningField),
93
+ },
94
+ },
95
+ schedule: config.schedule,
96
+ notificationPubsubTopic: `projects/${config.projectId}/topics/${config.pubSubTopic}`,
97
+ },
98
+ };
99
+ }
100
+
101
+ export async function getTransferConfig(
102
+ client: DataTransferClient,
103
+ transferConfigName: string
104
+ ): Promise<TransferConfig | null> {
105
+ try {
106
+ const [config] = await client.getTransferConfig({
107
+ name: transferConfigName,
108
+ });
109
+ return config;
110
+ } catch (err) {
111
+ if (isNotFoundError(err)) {
112
+ logs.transferConfigNotFound(transferConfigName);
113
+ return null;
114
+ }
115
+ logs.getTransferConfigFailed(transferConfigName, err);
116
+ throw err;
117
+ }
118
+ }
119
+
120
+ export async function createTransferConfig(
121
+ client: DataTransferClient,
122
+ config: ResolvedBigqueryFirestoreExportConfig
123
+ ): Promise<TransferConfig> {
124
+ logs.createTransferConfig();
125
+ const [created] = await client.createTransferConfig(
126
+ createTransferConfigRequest(config)
127
+ );
128
+ if (!created.name) {
129
+ throw new Error("BigQuery API returned a transfer config without a name");
130
+ }
131
+ logs.transferConfigCreated(created.name);
132
+ return created;
133
+ }
134
+
135
+ /** Builds a minimal update mask while retaining unsupported immutable fields. */
136
+ export async function constructUpdateTransferConfigRequest(
137
+ client: DataTransferClient,
138
+ transferConfigName: string,
139
+ config: ResolvedBigqueryFirestoreExportConfig
140
+ ): Promise<bigqueryDataTransfer.protos.google.cloud.bigquery.datatransfer.v1.IUpdateTransferConfigRequest> {
141
+ const transferConfig = await getTransferConfig(client, transferConfigName);
142
+ if (!transferConfig) throw new Error("Transfer config not found");
143
+
144
+ const fields = transferConfigFields(transferConfig);
145
+ const updatedConfig = JSON.parse(
146
+ JSON.stringify(transferConfig)
147
+ ) as TransferConfig;
148
+ const updatedFields = transferConfigFields(updatedConfig);
149
+ const updateMask: string[] = [];
150
+
151
+ if (config.datasetId !== transferConfig.destinationDatasetId) {
152
+ updateMask.push("destination_dataset_id");
153
+ updatedConfig.destinationDatasetId = config.datasetId;
154
+ }
155
+ if (config.queryString !== fields.query.stringValue) {
156
+ updateMask.push("params");
157
+ updatedFields.query.stringValue = config.queryString;
158
+ }
159
+
160
+ const tableTemplate = `${config.tableName}_{run_time|"%H%M%S"}`;
161
+ if (tableTemplate !== fields.destination_table_name_template.stringValue) {
162
+ updateMask.push("params");
163
+ updatedFields.destination_table_name_template.stringValue = tableTemplate;
164
+ }
165
+
166
+ const existingPartitioningField =
167
+ fields.partitioning_field?.stringValue ?? "";
168
+ const newPartitioningField = config.partitioningField ?? "";
169
+ if (newPartitioningField !== existingPartitioningField) {
170
+ if (!newPartitioningField) {
171
+ logs.partitioningFieldRemovalAttempted(
172
+ transferConfigName,
173
+ existingPartitioningField
174
+ );
175
+ throw new Error(PARTITIONING_FIELD_REMOVAL_ERROR);
176
+ }
177
+ updateMask.push("params");
178
+ updatedFields.partitioning_field ??= {};
179
+ updatedFields.partitioning_field.stringValue = newPartitioningField;
180
+ }
181
+
182
+ if (config.schedule !== transferConfig.schedule) {
183
+ updateMask.push("schedule");
184
+ updatedConfig.schedule = config.schedule;
185
+ }
186
+
187
+ const expectedTopic = `projects/${config.projectId}/topics/${config.pubSubTopic}`;
188
+ if (expectedTopic !== transferConfig.notificationPubsubTopic) {
189
+ updateMask.push("notification_pubsub_topic");
190
+ updatedConfig.notificationPubsubTopic = expectedTopic;
191
+ }
192
+
193
+ return {
194
+ transferConfig: updatedConfig,
195
+ updateMask: { paths: [...new Set(updateMask)] },
196
+ };
197
+ }
198
+
199
+ export async function updateTransferConfig(
200
+ client: DataTransferClient,
201
+ transferConfigName: string,
202
+ config: ResolvedBigqueryFirestoreExportConfig
203
+ ): Promise<TransferConfig> {
204
+ const request = await constructUpdateTransferConfigRequest(
205
+ client,
206
+ transferConfigName,
207
+ config
208
+ );
209
+ logs.updateTransferConfig(transferConfigName);
210
+ const converted =
211
+ bigqueryDataTransfer.protos.google.cloud.bigquery.datatransfer.v1.UpdateTransferConfigRequest.fromObject(
212
+ request
213
+ );
214
+ const [updated] = await client.updateTransferConfig(converted);
215
+ logs.transferConfigUpdated(transferConfigName);
216
+ return updated;
217
+ }
@@ -0,0 +1,119 @@
1
+ /*
2
+ * Copyright 2026 Google LLC
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * https://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import type { Expression } from "firebase-functions/params";
18
+
19
+ /** Log levels supported by the original extension. */
20
+ export type LogLevel = "debug" | "info" | "warn" | "error" | "silent";
21
+
22
+ /** Public configuration for the BigQuery-to-Firestore functions. */
23
+ export interface BigqueryFirestoreExportConfig {
24
+ /** Location of the BigQuery destination dataset, for example `US`. */
25
+ bigqueryDatasetLocation: string;
26
+ /** Google Cloud project id. */
27
+ projectId: string;
28
+ /** Stable id used to associate a DTS config with this deployment. */
29
+ instanceId: string;
30
+ /** Link this existing DTS config instead of creating one. */
31
+ transferConfigName?: string;
32
+ /** BigQuery destination dataset id. */
33
+ datasetId: string;
34
+ /** Prefix for per-run destination tables. */
35
+ tableName: string;
36
+ /** Scheduled query text. */
37
+ queryString: string;
38
+ /** Human-readable DTS scheduled-query name. */
39
+ displayName: string;
40
+ /** Optional destination-table partitioning field. */
41
+ partitioningField?: string;
42
+ /** BigQuery Data Transfer schedule, for example `every 15 minutes`. */
43
+ schedule: string;
44
+ /** Pub/Sub topic receiving DTS completion notifications. */
45
+ pubSubTopic?: string;
46
+ /** Root Firestore collection for configs and run output. */
47
+ firestoreCollection?: string;
48
+ /** Log verbosity. Defaults to `info`. */
49
+ logLevel?: LogLevel;
50
+ }
51
+
52
+ /** Configuration after defaults and validation have been applied. */
53
+ export interface ResolvedBigqueryFirestoreExportConfig {
54
+ bigqueryDatasetLocation: string;
55
+ projectId: string;
56
+ instanceId: string;
57
+ transferConfigName?: string;
58
+ datasetId: string;
59
+ tableName: string;
60
+ queryString: string;
61
+ displayName: string;
62
+ partitioningField?: string;
63
+ schedule: string;
64
+ pubSubTopic: string;
65
+ firestoreCollection: string;
66
+ logLevel: LogLevel;
67
+ }
68
+
69
+ /** Deploy-time values used to construct the v2 triggers. */
70
+ export interface DeployTimeOptions {
71
+ pubSubTopic: string | Expression<string>;
72
+ }
73
+
74
+ function required(value: string, field: string): string {
75
+ const normalized = value.trim();
76
+ if (!normalized) {
77
+ throw new Error(`${field} must be a non-empty string.`);
78
+ }
79
+ return normalized;
80
+ }
81
+
82
+ function optional(value: string | undefined): string | undefined {
83
+ if (value === undefined) return undefined;
84
+ const normalized = value.trim();
85
+ return normalized || undefined;
86
+ }
87
+
88
+ /** Applies package defaults and validates a user-supplied configuration. */
89
+ export function resolveConfig(
90
+ config: BigqueryFirestoreExportConfig
91
+ ): ResolvedBigqueryFirestoreExportConfig {
92
+ const instanceId = required(config.instanceId, "instanceId");
93
+ const logLevel = config.logLevel ?? "info";
94
+
95
+ if (!["debug", "info", "warn", "error", "silent"].includes(logLevel)) {
96
+ throw new Error(`Unsupported logLevel: ${logLevel}`);
97
+ }
98
+
99
+ return {
100
+ bigqueryDatasetLocation: required(
101
+ config.bigqueryDatasetLocation,
102
+ "bigqueryDatasetLocation"
103
+ ),
104
+ projectId: required(config.projectId, "projectId"),
105
+ instanceId,
106
+ transferConfigName: optional(config.transferConfigName),
107
+ datasetId: required(config.datasetId, "datasetId"),
108
+ tableName: required(config.tableName, "tableName"),
109
+ queryString: required(config.queryString, "queryString"),
110
+ displayName: required(config.displayName, "displayName"),
111
+ partitioningField: optional(config.partitioningField),
112
+ schedule: required(config.schedule, "schedule"),
113
+ pubSubTopic:
114
+ optional(config.pubSubTopic) ?? `kit-${instanceId}-processMessages`,
115
+ firestoreCollection:
116
+ optional(config.firestoreCollection) ?? "transferConfigs",
117
+ logLevel,
118
+ };
119
+ }