@firebase-function-kits/delete-user-data 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.
- package/CHANGELOG.md +1 -0
- package/README.md +151 -0
- package/lib/config.d.ts +22 -0
- package/lib/config.d.ts.map +1 -0
- package/lib/config.js +86 -0
- package/lib/config.js.map +1 -0
- package/lib/events.d.ts +18 -0
- package/lib/events.d.ts.map +1 -0
- package/lib/events.js +71 -0
- package/lib/events.js.map +1 -0
- package/lib/export-config.d.ts +55 -0
- package/lib/export-config.d.ts.map +1 -0
- package/lib/export-config.js +56 -0
- package/lib/export-config.js.map +1 -0
- package/lib/handlers.d.ts +35 -0
- package/lib/handlers.d.ts.map +1 -0
- package/lib/handlers.js +243 -0
- package/lib/handlers.js.map +1 -0
- package/lib/helpers.d.ts +19 -0
- package/lib/helpers.d.ts.map +1 -0
- package/lib/helpers.js +39 -0
- package/lib/helpers.js.map +1 -0
- package/lib/index.d.ts +6 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +112 -0
- package/lib/index.js.map +1 -0
- package/lib/lib.d.ts +23 -0
- package/lib/lib.d.ts.map +1 -0
- package/lib/lib.js +38 -0
- package/lib/lib.js.map +1 -0
- package/lib/logs.d.ts +26 -0
- package/lib/logs.d.ts.map +1 -0
- package/lib/logs.js +116 -0
- package/lib/logs.js.map +1 -0
- package/lib/recursiveDelete.d.ts +18 -0
- package/lib/recursiveDelete.d.ts.map +1 -0
- package/lib/recursiveDelete.js +37 -0
- package/lib/recursiveDelete.js.map +1 -0
- package/lib/runBatchPubSubDeletions.d.ts +27 -0
- package/lib/runBatchPubSubDeletions.d.ts.map +1 -0
- package/lib/runBatchPubSubDeletions.js +47 -0
- package/lib/runBatchPubSubDeletions.js.map +1 -0
- package/lib/runCustomSearchFunction.d.ts +18 -0
- package/lib/runCustomSearchFunction.d.ts.map +1 -0
- package/lib/runCustomSearchFunction.js +78 -0
- package/lib/runCustomSearchFunction.js.map +1 -0
- package/lib/search.d.ts +19 -0
- package/lib/search.d.ts.map +1 -0
- package/lib/search.js +29 -0
- package/lib/search.js.map +1 -0
- package/package.json +37 -4
- package/src/config.ts +98 -0
- package/src/events.ts +38 -0
- package/src/export-config.ts +112 -0
- package/src/handlers.ts +271 -0
- package/src/helpers.ts +42 -0
- package/src/index.ts +100 -0
- package/src/lib.ts +41 -0
- package/src/logs.ts +127 -0
- package/src/recursiveDelete.ts +42 -0
- package/src/runBatchPubSubDeletions.ts +66 -0
- package/src/runCustomSearchFunction.ts +50 -0
- package/src/search.ts +37 -0
- package/tests/config.test.ts +180 -0
- package/tests/export-config.test.ts +117 -0
- package/tests/fakes.ts +325 -0
- package/tests/handlers.test.ts +517 -0
- package/tests/helpers.test.ts +126 -0
- package/tests/lib.test.ts +40 -0
- package/tests/recursiveDelete.test.ts +102 -0
- package/tests/runBatchPubSubDeletions.test.ts +164 -0
- package/tests/runCustomSearchFunction.test.ts +125 -0
- package/tests/search.test.ts +127 -0
- package/tsconfig.json +18 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/index.js +0 -0
|
@@ -0,0 +1,102 @@
|
|
|
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, describe, expect, test, vi } from "vitest";
|
|
18
|
+
import { recursiveDelete } from "../src/recursiveDelete";
|
|
19
|
+
import { createFakeFirestore } from "./fakes";
|
|
20
|
+
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
vi.restoreAllMocks();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Parity: delete-user-data/functions/__tests__/recursiveDelete.test.ts
|
|
26
|
+
describe("recursiveDelete", () => {
|
|
27
|
+
test("successfully deletes a document reference", async () => {
|
|
28
|
+
const db = createFakeFirestore({ "documents/doc1": { foo: "bar" } });
|
|
29
|
+
|
|
30
|
+
await recursiveDelete("documents/doc1", db as any);
|
|
31
|
+
|
|
32
|
+
expect(db.exists("documents/doc1")).toBe(false);
|
|
33
|
+
expect(db.recursiveDeleteCalls).toEqual([
|
|
34
|
+
{ path: "documents/doc1", type: "document" },
|
|
35
|
+
]);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("successfully deletes a collection reference", async () => {
|
|
39
|
+
const db = createFakeFirestore({
|
|
40
|
+
"documents/doc1/collection1/doc2": { foo: "bar" },
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
await recursiveDelete("documents/doc1/collection1", db as any);
|
|
44
|
+
|
|
45
|
+
const collection = await db.collection("documents/doc1/collection1").get();
|
|
46
|
+
expect(collection.docs.length).toBe(0);
|
|
47
|
+
expect(db.recursiveDeleteCalls).toEqual([
|
|
48
|
+
{ path: "documents/doc1/collection1", type: "collection" },
|
|
49
|
+
]);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("successfully deletes a document with a subcollection", async () => {
|
|
53
|
+
const db = createFakeFirestore({
|
|
54
|
+
"documents/doc1": { foo: "bar" },
|
|
55
|
+
"documents/doc1/collection1/doc2/collection2/doc3": { foo: "bar" },
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
await recursiveDelete("documents/doc1", db as any);
|
|
59
|
+
|
|
60
|
+
const collection = await db
|
|
61
|
+
.collection("documents/doc1/collection1/doc2/collection2")
|
|
62
|
+
.get();
|
|
63
|
+
expect(collection.docs.length).toBe(0);
|
|
64
|
+
expect(db.exists("documents/doc1")).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("leaves sibling documents untouched", async () => {
|
|
68
|
+
const db = createFakeFirestore({
|
|
69
|
+
"documents/doc1": { foo: "bar" },
|
|
70
|
+
"documents/doc10": { foo: "bar" },
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
await recursiveDelete("documents/doc1", db as any);
|
|
74
|
+
|
|
75
|
+
expect(db.exists("documents/doc1")).toBe(false);
|
|
76
|
+
expect(db.exists("documents/doc10")).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("retries a failed write up to the retry limit", async () => {
|
|
80
|
+
const db = createFakeFirestore();
|
|
81
|
+
const onWriteError = vi.fn();
|
|
82
|
+
db.bulkWriter = () => ({ onWriteError, close: async () => undefined });
|
|
83
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => undefined);
|
|
84
|
+
|
|
85
|
+
await recursiveDelete("documents/doc1", db as any);
|
|
86
|
+
|
|
87
|
+
const handler = onWriteError.mock.calls[0][0] as (
|
|
88
|
+
error: unknown
|
|
89
|
+
) => boolean;
|
|
90
|
+
const error = {
|
|
91
|
+
failedAttempts: 1,
|
|
92
|
+
documentRef: { path: "documents/doc1" },
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
expect(handler(error)).toBe(true);
|
|
96
|
+
expect(handler({ ...error, failedAttempts: 3 })).toBe(false);
|
|
97
|
+
expect(warn).toHaveBeenCalledWith(
|
|
98
|
+
"Failed to delete document: ",
|
|
99
|
+
"documents/doc1"
|
|
100
|
+
);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
@@ -0,0 +1,164 @@
|
|
|
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
|
+
vi.mock("../src/logs");
|
|
20
|
+
vi.mock("../src/events");
|
|
21
|
+
|
|
22
|
+
import {
|
|
23
|
+
publishSearch,
|
|
24
|
+
runBatchPubSubDeletions,
|
|
25
|
+
} from "../src/runBatchPubSubDeletions";
|
|
26
|
+
import { createFakeFirestore, makeContext } from "./fakes";
|
|
27
|
+
|
|
28
|
+
const UID = "testUid";
|
|
29
|
+
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
vi.clearAllMocks();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
vi.unstubAllEnvs();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe("runBatchPubSubDeletions", () => {
|
|
39
|
+
// Parity: delete-user-data/functions/__tests__/runBatchPubSubDeletions.test.ts
|
|
40
|
+
test("cannot delete paths with an invalid userId", async () => {
|
|
41
|
+
const firestore = createFakeFirestore({
|
|
42
|
+
"runBatchPubSubDeletions/doc1": { testing: "testing" },
|
|
43
|
+
});
|
|
44
|
+
const ctx = makeContext({ firestore });
|
|
45
|
+
|
|
46
|
+
await runBatchPubSubDeletions(
|
|
47
|
+
{ firestorePaths: ["runBatchPubSubDeletions/doc1"] },
|
|
48
|
+
"invalidUserId",
|
|
49
|
+
ctx
|
|
50
|
+
);
|
|
51
|
+
await ctx.drain();
|
|
52
|
+
|
|
53
|
+
expect(firestore.exists("runBatchPubSubDeletions/doc1")).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("publishes the paths to the deletion topic", async () => {
|
|
57
|
+
const ctx = makeContext();
|
|
58
|
+
|
|
59
|
+
await runBatchPubSubDeletions(
|
|
60
|
+
{ firestorePaths: ["users/doc1", "users/doc2"] },
|
|
61
|
+
UID,
|
|
62
|
+
ctx
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
expect(ctx.pubsub.published).toEqual([
|
|
66
|
+
{
|
|
67
|
+
topic: "projects/demo-test/topics/kit-test-instance-deletion",
|
|
68
|
+
json: { paths: ["users/doc1", "users/doc2"], uid: UID },
|
|
69
|
+
},
|
|
70
|
+
]);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("chunks the paths into messages of 450", async () => {
|
|
74
|
+
const ctx = makeContext();
|
|
75
|
+
const firestorePaths = Array.from(
|
|
76
|
+
{ length: 901 },
|
|
77
|
+
(_value, index) => `users/doc${index}`
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
await runBatchPubSubDeletions({ firestorePaths }, UID, ctx);
|
|
81
|
+
|
|
82
|
+
expect(ctx.pubsub.published).toHaveLength(3);
|
|
83
|
+
expect(
|
|
84
|
+
ctx.pubsub.published.map(
|
|
85
|
+
(message) => (message.json as { paths: string[] }).paths.length
|
|
86
|
+
)
|
|
87
|
+
).toEqual([450, 450, 1]);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("publishes nothing for an empty path list", async () => {
|
|
91
|
+
const ctx = makeContext();
|
|
92
|
+
|
|
93
|
+
await runBatchPubSubDeletions({ firestorePaths: [] }, UID, ctx);
|
|
94
|
+
|
|
95
|
+
expect(ctx.pubsub.published).toEqual([]);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("publishes nothing when firestorePaths is not an array", async () => {
|
|
99
|
+
const ctx = makeContext();
|
|
100
|
+
|
|
101
|
+
await runBatchPubSubDeletions(
|
|
102
|
+
{ firestorePaths: undefined as unknown as string[] },
|
|
103
|
+
UID,
|
|
104
|
+
ctx
|
|
105
|
+
);
|
|
106
|
+
await runBatchPubSubDeletions(
|
|
107
|
+
{ firestorePaths: "users/doc1" as unknown as string[] },
|
|
108
|
+
UID,
|
|
109
|
+
ctx
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
expect(ctx.pubsub.published).toEqual([]);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe("topic resolution", () => {
|
|
117
|
+
test("falls back to GOOGLE_CLOUD_PROJECT when the config has no project id", async () => {
|
|
118
|
+
vi.stubEnv("GOOGLE_CLOUD_PROJECT", "env-project");
|
|
119
|
+
const ctx = makeContext({ config: { projectId: undefined } });
|
|
120
|
+
|
|
121
|
+
await publishSearch(UID, 1, "users", ctx);
|
|
122
|
+
|
|
123
|
+
expect(ctx.pubsub.published[0].topic).toBe(
|
|
124
|
+
"projects/env-project/topics/kit-test-instance-discovery"
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("falls back to PROJECT_ID", async () => {
|
|
129
|
+
vi.stubEnv("GOOGLE_CLOUD_PROJECT", undefined);
|
|
130
|
+
vi.stubEnv("PROJECT_ID", "legacy-project");
|
|
131
|
+
const ctx = makeContext({ config: { projectId: undefined } });
|
|
132
|
+
|
|
133
|
+
await publishSearch(UID, 1, "users", ctx);
|
|
134
|
+
|
|
135
|
+
expect(ctx.pubsub.published[0].topic).toBe(
|
|
136
|
+
"projects/legacy-project/topics/kit-test-instance-discovery"
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("uses the bare topic name when no project id can be resolved", async () => {
|
|
141
|
+
vi.stubEnv("GOOGLE_CLOUD_PROJECT", undefined);
|
|
142
|
+
vi.stubEnv("PROJECT_ID", undefined);
|
|
143
|
+
const ctx = makeContext({ config: { projectId: undefined } });
|
|
144
|
+
|
|
145
|
+
await publishSearch(UID, 1, "users", ctx);
|
|
146
|
+
|
|
147
|
+
expect(ctx.pubsub.published[0].topic).toBe("kit-test-instance-discovery");
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe("publishSearch", () => {
|
|
152
|
+
test("publishes the path, uid and depth", async () => {
|
|
153
|
+
const ctx = makeContext();
|
|
154
|
+
|
|
155
|
+
await publishSearch(UID, 2, "users/doc1/posts", ctx);
|
|
156
|
+
|
|
157
|
+
expect(ctx.pubsub.published).toEqual([
|
|
158
|
+
{
|
|
159
|
+
topic: "projects/demo-test/topics/kit-test-instance-discovery",
|
|
160
|
+
json: { path: "users/doc1/posts", uid: UID, depth: 2 },
|
|
161
|
+
},
|
|
162
|
+
]);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
@@ -0,0 +1,125 @@
|
|
|
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 { beforeEach, describe, expect, test, vi } from "vitest";
|
|
18
|
+
|
|
19
|
+
const mocks = vi.hoisted(() => ({ fetch: vi.fn() }));
|
|
20
|
+
|
|
21
|
+
vi.mock("../src/logs");
|
|
22
|
+
vi.mock("../src/events");
|
|
23
|
+
vi.mock("node-fetch", () => ({ default: mocks.fetch }));
|
|
24
|
+
|
|
25
|
+
import * as logs from "../src/logs";
|
|
26
|
+
import { runCustomSearchFunction } from "../src/runCustomSearchFunction";
|
|
27
|
+
import { createFakeFirestore, deletionMessages, makeContext } from "./fakes";
|
|
28
|
+
|
|
29
|
+
const UID = "testUid";
|
|
30
|
+
const SEARCH_FUNCTION = "https://example.com/search";
|
|
31
|
+
const log = vi.mocked(logs);
|
|
32
|
+
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
vi.clearAllMocks();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Parity: delete-user-data/functions/__tests__/searchFunction.test.ts, which
|
|
38
|
+
// exercises the same path end-to-end through the emulator.
|
|
39
|
+
describe("runCustomSearchFunction", () => {
|
|
40
|
+
test("posts the uid to the configured function", async () => {
|
|
41
|
+
mocks.fetch.mockResolvedValue({ ok: true, json: async () => [] });
|
|
42
|
+
const ctx = makeContext({ config: { searchFunction: SEARCH_FUNCTION } });
|
|
43
|
+
|
|
44
|
+
await runCustomSearchFunction(UID, ctx);
|
|
45
|
+
|
|
46
|
+
expect(mocks.fetch).toHaveBeenCalledWith(SEARCH_FUNCTION, {
|
|
47
|
+
method: "POST",
|
|
48
|
+
body: JSON.stringify({ uid: UID }),
|
|
49
|
+
headers: { "Content-Type": "application/json" },
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("queues the paths from an array response", async () => {
|
|
54
|
+
mocks.fetch.mockResolvedValue({
|
|
55
|
+
ok: true,
|
|
56
|
+
json: async () => ["users/doc1", "users/doc2"],
|
|
57
|
+
});
|
|
58
|
+
const ctx = makeContext({ config: { searchFunction: SEARCH_FUNCTION } });
|
|
59
|
+
|
|
60
|
+
await runCustomSearchFunction(UID, ctx);
|
|
61
|
+
|
|
62
|
+
expect(deletionMessages(ctx)).toEqual([
|
|
63
|
+
{ paths: ["users/doc1", "users/doc2"], uid: UID },
|
|
64
|
+
]);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("queues the paths from a { firestorePaths } response", async () => {
|
|
68
|
+
mocks.fetch.mockResolvedValue({
|
|
69
|
+
ok: true,
|
|
70
|
+
json: async () => ({ firestorePaths: ["users/doc1"] }),
|
|
71
|
+
});
|
|
72
|
+
const ctx = makeContext({ config: { searchFunction: SEARCH_FUNCTION } });
|
|
73
|
+
|
|
74
|
+
await runCustomSearchFunction(UID, ctx);
|
|
75
|
+
|
|
76
|
+
expect(deletionMessages(ctx)).toEqual([
|
|
77
|
+
{ paths: ["users/doc1"], uid: UID },
|
|
78
|
+
]);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("deletes the returned documents once the deletion is dispatched", async () => {
|
|
82
|
+
const firestore = createFakeFirestore({
|
|
83
|
+
"searchFunction/doc1": { uid: UID },
|
|
84
|
+
"searchFunction/doc2": { uid: "someoneElse" },
|
|
85
|
+
});
|
|
86
|
+
mocks.fetch.mockResolvedValue({
|
|
87
|
+
ok: true,
|
|
88
|
+
json: async () => ["searchFunction/doc1", "searchFunction/doc2"],
|
|
89
|
+
});
|
|
90
|
+
const ctx = makeContext({
|
|
91
|
+
firestore,
|
|
92
|
+
config: { searchFunction: SEARCH_FUNCTION },
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
await runCustomSearchFunction(UID, ctx);
|
|
96
|
+
await ctx.drain();
|
|
97
|
+
|
|
98
|
+
expect(firestore.exists("searchFunction/doc1")).toBe(false);
|
|
99
|
+
expect(firestore.exists("searchFunction/doc2")).toBe(true);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("logs and stops when the function responds with an error", async () => {
|
|
103
|
+
mocks.fetch.mockResolvedValue({
|
|
104
|
+
ok: false,
|
|
105
|
+
text: async () => "internal error",
|
|
106
|
+
});
|
|
107
|
+
const ctx = makeContext({ config: { searchFunction: SEARCH_FUNCTION } });
|
|
108
|
+
|
|
109
|
+
await runCustomSearchFunction(UID, ctx);
|
|
110
|
+
|
|
111
|
+
expect(log.customFunctionError).toHaveBeenCalledWith(
|
|
112
|
+
new Error("internal error")
|
|
113
|
+
);
|
|
114
|
+
expect(ctx.pubsub.published).toEqual([]);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("does nothing when no search function is configured", async () => {
|
|
118
|
+
const ctx = makeContext();
|
|
119
|
+
|
|
120
|
+
await runCustomSearchFunction(UID, ctx);
|
|
121
|
+
|
|
122
|
+
expect(mocks.fetch).not.toHaveBeenCalled();
|
|
123
|
+
expect(ctx.pubsub.published).toEqual([]);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
@@ -0,0 +1,127 @@
|
|
|
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 { beforeEach, describe, expect, test, vi } from "vitest";
|
|
18
|
+
|
|
19
|
+
vi.mock("../src/logs", () => ({
|
|
20
|
+
complete: vi.fn(),
|
|
21
|
+
customFunctionError: vi.fn(),
|
|
22
|
+
firestoreDeleted: vi.fn(),
|
|
23
|
+
firestoreDeleting: vi.fn(),
|
|
24
|
+
firestoreNotConfigured: vi.fn(),
|
|
25
|
+
firestorePathDeleted: vi.fn(),
|
|
26
|
+
firestorePathDeleting: vi.fn(),
|
|
27
|
+
firestorePathError: vi.fn(),
|
|
28
|
+
init: vi.fn(),
|
|
29
|
+
rtdbDeleted: vi.fn(),
|
|
30
|
+
rtdbDeleting: vi.fn(),
|
|
31
|
+
rtdbNotConfigured: vi.fn(),
|
|
32
|
+
rtdbPathDeleted: vi.fn(),
|
|
33
|
+
rtdbPathDeleting: vi.fn(),
|
|
34
|
+
rtdbPathError: vi.fn(),
|
|
35
|
+
start: vi.fn(),
|
|
36
|
+
storageDeleted: vi.fn(),
|
|
37
|
+
storageDeleting: vi.fn(),
|
|
38
|
+
storageNotConfigured: vi.fn(),
|
|
39
|
+
storagePath404: vi.fn(),
|
|
40
|
+
storagePathDeleted: vi.fn(),
|
|
41
|
+
storagePathDeleting: vi.fn(),
|
|
42
|
+
storagePathError: vi.fn(),
|
|
43
|
+
warnInvalidPaths: vi.fn(),
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
import { search } from "../src/search";
|
|
47
|
+
import { createFakeFirestore, discoveryMessages, makeContext } from "./fakes";
|
|
48
|
+
|
|
49
|
+
const UID = "testUid";
|
|
50
|
+
|
|
51
|
+
beforeEach(() => {
|
|
52
|
+
vi.clearAllMocks();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Parity: the discovery fan-out asserted indirectly by
|
|
56
|
+
// delete-user-data/functions/__tests__/search.test.ts.
|
|
57
|
+
describe("search", () => {
|
|
58
|
+
test("queues every top level collection for discovery", async () => {
|
|
59
|
+
const firestore = createFakeFirestore({
|
|
60
|
+
"users/doc1": { foo: "bar" },
|
|
61
|
+
"rooms/room1": { foo: "bar" },
|
|
62
|
+
});
|
|
63
|
+
const ctx = makeContext({ firestore });
|
|
64
|
+
|
|
65
|
+
await search(UID, 1, firestore as any, ctx);
|
|
66
|
+
|
|
67
|
+
expect(discoveryMessages(ctx)).toEqual([
|
|
68
|
+
{ path: "users", uid: UID, depth: 1 },
|
|
69
|
+
{ path: "rooms", uid: UID, depth: 1 },
|
|
70
|
+
]);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("queues only the subcollections of the given document", async () => {
|
|
74
|
+
const firestore = createFakeFirestore({
|
|
75
|
+
"users/doc1/posts/post1": { foo: "bar" },
|
|
76
|
+
"users/doc1/comments/comment1": { foo: "bar" },
|
|
77
|
+
"users/doc2/posts/post1": { foo: "bar" },
|
|
78
|
+
});
|
|
79
|
+
const ctx = makeContext({ firestore });
|
|
80
|
+
|
|
81
|
+
await search(UID, 2, firestore as any, ctx, firestore.doc("users/doc1"));
|
|
82
|
+
|
|
83
|
+
expect(discoveryMessages(ctx)).toEqual([
|
|
84
|
+
{ path: "users/doc1/posts", uid: UID, depth: 2 },
|
|
85
|
+
{ path: "users/doc1/comments", uid: UID, depth: 2 },
|
|
86
|
+
]);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("queues nothing for a document without subcollections", async () => {
|
|
90
|
+
const firestore = createFakeFirestore({ "users/doc1": { foo: "bar" } });
|
|
91
|
+
const ctx = makeContext({ firestore });
|
|
92
|
+
|
|
93
|
+
await search(UID, -1, firestore as any, ctx, firestore.doc("users/doc1"));
|
|
94
|
+
|
|
95
|
+
expect(discoveryMessages(ctx)).toEqual([]);
|
|
96
|
+
expect(firestore.exists("users/doc1")).toBe(true);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("queues nothing for an empty database", async () => {
|
|
100
|
+
const firestore = createFakeFirestore();
|
|
101
|
+
const ctx = makeContext({ firestore });
|
|
102
|
+
|
|
103
|
+
await search(UID, 1, firestore as any, ctx);
|
|
104
|
+
|
|
105
|
+
expect(discoveryMessages(ctx)).toEqual([]);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("publishes to the configured discovery topic", async () => {
|
|
109
|
+
const firestore = createFakeFirestore({ "users/doc1": { foo: "bar" } });
|
|
110
|
+
const ctx = makeContext({
|
|
111
|
+
firestore,
|
|
112
|
+
config: {
|
|
113
|
+
discoveryTopicName: "custom-discovery",
|
|
114
|
+
projectId: "demo-test",
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
await search(UID, 1, firestore as any, ctx);
|
|
119
|
+
|
|
120
|
+
expect(ctx.pubsub.published).toEqual([
|
|
121
|
+
{
|
|
122
|
+
topic: "projects/demo-test/topics/custom-discovery",
|
|
123
|
+
json: { path: "users", uid: UID, depth: 1 },
|
|
124
|
+
},
|
|
125
|
+
]);
|
|
126
|
+
});
|
|
127
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"outDir": "lib",
|
|
4
|
+
"rootDir": "src",
|
|
5
|
+
"target": "ES2022",
|
|
6
|
+
"module": "nodenext",
|
|
7
|
+
"moduleResolution": "nodenext",
|
|
8
|
+
"declaration": true,
|
|
9
|
+
"declarationMap": true,
|
|
10
|
+
"sourceMap": true,
|
|
11
|
+
"strict": false,
|
|
12
|
+
"esModuleInterop": true,
|
|
13
|
+
"skipLibCheck": true,
|
|
14
|
+
"types": ["node"]
|
|
15
|
+
},
|
|
16
|
+
"include": ["src"],
|
|
17
|
+
"exclude": ["node_modules", "lib", "**/*.test.ts"]
|
|
18
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":"7.0.2","root":["./src/config.ts","./src/events.ts","./src/export-config.ts","./src/handlers.ts","./src/helpers.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/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"]}
|
package/index.js
DELETED
|
File without changes
|