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
18 changes: 16 additions & 2 deletions packages/orm/src/client/crud/dialects/mysql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,11 +120,15 @@ export class MySqlCrudDialect<Schema extends SchemaDef> extends LateralJoinDiale
}
}

override transformOutput(value: unknown, type: BuiltinType, array: boolean) {
override transformOutput(value: unknown, type: BuiltinType, array: boolean): unknown {
if (value === null || value === undefined) {
return value;
}

if (isPrimitiveTypeDef(this.schema, type)) {
return this.transformOutput(value, this.schema.typeDefs![type]!.base!, array);
}

switch (type) {
case 'Boolean':
return this.transformOutputBoolean(value);
Expand Down Expand Up @@ -185,7 +189,17 @@ export class MySqlCrudDialect<Schema extends SchemaDef> extends LateralJoinDiale
}

private transformOutputBytes(value: unknown) {
return Buffer.isBuffer(value) ? Uint8Array.from(value) : value;
if (Buffer.isBuffer(value)) {
return Uint8Array.from(value);
}
// MySQL encodes binary values embedded in JSON as `base64:type<N>:<data>`
if (typeof value === 'string') {
const match = /^base64:type\d+:(.*)$/s.exec(value);
if (match) {
return Uint8Array.from(Buffer.from(match[1]!, 'base64'));
}
}
return value;
}

// #endregion
Expand Down
6 changes: 5 additions & 1 deletion packages/orm/src/client/crud/dialects/postgresql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,11 +245,15 @@ export class PostgresCrudDialect<Schema extends SchemaDef> extends LateralJoinDi
}
}

override transformOutput(value: unknown, type: BuiltinType, array: boolean) {
override transformOutput(value: unknown, type: BuiltinType, array: boolean): unknown {
if (value === null || value === undefined) {
return value;
}

if (isPrimitiveTypeDef(this.schema, type)) {
return this.transformOutput(value, this.schema.typeDefs![type]!.base!, array);
}

switch (type) {
case 'DateTime':
return this.transformOutputDate(value);
Expand Down
110 changes: 110 additions & 0 deletions tests/e2e/orm/client-api/custom-type-primitive-output.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import type { ClientContract } from '@zenstackhq/orm';
import { createTestClient } from '@zenstackhq/testtools';
import Decimal from 'decimal.js';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { schema } from '../schemas/custom-type-primitive-output/schema';

const seenAt = new Date('2026-01-02T03:04:05.000Z');

const itemData = {
label: 'hello',
active: true,
quantity: 3,
ratio: 1.5,
views: 12345678901234n,
price: new Decimal('19.99'),
seenAt,
payload: new Uint8Array([1, 2, 3]),
meta: { a: 1, tags: ['x'] },
};

// asserts that each primitive type def field is returned in its base type's runtime representation
function expectConverted(item: Record<string, unknown>, { withPayload = true } = {}) {
expect(item['label']).toBe('hello');
expect(item['active']).toBe(true);
expect(item['quantity']).toBe(3);
expect(item['ratio']).toBe(1.5);
expect(item['views']).toBe(12345678901234n);
expect(Decimal.isDecimal(item['price'])).toBe(true);
expect((item['price'] as Decimal).toString()).toBe('19.99');
expect(item['seenAt']).toBeInstanceOf(Date);
expect((item['seenAt'] as Date).toISOString()).toBe(seenAt.toISOString());
if (withPayload) {
expect(item['payload']).toBeInstanceOf(Uint8Array);
expect(Buffer.isBuffer(item['payload'])).toBe(false);
expect(Array.from(item['payload'] as Uint8Array)).toEqual([1, 2, 3]);
}
expect(item['meta']).toEqual({ a: 1, tags: ['x'] });
expect(item['note']).toBeNull();
}

describe('Custom type primitive output conversion tests', () => {
let client: ClientContract<typeof schema>;
let ownerId: number;

beforeEach(async () => {
client = await createTestClient(schema);
const owner = await client.owner.create({ data: {} });
ownerId = owner.id;
});

afterEach(async () => {
await client?.$disconnect();
});

it('converts fields returned from create', async () => {
const item = await client.item.create({ data: { ...itemData, ownerId } });
expectConverted(item);
});

it('converts fields on direct reads', async () => {
const { id } = await client.item.create({ data: { ...itemData, ownerId } });

expectConverted(await client.item.findUniqueOrThrow({ where: { id } }));

const [item] = await client.item.findMany();
expectConverted(item!);

const selected = await client.item.findFirstOrThrow({ select: { views: true, price: true, active: true } });
expect(selected.views).toBe(12345678901234n);
expect(Decimal.isDecimal(selected.price)).toBe(true);
expect(selected.active).toBe(true);
});

it('converts fields of nested relations', async () => {
await client.item.create({ data: { ...itemData, ownerId } });

// SQLite can't put BLOB values into the JSON it builds for nested relations (same for plain `Bytes`)
const withPayload = client.$schema.provider.type !== 'sqlite';
const omit = { payload: !withPayload };

const owner = await client.owner.findUniqueOrThrow({ where: { id: ownerId }, include: { items: { omit } } });
expectConverted(owner.items[0]!, { withPayload });

const selected = await client.owner.findUniqueOrThrow({
where: { id: ownerId },
select: {
items: { select: { views: true, price: true, seenAt: true, payload: withPayload, active: true } },
},
});
expect(selected.items[0]!.views).toBe(12345678901234n);
expect(Decimal.isDecimal(selected.items[0]!.price)).toBe(true);
expect(selected.items[0]!.seenAt).toBeInstanceOf(Date);
expect(selected.items[0]!.active).toBe(true);
if (withPayload) {
expect(Buffer.isBuffer(selected.items[0]!.payload)).toBe(false);
expect(Array.from(selected.items[0]!.payload as Uint8Array)).toEqual([1, 2, 3]);
}

const item = await client.item.findFirstOrThrow({ include: { owner: { include: { items: { omit } } } } });
expectConverted(item);
expectConverted(item.owner.items[0]!, { withPayload });
});

it('converts fields returned from update', async () => {
const { id } = await client.item.create({ data: { ...itemData, ownerId } });
const updated = await client.item.update({ where: { id }, data: { quantity: 4 } });
expectConverted({ ...updated, quantity: 3 });
expect(updated.quantity).toBe(4);
});
});
16 changes: 16 additions & 0 deletions tests/e2e/orm/client-api/custom-type-primitive.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,4 +169,20 @@ describe('Custom type primitive tests', () => {
}),
).rejects.toThrow(/Too big/);
});

it('converts array items to the base type', async () => {
const user = await client.user.create({
data: { name: 'test', scores: [1n, 12345678901234n] },
});
expect(user.scores).toEqual([1n, 12345678901234n]);

const found = await client.user.findUniqueOrThrow({ where: { id: user.id } });
expect(found.scores).toEqual([1n, 12345678901234n]);

const post = await client.post.create({
data: { tags: ['abc', '123'], author: { connect: { id: user.id } } },
include: { author: true },
});
expect(post.author.scores).toEqual([1n, 12345678901234n]);
});
});
60 changes: 60 additions & 0 deletions tests/e2e/orm/schemas/custom-type-primitive-output/input.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
//////////////////////////////////////////////////////////////////////////////////////////////
// DO NOT MODIFY THIS FILE //
// This file is automatically generated by ZenStack CLI and should not be manually updated. //
//////////////////////////////////////////////////////////////////////////////////////////////

/* eslint-disable */

import { type SchemaType as $Schema } from "./schema";
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";
import type { SimplifiedPlainResult as $Result, SelectIncludeOmit as $SelectIncludeOmit } from "@zenstackhq/orm";
export type OwnerFindManyArgs = $FindManyArgs<$Schema, "Owner">;
export type OwnerFindUniqueArgs = $FindUniqueArgs<$Schema, "Owner">;
export type OwnerFindFirstArgs = $FindFirstArgs<$Schema, "Owner">;
export type OwnerExistsArgs = $ExistsArgs<$Schema, "Owner">;
export type OwnerCreateArgs = $CreateArgs<$Schema, "Owner">;
export type OwnerCreateManyArgs = $CreateManyArgs<$Schema, "Owner">;
export type OwnerCreateManyAndReturnArgs = $CreateManyAndReturnArgs<$Schema, "Owner">;
export type OwnerUpdateArgs = $UpdateArgs<$Schema, "Owner">;
export type OwnerUpdateManyArgs = $UpdateManyArgs<$Schema, "Owner">;
export type OwnerUpdateManyAndReturnArgs = $UpdateManyAndReturnArgs<$Schema, "Owner">;
export type OwnerUpsertArgs = $UpsertArgs<$Schema, "Owner">;
export type OwnerDeleteArgs = $DeleteArgs<$Schema, "Owner">;
export type OwnerDeleteManyArgs = $DeleteManyArgs<$Schema, "Owner">;
export type OwnerCountArgs = $CountArgs<$Schema, "Owner">;
export type OwnerAggregateArgs = $AggregateArgs<$Schema, "Owner">;
export type OwnerGroupByArgs = $GroupByArgs<$Schema, "Owner">;
export type OwnerWhereInput = $WhereInput<$Schema, "Owner">;
export type OwnerSelect = $SelectInput<$Schema, "Owner">;
export type OwnerInclude = $IncludeInput<$Schema, "Owner">;
export type OwnerOmit = $OmitInput<$Schema, "Owner">;
export type OwnerUncheckedCreateInput = $UncheckedCreateInput<$Schema, "Owner">;
export type OwnerCheckedCreateInput = $CheckedCreateInput<$Schema, "Owner">;
export type OwnerUncheckedUpdateInput = $UncheckedUpdateInput<$Schema, "Owner">;
export type OwnerCheckedUpdateInput = $CheckedUpdateInput<$Schema, "Owner">;
export type OwnerGetPayload<Args extends $SelectIncludeOmit<$Schema, "Owner", true>, Options extends $QueryOptions<$Schema> = $QueryOptions<$Schema>> = $Result<$Schema, "Owner", Args, Options>;
export type ItemFindManyArgs = $FindManyArgs<$Schema, "Item">;
export type ItemFindUniqueArgs = $FindUniqueArgs<$Schema, "Item">;
export type ItemFindFirstArgs = $FindFirstArgs<$Schema, "Item">;
export type ItemExistsArgs = $ExistsArgs<$Schema, "Item">;
export type ItemCreateArgs = $CreateArgs<$Schema, "Item">;
export type ItemCreateManyArgs = $CreateManyArgs<$Schema, "Item">;
export type ItemCreateManyAndReturnArgs = $CreateManyAndReturnArgs<$Schema, "Item">;
export type ItemUpdateArgs = $UpdateArgs<$Schema, "Item">;
export type ItemUpdateManyArgs = $UpdateManyArgs<$Schema, "Item">;
export type ItemUpdateManyAndReturnArgs = $UpdateManyAndReturnArgs<$Schema, "Item">;
export type ItemUpsertArgs = $UpsertArgs<$Schema, "Item">;
export type ItemDeleteArgs = $DeleteArgs<$Schema, "Item">;
export type ItemDeleteManyArgs = $DeleteManyArgs<$Schema, "Item">;
export type ItemCountArgs = $CountArgs<$Schema, "Item">;
export type ItemAggregateArgs = $AggregateArgs<$Schema, "Item">;
export type ItemGroupByArgs = $GroupByArgs<$Schema, "Item">;
export type ItemWhereInput = $WhereInput<$Schema, "Item">;
export type ItemSelect = $SelectInput<$Schema, "Item">;
export type ItemInclude = $IncludeInput<$Schema, "Item">;
export type ItemOmit = $OmitInput<$Schema, "Item">;
export type ItemUncheckedCreateInput = $UncheckedCreateInput<$Schema, "Item">;
export type ItemCheckedCreateInput = $CheckedCreateInput<$Schema, "Item">;
export type ItemUncheckedUpdateInput = $UncheckedUpdateInput<$Schema, "Item">;
export type ItemCheckedUpdateInput = $CheckedUpdateInput<$Schema, "Item">;
export type ItemGetPayload<Args extends $SelectIncludeOmit<$Schema, "Item", true>, Options extends $QueryOptions<$Schema> = $QueryOptions<$Schema>> = $Result<$Schema, "Item", Args, Options>;
20 changes: 20 additions & 0 deletions tests/e2e/orm/schemas/custom-type-primitive-output/models.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//////////////////////////////////////////////////////////////////////////////////////////////
// DO NOT MODIFY THIS FILE //
// This file is automatically generated by ZenStack CLI and should not be manually updated. //
//////////////////////////////////////////////////////////////////////////////////////////////

/* eslint-disable */

import { type SchemaType as $Schema } from "./schema";
import type { ModelResult as $ModelResult, TypeDefResult as $TypeDefResult } from "@zenstackhq/orm";
export type Owner = $ModelResult<$Schema, "Owner">;
export type Item = $ModelResult<$Schema, "Item">;
export type Label = $TypeDefResult<$Schema, "Label">;
export type Flag = $TypeDefResult<$Schema, "Flag">;
export type Quantity = $TypeDefResult<$Schema, "Quantity">;
export type Ratio = $TypeDefResult<$Schema, "Ratio">;
export type ViewCount = $TypeDefResult<$Schema, "ViewCount">;
export type Price = $TypeDefResult<$Schema, "Price">;
export type Timestamp = $TypeDefResult<$Schema, "Timestamp">;
export type Blob = $TypeDefResult<$Schema, "Blob">;
export type Meta = $TypeDefResult<$Schema, "Meta">;
Loading
Loading