@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.
Files changed (45) hide show
  1. package/CHANGELOG.md +1 -1
  2. package/README.md +138 -4
  3. package/lib/config.d.ts.map +1 -1
  4. package/lib/config.js +128 -9
  5. package/lib/config.js.map +1 -1
  6. package/lib/dts.d.ts +17 -2
  7. package/lib/dts.d.ts.map +1 -1
  8. package/lib/dts.js +25 -8
  9. package/lib/dts.js.map +1 -1
  10. package/lib/export-config.d.ts +0 -3
  11. package/lib/export-config.d.ts.map +1 -1
  12. package/lib/export-config.js +0 -1
  13. package/lib/export-config.js.map +1 -1
  14. package/lib/handlers.d.ts.map +1 -1
  15. package/lib/handlers.js +26 -4
  16. package/lib/handlers.js.map +1 -1
  17. package/lib/helper.d.ts.map +1 -1
  18. package/lib/helper.js +1 -0
  19. package/lib/helper.js.map +1 -1
  20. package/lib/index.js +1 -1
  21. package/lib/index.js.map +1 -1
  22. package/lib/logs.d.ts +4 -0
  23. package/lib/logs.d.ts.map +1 -1
  24. package/lib/logs.js +18 -0
  25. package/lib/logs.js.map +1 -1
  26. package/lib/metadata.d.ts.map +1 -1
  27. package/npm-shrinkwrap.json +3841 -0
  28. package/package.json +2 -5
  29. package/src/config.ts +142 -9
  30. package/src/dts.ts +27 -9
  31. package/src/export-config.ts +0 -4
  32. package/src/handlers.ts +36 -9
  33. package/src/helper.ts +1 -0
  34. package/src/index.ts +1 -1
  35. package/src/logs.ts +28 -0
  36. package/tests/config.test.ts +54 -2
  37. package/tests/dts-transfer-config.test.ts +400 -0
  38. package/tests/dts.test.ts +18 -5
  39. package/tests/export-config.test.ts +0 -4
  40. package/tests/handlers.test.ts +113 -5
  41. package/tests/helper-values.test.ts +168 -0
  42. package/tests/helper.test.ts +65 -3
  43. package/tests/notification-topic.test.ts +151 -0
  44. package/tests/run-results.test.ts +373 -0
  45. 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.0",
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
- datasetId: defineString("DATASET_ID"),
39
- tableName: defineString("TABLE_NAME"),
40
- queryString: defineString("QUERY_STRING"),
41
- displayName: defineString("DISPLAY_NAME"),
42
- partitioningField: defineString("PARTITIONING_FIELD", { default: "" }),
43
- schedule: defineString("SCHEDULE"),
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([...LOG_LEVEL_OPTIONS]),
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: expr`kit-${instanceId}-processMessages`,
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: `kit-${resolvedInstanceId}-processMessages`,
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
- /** Creates the protobuf-shaped request used for a scheduled query. */
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: `projects/${config.projectId}/topics/${config.pubSubTopic}`,
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, config.serviceAccount)
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 = `projects/${config.projectId}/topics/${config.pubSubTopic}`;
202
+ const expectedTopic = notificationTopicName(config);
185
203
  if (expectedTopic !== transferConfig.notificationPubsubTopic) {
186
204
  updateMask.push("notification_pubsub_topic");
187
205
  updatedConfig.notificationPubsubTopic = expectedTopic;
@@ -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
- throw new Error(
109
- `Transfer config not found: ${ctx.config.transferConfigName}`
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
- const updated = await updateTransferConfig(
136
- ctx.dataTransfer,
137
- transferConfigName,
138
- ctx.config
139
- );
140
- await storeTransferConfig(ctx, updated);
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: true,
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
+ }
@@ -23,12 +23,41 @@ afterEach(() => {
23
23
  });
24
24
 
25
25
  describe("CONFIG_EXPRESSIONS", () => {
26
- test("namespaces the Pub/Sub topic with the required instance id", () => {
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
- "kit-{{ params.INSTANCE_ID }}-processMessages"
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
  });