@firebase-function-kits/bigquery-firestore-export 0.0.1 → 0.0.2-rc.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.
Files changed (62) hide show
  1. package/CHANGELOG.md +1 -0
  2. package/README.md +188 -0
  3. package/lib/config.d.ts +5 -0
  4. package/lib/config.d.ts.map +1 -0
  5. package/lib/config.js +75 -0
  6. package/lib/config.js.map +1 -0
  7. package/lib/dts.d.ts +14 -0
  8. package/lib/dts.d.ts.map +1 -0
  9. package/lib/dts.js +189 -0
  10. package/lib/dts.js.map +1 -0
  11. package/lib/export-config.d.ts +58 -0
  12. package/lib/export-config.d.ts.map +1 -0
  13. package/lib/export-config.js +56 -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/package.json +40 -4
  44. package/src/config.ts +88 -0
  45. package/src/dts.ts +214 -0
  46. package/src/export-config.ts +123 -0
  47. package/src/handlers.ts +141 -0
  48. package/src/helper.ts +320 -0
  49. package/src/index.ts +126 -0
  50. package/src/lib.ts +69 -0
  51. package/src/logs.ts +145 -0
  52. package/src/metadata.ts +31 -0
  53. package/src/types.ts +100 -0
  54. package/tests/config.test.ts +61 -0
  55. package/tests/dts.test.ts +124 -0
  56. package/tests/export-config.test.ts +78 -0
  57. package/tests/handlers.test.ts +170 -0
  58. package/tests/helper.test.ts +74 -0
  59. package/tests/lib.test.ts +43 -0
  60. package/tsconfig.json +18 -0
  61. package/tsconfig.tsbuildinfo +1 -0
  62. package/index.js +0 -0
package/src/lib.ts ADDED
@@ -0,0 +1,69 @@
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
+ /**
18
+ * Side-effect-free library surface. Importing this module registers no
19
+ * functions, lifecycle hooks, roles, or APIs and reads no environment params.
20
+ */
21
+
22
+ export {
23
+ constructUpdateTransferConfigRequest,
24
+ createTransferConfig,
25
+ createTransferConfigRequest,
26
+ type DataTransferClient,
27
+ getTransferConfig,
28
+ PARTITIONING_FIELD_REMOVAL_ERROR,
29
+ PARTITIONING_FIELD_REMOVAL_ERROR_PREFIX,
30
+ type TransferConfig,
31
+ updateTransferConfig,
32
+ } from "./dts";
33
+ export {
34
+ type BigqueryFirestoreExportConfig,
35
+ type DeployTimeOptions,
36
+ type LogLevel,
37
+ type ResolvedBigqueryFirestoreExportConfig,
38
+ resolveConfig,
39
+ } from "./export-config";
40
+ export {
41
+ type HandlerContext,
42
+ handleMessagePublished,
43
+ handleUpsertTransferConfig,
44
+ type TransferRunEvent,
45
+ } from "./handlers";
46
+ export {
47
+ convertUnsupportedDataTypes,
48
+ getBigqueryResults,
49
+ handleTransferRunMessage,
50
+ type ParsedTransferConfigName,
51
+ type ParsedTransferRunName,
52
+ parseTransferConfigName,
53
+ parseTransferRunName,
54
+ type ResultHandlerContext,
55
+ transferConfigAssociatedWithInstance,
56
+ updateLatestRunDocument,
57
+ writeRunResultsToFirestore,
58
+ } from "./helper";
59
+ export { metadata } from "./metadata";
60
+ export type {
61
+ BigQueryRow,
62
+ BigQueryRowValue,
63
+ FirestoreRow,
64
+ FirestoreRowValue,
65
+ TransferRunMessage,
66
+ TransferRunParams,
67
+ TransferRunPayload,
68
+ TransferRunState,
69
+ } from "./types";
package/src/logs.ts ADDED
@@ -0,0 +1,145 @@
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 { logger } from "firebase-functions";
18
+ import type { ResolvedBigqueryFirestoreExportConfig } from "./export-config";
19
+
20
+ export function init(config: ResolvedBigqueryFirestoreExportConfig): void {
21
+ logger.info("Initializing BigQuery Firestore export", {
22
+ ...config,
23
+ queryString: "<omitted>",
24
+ });
25
+ }
26
+
27
+ export function start(): void {
28
+ logger.info("Started BigQuery Firestore export execution");
29
+ }
30
+
31
+ export function complete(): void {
32
+ logger.info("Completed BigQuery Firestore export execution");
33
+ }
34
+
35
+ export function error(err: unknown): void {
36
+ logger.error("Unhandled error during BigQuery Firestore export", err);
37
+ }
38
+
39
+ export function errorWritingToFirestore(err: unknown): void {
40
+ logger.error("Error writing a BigQuery row to Firestore", err);
41
+ }
42
+
43
+ export function bigqueryJobStarted(jobId: string | undefined): void {
44
+ logger.info(`BigQuery job ${jobId ?? "<unknown>"} started`);
45
+ }
46
+
47
+ export function bigqueryResultsRowCount(
48
+ transferConfigId: string,
49
+ runId: string,
50
+ count: number
51
+ ): void {
52
+ logger.debug("Read BigQuery transfer results", {
53
+ transferConfigId,
54
+ runId,
55
+ count,
56
+ });
57
+ }
58
+
59
+ export function bigqueryQueryFailed(
60
+ transferConfigId: string,
61
+ runId: string,
62
+ tableName: string,
63
+ err: unknown
64
+ ): void {
65
+ logger.error("BigQuery result query failed", {
66
+ transferConfigId,
67
+ runId,
68
+ tableName,
69
+ err,
70
+ });
71
+ }
72
+
73
+ export function runResultsWrittenToFirestore(
74
+ runId: string,
75
+ successCount: number,
76
+ totalCount: number
77
+ ): void {
78
+ logger.info("Finished writing BigQuery results to Firestore", {
79
+ runId,
80
+ successCount,
81
+ totalCount,
82
+ });
83
+ }
84
+
85
+ export function latestDocUpdateSkipped(
86
+ transferConfigId: string,
87
+ runId: string,
88
+ reason: string
89
+ ): void {
90
+ logger.debug("Skipped updating latest transfer-run document", {
91
+ transferConfigId,
92
+ runId,
93
+ reason,
94
+ });
95
+ }
96
+
97
+ export function handlingNonSuccessRun(
98
+ transferConfigId: string,
99
+ runId: string,
100
+ state: string
101
+ ): void {
102
+ logger.info("Recording non-successful BigQuery transfer run", {
103
+ transferConfigId,
104
+ runId,
105
+ state,
106
+ });
107
+ }
108
+
109
+ export function createTransferConfig(): void {
110
+ logger.info("Creating BigQuery Data Transfer config");
111
+ }
112
+
113
+ export function transferConfigCreated(name: string): void {
114
+ logger.info("Created BigQuery Data Transfer config", { name });
115
+ }
116
+
117
+ export function updateTransferConfig(name: string): void {
118
+ logger.info("Updating BigQuery Data Transfer config", { name });
119
+ }
120
+
121
+ export function transferConfigUpdated(name: string): void {
122
+ logger.info("Updated BigQuery Data Transfer config", { name });
123
+ }
124
+
125
+ export function transferConfigNotFound(name: string): void {
126
+ logger.error("BigQuery Data Transfer config not found", { name });
127
+ }
128
+
129
+ export function getTransferConfigFailed(name: string, err: unknown): void {
130
+ logger.error("Failed to read BigQuery Data Transfer config", { name, err });
131
+ }
132
+
133
+ export function partitioningFieldRemovalAttempted(
134
+ name: string,
135
+ existingField: string
136
+ ): void {
137
+ logger.warn("Cannot remove an existing DTS partitioning field", {
138
+ name,
139
+ existingField,
140
+ });
141
+ }
142
+
143
+ export function topicCreated(name: string): void {
144
+ logger.info("Created Pub/Sub topic for transfer notifications", { name });
145
+ }
@@ -0,0 +1,31 @@
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
+ export const metadata = {
18
+ roles: [
19
+ "datastore.user",
20
+ "bigquery.admin",
21
+ "pubsub.admin",
22
+ "eventarc.eventReceiver",
23
+ "run.invoker",
24
+ ],
25
+ apis: [
26
+ "bigquery.googleapis.com",
27
+ "bigquerydatatransfer.googleapis.com",
28
+ "pubsub.googleapis.com",
29
+ ],
30
+ functionNames: ["processMessages", "upsertTransferConfig"],
31
+ } as const;
package/src/types.ts ADDED
@@ -0,0 +1,100 @@
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 type {
18
+ BigQueryDate,
19
+ BigQueryDatetime,
20
+ BigQueryTime,
21
+ BigQueryTimestamp,
22
+ Geography,
23
+ } from "@google-cloud/bigquery";
24
+ import type { Timestamp } from "firebase-admin/firestore";
25
+
26
+ /** Parameters attached to a BigQuery scheduled-query transfer run. */
27
+ export interface TransferRunParams {
28
+ destination_table_name_template: string;
29
+ partitioning_field: string;
30
+ query: string;
31
+ write_disposition: string;
32
+ }
33
+
34
+ /** State of a BigQuery Data Transfer run. */
35
+ export type TransferRunState =
36
+ | "SUCCEEDED"
37
+ | "FAILED"
38
+ | "CANCELLED"
39
+ | "PENDING"
40
+ | "RUNNING"
41
+ | "TRANSFER_STATE_UNSPECIFIED";
42
+
43
+ /** JSON payload from a BigQuery Data Transfer Pub/Sub notification. */
44
+ export interface TransferRunPayload {
45
+ name: string;
46
+ runTime: string;
47
+ state: TransferRunState;
48
+ destinationDatasetId: string;
49
+ dataSourceId: string;
50
+ schedule: string;
51
+ scheduleTime: string;
52
+ startTime: string;
53
+ endTime: string;
54
+ updateTime: string;
55
+ userId: string;
56
+ notificationPubsubTopic: string;
57
+ params: TransferRunParams;
58
+ emailPreferences: Record<string, unknown>;
59
+ errorStatus: Record<string, unknown>;
60
+ }
61
+
62
+ /** Message-shaped wrapper retained for the core handler boundary. */
63
+ export interface TransferRunMessage {
64
+ json: TransferRunPayload;
65
+ }
66
+
67
+ /** Values returned in a BigQuery row before conversion. */
68
+ export type BigQueryRowValue =
69
+ | string
70
+ | number
71
+ | boolean
72
+ | null
73
+ | Date
74
+ | Buffer
75
+ | BigQueryTimestamp
76
+ | BigQueryDate
77
+ | BigQueryTime
78
+ | BigQueryDatetime
79
+ | Geography
80
+ | BigQueryRow
81
+ | BigQueryRowValue[];
82
+
83
+ export interface BigQueryRow {
84
+ [key: string]: BigQueryRowValue;
85
+ }
86
+
87
+ /** Firestore-compatible equivalent of a BigQuery row value. */
88
+ export type FirestoreRowValue =
89
+ | string
90
+ | number
91
+ | boolean
92
+ | null
93
+ | Timestamp
94
+ | Uint8Array
95
+ | FirestoreRow
96
+ | FirestoreRowValue[];
97
+
98
+ export interface FirestoreRow {
99
+ [key: string]: FirestoreRowValue;
100
+ }
@@ -0,0 +1,61 @@
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 { Expression } from "firebase-functions/params";
18
+ import { afterEach, describe, expect, test, vi } from "vitest";
19
+ import { CONFIG_EXPRESSIONS, configFromEnv } from "../src/config";
20
+
21
+ afterEach(() => {
22
+ vi.unstubAllEnvs();
23
+ });
24
+
25
+ describe("CONFIG_EXPRESSIONS", () => {
26
+ test("namespaces the Pub/Sub topic with the required instance id", () => {
27
+ expect(CONFIG_EXPRESSIONS.pubSubTopic).toBeInstanceOf(Expression);
28
+ expect((CONFIG_EXPRESSIONS.pubSubTopic as Expression<string>).toCEL()).toBe(
29
+ "kit-{{ params.INSTANCE_ID }}-processMessages"
30
+ );
31
+ });
32
+ });
33
+
34
+ describe("configFromEnv", () => {
35
+ test("reads runtime parameters and derives the same topic", () => {
36
+ vi.stubEnv(
37
+ "FIREBASE_CONFIG",
38
+ JSON.stringify({ projectId: "test-project" })
39
+ );
40
+ vi.stubEnv("INSTANCE_ID", "users-export");
41
+ vi.stubEnv("BIGQUERY_DATASET_LOCATION", "EU");
42
+ vi.stubEnv("DATASET_ID", "analytics");
43
+ vi.stubEnv("TABLE_NAME", "users");
44
+ vi.stubEnv("QUERY_STRING", "SELECT * FROM source.users");
45
+ vi.stubEnv("DISPLAY_NAME", "Users export");
46
+ vi.stubEnv("SCHEDULE", "every 24 hours");
47
+ vi.stubEnv("COLLECTION_PATH", "transferConfigs");
48
+ vi.stubEnv("LOG_LEVEL", "info");
49
+
50
+ expect(configFromEnv()).toMatchObject({
51
+ projectId: "test-project",
52
+ instanceId: "users-export",
53
+ bigqueryDatasetLocation: "EU",
54
+ datasetId: "analytics",
55
+ tableName: "users",
56
+ pubSubTopic: "kit-users-export-processMessages",
57
+ firestoreCollection: "transferConfigs",
58
+ logLevel: "info",
59
+ });
60
+ });
61
+ });
@@ -0,0 +1,124 @@
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 { describe, expect, test, vi } from "vitest";
18
+ import {
19
+ constructUpdateTransferConfigRequest,
20
+ createTransferConfigRequest,
21
+ type DataTransferClient,
22
+ PARTITIONING_FIELD_REMOVAL_ERROR,
23
+ } from "../src/dts";
24
+ import { resolveConfig } from "../src/export-config";
25
+
26
+ const config = resolveConfig({
27
+ bigqueryDatasetLocation: "US",
28
+ projectId: "test-project",
29
+ instanceId: "users-export",
30
+ datasetId: "analytics",
31
+ tableName: "users",
32
+ queryString: "SELECT * FROM source.users",
33
+ displayName: "Users export",
34
+ partitioningField: "created_at",
35
+ schedule: "every 24 hours",
36
+ });
37
+
38
+ function clientWithTransferConfig(transferConfig: object): DataTransferClient {
39
+ return {
40
+ getTransferConfig: vi.fn().mockResolvedValue([transferConfig]),
41
+ } as unknown as DataTransferClient;
42
+ }
43
+
44
+ describe("createTransferConfigRequest", () => {
45
+ test("creates the scheduled-query request", () => {
46
+ const request = createTransferConfigRequest(
47
+ config,
48
+ "runtime@test-project.iam.gserviceaccount.com"
49
+ );
50
+
51
+ expect(request).toMatchObject({
52
+ parent: "projects/test-project",
53
+ transferConfig: {
54
+ destinationDatasetId: "analytics",
55
+ displayName: "Users export",
56
+ dataSourceId: "scheduled_query",
57
+ schedule: "every 24 hours",
58
+ notificationPubsubTopic:
59
+ "projects/test-project/topics/kit-users-export-processMessages",
60
+ serviceAccountName: "runtime@test-project.iam.gserviceaccount.com",
61
+ },
62
+ });
63
+ expect(
64
+ request.transferConfig?.params?.fields?.destination_table_name_template
65
+ ?.stringValue
66
+ ).toBe('users_{run_time|"%H%M%S"}');
67
+ });
68
+ });
69
+
70
+ describe("constructUpdateTransferConfigRequest", () => {
71
+ test("deduplicates the params update mask", async () => {
72
+ const client = clientWithTransferConfig({
73
+ name: "projects/p/locations/us/transferConfigs/c",
74
+ destinationDatasetId: "analytics",
75
+ schedule: "every 24 hours",
76
+ notificationPubsubTopic:
77
+ "projects/test-project/topics/kit-users-export-processMessages",
78
+ params: {
79
+ fields: {
80
+ query: { stringValue: "SELECT 1" },
81
+ destination_table_name_template: {
82
+ stringValue: 'old_{run_time|"%H%M%S"}',
83
+ },
84
+ partitioning_field: { stringValue: "created_at" },
85
+ },
86
+ },
87
+ });
88
+
89
+ const request = await constructUpdateTransferConfigRequest(
90
+ client,
91
+ "projects/p/locations/us/transferConfigs/c",
92
+ config
93
+ );
94
+
95
+ expect(request.updateMask?.paths).toEqual(["params"]);
96
+ });
97
+
98
+ test("rejects clearing an existing partitioning field", async () => {
99
+ const client = clientWithTransferConfig({
100
+ name: "projects/p/locations/us/transferConfigs/c",
101
+ destinationDatasetId: "analytics",
102
+ schedule: "every 24 hours",
103
+ notificationPubsubTopic:
104
+ "projects/test-project/topics/kit-users-export-processMessages",
105
+ params: {
106
+ fields: {
107
+ query: { stringValue: config.queryString },
108
+ destination_table_name_template: {
109
+ stringValue: 'users_{run_time|"%H%M%S"}',
110
+ },
111
+ partitioning_field: { stringValue: "created_at" },
112
+ },
113
+ },
114
+ });
115
+
116
+ await expect(
117
+ constructUpdateTransferConfigRequest(
118
+ client,
119
+ "projects/p/locations/us/transferConfigs/c",
120
+ { ...config, partitioningField: undefined }
121
+ )
122
+ ).rejects.toThrow(PARTITIONING_FIELD_REMOVAL_ERROR);
123
+ });
124
+ });
@@ -0,0 +1,78 @@
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 { describe, expect, test } from "vitest";
18
+ import {
19
+ type BigqueryFirestoreExportConfig,
20
+ resolveConfig,
21
+ } from "../src/export-config";
22
+
23
+ const minimal: BigqueryFirestoreExportConfig = {
24
+ bigqueryDatasetLocation: "US",
25
+ projectId: "test-project",
26
+ instanceId: "users-export",
27
+ datasetId: "analytics",
28
+ tableName: "users",
29
+ queryString: "SELECT * FROM source.users",
30
+ displayName: "Users export",
31
+ schedule: "every 24 hours",
32
+ };
33
+
34
+ describe("resolveConfig", () => {
35
+ test("applies kit defaults", () => {
36
+ expect(resolveConfig(minimal)).toEqual({
37
+ ...minimal,
38
+ pubSubTopic: "kit-users-export-processMessages",
39
+ firestoreCollection: "transferConfigs",
40
+ logLevel: "info",
41
+ });
42
+ });
43
+
44
+ test("normalizes optional strings", () => {
45
+ const resolved = resolveConfig({
46
+ ...minimal,
47
+ transferConfigName: " projects/p/locations/us/transferConfigs/c ",
48
+ partitioningField: " created_at ",
49
+ serviceAccount: " runtime@test-project.iam.gserviceaccount.com ",
50
+ });
51
+
52
+ expect(resolved.transferConfigName).toBe(
53
+ "projects/p/locations/us/transferConfigs/c"
54
+ );
55
+ expect(resolved.partitioningField).toBe("created_at");
56
+ expect(resolved.serviceAccount).toBe(
57
+ "runtime@test-project.iam.gserviceaccount.com"
58
+ );
59
+ });
60
+
61
+ test.each(["instanceId", "datasetId", "tableName", "queryString"] as const)(
62
+ "rejects an empty %s",
63
+ (field) => {
64
+ expect(() => resolveConfig({ ...minimal, [field]: " " })).toThrow(
65
+ `${field} must be a non-empty string.`
66
+ );
67
+ }
68
+ );
69
+
70
+ test("rejects unsupported log levels", () => {
71
+ expect(() =>
72
+ resolveConfig({
73
+ ...minimal,
74
+ logLevel: "verbose" as BigqueryFirestoreExportConfig["logLevel"],
75
+ })
76
+ ).toThrow("Unsupported logLevel: verbose");
77
+ });
78
+ });