diff --git a/packages/zod/src/types.ts b/packages/zod/src/types.ts index b8d1b05b0..c4db2c025 100644 --- a/packages/zod/src/types.ts +++ b/packages/zod/src/types.ts @@ -14,6 +14,7 @@ import type { GetTypeDefs, ModelFieldIsOptional, SchemaDef, + TypeDefFieldIsArray, TypeDefFieldIsOptional, } from '@zenstackhq/schema'; import type Decimal from 'decimal.js'; @@ -24,7 +25,7 @@ export type GetModelFieldsShape as FieldIsRelation extends true ? never : Field]: ZodOptionalAndNullableIf< - MapModelFieldToZod, + ZodArrayIf, FieldIsArray>, ModelFieldIsOptional >; } & { @@ -58,7 +59,10 @@ export type GetModelCreateFieldsShape extends true ? never : Field]: ZodOptionalIf< - ZodOptionalAndNullableIf, ModelFieldIsOptional>, + ZodOptionalAndNullableIf< + ZodArrayIf, FieldIsArray>, + ModelFieldIsOptional + >, FieldHasDefault >; }; @@ -71,13 +75,16 @@ export type GetModelUpdateFieldsShape extends true ? never : Field]: z.ZodOptional< - ZodOptionalAndNullableIf, ModelFieldIsOptional> + ZodOptionalAndNullableIf< + ZodArrayIf, FieldIsArray>, + ModelFieldIsOptional + > >; }; export type GetTypeDefFieldsShape> = { [Field in GetTypeDefFields]: ZodOptionalAndNullableIf< - MapTypeDefFieldToZod, + ZodArrayIf, TypeDefFieldIsArray>, TypeDefFieldIsOptional >; }; diff --git a/packages/zod/test/factory.test.ts b/packages/zod/test/factory.test.ts index e8368a385..40160a040 100644 --- a/packages/zod/test/factory.test.ts +++ b/packages/zod/test/factory.test.ts @@ -31,6 +31,7 @@ const validPost = { title: 'My First Post', published: true, authorId: null, + tags: ['announcement', 'update'], }; describe('SchemaFactory - makeModelSchema', () => { @@ -103,6 +104,19 @@ describe('SchemaFactory - makeModelSchema', () => { // optional scalar (foreign key) expectTypeOf().toEqualTypeOf(); + // scalar array + expectTypeOf().toEqualTypeOf(); + + const createPostSchema = factory.makeModelCreateSchema('Post'); + type PostCreate = z.infer; + + expectTypeOf().toEqualTypeOf(); + + const updatePostSchema = factory.makeModelUpdateSchema('Post'); + type PostUpdate = z.infer; + + expectTypeOf().toEqualTypeOf(); + // optional relation field present in type expectTypeOf().toHaveProperty('author'); const _userSchema = factory.makeModelSchema('User'); @@ -362,7 +376,7 @@ describe('SchemaFactory - makeModelSchema', () => { const userSchema = factory.makeModelSchema('User'); const result = userSchema.safeParse({ ...validUser, - address: { street: '123 Main St', city: 'Springfield', zip: null }, + address: { residents: [], street: '123 Main St', city: 'Springfield', zip: null }, }); expect(result.success).toBe(true); }); @@ -371,7 +385,7 @@ describe('SchemaFactory - makeModelSchema', () => { const userSchema = factory.makeModelSchema('User'); const result = userSchema.safeParse({ ...validUser, - address: { street: '123 Main St', city: 'Springfield', zip: '12345' }, + address: { residents: [], street: '123 Main St', city: 'Springfield', zip: '12345' }, }); expect(result.success).toBe(true); }); @@ -380,7 +394,7 @@ describe('SchemaFactory - makeModelSchema', () => { const userSchema = factory.makeModelSchema('User'); const result = userSchema.safeParse({ ...validUser, - address: { street: '123 Main St', city: 'Springfield', zip: null, extra: 'field' }, + address: { residents: [], street: '123 Main St', city: 'Springfield', zip: null, extra: 'field' }, }); expect(result.success).toBe(false); }); @@ -389,7 +403,7 @@ describe('SchemaFactory - makeModelSchema', () => { const userSchema = factory.makeModelSchema('User'); const result = userSchema.safeParse({ ...validUser, - address: { street: '123 Main St' }, + address: { residents: [], street: '123 Main St' }, }); expect(result.success).toBe(false); }); @@ -440,18 +454,21 @@ describe('SchemaFactory - makeModelSchema', () => { describe('SchemaFactory - makeTypeSchema', () => { it('generates schema for Address typedef', () => { const addressSchema = factory.makeTypeSchema('Address'); - expect(addressSchema.safeParse({ street: '123 Main', city: 'Springfield', zip: null }).success).toBe(true); + expect( + addressSchema.safeParse({ residents: [], street: '123 Main', city: 'Springfield', zip: null }).success, + ).toBe(true); }); it('rejects Address with missing required field', () => { const addressSchema = factory.makeTypeSchema('Address'); - const result = addressSchema.safeParse({ street: '123 Main' }); + const result = addressSchema.safeParse({ residents: [], street: '123 Main' }); expect(result.success).toBe(false); }); it('rejects Address with extra fields (strict object)', () => { const addressSchema = factory.makeTypeSchema('Address'); const result = addressSchema.safeParse({ + residents: [], street: '123 Main', city: 'Springfield', zip: null, @@ -462,47 +479,71 @@ describe('SchemaFactory - makeTypeSchema', () => { it('accepts Address with optional zip as null', () => { const addressSchema = factory.makeTypeSchema('Address'); - expect(addressSchema.safeParse({ street: '123 Main', city: 'Springfield', zip: null }).success).toBe(true); + expect( + addressSchema.safeParse({ residents: [], street: '123 Main', city: 'Springfield', zip: null }).success, + ).toBe(true); }); it('accepts Address with optional zip as a string', () => { const addressSchema = factory.makeTypeSchema('Address'); - expect(addressSchema.safeParse({ street: '123 Main', city: 'Springfield', zip: '12345' }).success).toBe(true); + expect( + addressSchema.safeParse({ residents: [], street: '123 Main', city: 'Springfield', zip: '12345' }).success, + ).toBe(true); }); describe('extra validations', () => { it('passes when zip is null', () => { const addressSchema = factory.makeTypeSchema('Address'); - expect(addressSchema.safeParse({ street: '123 Main', city: 'Springfield', zip: null }).success).toBe(true); + expect( + addressSchema.safeParse({ residents: [], street: '123 Main', city: 'Springfield', zip: null }).success, + ).toBe(true); }); it('passes when zip is omitted', () => { const addressSchema = factory.makeTypeSchema('Address'); - expect(addressSchema.safeParse({ street: '123 Main', city: 'Springfield' }).success).toBe(true); + expect(addressSchema.safeParse({ residents: [], street: '123 Main', city: 'Springfield' }).success).toBe( + true, + ); }); it('passes when zip is exactly 5 characters', () => { const addressSchema = factory.makeTypeSchema('Address'); - expect(addressSchema.safeParse({ street: '123 Main', city: 'Springfield', zip: '90210' }).success).toBe( - true, - ); + expect( + addressSchema.safeParse({ residents: [], street: '123 Main', city: 'Springfield', zip: '90210' }) + .success, + ).toBe(true); }); it('fails when zip is fewer than 5 characters', () => { const addressSchema = factory.makeTypeSchema('Address'); - const result = addressSchema.safeParse({ street: '123 Main', city: 'Springfield', zip: '123' }); + const result = addressSchema.safeParse({ + residents: [], + street: '123 Main', + city: 'Springfield', + zip: '123', + }); expect(result.success).toBe(false); }); it('fails when zip is more than 5 characters', () => { const addressSchema = factory.makeTypeSchema('Address'); - const result = addressSchema.safeParse({ street: '123 Main', city: 'Springfield', zip: '123456' }); + const result = addressSchema.safeParse({ + residents: [], + street: '123 Main', + city: 'Springfield', + zip: '123456', + }); expect(result.success).toBe(false); }); it('error message matches the configured message', () => { const addressSchema = factory.makeTypeSchema('Address'); - const result = addressSchema.safeParse({ street: '123 Main', city: 'Springfield', zip: '123' }); + const result = addressSchema.safeParse({ + residents: [], + street: '123 Main', + city: 'Springfield', + zip: '123', + }); expect(result.success).toBe(false); if (!result.success) { expect(result.error.issues.map((i) => i.message)).toContain('Zip code must be exactly 5 characters'); @@ -511,7 +552,12 @@ describe('SchemaFactory - makeTypeSchema', () => { it('error path points to the zip field', () => { const addressSchema = factory.makeTypeSchema('Address'); - const result = addressSchema.safeParse({ street: '123 Main', city: 'Springfield', zip: '123' }); + const result = addressSchema.safeParse({ + residents: [], + street: '123 Main', + city: 'Springfield', + zip: '123', + }); expect(result.success).toBe(false); if (!result.success) { expect(result.error.issues.map((i) => i.path)).toContainEqual(['zip']); @@ -520,7 +566,7 @@ describe('SchemaFactory - makeTypeSchema', () => { it('fails when city is too short', () => { const addressSchema = factory.makeTypeSchema('Address'); - const result = addressSchema.safeParse({ street: '123 Main', city: '', zip: '12345' }); + const result = addressSchema.safeParse({ residents: [], street: '123 Main', city: '', zip: '12345' }); expect(result.success).toBe(false); }); @@ -541,12 +587,14 @@ describe('SchemaFactory - makeTypeSchema', () => { avatar: null, metadata: null, status: 'ACTIVE', - address: { street: '123 Main', city: 'Springfield', zip: '90210' }, + address: { residents: [], street: '123 Main', city: 'Springfield', zip: '90210' }, }; expect(userSchema.safeParse(validUser).success).toBe(true); expect( - userSchema.safeParse({ ...validUser, address: { street: '123 Main', city: 'Springfield', zip: '123' } }) - .success, + userSchema.safeParse({ + ...validUser, + address: { residents: ['Alice'], street: '123 Main', city: 'Springfield', zip: '123' }, + }).success, ).toBe(false); }); }); diff --git a/packages/zod/test/schema/schema.ts b/packages/zod/test/schema/schema.ts index e2cdd212d..8abbf29c0 100644 --- a/packages/zod/test/schema/schema.ts +++ b/packages/zod/test/schema/schema.ts @@ -8,7 +8,7 @@ import { type SchemaDef, ExpressionUtils } from "@zenstackhq/schema"; export class SchemaType implements SchemaDef { provider = { - type: "sqlite" + type: "postgresql" } as const; models = { User: { @@ -125,6 +125,11 @@ export class SchemaType implements SchemaDef { name: "published", type: "Boolean" }, + tags: { + name: "tags", + type: "String", + array: true + }, author: { name: "author", type: "User", @@ -302,6 +307,11 @@ export class SchemaType implements SchemaDef { Address: { name: "Address", fields: { + residents: { + name: "residents", + type: "String", + array: true + }, street: { name: "street", type: "String", diff --git a/packages/zod/test/schema/schema.zmodel b/packages/zod/test/schema/schema.zmodel index 07e26a1ec..9e0d5716a 100644 --- a/packages/zod/test/schema/schema.zmodel +++ b/packages/zod/test/schema/schema.zmodel @@ -1,5 +1,5 @@ datasource db { - provider = 'sqlite' + provider = 'postgresql' } enum Status { @@ -11,9 +11,10 @@ enum Status { } type Address { - street String @meta("description", "Street address line") - city String @length(2) - zip String? + residents String[] + street String @meta("description", "Street address line") + city String @length(2) + zip String? @@validate(zip == null || length(zip) == 5, "Zip code must be exactly 5 characters", ["zip"]) @@meta("description", "A mailing address") @@ -42,20 +43,21 @@ model User { } model Post { - id String @id @default(cuid()) + id String @id @default(cuid()) title String published Boolean - author User? @relation(fields: [authorId], references: [id]) + tags String[] + author User? @relation(fields: [authorId], references: [id]) authorId String? } // --- Computed fields --- model Product { - id String @id @default(cuid()) - name String - price Float - discount Float @default(0) - finalPrice Float @computed + id String @id @default(cuid()) + name String + price Float + discount Float @default(0) + finalPrice Float @computed } // --- Delegate models ---