Skip to content

Commit 192f2b8

Browse files
committed
addressing PR comments
1 parent 0568d3a commit 192f2b8

6 files changed

Lines changed: 38 additions & 29 deletions

File tree

packages/orm/src/client/crud/validator/validator.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,12 @@ import {
2121
type UpsertArgs,
2222
} from '../../crud-types';
2323
import { createInvalidInputError } from '../../errors';
24-
import type { ClientOptions } from '../../options';
2524
import { ZodSchemaFactory } from '../../zod/factory';
2625

2726
type GetSchemaFunc<Schema extends SchemaDef> = (model: GetModels<Schema>) => ZodType;
2827

2928
export class InputValidator<Schema extends SchemaDef> {
30-
readonly zodFactory: ZodSchemaFactory<Schema, ClientOptions<Schema>>;
29+
readonly zodFactory: ZodSchemaFactory<Schema>;
3130

3231
constructor(private readonly client: ClientContract<Schema>) {
3332
this.zodFactory = new ZodSchemaFactory(client);

packages/orm/src/client/zod/factory.ts

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1473,6 +1473,7 @@ export class ZodSchemaFactory<
14731473
fieldSchema,
14741474
z
14751475
.object({
1476+
// TODO: use Decimal/BigInt for incremental updates
14761477
set: this.nullableIf(z.number().optional(), !!fieldDef.optional).optional(),
14771478
increment: z.number().optional(),
14781479
decrement: z.number().optional(),
@@ -1694,7 +1695,7 @@ export class ZodSchemaFactory<
16941695

16951696
// fields used in `having` must be either in the `by` list, or aggregations
16961697
schema = schema.refine((value: any) => {
1697-
const bys = typeof value.by === 'string' ? [value.by] : value.by;
1698+
const bys = enumerate(value.by);
16981699
if (value.having && typeof value.having === 'object') {
16991700
for (const [key, val] of Object.entries(value.having)) {
17001701
if (AggregateOperators.includes(key as any)) {
@@ -1721,17 +1722,18 @@ export class ZodSchemaFactory<
17211722

17221723
// fields used in `orderBy` must be either in the `by` list, or aggregations
17231724
schema = schema.refine((value: any) => {
1724-
const bys = typeof value.by === 'string' ? [value.by] : value.by;
1725-
if (
1726-
value.orderBy &&
1727-
Object.keys(value.orderBy)
1728-
.filter((f) => !AggregateOperators.includes(f as AggregateOperators))
1729-
.some((key) => !bys.includes(key))
1730-
) {
1731-
return false;
1732-
} else {
1733-
return true;
1725+
const bys = enumerate(value.by);
1726+
for (const orderBy of enumerate(value.orderBy)) {
1727+
if (
1728+
orderBy &&
1729+
Object.keys(orderBy)
1730+
.filter((f) => !AggregateOperators.includes(f as AggregateOperators))
1731+
.some((key) => !bys.includes(key))
1732+
) {
1733+
return false;
1734+
}
17341735
}
1736+
return true;
17351737
}, 'fields in "orderBy" must be in "by"');
17361738

17371739
return schema as ZodType<GroupByArgs<Schema, Model, Options, ExtQueryArgs>>;

packages/schema/src/accessor.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ const accessors: Accessors = {
173173
const modelDef = _requireModel(this.schema, model);
174174
const result: Array<{ name: string; def: FieldDef } | { name: string; defs: Record<string, FieldDef> }> = [];
175175
for (const [key, value] of Object.entries(modelDef.uniqueFields)) {
176-
if (typeof value !== 'object') {
176+
if (value === null || typeof value !== 'object') {
177177
throw new InvalidSchemaError(`Invalid unique field definition for "${model}.${key}"`);
178178
}
179179

packages/zod/src/factory.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,9 @@ class SchemaFactory<Schema extends SchemaDef> {
5050
for (const [fieldName, fieldDef] of Object.entries(modelDef.fields)) {
5151
if (fieldDef.relation) {
5252
const relatedModelName = fieldDef.type;
53-
const lazySchema: z.ZodType = z.lazy(() =>
54-
this.applyCardinality(this.makeModelSchema(relatedModelName as GetModels<Schema>), fieldDef),
55-
);
56-
fields[fieldName] = lazySchema.optional();
53+
const lazySchema: z.ZodType = z.lazy(() => this.makeModelSchema(relatedModelName as GetModels<Schema>));
54+
// relation fields are always optional
55+
fields[fieldName] = this.applyCardinality(lazySchema, fieldDef).optional();
5756
} else {
5857
fields[fieldName] = this.makeScalarFieldSchema(fieldDef);
5958
}
@@ -115,17 +114,18 @@ class SchemaFactory<Schema extends SchemaDef> {
115114
}
116115

117116
private applyCardinality(schema: z.ZodType, fieldDef: FieldDef): z.ZodType {
117+
let result = schema;
118118
if (fieldDef.array) {
119-
return schema.array();
119+
result = result.array();
120120
}
121121
if (fieldDef.optional) {
122-
return schema.nullable().optional();
122+
result = result.nullable().optional();
123123
}
124-
return schema;
124+
return result;
125125
}
126126

127127
makeTypeSchema<Type extends GetTypeDefs<Schema>>(
128-
type: GetTypeDefs<Schema>,
128+
type: Type,
129129
): z.ZodObject<GetTypeDefFieldsShape<Schema, Type>, z.core.$strict> {
130130
const typeDef = this.schema.requireTypeDef(type);
131131
const fields: Record<string, z.ZodType> = {};

packages/zod/test/factory.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ describe('SchemaFactory - makeModelSchema', () => {
6060
// boolean
6161
expectTypeOf<User['active']>().toEqualTypeOf<boolean>();
6262

63-
// optional DateTime — FieldTypeZodMap key is 'Date' not 'DateTime', falls to ZodUnknown
63+
// DateTime
6464
expectTypeOf<User['birthdate']>().toEqualTypeOf<Date | null | undefined>();
6565

6666
// optional Bytes

tests/e2e/orm/client-api/zod.test.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
import { createQuerySchemaFactory, definePlugin, type ClientContract, type ClientOptions } from '@zenstackhq/orm';
2-
import { createTestClient } from '@zenstackhq/testtools';
2+
import { createTestClient, getTestDbProvider } from '@zenstackhq/testtools';
33
import { afterEach, beforeEach, describe, expect, expectTypeOf, it } from 'vitest';
44
import z from 'zod';
55
import { schema } from '../schemas/basic';
66

77
describe('Zod schema factory test', () => {
8+
if (getTestDbProvider() !== 'sqlite') {
9+
return;
10+
}
11+
812
let client: ClientContract<typeof schema>;
913

1014
beforeEach(async () => {
@@ -1395,11 +1399,15 @@ describe('Zod schema factory test', () => {
13951399

13961400
describe('create factory functions tests', () => {
13971401
it('can be constructed directly from client', async () => {
1398-
const client = await createTestClient(schema);
1399-
const factory = createQuerySchemaFactory(client);
1400-
const s = factory.makeFindManySchema('User');
1401-
expect(s.safeParse({ where: { email: 'u@test.com' } }).success).toBe(true);
1402-
expect(s.safeParse({ where: { notAField: 'val' } }).success).toBe(false);
1402+
try {
1403+
const client = await createTestClient(schema);
1404+
const factory = createQuerySchemaFactory(client);
1405+
const s = factory.makeFindManySchema('User');
1406+
expect(s.safeParse({ where: { email: 'u@test.com' } }).success).toBe(true);
1407+
expect(s.safeParse({ where: { notAField: 'val' } }).success).toBe(false);
1408+
} finally {
1409+
await client.$disconnect();
1410+
}
14031411
});
14041412

14051413
it('can be constructed directly from schema and options and produces equivalent schemas', () => {

0 commit comments

Comments
 (0)