Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions .changeset/datetime-back-to-datefromself.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
---
"prisma-effect-kysely": minor
---

fix: DateTime maps back to `Schema.DateFromSelf` (Date ↔ Date) — Prisma+Kysely canonical

Reverts the 5.6.0 change that mapped `DateTime` to `DateFromInput` (a
`Schema.Union(DateFromSelf, Date)` with `Encoded = Date | string`).

**Why revert**: the dual-input Union pushed the boundary problem onto
DA-layer consumers. Kysely's pg driver returns native `Date` instances,
but `Selectable<X>.created_at` typed as `Date | string` forced every DA
mapper that copies `result.created_at` into a contract type to either
narrow manually (no cast-free path) or wrap the read in
`Schema.decode(Selectable(X))` (heavy refactor across hundreds of sites).

**Why DateFromSelf is correct**:

- **Prisma docs**: *"Prisma Client returns all DateTime values as native
JavaScript Date objects. ... DateTime values must be passed as Date
objects, not strings, to avoid runtime errors."*
- **Kysely docs**: idiomatic DateTime column is
`created_at: ColumnType<Date, string | undefined, never>` — SELECT
yields `Date`. *"TypeScript is a compile-time concept and cannot
alter runtime JavaScript types. If your TypeScript definition for a
column differs from the database's actual return type, the runtime
type will not change automatically."*
- **Effect Schema docs (Doc 10944)**: *"schemas should be defined such
that encode + decode return the original value"* — one Type, one
Encoded per schema. The dual-boundary problem (DA Date ↔ Date vs
RPC string ↔ Date) is solved by **two schemas** (one per boundary),
not one Union. Doc 4312 (`@effect/sql/Model.Class`) shows this
canonical variant pattern (`select`/`insert`/`update` vs
`json`/`jsonCreate`/`jsonUpdate`).

**For RPC/HTTP wire boundaries**: define a contract-layer schema that
overrides date columns with `Schema.Date` (Encoded = string) before the
RPC framework calls `Schema.decode`. This is the same pattern as
`@effect/sql`'s `json` variants — one schema per boundary.

**`DateFromInput` is still exported** from the package for consumers that
specifically want the dual-input behavior at a single call site. The
codegen just no longer auto-emits it for every DateTime column.

**The Schema.Schema.Encoded fix in 5.7.0 stays** — that's still correct
for join-table column exposure (`_product_tags.A`/`B`).

**Migration**: most consumers benefit immediately (DA mappers stop
seeing `Date | string`). For RPC contracts that previously didn't have
a Date override (because they relied on `DateFromInput`), re-add a
`Schema.extend` with `Schema.Date` overrides for date columns to keep
wire decode working.
15 changes: 9 additions & 6 deletions src/__tests__/kysely-native-integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,15 +155,18 @@ describe('Kysely Native Integration', () => {
expect(typesContent).toMatch(/author_id:\s*Schema\.UUID/);
});

it('should map DateTime to DateFromInput for Effect schemas', async () => {
it('should map DateTime to Schema.DateFromSelf for Effect schemas', async () => {
const typesContent = await fs.readFile(path.join(outputDir, 'types.ts'), 'utf-8');

// DateTime fields use DateFromInput (dual-boundary Date schema):
// - Encoded = Date | string (Kysely native AND JSON wire)
// - Type = Date (runtime)
expect(typesContent).toContain('generated(DateFromInput)');
// DateTime fields use Schema.DateFromSelf (Type === Encoded === Date) —
// matches Prisma's contract that DateTime values are native Date instances
// and Kysely's idiomatic ColumnType<Date, ...> pattern.
expect(typesContent).toContain('generated(Schema.DateFromSelf)');

// Codegen no longer auto-imports DateFromInput. The Union schema is still
// exported from the package for consumers that explicitly want it.
expect(typesContent).toContain(
'import { columnType, generated, JsonValue, DateFromInput } from "prisma-effect-kysely"'
'import { columnType, generated, JsonValue } from "prisma-effect-kysely"'
);
});

Expand Down
9 changes: 5 additions & 4 deletions src/effect/generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,12 +82,13 @@ export type ${name} = typeof ${name};`;
generateTypesHeader(hasEnums: boolean) {
const header = generateFileHeader();

// Import runtime helpers from prisma-effect-kysely
// columnType and generated are used for field type annotations
// DateFromInput is the dual-boundary Date schema (Date | string ↔ Date)
// Import runtime helpers from prisma-effect-kysely.
// DateTime fields use Schema.DateFromSelf (Date ↔ Date), matching
// Prisma's contract that DateTime values are native Date instances.
// Decode through a Schema.Date contract schema at JSON wire boundaries.
const imports = [
`import { Schema } from "effect";`,
`import { columnType, generated, JsonValue, DateFromInput } from "prisma-effect-kysely";`,
`import { columnType, generated, JsonValue } from "prisma-effect-kysely";`,
];

if (hasEnums) {
Expand Down
19 changes: 11 additions & 8 deletions src/utils/type-mappings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,17 @@
* Prisma scalar type mapping to Effect Schema types
* Uses const assertion for type safety
*
* DateTime uses `DateFromInput` (this package's dual-input Date schema):
* - Type = Date (runtime)
* - Encoded = Date | string (accepts native Date from Kysely AND ISO
* string from JSON wire transport)
* DateTime uses `Schema.DateFromSelf` (Type === Encoded === Date) — matches
* Prisma's contract that Client returns DateTime as native `Date` instances
* (https://www.prisma.io/docs — "Prisma Client returns all DateTime values
* as native JavaScript Date objects"). Also matches Kysely's idiomatic
* `ColumnType<Date, ...>` pattern: SELECT yields Date.
*
* Single primitive serves both consumer boundaries — DA layer and RPC
* wire — so consumers don't need parallel schemas or boundary-specific
* helpers. Mirrors the JsonValue dual-boundary pattern.
* For RPC/JSON wire boundaries (where dates serialize to ISO strings),
* decode through a `Schema.Date`-typed contract schema at the boundary.
* Effect's Schema is single-pair (Type, Encoded) by design — see Doc 10944
* "The Rule of Schemas" — and the dual-boundary problem is solved by
* having two schemas (DA-side vs wire-side), not one Union.
*/
export const PRISMA_TO_EFFECT_SCHEMA = {
String: 'Schema.String',
Expand All @@ -28,7 +31,7 @@ export const PRISMA_TO_EFFECT_SCHEMA = {
BigInt: 'Schema.BigInt',
Decimal: 'Schema.String', // For precision
Boolean: 'Schema.Boolean',
DateTime: 'DateFromInput', // Date | string ↔ Date — dual-boundary safe
DateTime: 'Schema.DateFromSelf', // Date ↔ Date — Prisma+Kysely canonical

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

DateTime mapping violates generator contract rule

At Line 34, mapping DateTime to Schema.DateFromSelf breaks the repository’s required generation contract for DateTime fields. Please map it back to DateFromInput in the centralized mapping (and keep header/import generation aligned accordingly).

Suggested fix
-  DateTime: 'Schema.DateFromSelf', // Date ↔ Date — Prisma+Kysely canonical
+  DateTime: 'DateFromInput', // Accepts Date | string input, encodes as Date

As per coding guidelines, "DateTime fields must map to DateFromInput schema accepting Date | string input and encoding as Date".

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
DateTime: 'Schema.DateFromSelf', // Date ↔ Date — Prisma+Kysely canonical
DateTime: 'DateFromInput', // Accepts Date | string input, encodes as Date
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/type-mappings.ts` at line 34, The mapping for DateTime currently
points to Schema.DateFromSelf which violates the generator contract; change the
centralized mapping so the key DateTime maps to DateFromInput instead of
Schema.DateFromSelf, and update any related header/import generation to
reference DateFromInput (ensuring DateFromInput accepts Date | string and
encodes as Date) so functions/classes referencing DateTime use DateFromInput
consistently.

Json: 'JsonValue', // Recursive JSON type — prevents null absorption in NullOr
Bytes: 'Schema.Uint8Array',
} as const;
Expand Down
Loading