From ebf0d792955dd70ba194083be619bf31f00ff41c Mon Sep 17 00:00:00 2001 From: Saatvik Arya Date: Wed, 24 Jun 2026 15:15:40 +0530 Subject: [PATCH] fix(storage): mirror bulk upsert validation --- .../core/fumadb/src/adapters/drizzle/query.ts | 4 +++ .../core/fumadb/src/adapters/memory/index.ts | 4 +++ packages/core/fumadb/src/query/orm/index.ts | 8 +++++ .../fumadb/src/query/table-policy.test.ts | 31 +++++++++++++++++++ 4 files changed, 47 insertions(+) diff --git a/packages/core/fumadb/src/adapters/drizzle/query.ts b/packages/core/fumadb/src/adapters/drizzle/query.ts index c6d63a63f..96441b420 100644 --- a/packages/core/fumadb/src/adapters/drizzle/query.ts +++ b/packages/core/fumadb/src/adapters/drizzle/query.ts @@ -435,6 +435,10 @@ export function fromDrizzle( }, async upsertMany(table, v) { if (v.values.length === 0) return; + if (v.target.length === 0) { + // oxlint-disable-next-line executor/no-try-catch-or-throw, executor/no-error-constructor -- boundary: adapter rejects invalid upsert shape + throw new Error("[FumaDB] upsertMany requires at least one target column."); + } if (v.update.length === 0) { // oxlint-disable-next-line executor/no-try-catch-or-throw, executor/no-error-constructor -- boundary: adapter rejects invalid upsert shape throw new Error("[FumaDB] upsertMany requires at least one update column."); diff --git a/packages/core/fumadb/src/adapters/memory/index.ts b/packages/core/fumadb/src/adapters/memory/index.ts index 1c8c3d59e..307e581a1 100644 --- a/packages/core/fumadb/src/adapters/memory/index.ts +++ b/packages/core/fumadb/src/adapters/memory/index.ts @@ -184,6 +184,10 @@ export function memoryAdapter(options: MemoryAdapterOptions = {}): FumaDBAdapter await this.create(table, v.create); }, async upsertMany(table, v) { + if (v.target.length === 0) { + // oxlint-disable-next-line executor/no-try-catch-or-throw, executor/no-error-constructor -- boundary: adapter rejects invalid upsert shape + throw new Error("[FumaDB] upsertMany requires at least one target column."); + } if (v.update.length === 0) { // oxlint-disable-next-line executor/no-try-catch-or-throw, executor/no-error-constructor -- boundary: adapter rejects invalid upsert shape throw new Error("[FumaDB] upsertMany requires at least one update column."); diff --git a/packages/core/fumadb/src/query/orm/index.ts b/packages/core/fumadb/src/query/orm/index.ts index 0426e08c7..f5565317a 100644 --- a/packages/core/fumadb/src/query/orm/index.ts +++ b/packages/core/fumadb/src/query/orm/index.ts @@ -463,6 +463,14 @@ export function toORM( async upsertMany(name, { target, update, values }) { const table = toTable(name); if (values.length === 0) return; + if (target.length === 0) { + // oxlint-disable-next-line executor/no-try-catch-or-throw, executor/no-error-constructor -- boundary: public query rejects invalid upsert shape + throw new Error("[FumaDB] upsertMany requires at least one target column."); + } + if (update.length === 0) { + // oxlint-disable-next-line executor/no-try-catch-or-throw, executor/no-error-constructor -- boundary: public query rejects invalid upsert shape + throw new Error("[FumaDB] upsertMany requires at least one update column."); + } const targetColumns = target.map((columnName) => { const column = table.columns[columnName as string]; diff --git a/packages/core/fumadb/src/query/table-policy.test.ts b/packages/core/fumadb/src/query/table-policy.test.ts index 3ff5a8152..0503ff1e8 100644 --- a/packages/core/fumadb/src/query/table-policy.test.ts +++ b/packages/core/fumadb/src/query/table-policy.test.ts @@ -483,6 +483,37 @@ describe("FumaDB table policies", () => { }), ); + it.effect("rejects invalid bulk upsert conflict shapes", () => + useHarness(async (orm) => { + await seedTenants(orm); + const tenantA = withQueryContext(orm, makeContext(["tenant-a"], "tenant-a")); + const values = [ + { + id: "post-a-bulk-upsert", + tenantId: "tenant-a", + authorId: "author-a", + title: "A bulk upsert", + }, + ]; + + await expect( + tenantA.upsertMany("posts", { + target: [], + update: ["title"], + values, + }), + ).rejects.toThrow("[FumaDB] upsertMany requires at least one target column."); + + await expect( + tenantA.upsertMany("posts", { + target: ["id"], + update: [], + values, + }), + ).rejects.toThrow("[FumaDB] upsertMany requires at least one update column."); + }), + ); + it.effect("fails closed when a query wrapper does not forward context rebinding", () => useHarness(async (orm) => { const wrapped = { ...orm };