Skip to content

Commit 710aeb0

Browse files
sanny-ioclaude
andauthored
fix(orm): primitive custom type outputs (#2892)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 862d1c2 commit 710aeb0

10 files changed

Lines changed: 509 additions & 3 deletions

File tree

‎packages/orm/src/client/crud/dialects/mysql.ts‎

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,11 +120,15 @@ export class MySqlCrudDialect<Schema extends SchemaDef> extends LateralJoinDiale
120120
}
121121
}
122122

123-
override transformOutput(value: unknown, type: BuiltinType, array: boolean) {
123+
override transformOutput(value: unknown, type: BuiltinType, array: boolean): unknown {
124124
if (value === null || value === undefined) {
125125
return value;
126126
}
127127

128+
if (isPrimitiveTypeDef(this.schema, type)) {
129+
return this.transformOutput(value, this.schema.typeDefs![type]!.base!, array);
130+
}
131+
128132
switch (type) {
129133
case 'Boolean':
130134
return this.transformOutputBoolean(value);
@@ -185,7 +189,17 @@ export class MySqlCrudDialect<Schema extends SchemaDef> extends LateralJoinDiale
185189
}
186190

187191
private transformOutputBytes(value: unknown) {
188-
return Buffer.isBuffer(value) ? Uint8Array.from(value) : value;
192+
if (Buffer.isBuffer(value)) {
193+
return Uint8Array.from(value);
194+
}
195+
// MySQL encodes binary values embedded in JSON as `base64:type<N>:<data>`
196+
if (typeof value === 'string') {
197+
const match = /^base64:type\d+:(.*)$/s.exec(value);
198+
if (match) {
199+
return Uint8Array.from(Buffer.from(match[1]!, 'base64'));
200+
}
201+
}
202+
return value;
189203
}
190204

191205
// #endregion

‎packages/orm/src/client/crud/dialects/postgresql.ts‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,11 +245,15 @@ export class PostgresCrudDialect<Schema extends SchemaDef> extends LateralJoinDi
245245
}
246246
}
247247

248-
override transformOutput(value: unknown, type: BuiltinType, array: boolean) {
248+
override transformOutput(value: unknown, type: BuiltinType, array: boolean): unknown {
249249
if (value === null || value === undefined) {
250250
return value;
251251
}
252252

253+
if (isPrimitiveTypeDef(this.schema, type)) {
254+
return this.transformOutput(value, this.schema.typeDefs![type]!.base!, array);
255+
}
256+
253257
switch (type) {
254258
case 'DateTime':
255259
return this.transformOutputDate(value);
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import type { ClientContract } from '@zenstackhq/orm';
2+
import { createTestClient } from '@zenstackhq/testtools';
3+
import Decimal from 'decimal.js';
4+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
5+
import { schema } from '../schemas/custom-type-primitive-output/schema';
6+
7+
const seenAt = new Date('2026-01-02T03:04:05.000Z');
8+
9+
const itemData = {
10+
label: 'hello',
11+
active: true,
12+
quantity: 3,
13+
ratio: 1.5,
14+
views: 12345678901234n,
15+
price: new Decimal('19.99'),
16+
seenAt,
17+
payload: new Uint8Array([1, 2, 3]),
18+
meta: { a: 1, tags: ['x'] },
19+
};
20+
21+
// asserts that each primitive type def field is returned in its base type's runtime representation
22+
function expectConverted(item: Record<string, unknown>, { withPayload = true } = {}) {
23+
expect(item['label']).toBe('hello');
24+
expect(item['active']).toBe(true);
25+
expect(item['quantity']).toBe(3);
26+
expect(item['ratio']).toBe(1.5);
27+
expect(item['views']).toBe(12345678901234n);
28+
expect(Decimal.isDecimal(item['price'])).toBe(true);
29+
expect((item['price'] as Decimal).toString()).toBe('19.99');
30+
expect(item['seenAt']).toBeInstanceOf(Date);
31+
expect((item['seenAt'] as Date).toISOString()).toBe(seenAt.toISOString());
32+
if (withPayload) {
33+
expect(item['payload']).toBeInstanceOf(Uint8Array);
34+
expect(Buffer.isBuffer(item['payload'])).toBe(false);
35+
expect(Array.from(item['payload'] as Uint8Array)).toEqual([1, 2, 3]);
36+
}
37+
expect(item['meta']).toEqual({ a: 1, tags: ['x'] });
38+
expect(item['note']).toBeNull();
39+
}
40+
41+
describe('Custom type primitive output conversion tests', () => {
42+
let client: ClientContract<typeof schema>;
43+
let ownerId: number;
44+
45+
beforeEach(async () => {
46+
client = await createTestClient(schema);
47+
const owner = await client.owner.create({ data: {} });
48+
ownerId = owner.id;
49+
});
50+
51+
afterEach(async () => {
52+
await client?.$disconnect();
53+
});
54+
55+
it('converts fields returned from create', async () => {
56+
const item = await client.item.create({ data: { ...itemData, ownerId } });
57+
expectConverted(item);
58+
});
59+
60+
it('converts fields on direct reads', async () => {
61+
const { id } = await client.item.create({ data: { ...itemData, ownerId } });
62+
63+
expectConverted(await client.item.findUniqueOrThrow({ where: { id } }));
64+
65+
const [item] = await client.item.findMany();
66+
expectConverted(item!);
67+
68+
const selected = await client.item.findFirstOrThrow({ select: { views: true, price: true, active: true } });
69+
expect(selected.views).toBe(12345678901234n);
70+
expect(Decimal.isDecimal(selected.price)).toBe(true);
71+
expect(selected.active).toBe(true);
72+
});
73+
74+
it('converts fields of nested relations', async () => {
75+
await client.item.create({ data: { ...itemData, ownerId } });
76+
77+
// SQLite can't put BLOB values into the JSON it builds for nested relations (same for plain `Bytes`)
78+
const withPayload = client.$schema.provider.type !== 'sqlite';
79+
const omit = { payload: !withPayload };
80+
81+
const owner = await client.owner.findUniqueOrThrow({ where: { id: ownerId }, include: { items: { omit } } });
82+
expectConverted(owner.items[0]!, { withPayload });
83+
84+
const selected = await client.owner.findUniqueOrThrow({
85+
where: { id: ownerId },
86+
select: {
87+
items: { select: { views: true, price: true, seenAt: true, payload: withPayload, active: true } },
88+
},
89+
});
90+
expect(selected.items[0]!.views).toBe(12345678901234n);
91+
expect(Decimal.isDecimal(selected.items[0]!.price)).toBe(true);
92+
expect(selected.items[0]!.seenAt).toBeInstanceOf(Date);
93+
expect(selected.items[0]!.active).toBe(true);
94+
if (withPayload) {
95+
expect(Buffer.isBuffer(selected.items[0]!.payload)).toBe(false);
96+
expect(Array.from(selected.items[0]!.payload as Uint8Array)).toEqual([1, 2, 3]);
97+
}
98+
99+
const item = await client.item.findFirstOrThrow({ include: { owner: { include: { items: { omit } } } } });
100+
expectConverted(item);
101+
expectConverted(item.owner.items[0]!, { withPayload });
102+
});
103+
104+
it('converts fields returned from update', async () => {
105+
const { id } = await client.item.create({ data: { ...itemData, ownerId } });
106+
const updated = await client.item.update({ where: { id }, data: { quantity: 4 } });
107+
expectConverted({ ...updated, quantity: 3 });
108+
expect(updated.quantity).toBe(4);
109+
});
110+
});

‎tests/e2e/orm/client-api/custom-type-primitive.test.ts‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,4 +169,20 @@ describe('Custom type primitive tests', () => {
169169
}),
170170
).rejects.toThrow(/Too big/);
171171
});
172+
173+
it('converts array items to the base type', async () => {
174+
const user = await client.user.create({
175+
data: { name: 'test', scores: [1n, 12345678901234n] },
176+
});
177+
expect(user.scores).toEqual([1n, 12345678901234n]);
178+
179+
const found = await client.user.findUniqueOrThrow({ where: { id: user.id } });
180+
expect(found.scores).toEqual([1n, 12345678901234n]);
181+
182+
const post = await client.post.create({
183+
data: { tags: ['abc', '123'], author: { connect: { id: user.id } } },
184+
include: { author: true },
185+
});
186+
expect(post.author.scores).toEqual([1n, 12345678901234n]);
187+
});
172188
});
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
//////////////////////////////////////////////////////////////////////////////////////////////
2+
// DO NOT MODIFY THIS FILE //
3+
// This file is automatically generated by ZenStack CLI and should not be manually updated. //
4+
//////////////////////////////////////////////////////////////////////////////////////////////
5+
6+
/* eslint-disable */
7+
8+
import { type SchemaType as $Schema } from "./schema";
9+
import type { FindManyArgs as $FindManyArgs, FindUniqueArgs as $FindUniqueArgs, FindFirstArgs as $FindFirstArgs, ExistsArgs as $ExistsArgs, CreateArgs as $CreateArgs, CreateManyArgs as $CreateManyArgs, CreateManyAndReturnArgs as $CreateManyAndReturnArgs, UpdateArgs as $UpdateArgs, UpdateManyArgs as $UpdateManyArgs, UpdateManyAndReturnArgs as $UpdateManyAndReturnArgs, UpsertArgs as $UpsertArgs, DeleteArgs as $DeleteArgs, DeleteManyArgs as $DeleteManyArgs, CountArgs as $CountArgs, AggregateArgs as $AggregateArgs, GroupByArgs as $GroupByArgs, WhereInput as $WhereInput, SelectInput as $SelectInput, IncludeInput as $IncludeInput, OmitInput as $OmitInput, UncheckedCreateInput as $UncheckedCreateInput, CheckedCreateInput as $CheckedCreateInput, UncheckedUpdateInput as $UncheckedUpdateInput, CheckedUpdateInput as $CheckedUpdateInput, QueryOptions as $QueryOptions } from "@zenstackhq/orm";
10+
import type { SimplifiedPlainResult as $Result, SelectIncludeOmit as $SelectIncludeOmit } from "@zenstackhq/orm";
11+
export type OwnerFindManyArgs = $FindManyArgs<$Schema, "Owner">;
12+
export type OwnerFindUniqueArgs = $FindUniqueArgs<$Schema, "Owner">;
13+
export type OwnerFindFirstArgs = $FindFirstArgs<$Schema, "Owner">;
14+
export type OwnerExistsArgs = $ExistsArgs<$Schema, "Owner">;
15+
export type OwnerCreateArgs = $CreateArgs<$Schema, "Owner">;
16+
export type OwnerCreateManyArgs = $CreateManyArgs<$Schema, "Owner">;
17+
export type OwnerCreateManyAndReturnArgs = $CreateManyAndReturnArgs<$Schema, "Owner">;
18+
export type OwnerUpdateArgs = $UpdateArgs<$Schema, "Owner">;
19+
export type OwnerUpdateManyArgs = $UpdateManyArgs<$Schema, "Owner">;
20+
export type OwnerUpdateManyAndReturnArgs = $UpdateManyAndReturnArgs<$Schema, "Owner">;
21+
export type OwnerUpsertArgs = $UpsertArgs<$Schema, "Owner">;
22+
export type OwnerDeleteArgs = $DeleteArgs<$Schema, "Owner">;
23+
export type OwnerDeleteManyArgs = $DeleteManyArgs<$Schema, "Owner">;
24+
export type OwnerCountArgs = $CountArgs<$Schema, "Owner">;
25+
export type OwnerAggregateArgs = $AggregateArgs<$Schema, "Owner">;
26+
export type OwnerGroupByArgs = $GroupByArgs<$Schema, "Owner">;
27+
export type OwnerWhereInput = $WhereInput<$Schema, "Owner">;
28+
export type OwnerSelect = $SelectInput<$Schema, "Owner">;
29+
export type OwnerInclude = $IncludeInput<$Schema, "Owner">;
30+
export type OwnerOmit = $OmitInput<$Schema, "Owner">;
31+
export type OwnerUncheckedCreateInput = $UncheckedCreateInput<$Schema, "Owner">;
32+
export type OwnerCheckedCreateInput = $CheckedCreateInput<$Schema, "Owner">;
33+
export type OwnerUncheckedUpdateInput = $UncheckedUpdateInput<$Schema, "Owner">;
34+
export type OwnerCheckedUpdateInput = $CheckedUpdateInput<$Schema, "Owner">;
35+
export type OwnerGetPayload<Args extends $SelectIncludeOmit<$Schema, "Owner", true>, Options extends $QueryOptions<$Schema> = $QueryOptions<$Schema>> = $Result<$Schema, "Owner", Args, Options>;
36+
export type ItemFindManyArgs = $FindManyArgs<$Schema, "Item">;
37+
export type ItemFindUniqueArgs = $FindUniqueArgs<$Schema, "Item">;
38+
export type ItemFindFirstArgs = $FindFirstArgs<$Schema, "Item">;
39+
export type ItemExistsArgs = $ExistsArgs<$Schema, "Item">;
40+
export type ItemCreateArgs = $CreateArgs<$Schema, "Item">;
41+
export type ItemCreateManyArgs = $CreateManyArgs<$Schema, "Item">;
42+
export type ItemCreateManyAndReturnArgs = $CreateManyAndReturnArgs<$Schema, "Item">;
43+
export type ItemUpdateArgs = $UpdateArgs<$Schema, "Item">;
44+
export type ItemUpdateManyArgs = $UpdateManyArgs<$Schema, "Item">;
45+
export type ItemUpdateManyAndReturnArgs = $UpdateManyAndReturnArgs<$Schema, "Item">;
46+
export type ItemUpsertArgs = $UpsertArgs<$Schema, "Item">;
47+
export type ItemDeleteArgs = $DeleteArgs<$Schema, "Item">;
48+
export type ItemDeleteManyArgs = $DeleteManyArgs<$Schema, "Item">;
49+
export type ItemCountArgs = $CountArgs<$Schema, "Item">;
50+
export type ItemAggregateArgs = $AggregateArgs<$Schema, "Item">;
51+
export type ItemGroupByArgs = $GroupByArgs<$Schema, "Item">;
52+
export type ItemWhereInput = $WhereInput<$Schema, "Item">;
53+
export type ItemSelect = $SelectInput<$Schema, "Item">;
54+
export type ItemInclude = $IncludeInput<$Schema, "Item">;
55+
export type ItemOmit = $OmitInput<$Schema, "Item">;
56+
export type ItemUncheckedCreateInput = $UncheckedCreateInput<$Schema, "Item">;
57+
export type ItemCheckedCreateInput = $CheckedCreateInput<$Schema, "Item">;
58+
export type ItemUncheckedUpdateInput = $UncheckedUpdateInput<$Schema, "Item">;
59+
export type ItemCheckedUpdateInput = $CheckedUpdateInput<$Schema, "Item">;
60+
export type ItemGetPayload<Args extends $SelectIncludeOmit<$Schema, "Item", true>, Options extends $QueryOptions<$Schema> = $QueryOptions<$Schema>> = $Result<$Schema, "Item", Args, Options>;
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
//////////////////////////////////////////////////////////////////////////////////////////////
2+
// DO NOT MODIFY THIS FILE //
3+
// This file is automatically generated by ZenStack CLI and should not be manually updated. //
4+
//////////////////////////////////////////////////////////////////////////////////////////////
5+
6+
/* eslint-disable */
7+
8+
import { type SchemaType as $Schema } from "./schema";
9+
import type { ModelResult as $ModelResult, TypeDefResult as $TypeDefResult } from "@zenstackhq/orm";
10+
export type Owner = $ModelResult<$Schema, "Owner">;
11+
export type Item = $ModelResult<$Schema, "Item">;
12+
export type Label = $TypeDefResult<$Schema, "Label">;
13+
export type Flag = $TypeDefResult<$Schema, "Flag">;
14+
export type Quantity = $TypeDefResult<$Schema, "Quantity">;
15+
export type Ratio = $TypeDefResult<$Schema, "Ratio">;
16+
export type ViewCount = $TypeDefResult<$Schema, "ViewCount">;
17+
export type Price = $TypeDefResult<$Schema, "Price">;
18+
export type Timestamp = $TypeDefResult<$Schema, "Timestamp">;
19+
export type Blob = $TypeDefResult<$Schema, "Blob">;
20+
export type Meta = $TypeDefResult<$Schema, "Meta">;

0 commit comments

Comments
 (0)