diff --git a/packages/orm/src/client/crud/dialects/sqlite.ts b/packages/orm/src/client/crud/dialects/sqlite.ts index 286c215be..4559ffb6e 100644 --- a/packages/orm/src/client/crud/dialects/sqlite.ts +++ b/packages/orm/src/client/crud/dialects/sqlite.ts @@ -43,27 +43,31 @@ export class SqliteCrudDialect extends BaseCrudDialect invariant(false, 'should not reach here: AnyNull is not a valid input value'); } + // Handle JSON type before array check - JSON values should be stringified as a whole + if (type === 'Json') { + return JSON.stringify(value); + } + + // Handle typed JSON field before array check + if (this.schema.typeDefs && type in this.schema.typeDefs) { + return JSON.stringify(value); + } + if (Array.isArray(value)) { return value.map((v) => this.transformPrimitive(v, type, false)); } else { - if (this.schema.typeDefs && type in this.schema.typeDefs) { - // typed JSON field - return JSON.stringify(value); - } else { - return match(type) - .with('Boolean', () => (value ? 1 : 0)) - .with('DateTime', () => - value instanceof Date - ? value.toISOString() - : typeof value === 'string' - ? new Date(value).toISOString() - : value, - ) - .with('Decimal', () => (value as Decimal).toString()) - .with('Bytes', () => Buffer.from(value as Uint8Array)) - .with('Json', () => JSON.stringify(value)) - .otherwise(() => value); - } + return match(type) + .with('Boolean', () => (value ? 1 : 0)) + .with('DateTime', () => + value instanceof Date + ? value.toISOString() + : typeof value === 'string' + ? new Date(value).toISOString() + : value, + ) + .with('Decimal', () => (value as Decimal).toString()) + .with('Bytes', () => Buffer.from(value as Uint8Array)) + .otherwise(() => value); } } diff --git a/packages/orm/src/client/crud/validator/index.ts b/packages/orm/src/client/crud/validator/index.ts index d71bd8010..2a5ddb897 100644 --- a/packages/orm/src/client/crud/validator/index.ts +++ b/packages/orm/src/client/crud/validator/index.ts @@ -581,7 +581,7 @@ export class InputValidator { const schema = z.union([ ...options, - z.lazy(() => this.makeJsonValueSchema(false, false).array()), + z.lazy(() => z.union([this.makeJsonValueSchema(false, false), z.null()]).array()), z.record( z.string(), z.lazy(() => z.union([this.makeJsonValueSchema(false, false), z.null()])), diff --git a/tests/e2e/orm/client-api/json-filter.test.ts b/tests/e2e/orm/client-api/json-filter.test.ts index 8304b6e5f..4937b762a 100644 --- a/tests/e2e/orm/client-api/json-filter.test.ts +++ b/tests/e2e/orm/client-api/json-filter.test.ts @@ -150,4 +150,79 @@ model PlainJson { }); await expect(db.plainJson.create({ data: { data: DbNull } })).toBeRejectedByValidation(); }); + + it('works with JSON objects containing null values', async () => { + const db = await createTestClient(schema); + + // Create a record with an object containing a null property value + const rec1 = await db.plainJson.create({ data: { data: { key: null } } }); + expect(rec1.data).toEqual({ key: null }); + + // Create a record with nested object containing null values + const rec2 = await db.plainJson.create({ data: { data: { outer: { inner: null }, valid: 'value' } } }); + expect(rec2.data).toEqual({ outer: { inner: null }, valid: 'value' }); + + // Query with equality filter for object with null value + await expect( + db.plainJson.findFirst({ where: { data: { equals: { key: null } } } }), + ).resolves.toMatchObject({ + id: rec1.id, + data: { key: null }, + }); + + // Query with equality filter for nested object with null value + await expect( + db.plainJson.findFirst({ where: { data: { equals: { outer: { inner: null }, valid: 'value' } } } }), + ).resolves.toMatchObject({ + id: rec2.id, + data: { outer: { inner: null }, valid: 'value' }, + }); + + // Query with not filter for object with null value + const notResults = await db.plainJson.findMany({ + where: { data: { not: { key: null } } }, + }); + expect(notResults.find((r) => r.id === rec1.id)).toBeUndefined(); + expect(notResults.find((r) => r.id === rec2.id)).toBeDefined(); + }); + + it('works with JSON arrays containing null values', async () => { + const db = await createTestClient(schema); + + // Create a record with an array containing null values + const rec1 = await db.plainJson.create({ data: { data: [1, null, 3] } }); + expect(rec1.data).toEqual([1, null, 3]); + + // Create a record with an array of objects including null + const rec2 = await db.plainJson.create({ data: { data: [{ a: 1 }, null, { b: 2 }] } }); + expect(rec2.data).toEqual([{ a: 1 }, null, { b: 2 }]); + + // Create a record with nested arrays containing null + const rec3 = await db.plainJson.create({ data: { data: [[1, null], [null, 2]] } }); + expect(rec3.data).toEqual([[1, null], [null, 2]]); + + // Query with equality filter for array with null value + await expect( + db.plainJson.findFirst({ where: { data: { equals: [1, null, 3] } } }), + ).resolves.toMatchObject({ + id: rec1.id, + data: [1, null, 3], + }); + + // Query with equality filter for array of objects with null + await expect( + db.plainJson.findFirst({ where: { data: { equals: [{ a: 1 }, null, { b: 2 }] } } }), + ).resolves.toMatchObject({ + id: rec2.id, + data: [{ a: 1 }, null, { b: 2 }], + }); + + // Query with not filter for array with null value + const notResults = await db.plainJson.findMany({ + where: { data: { not: [1, null, 3] } }, + }); + expect(notResults.find((r) => r.id === rec1.id)).toBeUndefined(); + expect(notResults.find((r) => r.id === rec2.id)).toBeDefined(); + expect(notResults.find((r) => r.id === rec3.id)).toBeDefined(); + }); });