@firebase-function-kits/delete-user-data 0.0.2-rc.2 → 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/CHANGELOG.md +1 -0
- package/README.md +29 -28
- package/lib/config.d.ts.map +1 -1
- package/lib/config.js +24 -6
- package/lib/config.js.map +1 -1
- package/lib/events.js.map +1 -1
- package/lib/export-config.js.map +1 -1
- package/lib/handlers.d.ts.map +1 -1
- package/lib/handlers.js +3 -1
- package/lib/handlers.js.map +1 -1
- package/lib/helpers.d.ts.map +1 -1
- package/lib/helpers.js.map +1 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +5 -1
- package/lib/index.js.map +1 -1
- package/lib/logs.d.ts.map +1 -1
- package/lib/logs.js.map +1 -1
- package/lib/recursiveDelete.d.ts.map +1 -1
- package/lib/recursiveDelete.js.map +1 -1
- package/lib/runBatchPubSubDeletions.js.map +1 -1
- package/lib/runCustomSearchFunction.d.ts.map +1 -1
- package/lib/runCustomSearchFunction.js +1 -5
- package/lib/runCustomSearchFunction.js.map +1 -1
- package/lib/search.d.ts.map +1 -1
- package/lib/search.js.map +1 -1
- package/npm-shrinkwrap.json +14 -1293
- package/package.json +3 -9
- package/src/config.ts +28 -7
- package/src/handlers.ts +5 -1
- package/src/index.ts +5 -1
- package/src/runCustomSearchFunction.ts +0 -1
- package/tests/config-runtime.test.ts +69 -0
- package/tests/config.test.ts +37 -22
- package/tests/context.test.ts +121 -0
- package/tests/handlers.test.ts +22 -3
- package/tests/runCustomSearchFunction.test.ts +1 -1
- package/tsconfig.tsbuildinfo +1 -1
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
|
+
"version": "0.0.2-rc.4",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/firebase/extensions.git",
|
|
@@ -33,13 +33,7 @@
|
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@google-cloud/pubsub": "^4.3.3",
|
|
35
35
|
"firebase-admin": "^13.2.0",
|
|
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
|
-
"vitest": "^4.1.10"
|
|
36
|
+
"firebase-functions": "^7.3.2",
|
|
37
|
+
"lodash.chunk": "^4.2.0"
|
|
44
38
|
}
|
|
45
39
|
}
|
package/src/config.ts
CHANGED
|
@@ -18,17 +18,20 @@ import {
|
|
|
18
18
|
defineBoolean,
|
|
19
19
|
defineInt,
|
|
20
20
|
defineString,
|
|
21
|
-
|
|
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
|
-
|
|
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:
|
|
154
|
+
default: `kit-${instanceId}-discovery`,
|
|
152
155
|
}),
|
|
153
156
|
deletionTopicName: defineString("DELETION_TOPIC_NAME", {
|
|
154
|
-
default:
|
|
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
|
|
201
|
+
searchDepth: optionalInt(params.searchDepth),
|
|
181
202
|
searchFields: params.searchFields.value(),
|
|
182
203
|
searchFunction: optional(params.searchFunction.value()),
|
|
183
|
-
instanceId
|
|
204
|
+
instanceId,
|
|
184
205
|
discoveryTopicName: optional(params.discoveryTopicName.value()),
|
|
185
206
|
deletionTopicName: optional(params.deletionTopicName.value()),
|
|
186
207
|
projectId: projectID.value(),
|
package/src/handlers.ts
CHANGED
|
@@ -19,6 +19,7 @@ import type { DocumentReference } from "firebase-admin/firestore";
|
|
|
19
19
|
import { FieldPath } from "firebase-admin/firestore";
|
|
20
20
|
import chunk from "lodash.chunk";
|
|
21
21
|
import * as events from "./events";
|
|
22
|
+
import { getDatabaseUrl } from "./export-config";
|
|
22
23
|
import { extractUserPaths, hasValidUserPath } from "./helpers";
|
|
23
24
|
import * as logs from "./logs";
|
|
24
25
|
import { recursiveDelete } from "./recursiveDelete";
|
|
@@ -156,7 +157,10 @@ export async function handleClear(
|
|
|
156
157
|
} else {
|
|
157
158
|
logs.firestoreNotConfigured();
|
|
158
159
|
}
|
|
159
|
-
if (
|
|
160
|
+
if (
|
|
161
|
+
ctx.config.rtdbPaths &&
|
|
162
|
+
getDatabaseUrl(ctx.config.rtdbInstance, ctx.config.rtdbLocation)
|
|
163
|
+
) {
|
|
160
164
|
promises.push(clearDatabaseData(ctx.config.rtdbPaths, uid, ctx));
|
|
161
165
|
} else {
|
|
162
166
|
logs.rtdbNotConfigured();
|
package/src/index.ts
CHANGED
|
@@ -74,7 +74,11 @@ function getContext(): HandlerContext {
|
|
|
74
74
|
ctx = {
|
|
75
75
|
firestore: getFirestore(resolved.firestoreDatabaseId),
|
|
76
76
|
storage: admin.storage(),
|
|
77
|
-
|
|
77
|
+
// Resolved on first use. Without a configured RTDB instance there is no
|
|
78
|
+
// databaseURL to initialize the app with, and admin.database() throws.
|
|
79
|
+
get database() {
|
|
80
|
+
return admin.database();
|
|
81
|
+
},
|
|
78
82
|
pubsub: new PubSub({ projectId: resolved.projectId }),
|
|
79
83
|
config: resolved,
|
|
80
84
|
};
|
|
@@ -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
|
+
});
|
package/tests/config.test.ts
CHANGED
|
@@ -48,7 +48,9 @@ const defineString = vi.fn(
|
|
|
48
48
|
new FakeStringParam(name, opts?.default)
|
|
49
49
|
);
|
|
50
50
|
|
|
51
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
|
@@ -0,0 +1,121 @@
|
|
|
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
|
+
|
|
19
|
+
// Stands in for a project with no Realtime Database URL available, which is what
|
|
20
|
+
// an empty SELECTED_DATABASE_INSTANCE leaves behind.
|
|
21
|
+
const NO_DATABASE_URL = "Can't determine Firebase Database URL.";
|
|
22
|
+
|
|
23
|
+
const FIRESTORE_DATABASE_ID = "user-data";
|
|
24
|
+
const PROJECT_ID = "test-project";
|
|
25
|
+
|
|
26
|
+
vi.mock("../src/logs");
|
|
27
|
+
vi.mock("../src/handlers", async (importOriginal) => ({
|
|
28
|
+
...(await importOriginal<typeof import("../src/handlers")>()),
|
|
29
|
+
handleClear: vi.fn(),
|
|
30
|
+
}));
|
|
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
|
+
}));
|
|
38
|
+
vi.mock("firebase-admin/firestore", () => ({
|
|
39
|
+
getFirestore: vi.fn((databaseId?: string) => ({ databaseId })),
|
|
40
|
+
}));
|
|
41
|
+
vi.mock("firebase-admin", () => ({
|
|
42
|
+
apps: [],
|
|
43
|
+
credential: { applicationDefault: vi.fn() },
|
|
44
|
+
initializeApp: vi.fn(),
|
|
45
|
+
storage: vi.fn(() => ({})),
|
|
46
|
+
database: vi.fn(() => {
|
|
47
|
+
throw new Error(NO_DATABASE_URL);
|
|
48
|
+
}),
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
import * as admin from "firebase-admin";
|
|
52
|
+
import type { HandlerContext } from "../src/handlers";
|
|
53
|
+
import { handleClear } from "../src/handlers";
|
|
54
|
+
import { clearData } from "../src/index";
|
|
55
|
+
|
|
56
|
+
function deletionEvent(uid: string) {
|
|
57
|
+
return {
|
|
58
|
+
specversion: "1.0",
|
|
59
|
+
id: "event-id",
|
|
60
|
+
type: "google.firebase.auth.user.v2.deleted",
|
|
61
|
+
source: "//identitytoolkit.googleapis.com/projects/test-project",
|
|
62
|
+
time: "2026-01-01T00:00:00.000Z",
|
|
63
|
+
data: { uid },
|
|
64
|
+
} as any;
|
|
65
|
+
}
|
|
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
|
+
|
|
77
|
+
describe("handler context", () => {
|
|
78
|
+
// Stubbed per test: the afterEach unstubAllEnvs would wipe beforeAll stubs
|
|
79
|
+
// after the first test.
|
|
80
|
+
beforeEach(() => {
|
|
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();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("builds without resolving the RTDB client", () => {
|
|
93
|
+
expect(() => clearData(deletionEvent("uid-1"))).not.toThrow();
|
|
94
|
+
|
|
95
|
+
expect(handleClear).toHaveBeenCalledOnce();
|
|
96
|
+
expect(admin.database).not.toHaveBeenCalled();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("resolves the RTDB client when the deletion path reads it", () => {
|
|
100
|
+
const ctx = contextFrom("uid-2");
|
|
101
|
+
|
|
102
|
+
expect(() => ctx.database).toThrow(NO_DATABASE_URL);
|
|
103
|
+
expect(admin.database).toHaveBeenCalled();
|
|
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
|
+
});
|
|
121
|
+
});
|
package/tests/handlers.test.ts
CHANGED
|
@@ -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.
|
|
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";
|
|
@@ -406,7 +406,11 @@ describe("handleClear", () => {
|
|
|
406
406
|
|
|
407
407
|
test("deletes the configured rtdb paths", async () => {
|
|
408
408
|
const ctx = makeContext({
|
|
409
|
-
config: {
|
|
409
|
+
config: {
|
|
410
|
+
rtdbPaths: "users/{UID},admins/{UID}",
|
|
411
|
+
rtdbInstance: "test-rtdb-instance",
|
|
412
|
+
rtdbLocation: "us-central1",
|
|
413
|
+
},
|
|
410
414
|
});
|
|
411
415
|
|
|
412
416
|
await handleClear(UID, ctx);
|
|
@@ -418,6 +422,17 @@ describe("handleClear", () => {
|
|
|
418
422
|
});
|
|
419
423
|
});
|
|
420
424
|
|
|
425
|
+
test("skips rtdb deletion when no database instance is configured", async () => {
|
|
426
|
+
const ctx = makeContext({
|
|
427
|
+
config: { rtdbPaths: "users/{UID}" },
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
await handleClear(UID, ctx);
|
|
431
|
+
|
|
432
|
+
expect(ctx.rtdbRemovals).toEqual([]);
|
|
433
|
+
expect(log.rtdbNotConfigured).toHaveBeenCalled();
|
|
434
|
+
});
|
|
435
|
+
|
|
421
436
|
test("deletes the configured storage paths", async () => {
|
|
422
437
|
const ctx = makeContext({
|
|
423
438
|
config: {
|
|
@@ -463,7 +478,11 @@ describe("handleClear", () => {
|
|
|
463
478
|
test("logs rtdb errors without failing", async () => {
|
|
464
479
|
const error = new Error("boom");
|
|
465
480
|
const ctx = makeContext({
|
|
466
|
-
config: {
|
|
481
|
+
config: {
|
|
482
|
+
rtdbPaths: "users/{UID}",
|
|
483
|
+
rtdbInstance: "test-rtdb-instance",
|
|
484
|
+
rtdbLocation: "us-central1",
|
|
485
|
+
},
|
|
467
486
|
rtdbError: error,
|
|
468
487
|
});
|
|
469
488
|
|
|
@@ -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.
|
|
23
|
+
vi.stubGlobal("fetch", mocks.fetch);
|
|
24
24
|
|
|
25
25
|
import * as logs from "../src/logs";
|
|
26
26
|
import { runCustomSearchFunction } from "../src/runCustomSearchFunction";
|
package/tsconfig.tsbuildinfo
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"7.0.2","root":["./src/config.ts","./src/events.ts","./src/export-config.ts","./src/handlers.ts","./src/helpers.ts","./src/identity-shim.d.ts","./src/index.ts","./src/lib.ts","./src/logs.ts","./src/recursiveDelete.ts","./src/runBatchPubSubDeletions.ts","./src/runCustomSearchFunction.ts","./src/search.ts"],"packageJsons":["./node_modules/@firebase/app-types/package.json","./node_modules/@firebase/component/package.json","./node_modules/@firebase/database-types/package.json","./node_modules/@firebase/logger/package.json","./node_modules/@firebase/util/package.json","./node_modules/@google-cloud/firestore/package.json","./node_modules/@google-cloud/precise-date/package.json","./node_modules/@google-cloud/pubsub/package.json","./node_modules/@google-cloud/storage/build/cjs/package.json","./node_modules/@google-cloud/storage/package.json","./node_modules/@grpc/grpc-js/node_modules/@grpc/proto-loader/package.json","./node_modules/@grpc/grpc-js/package.json","./node_modules/@grpc/proto-loader/package.json","./node_modules/@js-sdsl/ordered-map/package.json","./node_modules/@opentelemetry/api/package.json","./node_modules/@types/body-parser/package.json","./node_modules/@types/connect/package.json","./node_modules/@types/cors/package.json","./node_modules/@types/express-serve-static-core/package.json","./node_modules/@types/express/package.json","./node_modules/@types/http-errors/package.json","./node_modules/@types/lodash.chunk/package.json","./node_modules/@types/lodash/package.json","./node_modules/@types/node-fetch/package.json","./node_modules/@types/node/package.json","./node_modules/@types/qs/package.json","./node_modules/@types/range-parser/package.json","./node_modules/@types/send/package.json","./node_modules/@types/serve-static/package.json","./node_modules/abort-controller/package.json","./node_modules/body-parser/package.json","./node_modules/cors/package.json","./node_modules/event-target-shim/package.json","./node_modules/express/package.json","./node_modules/firebase-admin/package.json","./node_modules/firebase-functions/package.json","./node_modules/form-data/package.json","./node_modules/gaxios/package.json","./node_modules/gcp-metadata/package.json","./node_modules/google-auth-library/package.json","./node_modules/google-gax/package.json","./node_modules/gtoken/package.json","./node_modules/http-errors/package.json","./node_modules/lodash.chunk/package.json","./node_modules/long/package.json","./node_modules/long/umd/package.json","./node_modules/node-fetch/package.json","./node_modules/p-defer/package.json","./node_modules/proto3-json-serializer/package.json","./node_modules/protobufjs/package.json","./node_modules/qs/package.json","./node_modules/range-parser/package.json","./node_modules/send/package.json","./node_modules/serve-static/package.json","./node_modules/string_decoder/package.json","./node_modules/teeny-request/package.json","./node_modules/undici-types/package.json","./package.json"],"missingPackageJsons":["./node_modules/@apollo/server/package.json","./node_modules/@as-integrations/express4/package.json","./node_modules/@firebase/logger/dist/package.json","./node_modules/@firebase/logger/dist/src/package.json","./node_modules/@firebase/util/dist/package.json","./node_modules/@google-cloud/firestore/types/package.json","./node_modules/@google-cloud/firestore/types/protos/package.json","./node_modules/@google-cloud/firestore/types/v1/package.json","./node_modules/@google-cloud/firestore/types/v1beta1/package.json","./node_modules/@google-cloud/precise-date/build/package.json","./node_modules/@google-cloud/precise-date/build/src/package.json","./node_modules/@google-cloud/pubsub/build/package.json","./node_modules/@google-cloud/pubsub/build/protos/package.json","./node_modules/@google-cloud/pubsub/build/src/package.json","./node_modules/@google-cloud/pubsub/build/src/publisher/package.json","./node_modules/@google-cloud/pubsub/build/src/v1/package.json","./node_modules/@google-cloud/storage/build/cjs/src/nodejs-common/package.json","./node_modules/@google-cloud/storage/build/cjs/src/package.json","./node_modules/@grpc/grpc-js/build/package.json","./node_modules/@grpc/grpc-js/build/src/client/package.json","./node_modules/@grpc/grpc-js/build/src/generated/google/package.json","./node_modules/@grpc/grpc-js/build/src/generated/google/protobuf/package.json","./node_modules/@grpc/grpc-js/build/src/generated/grpc/channelz/package.json","./node_modules/@grpc/grpc-js/build/src/generated/grpc/channelz/v1/package.json","./node_modules/@grpc/grpc-js/build/src/generated/grpc/package.json","./node_modules/@grpc/grpc-js/build/src/generated/package.json","./node_modules/@grpc/grpc-js/build/src/generated/xds/data/orca/package.json","./node_modules/@grpc/grpc-js/build/src/generated/xds/data/orca/v3/package.json","./node_modules/@grpc/grpc-js/build/src/generated/xds/data/package.json","./node_modules/@grpc/grpc-js/build/src/generated/xds/package.json","./node_modules/@grpc/grpc-js/build/src/generated/xds/service/orca/package.json","./node_modules/@grpc/grpc-js/build/src/generated/xds/service/orca/v3/package.json","./node_modules/@grpc/grpc-js/build/src/generated/xds/service/package.json","./node_modules/@grpc/grpc-js/build/src/package.json","./node_modules/@grpc/grpc-js/node_modules/@grpc/proto-loader/build/package.json","./node_modules/@grpc/grpc-js/node_modules/@grpc/proto-loader/build/src/package.json","./node_modules/@grpc/grpc-js/node_modules/@js-sdsl/ordered-map/package.json","./node_modules/@grpc/grpc-js/node_modules/events/package.json","./node_modules/@grpc/grpc-js/node_modules/http2/package.json","./node_modules/@grpc/grpc-js/node_modules/long/package.json","./node_modules/@grpc/grpc-js/node_modules/net/package.json","./node_modules/@grpc/grpc-js/node_modules/protobufjs/ext/descriptor/package.json","./node_modules/@grpc/grpc-js/node_modules/protobufjs/package.json","./node_modules/@grpc/grpc-js/node_modules/stream/package.json","./node_modules/@grpc/grpc-js/node_modules/tls/package.json","./node_modules/@grpc/proto-loader/build/package.json","./node_modules/@grpc/proto-loader/build/src/package.json","./node_modules/@js-sdsl/ordered-map/dist/esm/package.json","./node_modules/@js-sdsl/ordered-map/dist/package.json","./node_modules/@opentelemetry/api/build/package.json","./node_modules/@opentelemetry/api/build/src/api/package.json","./node_modules/@opentelemetry/api/build/src/baggage/internal/package.json","./node_modules/@opentelemetry/api/build/src/baggage/package.json","./node_modules/@opentelemetry/api/build/src/common/package.json","./node_modules/@opentelemetry/api/build/src/context/package.json","./node_modules/@opentelemetry/api/build/src/diag/package.json","./node_modules/@opentelemetry/api/build/src/metrics/package.json","./node_modules/@opentelemetry/api/build/src/package.json","./node_modules/@opentelemetry/api/build/src/propagation/package.json","./node_modules/@opentelemetry/api/build/src/trace/internal/package.json","./node_modules/@opentelemetry/api/build/src/trace/package.json","./node_modules/@types/assert/package.json","./node_modules/@types/assert/strict/package.json","./node_modules/@types/async_hooks/package.json","./node_modules/@types/buffer/package.json","./node_modules/@types/child_process/package.json","./node_modules/@types/cluster/package.json","./node_modules/@types/console/package.json","./node_modules/@types/constants/package.json","./node_modules/@types/crypto/package.json","./node_modules/@types/dgram/package.json","./node_modules/@types/diagnostics_channel/package.json","./node_modules/@types/dns/package.json","./node_modules/@types/dns/promises/package.json","./node_modules/@types/domain/package.json","./node_modules/@types/events/package.json","./node_modules/@types/fs/package.json","./node_modules/@types/fs/promises/package.json","./node_modules/@types/http/package.json","./node_modules/@types/http2/package.json","./node_modules/@types/https/package.json","./node_modules/@types/inspector/package.json","./node_modules/@types/inspector/promises/package.json","./node_modules/@types/lodash/common/package.json","./node_modules/@types/module/package.json","./node_modules/@types/net/package.json","./node_modules/@types/node/assert/package.json","./node_modules/@types/node/dns/package.json","./node_modules/@types/node/fs/package.json","./node_modules/@types/node/inspector/package.json","./node_modules/@types/node/path/package.json","./node_modules/@types/node/readline/package.json","./node_modules/@types/node/stream/package.json","./node_modules/@types/node/test/package.json","./node_modules/@types/node/timers/package.json","./node_modules/@types/node/util/package.json","./node_modules/@types/node/web-globals/package.json","./node_modules/@types/node/zlib/package.json","./node_modules/@types/os/package.json","./node_modules/@types/path/package.json","./node_modules/@types/path/posix/package.json","./node_modules/@types/path/win32/package.json","./node_modules/@types/perf_hooks/package.json","./node_modules/@types/process/package.json","./node_modules/@types/punycode/package.json","./node_modules/@types/querystring/package.json","./node_modules/@types/readline/package.json","./node_modules/@types/readline/promises/package.json","./node_modules/@types/repl/package.json","./node_modules/@types/stream/consumers/package.json","./node_modules/@types/stream/iter/package.json","./node_modules/@types/stream/package.json","./node_modules/@types/stream/promises/package.json","./node_modules/@types/stream/web/package.json","./node_modules/@types/string_decoder/package.json","./node_modules/@types/timers/package.json","./node_modules/@types/timers/promises/package.json","./node_modules/@types/tls/package.json","./node_modules/@types/trace_events/package.json","./node_modules/@types/tty/package.json","./node_modules/@types/url/package.json","./node_modules/@types/util/package.json","./node_modules/@types/util/types/package.json","./node_modules/@types/v8/package.json","./node_modules/@types/vm/package.json","./node_modules/@types/wasi/package.json","./node_modules/@types/worker_threads/package.json","./node_modules/@types/zlib/package.json","./node_modules/abort-controller/dist/package.json","./node_modules/assert/package.json","./node_modules/assert/strict/package.json","./node_modules/async_hooks/package.json","./node_modules/buffer/package.json","./node_modules/child_process/package.json","./node_modules/cluster/package.json","./node_modules/connect/package.json","./node_modules/console/package.json","./node_modules/constants/package.json","./node_modules/crypto/package.json","./node_modules/dgram/package.json","./node_modules/diagnostics_channel/package.json","./node_modules/dns/package.json","./node_modules/dns/promises/package.json","./node_modules/domain/package.json","./node_modules/events/package.json","./node_modules/express-serve-static-core/package.json","./node_modules/firebase-admin/app-check/package.json","./node_modules/firebase-admin/app/package.json","./node_modules/firebase-admin/auth/package.json","./node_modules/firebase-admin/database/package.json","./node_modules/firebase-admin/eventarc/package.json","./node_modules/firebase-admin/firestore/package.json","./node_modules/firebase-admin/lib/app-check/package.json","./node_modules/firebase-admin/lib/app/package.json","./node_modules/firebase-admin/lib/auth/package.json","./node_modules/firebase-admin/lib/credential/package.json","./node_modules/firebase-admin/lib/database/package.json","./node_modules/firebase-admin/lib/eventarc/package.json","./node_modules/firebase-admin/lib/firestore/package.json","./node_modules/firebase-admin/lib/installations/package.json","./node_modules/firebase-admin/lib/instance-id/package.json","./node_modules/firebase-admin/lib/machine-learning/package.json","./node_modules/firebase-admin/lib/messaging/package.json","./node_modules/firebase-admin/lib/package.json","./node_modules/firebase-admin/lib/project-management/package.json","./node_modules/firebase-admin/lib/remote-config/package.json","./node_modules/firebase-admin/lib/security-rules/package.json","./node_modules/firebase-admin/lib/storage/package.json","./node_modules/firebase-admin/lib/utils/package.json","./node_modules/firebase-admin/node_modules/@firebase/database-types/package.json","./node_modules/firebase-admin/node_modules/@google-cloud/firestore/package.json","./node_modules/firebase-admin/node_modules/@google-cloud/storage/package.json","./node_modules/firebase-admin/node_modules/http/package.json","./node_modules/firebase-functions/lib/common/package.json","./node_modules/firebase-functions/lib/common/providers/package.json","./node_modules/firebase-functions/lib/logger/package.json","./node_modules/firebase-functions/lib/package.json","./node_modules/firebase-functions/lib/params/package.json","./node_modules/firebase-functions/lib/runtime/package.json","./node_modules/firebase-functions/lib/v1/package.json","./node_modules/firebase-functions/lib/v1/providers/package.json","./node_modules/firebase-functions/lib/v2/package.json","./node_modules/firebase-functions/lib/v2/providers/alerts/package.json","./node_modules/firebase-functions/lib/v2/providers/dataconnect/package.json","./node_modules/firebase-functions/lib/v2/providers/package.json","./node_modules/firebase-functions/params/package.json","./node_modules/firebase-functions/v1/auth/package.json","./node_modules/firebase-functions/v2/identity/package.json","./node_modules/firebase-functions/v2/package.json","./node_modules/firebase-functions/v2/pubsub/package.json","./node_modules/form-data/node_modules/http/package.json","./node_modules/form-data/node_modules/stream/package.json","./node_modules/fs/package.json","./node_modules/fs/promises/package.json","./node_modules/gaxios/build/package.json","./node_modules/gaxios/build/src/package.json","./node_modules/gcp-metadata/build/package.json","./node_modules/gcp-metadata/build/src/package.json","./node_modules/google-auth-library/build/package.json","./node_modules/google-auth-library/build/src/auth/googleauth/package.json","./node_modules/google-auth-library/build/src/auth/package.json","./node_modules/google-auth-library/build/src/crypto/package.json","./node_modules/google-auth-library/build/src/package.json","./node_modules/google-gax/build/package.json","./node_modules/google-gax/build/protos/package.json","./node_modules/google-gax/build/src/bundlingCalls/package.json","./node_modules/google-gax/build/src/longRunningCalls/package.json","./node_modules/google-gax/build/src/normalCalls/package.json","./node_modules/google-gax/build/src/package.json","./node_modules/google-gax/build/src/paginationCalls/package.json","./node_modules/google-gax/build/src/streamingCalls/package.json","./node_modules/graphql/package.json","./node_modules/gtoken/build/package.json","./node_modules/gtoken/build/src/package.json","./node_modules/http/package.json","./node_modules/http2/package.json","./node_modules/https/package.json","./node_modules/inspector/package.json","./node_modules/inspector/promises/package.json","./node_modules/lodash/package.json","./node_modules/module/package.json","./node_modules/net/package.json","./node_modules/os/package.json","./node_modules/path/package.json","./node_modules/path/posix/package.json","./node_modules/path/win32/package.json","./node_modules/perf_hooks/package.json","./node_modules/process/package.json","./node_modules/proto3-json-serializer/build/package.json","./node_modules/proto3-json-serializer/build/src/package.json","./node_modules/protobufjs/ext/descriptor/package.json","./node_modules/protobufjs/ext/package.json","./node_modules/protobufjs/minimal/package.json","./node_modules/punycode/package.json","./node_modules/querystring/package.json","./node_modules/readline/package.json","./node_modules/readline/promises/package.json","./node_modules/repl/package.json","./node_modules/stream/consumers/package.json","./node_modules/stream/iter/package.json","./node_modules/stream/package.json","./node_modules/stream/promises/package.json","./node_modules/stream/web/package.json","./node_modules/teeny-request/build/package.json","./node_modules/teeny-request/build/src/package.json","./node_modules/teeny-request/node_modules/http/package.json","./node_modules/teeny-request/node_modules/https/package.json","./node_modules/teeny-request/node_modules/stream/package.json","./node_modules/timers/package.json","./node_modules/timers/promises/package.json","./node_modules/tls/package.json","./node_modules/trace_events/package.json","./node_modules/tty/package.json","./node_modules/url/package.json","./node_modules/util/package.json","./node_modules/util/types/package.json","./node_modules/v8/package.json","./node_modules/vm/package.json","./node_modules/wasi/package.json","./node_modules/worker_threads/package.json","./node_modules/zlib/package.json"]}
|
|
1
|
+
{"root":["./src/config.ts","./src/events.ts","./src/export-config.ts","./src/handlers.ts","./src/helpers.ts","./src/identity-shim.d.ts","./src/index.ts","./src/lib.ts","./src/logs.ts","./src/recursiveDelete.ts","./src/runBatchPubSubDeletions.ts","./src/runCustomSearchFunction.ts","./src/search.ts"],"version":"5.9.3"}
|