Skip to content

Commit bf7d87c

Browse files
samuelho-devclaude
andauthored
feat: DateTime maps to DateFromInput dual-boundary schema (#24)
Generated DateTime columns previously used Schema.DateFromSelf (Encoded = Date) which broke RPC/HTTP wire decode where JSON-parsed input is a string. Adds a new exported DateFromInput schema — Schema.Union(Schema.DateFromSelf, Schema.Date) — wired into the codegen, so decode accepts native Date instances (Kysely DA layer) AND ISO strings (RPC wire layer). Encode picks the first union member (DateFromSelf, identity) so Kysely-bound encode keeps producing Date instances. Mirrors the JsonValue dual-boundary discipline already in this package (Schema<JsonValue, JsonValue> is wire-safe by construction). Internal: consolidated duplicated PRISMA_TO_EFFECT_SCHEMA / PRISMA_SCALAR_MAP constants. src/effect/type.ts now imports the canonical map from src/utils/type-mappings.ts. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 908ff2e commit bf7d87c

17 files changed

Lines changed: 250 additions & 149 deletions

.changeset/datetime-dual-input.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
---
2+
"prisma-effect-kysely": minor
3+
---
4+
5+
feat: DateTime columns now map to `DateFromInput` (dual-boundary Date schema)
6+
7+
`DateTime` columns previously mapped to `Schema.DateFromSelf`
8+
(`Encoded = Date`), which broke RPC/HTTP wire decode where JSON-parsed
9+
input is a string. Now maps to a new exported `DateFromInput` schema:
10+
11+
- **Type** = `Date` (runtime — unchanged)
12+
- **Encoded** = `Date | string` (was `Date`)
13+
14+
Defined as `Schema.Union(Schema.DateFromSelf, Schema.Date)`, so decode
15+
accepts native `Date` instances (Kysely DA layer — pg driver returns Date)
16+
AND ISO strings (RPC/HTTP wire layer — JSON.parse output). One primitive
17+
serves both consumer boundaries; consumers no longer need parallel
18+
schemas or `Schema.extend` overrides for date columns.
19+
20+
**Why minor (not major)**: existing public API behavior is preserved.
21+
`Selectable<T>` / `Insertable<T>` / `Updateable<T>` Type sides unchanged.
22+
Decode accepts MORE inputs (Date AND string), not fewer. Encode picks
23+
the first union member (`DateFromSelf`, identity) so Kysely-bound
24+
encode still produces Date instances — existing call sites keep working.
25+
26+
**Why the change**: `DateFromSelf` optimized for the in-memory Kysely
27+
boundary; `Schema.Date` optimizes for the JSON wire boundary. Modern
28+
apps cross both with the same generated schemas. Picking either single
29+
primitive forced consumers to patch around it at one boundary.
30+
`DateFromInput` accepts both encoded shapes natively. Mirrors the
31+
`JsonValue` dual-boundary discipline already in this package
32+
(`Schema<JsonValue, JsonValue>` is wire-safe by construction).
33+
34+
**Migration**: no code changes for typical consumers. If you imported
35+
`Schema.DateFromSelf` directly from generated `types.ts` in a way that
36+
depended on the literal symbol, switch to `DateFromInput` imported from
37+
`prisma-effect-kysely`.
38+
39+
**Internal**: consolidated duplicated `PRISMA_TO_EFFECT_SCHEMA` /
40+
`PRISMA_SCALAR_MAP` constants. `src/effect/type.ts` now imports the
41+
canonical map from `src/utils/type-mappings.ts`.

CLAUDE.md

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export type UserId = typeof UserId.Type;
6262
export const User = Schema.Struct({
6363
id: columnType(Schema.UUID, Schema.Never, Schema.Never),
6464
email: Schema.String,
65-
createdAt: generated(Schema.DateFromSelf),
65+
createdAt: generated(DateFromInput),
6666
});
6767
export type User = typeof User;
6868

@@ -97,17 +97,19 @@ Prisma stores `A`/`B` columns; we emit semantic snake_case fields via `Schema.pr
9797

9898
## Type mappings
9999

100-
| Prisma | Effect | Notes |
101-
| ----------- | --------------------- | ------------------ |
102-
| String | `Schema.String` | UUID → `Schema.UUID` |
103-
| Int / Float | `Schema.Number` | |
104-
| BigInt | `Schema.BigInt` | |
105-
| Decimal | `Schema.String` | precision |
106-
| Boolean | `Schema.Boolean` | |
107-
| DateTime | `Schema.DateFromSelf` | |
108-
| Json | `Schema.Unknown` | |
109-
| Bytes | `Schema.Uint8Array` | |
110-
| Enum | imported enum schema | |
100+
| Prisma | Effect | Notes |
101+
| ----------- | --------------------- | -------------------------------------- |
102+
| String | `Schema.String` | UUID → `Schema.UUID` |
103+
| Int / Float | `Schema.Number` | |
104+
| BigInt | `Schema.BigInt` | |
105+
| Decimal | `Schema.String` | precision |
106+
| Boolean | `Schema.Boolean` | |
107+
| DateTime | `DateFromInput` | dual-input: `Date \| string``Date` |
108+
| Json | `JsonValue` | recursive JSON, wire-safe |
109+
| Bytes | `Schema.Uint8Array` | |
110+
| Enum | imported enum schema | |
111+
112+
`DateFromInput` is `Schema.Union(Schema.DateFromSelf, Schema.Date)` — accepts native `Date` (Kysely DA boundary) and ISO strings (RPC/HTTP wire boundary). Encode picks the first union member (DateFromSelf identity) so Kysely-bound paths still receive Date instances.
111113

112114
Arrays → `Schema.Array(t)`. Nullable → `Schema.NullOr(t)`.
113115

@@ -130,7 +132,7 @@ Arrays → `Schema.Array(t)`. Nullable → `Schema.NullOr(t)`.
130132

131133
## Working in this repo
132134

133-
- Run `bun run test` (280 tests) to baseline before changes
135+
- Run `bun run test` to baseline before changes
134136
- Generator must be rebuilt before `prisma generate` picks up changes
135137
- Test fixtures: `src/__tests__/fixtures/test.prisma`
136138
- Generated headers include timestamp + edit warning

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ Three files: `enums.ts`, `types.ts`, `index.ts`.
2727

2828
```typescript
2929
import { Schema } from "effect";
30-
import { columnType, generated, Selectable } from "prisma-effect-kysely";
30+
import { columnType, DateFromInput, generated, Selectable } from "prisma-effect-kysely";
3131

3232
// Branded ID
3333
export const UserId = Schema.UUID.pipe(Schema.brand("UserId"));
@@ -37,7 +37,7 @@ export type UserId = typeof UserId.Type;
3737
export const User = Schema.Struct({
3838
id: columnType(Schema.UUID, Schema.Never, Schema.Never),
3939
email: Schema.String,
40-
createdAt: generated(Schema.DateFromSelf),
40+
createdAt: generated(DateFromInput),
4141
});
4242
export type User = typeof User;
4343

@@ -80,13 +80,13 @@ Schema names are PascalCase regardless of Prisma model name (`session_preference
8080
| BigInt | `Schema.BigInt` |
8181
| Decimal | `Schema.String` |
8282
| Boolean | `Schema.Boolean` |
83-
| DateTime | `Schema.DateFromSelf` |
84-
| Json | `Schema.Unknown` |
83+
| DateTime | `DateFromInput` |
84+
| Json | `JsonValue` |
8585
| Bytes | `Schema.Uint8Array` |
8686
| Enum | `Schema.Literal(...)` |
8787
| UUID | `Schema.UUID` |
8888

89-
Arrays → `Schema.Array(t)`. Nullable → `Schema.NullOr(t)`.
89+
Arrays → `Schema.Array(t)`. Nullable → `Schema.NullOr(t)`. `DateFromInput` accepts `Date | string` on the encoded side; `JsonValue` is `Schema<JsonValue, JsonValue>`. Both are wire-safe (`JSON.parse` output decodes cleanly).
9090

9191
## UUID Detection
9292

src/__tests__/brand-preservation.test.ts

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,14 @@
1010

1111
import { Schema } from 'effect';
1212
import { describe, it, expectTypeOf } from 'vitest';
13-
import { columnType, generated, Insertable, VariantTypeId, VariantMarker } from '../kysely/helpers';
13+
import {
14+
columnType,
15+
DateFromInput,
16+
generated,
17+
Insertable,
18+
VariantTypeId,
19+
VariantMarker,
20+
} from '../kysely/helpers';
1421

1522
describe('Brand Preservation Through Type System', () => {
1623
describe('Step 1: columnType return type', () => {
@@ -33,9 +40,9 @@ describe('Brand Preservation Through Type System', () => {
3340

3441
it('should preserve VariantMarker with different insert/update types', () => {
3542
const timestampField = columnType(
36-
Schema.DateFromSelf,
37-
Schema.optional(Schema.DateFromSelf),
38-
Schema.DateFromSelf
43+
DateFromInput,
44+
Schema.optional(DateFromInput),
45+
DateFromInput
3946
);
4047

4148
type TimestampType = Schema.Schema.Type<typeof timestampField>;
@@ -67,7 +74,7 @@ describe('Brand Preservation Through Type System', () => {
6774
it('should preserve VariantMarker on generated fields', () => {
6875
const TestModel = Schema.Struct({
6976
id: columnType(Schema.Number.pipe(Schema.brand('TestId')), Schema.Never, Schema.Never),
70-
createdAt: generated(Schema.DateFromSelf),
77+
createdAt: generated(DateFromInput),
7178
});
7279

7380
type ModelType = Schema.Schema.Type<typeof TestModel>;
@@ -105,8 +112,8 @@ describe('Brand Preservation Through Type System', () => {
105112
const TestModel = Schema.Struct({
106113
id: columnType(Schema.Number.pipe(Schema.brand('TestId')), Schema.Never, Schema.Never),
107114
name: Schema.String,
108-
createdAt: generated(Schema.DateFromSelf),
109-
updatedAt: generated(Schema.DateFromSelf),
115+
createdAt: generated(DateFromInput),
116+
updatedAt: generated(DateFromInput),
110117
});
111118

112119
type TestInsert = Insertable<typeof TestModel>;
@@ -130,7 +137,7 @@ describe('Brand Preservation Through Type System', () => {
130137
id: columnType(Schema.UUID.pipe(Schema.brand('UserId')), Schema.Never, Schema.Never),
131138
email: Schema.String,
132139
name: Schema.String,
133-
createdAt: generated(Schema.DateFromSelf),
140+
createdAt: generated(DateFromInput),
134141
});
135142
type User = typeof User;
136143

@@ -184,7 +191,7 @@ describe('Brand Preservation Through Type System', () => {
184191

185192
it('should correctly extract insert type from generated field', () => {
186193
const TestModel = Schema.Struct({
187-
createdAt: generated(Schema.DateFromSelf),
194+
createdAt: generated(DateFromInput),
188195
});
189196

190197
type ModelType = Schema.Schema.Type<typeof TestModel>;

src/__tests__/deep-type-instantiation.test.ts

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import type {
3434
import { describe, it, expectTypeOf } from 'vitest';
3535
import {
3636
columnType,
37+
DateFromInput,
3738
generated,
3839
JsonValue,
3940
type ColumnType,
@@ -75,19 +76,19 @@ const Seller = Schema.Struct({
7576
payout_schedule: generated(Schema.String),
7677
minimum_payout_threshold: generated(Schema.Number),
7778
verification_fields_needed: generated(Schema.Array(Schema.String)),
78-
activated_at: Schema.NullOr(Schema.DateFromSelf),
79+
activated_at: Schema.NullOr(DateFromInput),
7980
banned_reason: Schema.NullOr(Schema.String),
8081
business_type: Schema.NullOr(Schema.String),
8182
card_payments_capability: Schema.NullOr(Schema.String),
8283
flagged_reason: Schema.NullOr(Schema.String),
83-
last_verification_attempt: Schema.NullOr(Schema.DateFromSelf),
84+
last_verification_attempt: Schema.NullOr(DateFromInput),
8485
stripe_account_id: Schema.NullOr(Schema.UUID),
85-
tos_acceptance_date: Schema.NullOr(Schema.DateFromSelf),
86+
tos_acceptance_date: Schema.NullOr(DateFromInput),
8687
transfers_capability: Schema.NullOr(Schema.String),
87-
verification_due_by: Schema.NullOr(Schema.DateFromSelf),
88+
verification_due_by: Schema.NullOr(DateFromInput),
8889
verification_status: Schema.NullOr(Schema.String),
89-
created_at: generated(Schema.DateFromSelf),
90-
updated_at: generated(Schema.DateFromSelf),
90+
created_at: generated(DateFromInput),
91+
updated_at: generated(DateFromInput),
9192
});
9293
type SellerType = Schema.Schema.Type<typeof Seller>;
9394

@@ -110,15 +111,15 @@ const Product = Schema.Struct({
110111
is_subscription_only: generated(Schema.Boolean),
111112
content: Schema.NullOr(Schema.String),
112113
current_version_id: Schema.NullOr(Schema.UUID),
113-
end_at: Schema.NullOr(Schema.DateFromSelf),
114+
end_at: Schema.NullOr(DateFromInput),
114115
max_quantity: Schema.NullOr(Schema.Number),
115-
start_at: Schema.NullOr(Schema.DateFromSelf),
116+
start_at: Schema.NullOr(DateFromInput),
116117
subcategory_id: Schema.NullOr(Schema.UUID),
117118
suggested_price: Schema.NullOr(Schema.Number),
118119
thumbnail: Schema.NullOr(Schema.String),
119120
category_id: Schema.NullOr(Schema.UUID),
120-
created_at: generated(Schema.DateFromSelf),
121-
updated_at: generated(Schema.DateFromSelf),
121+
created_at: generated(DateFromInput),
122+
updated_at: generated(DateFromInput),
122123
});
123124
type ProductType = Schema.Schema.Type<typeof Product>;
124125

@@ -137,11 +138,11 @@ const Order = Schema.Struct({
137138
email: Schema.NullOr(Schema.String),
138139
notes: Schema.NullOr(Schema.String),
139140
stripe_payment_intent_id: Schema.NullOr(Schema.UUID),
140-
completed_at: Schema.NullOr(Schema.DateFromSelf),
141-
cancelled_at: Schema.NullOr(Schema.DateFromSelf),
142-
refunded_at: Schema.NullOr(Schema.DateFromSelf),
143-
created_at: generated(Schema.DateFromSelf),
144-
updated_at: generated(Schema.DateFromSelf),
141+
completed_at: Schema.NullOr(DateFromInput),
142+
cancelled_at: Schema.NullOr(DateFromInput),
143+
refunded_at: Schema.NullOr(DateFromInput),
144+
created_at: generated(DateFromInput),
145+
updated_at: generated(DateFromInput),
145146
});
146147
type OrderType = Schema.Schema.Type<typeof Order>;
147148

@@ -159,9 +160,9 @@ const Payment = Schema.Struct({
159160
failure_code: Schema.NullOr(Schema.String),
160161
failure_message: Schema.NullOr(Schema.String),
161162
refund_amount: Schema.NullOr(Schema.Number),
162-
refunded_at: Schema.NullOr(Schema.DateFromSelf),
163-
created_at: generated(Schema.DateFromSelf),
164-
updated_at: generated(Schema.DateFromSelf),
163+
refunded_at: Schema.NullOr(DateFromInput),
164+
created_at: generated(DateFromInput),
165+
updated_at: generated(DateFromInput),
165166
});
166167
type PaymentType = Schema.Schema.Type<typeof Payment>;
167168

@@ -321,8 +322,8 @@ const PaymentWithJson = Schema.Struct({
321322
last_payment_error: Schema.NullOr(columnType(JsonValue, JsonValue, JsonValue)),
322323
metadata: Schema.NullOr(columnType(JsonValue, JsonValue, JsonValue)),
323324
raw_response: columnType(JsonValue, JsonValue, JsonValue),
324-
created_at: generated(Schema.DateFromSelf),
325-
updated_at: generated(Schema.DateFromSelf),
325+
created_at: generated(DateFromInput),
326+
updated_at: generated(DateFromInput),
326327
});
327328

328329
interface JsonTestDB {

0 commit comments

Comments
 (0)