From ebf0a5cd71c06046b6a27c08a66b1fb14f7509f9 Mon Sep 17 00:00:00 2001 From: Izaak Gough Date: Fri, 17 Jul 2026 10:48:35 +0100 Subject: [PATCH 1/4] feat: support deletion of nested field values --- .../extensions/delete-user-data.env.local | 2 +- .../functions/__tests__/search.test.ts | 21 +++++++++++++++++++ delete-user-data/functions/src/index.ts | 2 +- 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/_emulator/extensions/delete-user-data.env.local b/_emulator/extensions/delete-user-data.env.local index c56b0354bb..4856c80993 100644 --- a/_emulator/extensions/delete-user-data.env.local +++ b/_emulator/extensions/delete-user-data.env.local @@ -1,7 +1,7 @@ LOCATION=europe-west2 FIRESTORE_QUERY_COLLECTION=queries ENABLE_AUTO_DISCOVERY=true -AUTO_DISCOVERY_SEARCH_FIELDS=field1,field2 +AUTO_DISCOVERY_SEARCH_FIELDS=field1,field2,scope.owner.id SEARCH_FUNCTION=http://127.0.0.1:5001/demo-test/us-central1/findDocumentReferences AUTO_DISCOVERY_TOPIC=discovery AUTO_DISCOVERY_SEARCH_DEPTH=4 diff --git a/delete-user-data/functions/__tests__/search.test.ts b/delete-user-data/functions/__tests__/search.test.ts index 161dd8167a..9142175f6b 100644 --- a/delete-user-data/functions/__tests__/search.test.ts +++ b/delete-user-data/functions/__tests__/search.test.ts @@ -67,6 +67,27 @@ describe("discovery", () => { await waitForDocumentDeletion(document, 60000); }, 60000); + test("can delete a document with a nested field value matching {uid}", async () => { + const document = await db + .collection(generateRandomId()) + .add({ scope: { owner: { id: user.uid } } }); + await search(user.uid, 1, db); + + await waitForDocumentDeletion(document, 60000); + }, 60000); + + test("cannot delete a document with a nested field value that does not match {uid}", async () => { + const document = await db + .collection(generateRandomId()) + .add({ scope: { owner: { id: "not-the-uid" } } }); + await search(user.uid, 1, db); + + await new Promise((resolve) => setTimeout(resolve, 10000)); + + const checkExists = await document.get().then((doc) => doc.exists); + expect(checkExists).toBe(true); + }, 60000); + test("can check a document without any field values", async () => { await db.collection(generateRandomId()).add({}); await search(user.uid, 1, db); diff --git a/delete-user-data/functions/src/index.ts b/delete-user-data/functions/src/index.ts index 5d1f9c985d..e0c4091dbc 100644 --- a/delete-user-data/functions/src/index.ts +++ b/delete-user-data/functions/src/index.ts @@ -177,7 +177,7 @@ export const handleSearch = functions.pubsub for (const snapshot of snapshots) { if (snapshot.exists) { for (const field of config.searchFields.split(",")) { - if (snapshot.get(new FieldPath(field)) === uid) { + if (snapshot.get(new FieldPath(...field.split("."))) === uid) { pathsToDelete.push(snapshot.ref.path); continue; } From 0893132ed3cb817b06340ca6fe4f286d4d8bc901 Mon Sep 17 00:00:00 2001 From: Izaak Gough Date: Fri, 17 Jul 2026 11:07:02 +0100 Subject: [PATCH 2/4] Update delete-user-data/functions/src/index.ts Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- delete-user-data/functions/src/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/delete-user-data/functions/src/index.ts b/delete-user-data/functions/src/index.ts index e0c4091dbc..266b5eb5b7 100644 --- a/delete-user-data/functions/src/index.ts +++ b/delete-user-data/functions/src/index.ts @@ -177,7 +177,8 @@ export const handleSearch = functions.pubsub for (const snapshot of snapshots) { if (snapshot.exists) { for (const field of config.searchFields.split(",")) { - if (snapshot.get(new FieldPath(...field.split("."))) === uid) { + const trimmedField = field.trim(); + if (trimmedField && snapshot.get(trimmedField) === uid) { pathsToDelete.push(snapshot.ref.path); continue; } From 016f482b8d1aae15dcbec7315310d2f4657b1032 Mon Sep 17 00:00:00 2001 From: Izaak Gough Date: Fri, 17 Jul 2026 13:11:00 +0100 Subject: [PATCH 3/4] fix --- delete-user-data/functions/src/index.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/delete-user-data/functions/src/index.ts b/delete-user-data/functions/src/index.ts index 266b5eb5b7..de1fb4dd21 100644 --- a/delete-user-data/functions/src/index.ts +++ b/delete-user-data/functions/src/index.ts @@ -178,7 +178,10 @@ export const handleSearch = functions.pubsub if (snapshot.exists) { for (const field of config.searchFields.split(",")) { const trimmedField = field.trim(); - if (trimmedField && snapshot.get(trimmedField) === uid) { + if ( + trimmedField && + snapshot.get(new FieldPath(...trimmedField.split("."))) === uid + ) { pathsToDelete.push(snapshot.ref.path); continue; } From a160bf60619824bc2cb3c3572289f8229712ee05 Mon Sep 17 00:00:00 2001 From: Izaak Gough Date: Fri, 17 Jul 2026 14:09:20 +0100 Subject: [PATCH 4/4] refactor: extract helper function --- delete-user-data/functions/src/helpers.ts | 13 +++++++++++-- delete-user-data/functions/src/index.ts | 17 ++++++++--------- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/delete-user-data/functions/src/helpers.ts b/delete-user-data/functions/src/helpers.ts index e5f64eb115..a47df725c1 100644 --- a/delete-user-data/functions/src/helpers.ts +++ b/delete-user-data/functions/src/helpers.ts @@ -14,9 +14,18 @@ * limitations under the License. */ -import { DocumentReference, FieldPath } from "firebase-admin/firestore"; +import { + DocumentReference, + DocumentSnapshot, + FieldPath, +} from "firebase-admin/firestore"; import config from "./config"; +export const getNestedFieldValue = ( + snapshot: DocumentSnapshot, + field: string +) => snapshot.get(new FieldPath(...field.trim().split("."))); + export const getDatabaseUrl = ( selectedDatabaseInstance: string | undefined, selectedDatabaseLocation: string | undefined @@ -42,7 +51,7 @@ export const hasValidUserPath = async ( if (snapshot.exists) { for (const field of config.searchFields.split(",")) { - const fieldValue = snapshot.get(new FieldPath(field)); + const fieldValue = getNestedFieldValue(snapshot, field); /** Return if a matching string includes the id */ if (typeof fieldValue === "string" && fieldValue.includes(uid)) { diff --git a/delete-user-data/functions/src/index.ts b/delete-user-data/functions/src/index.ts index de1fb4dd21..f81f3beb45 100644 --- a/delete-user-data/functions/src/index.ts +++ b/delete-user-data/functions/src/index.ts @@ -15,13 +15,13 @@ */ import * as admin from "firebase-admin"; -import { - FieldPath, - DocumentReference, - getFirestore, -} from "firebase-admin/firestore"; +import { DocumentReference, getFirestore } from "firebase-admin/firestore"; import * as functions from "firebase-functions"; -import { getDatabaseUrl, hasValidUserPath } from "./helpers"; +import { + getDatabaseUrl, + getNestedFieldValue, + hasValidUserPath, +} from "./helpers"; import chunk from "lodash.chunk"; import { getEventarc } from "firebase-admin/eventarc"; @@ -177,10 +177,9 @@ export const handleSearch = functions.pubsub for (const snapshot of snapshots) { if (snapshot.exists) { for (const field of config.searchFields.split(",")) { - const trimmedField = field.trim(); if ( - trimmedField && - snapshot.get(new FieldPath(...trimmedField.split("."))) === uid + field.trim() && + getNestedFieldValue(snapshot, field) === uid ) { pathsToDelete.push(snapshot.ref.path); continue;