@firebase-function-kits/delete-user-data 0.0.2-rc.3 → 0.0.2-rc.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@firebase-function-kits/delete-user-data",
3
- "version": "0.0.2-rc.3",
3
+ "version": "0.0.2-rc.4",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/firebase/extensions.git",
@@ -34,13 +34,6 @@
34
34
  "@google-cloud/pubsub": "^4.3.3",
35
35
  "firebase-admin": "^13.2.0",
36
36
  "firebase-functions": "^7.3.2",
37
- "lodash.chunk": "^4.2.0",
38
- "node-fetch": "^2.6.2"
39
- },
40
- "devDependencies": {
41
- "@types/lodash.chunk": "^4.2.7",
42
- "@types/node-fetch": "^2.6.2",
43
- "typescript": "^5.9.3",
44
- "vitest": "^4.1.10"
37
+ "lodash.chunk": "^4.2.0"
45
38
  }
46
39
  }
package/src/config.ts CHANGED
@@ -18,17 +18,20 @@ import {
18
18
  defineBoolean,
19
19
  defineInt,
20
20
  defineString,
21
- expr,
21
+ type IntParam,
22
22
  projectID,
23
23
  select,
24
24
  storageBucket,
25
25
  } from "firebase-functions/params";
26
26
  import type { DeleteUserDataConfig } from "./export-config";
27
27
 
28
- const instanceId = defineString("INSTANCE_ID");
28
+ // firebase-tools injects this for kit instances (set to the instance's key in
29
+ // firebase.json) during discovery, in the emulator, and on deployed functions.
30
+ // The FIREBASE_ prefix is reserved in .env files and the params machinery never
31
+ // sees injected values, so it must be a plain env read, not a defineString.
32
+ const instanceId = process.env.FIREBASE_KIT_INSTANCE_ID;
29
33
 
30
34
  const params = {
31
- instanceId,
32
35
  firestorePaths: defineString("FIRESTORE_PATHS", {
33
36
  label: "Cloud Firestore paths",
34
37
  description:
@@ -148,10 +151,10 @@ const params = {
148
151
  // Non-empty defaults so Pub/Sub trigger bindings resolve during deploy
149
152
  // discovery without freezing an empty topic name into the manifest.
150
153
  discoveryTopicName: defineString("DISCOVERY_TOPIC_NAME", {
151
- default: expr`kit-${instanceId}-discovery`,
154
+ default: `kit-${instanceId}-discovery`,
152
155
  }),
153
156
  deletionTopicName: defineString("DELETION_TOPIC_NAME", {
154
- default: expr`kit-${instanceId}-deletion`,
157
+ default: `kit-${instanceId}-deletion`,
155
158
  }),
156
159
  };
157
160
 
@@ -164,7 +167,25 @@ function optional(value: string): string | undefined {
164
167
  return value.length > 0 ? value : undefined;
165
168
  }
166
169
 
170
+ // defineInt resolves a missing or blank env var to 0, so a declared default
171
+ // never reaches runtime. Report those as unset and let the resolver apply the
172
+ // documented default. An explicit 0 is a real setting and is preserved.
173
+ function optionalInt(param: IntParam): number | undefined {
174
+ // Quoted values keep their whitespace through the CLI's .env parser, and a
175
+ // whitespace-only value would otherwise parse to 0.
176
+ const raw = process.env[param.name]?.trim();
177
+ return raw === undefined || raw === "" ? undefined : param.value();
178
+ }
179
+
167
180
  export function configFromEnv(): DeleteUserDataConfig {
181
+ const instanceId = process.env.FIREBASE_KIT_INSTANCE_ID;
182
+ if (!instanceId) {
183
+ throw new Error(
184
+ "FIREBASE_KIT_INSTANCE_ID is not set. It is provided automatically to " +
185
+ "kit instances by firebase-tools >= 15.27.0; deploy or emulate this " +
186
+ "kit with a supported CLI version."
187
+ );
188
+ }
168
189
  return {
169
190
  firestorePaths: optional(params.firestorePaths.value()),
170
191
  firestoreDatabaseId: params.firestoreDatabaseId.value(),
@@ -177,10 +198,10 @@ export function configFromEnv(): DeleteUserDataConfig {
177
198
  optional(params.storageBucket.value()) ?? process.env.STORAGE_BUCKET,
178
199
  storagePaths: optional(params.storagePaths.value()),
179
200
  enableAutoDiscovery: params.enableAutoDiscovery.value(),
180
- searchDepth: params.searchDepth.value(),
201
+ searchDepth: optionalInt(params.searchDepth),
181
202
  searchFields: params.searchFields.value(),
182
203
  searchFunction: optional(params.searchFunction.value()),
183
- instanceId: params.instanceId.value(),
204
+ instanceId,
184
205
  discoveryTopicName: optional(params.discoveryTopicName.value()),
185
206
  deletionTopicName: optional(params.deletionTopicName.value()),
186
207
  projectId: projectID.value(),
@@ -14,7 +14,6 @@
14
14
  * limitations under the License.
15
15
  */
16
16
 
17
- import fetch from "node-fetch";
18
17
  import * as logs from "./logs";
19
18
  import type { PublisherContext } from "./runBatchPubSubDeletions";
20
19
  import { runBatchPubSubDeletions } from "./runBatchPubSubDeletions";
@@ -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
+ import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
18
+ import { configFromEnv } from "../src/config";
19
+ import { resolveDeleteUserDataConfig } from "../src/export-config";
20
+
21
+ // config.test.ts fakes firebase-functions/params, so it cannot see how the real
22
+ // IntParam resolves a missing env var. These cases run against the real one.
23
+ function resolvedSearchDepth(raw?: string): number {
24
+ if (raw === undefined) {
25
+ delete process.env.AUTO_DISCOVERY_SEARCH_DEPTH;
26
+ } else {
27
+ process.env.AUTO_DISCOVERY_SEARCH_DEPTH = raw;
28
+ }
29
+ return resolveDeleteUserDataConfig(configFromEnv()).searchDepth;
30
+ }
31
+
32
+ describe("searchDepth from the runtime environment", () => {
33
+ const original = process.env.AUTO_DISCOVERY_SEARCH_DEPTH;
34
+
35
+ beforeEach(() => {
36
+ vi.stubEnv("FIREBASE_KIT_INSTANCE_ID", "test-instance");
37
+ });
38
+
39
+ afterEach(() => {
40
+ vi.unstubAllEnvs();
41
+ if (original === undefined) {
42
+ delete process.env.AUTO_DISCOVERY_SEARCH_DEPTH;
43
+ } else {
44
+ process.env.AUTO_DISCOVERY_SEARCH_DEPTH = original;
45
+ }
46
+ });
47
+
48
+ test("falls back to the documented default when unset", () => {
49
+ expect(resolvedSearchDepth(undefined)).toBe(3);
50
+ });
51
+
52
+ // An extension .env that left the value empty resolved to 3, not 0.
53
+ test("falls back to the documented default when blank", () => {
54
+ expect(resolvedSearchDepth("")).toBe(3);
55
+ });
56
+
57
+ // A quoted " " survives the CLI's .env parser untrimmed.
58
+ test("falls back to the documented default when whitespace only", () => {
59
+ expect(resolvedSearchDepth(" ")).toBe(3);
60
+ });
61
+
62
+ test("preserves an explicit zero", () => {
63
+ expect(resolvedSearchDepth("0")).toBe(0);
64
+ });
65
+
66
+ test("preserves an explicit depth", () => {
67
+ expect(resolvedSearchDepth("5")).toBe(5);
68
+ });
69
+ });
@@ -48,7 +48,9 @@ const defineString = vi.fn(
48
48
  new FakeStringParam(name, opts?.default)
49
49
  );
50
50
 
51
- const defineInt = vi.fn((_name: string, opts?: { default?: number }) => ({
51
+ // Carries name so configFromEnv can look the variable up, as the real one does.
52
+ const defineInt = vi.fn((name: string, opts?: { default?: number }) => ({
53
+ name,
52
54
  value: () => opts?.default ?? 0,
53
55
  }));
54
56
 
@@ -56,17 +58,6 @@ const defineBoolean = vi.fn((_name: string, opts?: { default?: boolean }) => ({
56
58
  value: () => opts?.default ?? false,
57
59
  }));
58
60
 
59
- const expr = vi.fn(
60
- (strings: TemplateStringsArray, ...values: unknown[]) =>
61
- new FakeExpression(
62
- strings.reduce(
63
- (result, part, index) =>
64
- result + part + (index < values.length ? cel(values[index]) : ""),
65
- ""
66
- )
67
- )
68
- );
69
-
70
61
  function cel(value: unknown): string {
71
62
  return value instanceof FakeExpression ? value.toCEL() : String(value);
72
63
  }
@@ -76,7 +67,6 @@ vi.mock("firebase-functions/params", () => ({
76
67
  defineBoolean,
77
68
  defineInt,
78
69
  defineString,
79
- expr,
80
70
  projectID: { value: () => "demo-test" },
81
71
  select: vi.fn((options: string[]) => ({ options })),
82
72
  storageBucket: new FakeStringParam("STORAGE_BUCKET", "demo-test.appspot.com"),
@@ -87,7 +77,7 @@ async function importConfig() {
87
77
  defineString.mockClear();
88
78
  defineInt.mockClear();
89
79
  defineBoolean.mockClear();
90
- expr.mockClear();
80
+ vi.stubEnv("FIREBASE_KIT_INSTANCE_ID", "test-instance");
91
81
 
92
82
  return import("../src/config");
93
83
  }
@@ -105,7 +95,6 @@ describe("configFromEnv", () => {
105
95
  firestoreDeleteMode: "shallow",
106
96
  rtdbLocation: "us-central1",
107
97
  enableAutoDiscovery: false,
108
- searchDepth: 3,
109
98
  searchFields: "id,uid,userId",
110
99
  projectId: "demo-test",
111
100
  });
@@ -120,6 +109,7 @@ describe("configFromEnv", () => {
120
109
  expect(config.storagePaths).toBeUndefined();
121
110
  expect(config.searchFunction).toBeUndefined();
122
111
  expect(config.rtdbInstance).toBeUndefined();
112
+ expect(config.searchDepth).toBeUndefined();
123
113
  });
124
114
 
125
115
  test("declares the params the extension exposes", async () => {
@@ -128,7 +118,6 @@ describe("configFromEnv", () => {
128
118
  const declared = defineString.mock.calls.map(([name]) => name);
129
119
  expect(declared).toEqual(
130
120
  expect.arrayContaining([
131
- "INSTANCE_ID",
132
121
  "FIRESTORE_PATHS",
133
122
  "FIRESTORE_DATABASE_ID",
134
123
  "FIRESTORE_DELETE_MODE",
@@ -153,7 +142,33 @@ describe("configFromEnv", () => {
153
142
  ]);
154
143
  });
155
144
 
156
- test("defaults the topic names to kit-{instanceId}-* expressions", async () => {
145
+ // The CLI injects FIREBASE_KIT_INSTANCE_ID as a reserved env var; declaring
146
+ // it (or INSTANCE_ID) as a param makes the CLI prompt for a value it cannot
147
+ // accept and abort loading the kit.
148
+ test("does not declare an instance-id param", async () => {
149
+ await importConfig();
150
+
151
+ const declared = defineString.mock.calls.map(([name]) => name);
152
+ expect(declared).not.toContain("INSTANCE_ID");
153
+ expect(declared).not.toContain("FIREBASE_KIT_INSTANCE_ID");
154
+ });
155
+
156
+ test("reads the instance id from the injected environment", async () => {
157
+ const { configFromEnv } = await importConfig();
158
+
159
+ expect(configFromEnv().instanceId).toBe("test-instance");
160
+ });
161
+
162
+ test("throws when FIREBASE_KIT_INSTANCE_ID is missing", async () => {
163
+ const { configFromEnv } = await importConfig();
164
+ vi.stubEnv("FIREBASE_KIT_INSTANCE_ID", undefined);
165
+
166
+ expect(() => configFromEnv()).toThrow(
167
+ /FIREBASE_KIT_INSTANCE_ID is not set/
168
+ );
169
+ });
170
+
171
+ test("defaults the topic names to kit-{instanceId}-*", async () => {
157
172
  const { CONFIG_EXPRESSIONS } = await importConfig();
158
173
 
159
174
  expect(cel(CONFIG_EXPRESSIONS.discoveryTopicName)).toBe(
@@ -162,13 +177,13 @@ describe("configFromEnv", () => {
162
177
  expect(cel(CONFIG_EXPRESSIONS.deletionTopicName)).toBe(
163
178
  "{{ params.DELETION_TOPIC_NAME }}"
164
179
  );
165
- expect(expr.mock.results.map((result) => cel(result.value))).toEqual([
166
- "kit-{{ params.INSTANCE_ID }}-discovery",
167
- "kit-{{ params.INSTANCE_ID }}-deletion",
168
- ]);
169
180
  expect(defineString.mock.calls).toContainEqual([
170
181
  "DISCOVERY_TOPIC_NAME",
171
- { default: expect.anything() },
182
+ { default: "kit-test-instance-discovery" },
183
+ ]);
184
+ expect(defineString.mock.calls).toContainEqual([
185
+ "DELETION_TOPIC_NAME",
186
+ { default: "kit-test-instance-deletion" },
172
187
  ]);
173
188
  });
174
189
 
@@ -14,20 +14,29 @@
14
14
  * limitations under the License.
15
15
  */
16
16
 
17
- import { beforeEach, describe, expect, test, vi } from "vitest";
17
+ import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
18
18
 
19
19
  // Stands in for a project with no Realtime Database URL available, which is what
20
20
  // an empty SELECTED_DATABASE_INSTANCE leaves behind.
21
21
  const NO_DATABASE_URL = "Can't determine Firebase Database URL.";
22
22
 
23
+ const FIRESTORE_DATABASE_ID = "user-data";
24
+ const PROJECT_ID = "test-project";
25
+
23
26
  vi.mock("../src/logs");
24
27
  vi.mock("../src/handlers", async (importOriginal) => ({
25
28
  ...(await importOriginal<typeof import("../src/handlers")>()),
26
29
  handleClear: vi.fn(),
27
30
  }));
28
- vi.mock("@google-cloud/pubsub", () => ({ PubSub: vi.fn() }));
31
+ // The clients carry their construction arguments, which survive the beforeEach
32
+ // that clears the call history recorded when the context was memoized.
33
+ vi.mock("@google-cloud/pubsub", () => ({
34
+ PubSub: class {
35
+ constructor(public readonly options?: { projectId?: string }) {}
36
+ },
37
+ }));
29
38
  vi.mock("firebase-admin/firestore", () => ({
30
- getFirestore: vi.fn(() => ({})),
39
+ getFirestore: vi.fn((databaseId?: string) => ({ databaseId })),
31
40
  }));
32
41
  vi.mock("firebase-admin", () => ({
33
42
  apps: [],
@@ -55,9 +64,29 @@ function deletionEvent(uid: string) {
55
64
  } as any;
56
65
  }
57
66
 
67
+ function contextFrom(uid: string): HandlerContext {
68
+ clearData(deletionEvent(uid));
69
+
70
+ const [, ctx] = vi.mocked(handleClear).mock.lastCall as [
71
+ string,
72
+ HandlerContext
73
+ ];
74
+ return ctx;
75
+ }
76
+
58
77
  describe("handler context", () => {
78
+ // Stubbed per test: the afterEach unstubAllEnvs would wipe beforeAll stubs
79
+ // after the first test.
59
80
  beforeEach(() => {
60
81
  vi.clearAllMocks();
82
+ vi.stubEnv("FIREBASE_KIT_INSTANCE_ID", "test-instance");
83
+ vi.stubEnv("FIRESTORE_DATABASE_ID", FIRESTORE_DATABASE_ID);
84
+ // The projectID param reads the project from FIREBASE_CONFIG.
85
+ vi.stubEnv("FIREBASE_CONFIG", JSON.stringify({ projectId: PROJECT_ID }));
86
+ });
87
+
88
+ afterEach(() => {
89
+ vi.unstubAllEnvs();
61
90
  });
62
91
 
63
92
  test("builds without resolving the RTDB client", () => {
@@ -68,14 +97,25 @@ describe("handler context", () => {
68
97
  });
69
98
 
70
99
  test("resolves the RTDB client when the deletion path reads it", () => {
71
- clearData(deletionEvent("uid-2"));
72
-
73
- const [, ctx] = vi.mocked(handleClear).mock.lastCall as [
74
- string,
75
- HandlerContext
76
- ];
100
+ const ctx = contextFrom("uid-2");
77
101
 
78
102
  expect(() => ctx.database).toThrow(NO_DATABASE_URL);
79
103
  expect(admin.database).toHaveBeenCalled();
80
104
  });
105
+
106
+ test("reuses one context across invocations", () => {
107
+ expect(contextFrom("uid-3")).toBe(contextFrom("uid-4"));
108
+ });
109
+
110
+ test("builds the Firestore client for the configured database", () => {
111
+ expect(contextFrom("uid-5").firestore).toEqual({
112
+ databaseId: FIRESTORE_DATABASE_ID,
113
+ });
114
+ });
115
+
116
+ test("builds the Pub/Sub client for the configured project", () => {
117
+ expect(contextFrom("uid-6").pubsub).toEqual({
118
+ options: { projectId: PROJECT_ID },
119
+ });
120
+ });
81
121
  });
@@ -20,7 +20,7 @@ const mocks = vi.hoisted(() => ({ fetch: vi.fn() }));
20
20
 
21
21
  vi.mock("../src/logs");
22
22
  vi.mock("../src/events");
23
- vi.mock("node-fetch", () => ({ default: mocks.fetch }));
23
+ vi.stubGlobal("fetch", mocks.fetch);
24
24
 
25
25
  import * as events from "../src/events";
26
26
  import { handleClear, handleDeletion, handleSearch } from "../src/handlers";
@@ -20,7 +20,7 @@ const mocks = vi.hoisted(() => ({ fetch: vi.fn() }));
20
20
 
21
21
  vi.mock("../src/logs");
22
22
  vi.mock("../src/events");
23
- vi.mock("node-fetch", () => ({ default: mocks.fetch }));
23
+ vi.stubGlobal("fetch", mocks.fetch);
24
24
 
25
25
  import * as logs from "../src/logs";
26
26
  import { runCustomSearchFunction } from "../src/runCustomSearchFunction";