From 08d8a8d7440b12fa9c384022c581ef0c309b27bd Mon Sep 17 00:00:00 2001 From: erwan-joly Date: Thu, 30 Apr 2026 23:43:19 +1200 Subject: [PATCH 1/5] fix(orm): coerce ISO strings (datetime, date, time-only) on DateTime input (#2631) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The strict zod union introduced in 3.5+ broke every caller passing ISO strings to `DateTime` fields, including bare time-only strings like "09:00:00" for `@db.Time` columns. Earlier versions coerced these via Prisma's input layer; the new validator rejected them outright with no migration path called out in the release notes. This restores Prisma-compatible coercion as the default while leaving strict validation available behind a new `ClientOptions.strictDateInput` flag (default `false`) for users who want to opt in. Changes: - `packages/orm/src/client/options.ts`: new `strictDateInput?: boolean` - `packages/orm/src/client/zod/factory.ts`: new exported helper `coercedDateTimeSchema()` that anchors time-only strings to the Unix epoch and falls through to `z.date()` for all other paths; `makeDateTimeValueSchema` switches on `strictDateInput` - `packages/zod/src/factory.ts`: same coercion applied in the standalone factory (no setting — these schemas are typically used for form validation where coercion is even more important) - `packages/zod/test/factory.test.ts`: regression tests for the four accepted forms (Date, ISO datetime, ISO date, time-only with and without timezone) plus a non-parseable rejection case Fixes #2631 --- packages/orm/src/client/options.ts | 10 ++++++ packages/orm/src/client/zod/factory.ts | 27 ++++++++++++++- packages/zod/src/factory.ts | 11 +++++- packages/zod/test/factory.test.ts | 47 ++++++++++++++++++++++++++ 4 files changed, 93 insertions(+), 2 deletions(-) diff --git a/packages/orm/src/client/options.ts b/packages/orm/src/client/options.ts index 136c4928b..7baa242a5 100644 --- a/packages/orm/src/client/options.ts +++ b/packages/orm/src/client/options.ts @@ -202,6 +202,16 @@ export type ClientOptions = QueryOptions & { */ validateInput?: boolean; + /** + * Whether to require `Date` objects (rather than ISO strings) for `DateTime` field inputs. Defaults + * to `false`, matching Prisma's longstanding behavior of coercing ISO strings — including bare + * time-only strings like `"09:00:00"` for `@db.Time` fields — to `Date`. + * + * Set to `true` to opt into strict input validation that rejects all string forms. + * @see https://github.com/zenstackhq/zenstack/issues/2631 + */ + strictDateInput?: boolean; + /** * Whether to use compact alias names (e.g., "$$t1", "$$t2") when transforming ORM queries to SQL. * Defaults to `true`. diff --git a/packages/orm/src/client/zod/factory.ts b/packages/orm/src/client/zod/factory.ts index 225bf6e34..ea05709dd 100644 --- a/packages/orm/src/client/zod/factory.ts +++ b/packages/orm/src/client/zod/factory.ts @@ -84,6 +84,29 @@ export function createQuerySchemaFactory(clientOrSchema: any, options?: any) { return new ZodSchemaFactory(clientOrSchema, options); } +/** + * Builds a `DateTime` value schema that accepts a `Date` object or an ISO + * datetime / date / time-only string and coerces it to a `Date`. Time-only + * strings (e.g. `"09:00:00"` for `@db.Time` fields) are anchored to the Unix + * epoch. Strings that don't parse fall through and are rejected by `z.date()` + * with the standard error. + * + * Used when `ClientOptions.strictDateInput` is left at its default (`false`). + * @see https://github.com/zenstackhq/zenstack/issues/2631 + */ +export function coercedDateTimeSchema(): ZodType { + return z.preprocess((val) => { + if (typeof val !== 'string') return val; + if (/^\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d\d(?::\d\d)?)?$/.test(val)) { + const hasTz = val.endsWith('Z') || /[+-]\d\d(?::\d\d)?$/.test(val); + const d = new Date(`1970-01-01T${val}${hasTz ? '' : 'Z'}`); + return isNaN(d.getTime()) ? val : d; + } + const d = new Date(val); + return isNaN(d.getTime()) ? val : d; + }, z.date()); +} + /** * Options for creating Zod schemas. */ @@ -854,7 +877,9 @@ export class ZodSchemaFactory< @cache() private makeDateTimeValueSchema(): ZodType { - const schema = z.union([z.iso.datetime(), z.iso.date(), z.date()]); + const schema = (this.options as ClientOptions)?.strictDateInput + ? z.union([z.iso.datetime(), z.iso.date(), z.date()]) + : coercedDateTimeSchema(); this.registerSchema('DateTime', schema); return schema; } diff --git a/packages/zod/src/factory.ts b/packages/zod/src/factory.ts index 6d2c50b8f..218d96dcb 100644 --- a/packages/zod/src/factory.ts +++ b/packages/zod/src/factory.ts @@ -401,7 +401,16 @@ class SchemaFactory { ]); break; case 'DateTime': - base = z.union([z.date(), z.iso.datetime()]); + base = z.preprocess((val) => { + if (typeof val !== 'string') return val; + if (/^\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d\d(?::\d\d)?)?$/.test(val)) { + const hasTz = val.endsWith('Z') || /[+-]\d\d(?::\d\d)?$/.test(val); + const d = new Date(`1970-01-01T${val}${hasTz ? '' : 'Z'}`); + return isNaN(d.getTime()) ? val : d; + } + const d = new Date(val); + return isNaN(d.getTime()) ? val : d; + }, z.date()); break; case 'Bytes': base = z.instanceof(Uint8Array); diff --git a/packages/zod/test/factory.test.ts b/packages/zod/test/factory.test.ts index cfbdaa4cc..72b139025 100644 --- a/packages/zod/test/factory.test.ts +++ b/packages/zod/test/factory.test.ts @@ -154,6 +154,53 @@ describe('SchemaFactory - makeModelSchema', () => { birthdate: '2024-01-15T10:30:00.000Z', }); expect(result.success).toBe(true); + // Coerced to a Date for the engine. + expect(result.data?.birthdate).toBeInstanceOf(Date); + }); + + // Regression: #2631 — earlier versions accepted ISO strings via + // Prisma's permissive coercion. The strict zod union introduced in + // 3.5+ rejected ISO date and time-only strings, breaking every + // `@db.Date` and `@db.Time` caller that had been passing strings. + it('accepts DateTime as an ISO date string (#2631)', () => { + const userSchema = factory.makeModelSchema('User'); + const result = userSchema.safeParse({ + ...validUser, + birthdate: '2024-01-15', + }); + expect(result.success).toBe(true); + expect(result.data?.birthdate).toBeInstanceOf(Date); + }); + + it('accepts DateTime as a bare time-only string for @db.Time fields (#2631)', () => { + const userSchema = factory.makeModelSchema('User'); + const result = userSchema.safeParse({ + ...validUser, + birthdate: '09:30:00', + }); + expect(result.success).toBe(true); + // Time-only strings are anchored to the Unix epoch. + expect(result.data?.birthdate).toBeInstanceOf(Date); + expect((result.data?.birthdate as Date).toISOString()).toBe('1970-01-01T09:30:00.000Z'); + }); + + it('accepts DateTime as a time-only string with timezone (#2631)', () => { + const userSchema = factory.makeModelSchema('User'); + const result = userSchema.safeParse({ + ...validUser, + birthdate: '09:30:00+12:00', + }); + expect(result.success).toBe(true); + expect(result.data?.birthdate).toBeInstanceOf(Date); + }); + + it('rejects DateTime as a non-parseable string', () => { + const userSchema = factory.makeModelSchema('User'); + const result = userSchema.safeParse({ + ...validUser, + birthdate: 'not-a-date', + }); + expect(result.success).toBe(false); }); it('accepts Bytes as Uint8Array', () => { From e8cc785ef286b7c77a3730d7ce158b2bd97a2218 Mon Sep 17 00:00:00 2001 From: erwan-joly Date: Fri, 1 May 2026 00:19:42 +1200 Subject: [PATCH 2/5] review fixes for #2632 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Strict mode now actually rejects all string forms (was still accepting ISO datetime/date because the union still included z.iso.datetime() / z.iso.date()). Switched to plain z.date() so the implementation matches the JSDoc on ClientOptions.strictDateInput. - Reverted the standalone @zenstackhq/zod factory back to its previous union (z.date() | z.iso.datetime()). The OpenAPI generator emits its spec from this factory and the preprocess version produced a different schema shape that broke the rpc-openapi baseline test. The ORM-side coercion in @zenstackhq/orm/zod/factory.ts still covers the runtime input validation that #2631 was about; standalone consumers can layer their own coercion if they need it. - Replaced the standalone-factory regression tests with an end-to-end ORM regression suite at tests/regression/test/issue-2631.test.ts covering both default coercion (Date, ISO datetime, ISO date, time-only with and without TZ) and the strict-mode contract (Date only — ISO datetime, ISO date, and time-only strings all rejected). --- packages/orm/src/client/zod/factory.ts | 7 ++- packages/zod/src/factory.ts | 11 +--- packages/zod/test/factory.test.ts | 47 -------------- tests/regression/test/issue-2631.test.ts | 78 ++++++++++++++++++++++++ 4 files changed, 83 insertions(+), 60 deletions(-) create mode 100644 tests/regression/test/issue-2631.test.ts diff --git a/packages/orm/src/client/zod/factory.ts b/packages/orm/src/client/zod/factory.ts index ea05709dd..8a2ee02fc 100644 --- a/packages/orm/src/client/zod/factory.ts +++ b/packages/orm/src/client/zod/factory.ts @@ -877,9 +877,10 @@ export class ZodSchemaFactory< @cache() private makeDateTimeValueSchema(): ZodType { - const schema = (this.options as ClientOptions)?.strictDateInput - ? z.union([z.iso.datetime(), z.iso.date(), z.date()]) - : coercedDateTimeSchema(); + // Strict mode: require an actual `Date` instance, matching what the + // engine ultimately wants. Default mode: coerce ISO strings (datetime, + // date, time-only) to `Date` for Prisma compatibility (#2631). + const schema = (this.options as ClientOptions)?.strictDateInput ? z.date() : coercedDateTimeSchema(); this.registerSchema('DateTime', schema); return schema; } diff --git a/packages/zod/src/factory.ts b/packages/zod/src/factory.ts index 218d96dcb..6d2c50b8f 100644 --- a/packages/zod/src/factory.ts +++ b/packages/zod/src/factory.ts @@ -401,16 +401,7 @@ class SchemaFactory { ]); break; case 'DateTime': - base = z.preprocess((val) => { - if (typeof val !== 'string') return val; - if (/^\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d\d(?::\d\d)?)?$/.test(val)) { - const hasTz = val.endsWith('Z') || /[+-]\d\d(?::\d\d)?$/.test(val); - const d = new Date(`1970-01-01T${val}${hasTz ? '' : 'Z'}`); - return isNaN(d.getTime()) ? val : d; - } - const d = new Date(val); - return isNaN(d.getTime()) ? val : d; - }, z.date()); + base = z.union([z.date(), z.iso.datetime()]); break; case 'Bytes': base = z.instanceof(Uint8Array); diff --git a/packages/zod/test/factory.test.ts b/packages/zod/test/factory.test.ts index 72b139025..cfbdaa4cc 100644 --- a/packages/zod/test/factory.test.ts +++ b/packages/zod/test/factory.test.ts @@ -154,53 +154,6 @@ describe('SchemaFactory - makeModelSchema', () => { birthdate: '2024-01-15T10:30:00.000Z', }); expect(result.success).toBe(true); - // Coerced to a Date for the engine. - expect(result.data?.birthdate).toBeInstanceOf(Date); - }); - - // Regression: #2631 — earlier versions accepted ISO strings via - // Prisma's permissive coercion. The strict zod union introduced in - // 3.5+ rejected ISO date and time-only strings, breaking every - // `@db.Date` and `@db.Time` caller that had been passing strings. - it('accepts DateTime as an ISO date string (#2631)', () => { - const userSchema = factory.makeModelSchema('User'); - const result = userSchema.safeParse({ - ...validUser, - birthdate: '2024-01-15', - }); - expect(result.success).toBe(true); - expect(result.data?.birthdate).toBeInstanceOf(Date); - }); - - it('accepts DateTime as a bare time-only string for @db.Time fields (#2631)', () => { - const userSchema = factory.makeModelSchema('User'); - const result = userSchema.safeParse({ - ...validUser, - birthdate: '09:30:00', - }); - expect(result.success).toBe(true); - // Time-only strings are anchored to the Unix epoch. - expect(result.data?.birthdate).toBeInstanceOf(Date); - expect((result.data?.birthdate as Date).toISOString()).toBe('1970-01-01T09:30:00.000Z'); - }); - - it('accepts DateTime as a time-only string with timezone (#2631)', () => { - const userSchema = factory.makeModelSchema('User'); - const result = userSchema.safeParse({ - ...validUser, - birthdate: '09:30:00+12:00', - }); - expect(result.success).toBe(true); - expect(result.data?.birthdate).toBeInstanceOf(Date); - }); - - it('rejects DateTime as a non-parseable string', () => { - const userSchema = factory.makeModelSchema('User'); - const result = userSchema.safeParse({ - ...validUser, - birthdate: 'not-a-date', - }); - expect(result.success).toBe(false); }); it('accepts Bytes as Uint8Array', () => { diff --git a/tests/regression/test/issue-2631.test.ts b/tests/regression/test/issue-2631.test.ts new file mode 100644 index 000000000..3088c73be --- /dev/null +++ b/tests/regression/test/issue-2631.test.ts @@ -0,0 +1,78 @@ +import { createTestClient } from '@zenstackhq/testtools'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; + +// Regression for #2631: ZenStack 3.5+ replaced Prisma's permissive +// datetime input coercion with a strict zod union, breaking every caller +// that passed ISO strings to `DateTime` fields. The default behaviour now +// coerces strings; `ClientOptions.strictDateInput: true` opts back in to +// the strict semantics. +describe('Issue 2631 — strictDateInput option', () => { + const schema = ` +model Event { + id Int @id @default(autoincrement()) + label String + when DateTime +} + `; + + describe('default (strictDateInput unset / false)', () => { + let db: any; + + beforeEach(async () => { + db = await createTestClient(schema, { usePrismaPush: true, provider: 'sqlite' }); + }); + afterEach(async () => db?.$disconnect()); + + it('accepts a Date object', async () => { + const e = await db.event.create({ data: { label: 'date', when: new Date('2024-01-15T10:30:00Z') } }); + expect(e.when).toBeInstanceOf(Date); + }); + + it('accepts an ISO datetime string and coerces to Date', async () => { + const e = await db.event.create({ data: { label: 'iso', when: '2024-01-15T10:30:00.000Z' } }); + expect(e.when).toBeInstanceOf(Date); + }); + + it('accepts an ISO date string and coerces to Date', async () => { + const e = await db.event.create({ data: { label: 'date-only', when: '2024-01-15' } }); + expect(e.when).toBeInstanceOf(Date); + }); + + it('accepts a bare time-only string anchored to the Unix epoch', async () => { + const e = await db.event.create({ data: { label: 'time-only', when: '09:30:00' } }); + expect(e.when).toBeInstanceOf(Date); + expect((e.when as Date).getUTCHours()).toBe(9); + expect((e.when as Date).getUTCMinutes()).toBe(30); + }); + + it('rejects a non-parseable string', async () => { + await expect(db.event.create({ data: { label: 'junk', when: 'not-a-date' as any } })).rejects.toThrow(); + }); + }); + + describe('strictDateInput: true', () => { + let db: any; + + beforeEach(async () => { + db = await createTestClient(schema, { usePrismaPush: true, provider: 'sqlite', strictDateInput: true }); + }); + afterEach(async () => db?.$disconnect()); + + it('accepts a Date object', async () => { + const e = await db.event.create({ data: { label: 'date', when: new Date('2024-01-15T10:30:00Z') } }); + expect(e.when).toBeInstanceOf(Date); + }); + + it('rejects an ISO datetime string', async () => { + await expect(db.event.create({ data: { label: 'iso', when: '2024-01-15T10:30:00.000Z' as any } })).rejects.toThrow(); + }); + + it('rejects an ISO date string', async () => { + await expect(db.event.create({ data: { label: 'date-only', when: '2024-01-15' as any } })).rejects.toThrow(); + }); + + it('rejects a bare time-only string', async () => { + await expect(db.event.create({ data: { label: 'time-only', when: '09:30:00' as any } })).rejects.toThrow(); + }); + }); +}); From 6956c7e5dcc85d9a6787cc199a2b48efddc7ef84 Mon Sep 17 00:00:00 2001 From: erwan-joly Date: Fri, 1 May 2026 00:35:27 +1200 Subject: [PATCH 3/5] docs(orm): clarify coercedDateTimeSchema accepts Date-parseable strings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses CodeRabbit nitpick on #2632: the implementation falls through to `new Date(val)` for non-time-only strings, so engine-dependent formats like "2024/01/15" are accepted. That is intentional — the schema mirrors Prisma's pre-3.5 behaviour for compatibility — but the JSDoc previously said "ISO strings" only. Reword to describe the actual contract and point users who want stricter validation at strictDateInput. --- packages/orm/src/client/zod/factory.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/orm/src/client/zod/factory.ts b/packages/orm/src/client/zod/factory.ts index 8a2ee02fc..cdc854c95 100644 --- a/packages/orm/src/client/zod/factory.ts +++ b/packages/orm/src/client/zod/factory.ts @@ -85,11 +85,14 @@ export function createQuerySchemaFactory(clientOrSchema: any, options?: any) { } /** - * Builds a `DateTime` value schema that accepts a `Date` object or an ISO - * datetime / date / time-only string and coerces it to a `Date`. Time-only - * strings (e.g. `"09:00:00"` for `@db.Time` fields) are anchored to the Unix - * epoch. Strings that don't parse fall through and are rejected by `z.date()` - * with the standard error. + * Builds a `DateTime` value schema that accepts a `Date` object or any string + * the JS `Date` constructor parses, and coerces it to a `Date`. ISO datetime, + * ISO date, and time-only strings (e.g. `"09:00:00"` for `@db.Time` fields, + * anchored to the Unix epoch) are the documented happy paths; other formats + * accepted by `new Date(...)` also pass through, mirroring Prisma's pre-3.5 + * behaviour. Strings the engine can't parse fall through and are rejected by + * `z.date()` with the standard error. Callers wanting strict ISO-or-Date + * validation should set `ClientOptions.strictDateInput: true`. * * Used when `ClientOptions.strictDateInput` is left at its default (`false`). * @see https://github.com/zenstackhq/zenstack/issues/2631 From 73fe8c8cde256c48641339ec9e06146da0d49fde Mon Sep 17 00:00:00 2001 From: erwan-joly Date: Fri, 1 May 2026 01:00:00 +1200 Subject: [PATCH 4/5] fix(orm): keep DateTime union shape for OpenAPI baseline `coercedDateTimeSchema` previously returned `z.preprocess(fn, z.date())`, which serialised to an empty `{}` and broke the rpc-openapi baseline (`packages/server/test/openapi/baseline/rpc.baseline.yaml`) that documents the accepted ISO datetime / ISO date / Date forms. Wrap the preprocess around the original `z.union([z.iso.datetime(), z.iso.date(), z.date()])` so OpenAPI generation still emits the documented variants. Runtime behaviour is unchanged: preprocess coerces strings into Dates first, the union's `z.date()` arm catches everything that parses, and non-parseable strings fall through and are rejected. --- packages/orm/src/client/zod/factory.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/orm/src/client/zod/factory.ts b/packages/orm/src/client/zod/factory.ts index cdc854c95..65dd817ad 100644 --- a/packages/orm/src/client/zod/factory.ts +++ b/packages/orm/src/client/zod/factory.ts @@ -98,6 +98,13 @@ export function createQuerySchemaFactory(clientOrSchema: any, options?: any) { * @see https://github.com/zenstackhq/zenstack/issues/2631 */ export function coercedDateTimeSchema(): ZodType { + // The schema keeps the original `z.iso.datetime() | z.iso.date() | z.date()` + // union so the generated OpenAPI spec still documents the accepted ISO + // forms. Preprocess runs first and coerces strings into `Date` objects, + // so the union's `z.date()` arm catches everything that successfully + // parses — including non-ISO formats like `"2024/01/15"` for Prisma + // compatibility (rejected with the standard error if `new Date(...)` + // returns Invalid Date). return z.preprocess((val) => { if (typeof val !== 'string') return val; if (/^\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d\d(?::\d\d)?)?$/.test(val)) { @@ -107,7 +114,7 @@ export function coercedDateTimeSchema(): ZodType { } const d = new Date(val); return isNaN(d.getTime()) ? val : d; - }, z.date()); + }, z.union([z.iso.datetime(), z.iso.date(), z.date()])); } /** From 8f0fdbe3454f4364984c8f252b090e9c01001c56 Mon Sep 17 00:00:00 2001 From: erwan-joly Date: Fri, 8 May 2026 17:47:22 +1200 Subject: [PATCH 5/5] refactor(orm): drop strictDateInput option and always coerce DateTime input Per review feedback on #2632: the more accommodating coercion behaviour is preferred for everyone, so the opt-in strict mode is unnecessary surface area. DateTime inputs now unconditionally accept Date objects and any string the JS Date constructor parses, mirroring Prisma's pre-3.5 behaviour. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/orm/src/client/options.ts | 10 --- packages/orm/src/client/zod/factory.ts | 9 +-- tests/regression/test/issue-2631.test.ts | 84 ++++++++---------------- 3 files changed, 30 insertions(+), 73 deletions(-) diff --git a/packages/orm/src/client/options.ts b/packages/orm/src/client/options.ts index 7baa242a5..136c4928b 100644 --- a/packages/orm/src/client/options.ts +++ b/packages/orm/src/client/options.ts @@ -202,16 +202,6 @@ export type ClientOptions = QueryOptions & { */ validateInput?: boolean; - /** - * Whether to require `Date` objects (rather than ISO strings) for `DateTime` field inputs. Defaults - * to `false`, matching Prisma's longstanding behavior of coercing ISO strings — including bare - * time-only strings like `"09:00:00"` for `@db.Time` fields — to `Date`. - * - * Set to `true` to opt into strict input validation that rejects all string forms. - * @see https://github.com/zenstackhq/zenstack/issues/2631 - */ - strictDateInput?: boolean; - /** * Whether to use compact alias names (e.g., "$$t1", "$$t2") when transforming ORM queries to SQL. * Defaults to `true`. diff --git a/packages/orm/src/client/zod/factory.ts b/packages/orm/src/client/zod/factory.ts index 65dd817ad..066541f67 100644 --- a/packages/orm/src/client/zod/factory.ts +++ b/packages/orm/src/client/zod/factory.ts @@ -91,10 +91,8 @@ export function createQuerySchemaFactory(clientOrSchema: any, options?: any) { * anchored to the Unix epoch) are the documented happy paths; other formats * accepted by `new Date(...)` also pass through, mirroring Prisma's pre-3.5 * behaviour. Strings the engine can't parse fall through and are rejected by - * `z.date()` with the standard error. Callers wanting strict ISO-or-Date - * validation should set `ClientOptions.strictDateInput: true`. + * `z.date()` with the standard error. * - * Used when `ClientOptions.strictDateInput` is left at its default (`false`). * @see https://github.com/zenstackhq/zenstack/issues/2631 */ export function coercedDateTimeSchema(): ZodType { @@ -887,10 +885,7 @@ export class ZodSchemaFactory< @cache() private makeDateTimeValueSchema(): ZodType { - // Strict mode: require an actual `Date` instance, matching what the - // engine ultimately wants. Default mode: coerce ISO strings (datetime, - // date, time-only) to `Date` for Prisma compatibility (#2631). - const schema = (this.options as ClientOptions)?.strictDateInput ? z.date() : coercedDateTimeSchema(); + const schema = coercedDateTimeSchema(); this.registerSchema('DateTime', schema); return schema; } diff --git a/tests/regression/test/issue-2631.test.ts b/tests/regression/test/issue-2631.test.ts index 3088c73be..84d270be6 100644 --- a/tests/regression/test/issue-2631.test.ts +++ b/tests/regression/test/issue-2631.test.ts @@ -3,10 +3,10 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'; // Regression for #2631: ZenStack 3.5+ replaced Prisma's permissive // datetime input coercion with a strict zod union, breaking every caller -// that passed ISO strings to `DateTime` fields. The default behaviour now -// coerces strings; `ClientOptions.strictDateInput: true` opts back in to -// the strict semantics. -describe('Issue 2631 — strictDateInput option', () => { +// that passed ISO strings to `DateTime` fields. `DateTime` inputs now +// coerce strings the JS `Date` constructor parses back to `Date`, +// mirroring Prisma's pre-3.5 behaviour. +describe('Issue 2631 — DateTime input coercion', () => { const schema = ` model Event { id Int @id @default(autoincrement()) @@ -15,64 +15,36 @@ model Event { } `; - describe('default (strictDateInput unset / false)', () => { - let db: any; + let db: any; - beforeEach(async () => { - db = await createTestClient(schema, { usePrismaPush: true, provider: 'sqlite' }); - }); - afterEach(async () => db?.$disconnect()); - - it('accepts a Date object', async () => { - const e = await db.event.create({ data: { label: 'date', when: new Date('2024-01-15T10:30:00Z') } }); - expect(e.when).toBeInstanceOf(Date); - }); - - it('accepts an ISO datetime string and coerces to Date', async () => { - const e = await db.event.create({ data: { label: 'iso', when: '2024-01-15T10:30:00.000Z' } }); - expect(e.when).toBeInstanceOf(Date); - }); - - it('accepts an ISO date string and coerces to Date', async () => { - const e = await db.event.create({ data: { label: 'date-only', when: '2024-01-15' } }); - expect(e.when).toBeInstanceOf(Date); - }); - - it('accepts a bare time-only string anchored to the Unix epoch', async () => { - const e = await db.event.create({ data: { label: 'time-only', when: '09:30:00' } }); - expect(e.when).toBeInstanceOf(Date); - expect((e.when as Date).getUTCHours()).toBe(9); - expect((e.when as Date).getUTCMinutes()).toBe(30); - }); - - it('rejects a non-parseable string', async () => { - await expect(db.event.create({ data: { label: 'junk', when: 'not-a-date' as any } })).rejects.toThrow(); - }); + beforeEach(async () => { + db = await createTestClient(schema, { usePrismaPush: true, provider: 'sqlite' }); }); + afterEach(async () => db?.$disconnect()); - describe('strictDateInput: true', () => { - let db: any; - - beforeEach(async () => { - db = await createTestClient(schema, { usePrismaPush: true, provider: 'sqlite', strictDateInput: true }); - }); - afterEach(async () => db?.$disconnect()); + it('accepts a Date object', async () => { + const e = await db.event.create({ data: { label: 'date', when: new Date('2024-01-15T10:30:00Z') } }); + expect(e.when).toBeInstanceOf(Date); + }); - it('accepts a Date object', async () => { - const e = await db.event.create({ data: { label: 'date', when: new Date('2024-01-15T10:30:00Z') } }); - expect(e.when).toBeInstanceOf(Date); - }); + it('accepts an ISO datetime string and coerces to Date', async () => { + const e = await db.event.create({ data: { label: 'iso', when: '2024-01-15T10:30:00.000Z' } }); + expect(e.when).toBeInstanceOf(Date); + }); - it('rejects an ISO datetime string', async () => { - await expect(db.event.create({ data: { label: 'iso', when: '2024-01-15T10:30:00.000Z' as any } })).rejects.toThrow(); - }); + it('accepts an ISO date string and coerces to Date', async () => { + const e = await db.event.create({ data: { label: 'date-only', when: '2024-01-15' } }); + expect(e.when).toBeInstanceOf(Date); + }); - it('rejects an ISO date string', async () => { - await expect(db.event.create({ data: { label: 'date-only', when: '2024-01-15' as any } })).rejects.toThrow(); - }); + it('accepts a bare time-only string anchored to the Unix epoch', async () => { + const e = await db.event.create({ data: { label: 'time-only', when: '09:30:00' } }); + expect(e.when).toBeInstanceOf(Date); + expect((e.when as Date).getUTCHours()).toBe(9); + expect((e.when as Date).getUTCMinutes()).toBe(30); + }); - it('rejects a bare time-only string', async () => { - await expect(db.event.create({ data: { label: 'time-only', when: '09:30:00' as any } })).rejects.toThrow(); - }); + it('rejects a non-parseable string', async () => { + await expect(db.event.create({ data: { label: 'junk', when: 'not-a-date' as any } })).rejects.toThrow(); }); });