From e01736fc36060aa53051a4b13456f88722f129a7 Mon Sep 17 00:00:00 2001 From: Pavel Kudinov Date: Sat, 7 Mar 2026 03:34:02 -0800 Subject: [PATCH 1/2] test(regression): add failing test for issue #2460 createManyAndReturn with PolicyPlugin fails when rows have asymmetric optional fields (one row provides a field the other omits). Refs: https://github.com/zenstackhq/zenstack/issues/2460 Co-Authored-By: Claude Opus 4.6 --- tests/regression/test/issue-2460.test.ts | 39 ++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 tests/regression/test/issue-2460.test.ts diff --git a/tests/regression/test/issue-2460.test.ts b/tests/regression/test/issue-2460.test.ts new file mode 100644 index 000000000..35ec62bdb --- /dev/null +++ b/tests/regression/test/issue-2460.test.ts @@ -0,0 +1,39 @@ +import { createPolicyTestClient } from '@zenstackhq/testtools'; +import { describe, expect, it } from 'vitest'; + +// createManyAndReturn fails with "Invariant failed: expecting a ValueNode" +// when rows in the batch have asymmetric columns (one row provides a field the other omits) +describe('Regression for issue #2460', () => { + it('createManyAndReturn with asymmetric optional fields across rows', async () => { + const db = await createPolicyTestClient( + ` +type AuthUser { + id String + role String + @@auth +} + +model Item { + id Int @id @default(autoincrement()) + key String + note String? + @@allow('all', auth().role == 'admin') +} + `, + { + provider: 'postgresql', + auth: { id: '1', role: 'admin' }, + }, + ); + + const result = await db.item.createManyAndReturn({ + data: [ + { key: 'a', note: 'hello' }, + { key: 'b' }, + ], + }); + + expect(result).toHaveLength(2); + await db.$disconnect(); + }); +}); From 22c0ad149eb615eaa656ecfcd8e99d6ae70c5f82 Mon Sep 17 00:00:00 2001 From: Pavel Kudinov Date: Sat, 7 Mar 2026 03:45:08 -0800 Subject: [PATCH 2/2] fix(policy): handle DefaultInsertValueNode in createManyAndReturn When rows have asymmetric optional fields, Kysely pads missing columns with DefaultInsertValueNode. Treat it as null in the policy handler. Fixes #2460 Co-Authored-By: Claude Opus 4.6 --- packages/plugins/policy/src/policy-handler.ts | 4 ++++ tests/regression/test/issue-2460.test.ts | 16 +++++++--------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/packages/plugins/policy/src/policy-handler.ts b/packages/plugins/policy/src/policy-handler.ts index f42459bc4..968703957 100644 --- a/packages/plugins/policy/src/policy-handler.ts +++ b/packages/plugins/policy/src/policy-handler.ts @@ -920,6 +920,10 @@ export class PolicyHandler extends OperationNodeTransf for (let i = 0; i < data.length; i++) { const item = data[i]!; if (typeof item === 'object' && item && 'kind' in item) { + if (item.kind === 'DefaultInsertValueNode') { + result.push({ node: ValueNode.create(null), raw: null }); + continue; + } const fieldDef = QueryUtils.requireField(this.client.$schema, model, fields[i]!); invariant(item.kind === 'ValueNode', 'expecting a ValueNode'); result.push({ diff --git a/tests/regression/test/issue-2460.test.ts b/tests/regression/test/issue-2460.test.ts index 35ec62bdb..2ad48a147 100644 --- a/tests/regression/test/issue-2460.test.ts +++ b/tests/regression/test/issue-2460.test.ts @@ -7,10 +7,10 @@ describe('Regression for issue #2460', () => { it('createManyAndReturn with asymmetric optional fields across rows', async () => { const db = await createPolicyTestClient( ` -type AuthUser { - id String +model User { + id Int @id @default(autoincrement()) role String - @@auth + @@allow('all', true) } model Item { @@ -20,13 +20,12 @@ model Item { @@allow('all', auth().role == 'admin') } `, - { - provider: 'postgresql', - auth: { id: '1', role: 'admin' }, - }, + { provider: 'postgresql' }, ); - const result = await db.item.createManyAndReturn({ + const user = await db.user.create({ data: { role: 'admin' } }); + + const result = await db.$setAuth(user).item.createManyAndReturn({ data: [ { key: 'a', note: 'hello' }, { key: 'b' }, @@ -34,6 +33,5 @@ model Item { }); expect(result).toHaveLength(2); - await db.$disconnect(); }); });