From 7d1429c9df6b19085e674666d184351befc19c85 Mon Sep 17 00:00:00 2001 From: Bo Lingen Date: Thu, 19 Mar 2026 13:14:49 +0000 Subject: [PATCH 1/7] fix(zod): properly infer scalar array types --- packages/zod/src/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/zod/src/types.ts b/packages/zod/src/types.ts index b8d1b05b0..270f7bccc 100644 --- a/packages/zod/src/types.ts +++ b/packages/zod/src/types.ts @@ -24,7 +24,7 @@ export type GetModelFieldsShape as FieldIsRelation extends true ? never : Field]: ZodOptionalAndNullableIf< - MapModelFieldToZod, + ZodArrayIf, FieldIsArray>, ModelFieldIsOptional >; } & { From da5135a080ba084d3db0d78ec65d4755ff1a8f2f Mon Sep 17 00:00:00 2001 From: Bo Lingen Date: Thu, 19 Mar 2026 13:31:15 +0000 Subject: [PATCH 2/7] change schema db provider to postgresql Required to support list types Regenerated schema with: `pnpm --package=@zenstackhq/cli dlx zen generate --schema ./test/schema/schema.zmodel --output ./test/schema --generate-models=false --generate-input=false` --- packages/zod/test/schema/schema.ts | 7 ++++++- packages/zod/test/schema/schema.zmodel | 17 +++++++++-------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/packages/zod/test/schema/schema.ts b/packages/zod/test/schema/schema.ts index e2cdd212d..c023eed9e 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", diff --git a/packages/zod/test/schema/schema.zmodel b/packages/zod/test/schema/schema.zmodel index 07e26a1ec..c1ab2ba44 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 { @@ -42,20 +42,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 --- From 5f4c0db3d8ce50ff700ac5c20931a108bf0daba0 Mon Sep 17 00:00:00 2001 From: Bo Lingen Date: Thu, 19 Mar 2026 13:31:32 +0000 Subject: [PATCH 3/7] add type test case for scalar array --- packages/zod/test/factory.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/zod/test/factory.test.ts b/packages/zod/test/factory.test.ts index e8368a385..2795b80e5 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,9 @@ describe('SchemaFactory - makeModelSchema', () => { // optional scalar (foreign key) expectTypeOf().toEqualTypeOf(); + // scalar array + expectTypeOf().toEqualTypeOf(); + // optional relation field present in type expectTypeOf().toHaveProperty('author'); const _userSchema = factory.makeModelSchema('User'); From 7256db1762b5624a9b1235ff2d30b44ed59f7a26 Mon Sep 17 00:00:00 2001 From: Bo Lingen Date: Thu, 19 Mar 2026 13:48:53 +0000 Subject: [PATCH 4/7] infer array types properly in create and update schemas --- packages/zod/src/types.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/zod/src/types.ts b/packages/zod/src/types.ts index 270f7bccc..dc6aa8578 100644 --- a/packages/zod/src/types.ts +++ b/packages/zod/src/types.ts @@ -58,7 +58,10 @@ export type GetModelCreateFieldsShape extends true ? never : Field]: ZodOptionalIf< - ZodOptionalAndNullableIf, ModelFieldIsOptional>, + ZodOptionalAndNullableIf< + ZodArrayIf, FieldIsArray>, + ModelFieldIsOptional + >, FieldHasDefault >; }; @@ -71,7 +74,10 @@ export type GetModelUpdateFieldsShape extends true ? never : Field]: z.ZodOptional< - ZodOptionalAndNullableIf, ModelFieldIsOptional> + ZodOptionalAndNullableIf< + ZodArrayIf, FieldIsArray>, + ModelFieldIsOptional + > >; }; From 03b35b9ce73fbee393f10ea1e980991503208db2 Mon Sep 17 00:00:00 2001 From: Bo Lingen Date: Thu, 19 Mar 2026 13:49:06 +0000 Subject: [PATCH 5/7] add test case for create and update schema types --- packages/zod/test/factory.test.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/zod/test/factory.test.ts b/packages/zod/test/factory.test.ts index 2795b80e5..036ce49a8 100644 --- a/packages/zod/test/factory.test.ts +++ b/packages/zod/test/factory.test.ts @@ -107,6 +107,16 @@ describe('SchemaFactory - makeModelSchema', () => { // 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'); From 81b6017f68ba9c4ea5be27fce9c822d03d247ee3 Mon Sep 17 00:00:00 2001 From: Bo Lingen Date: Fri, 20 Mar 2026 19:06:24 +0000 Subject: [PATCH 6/7] fix type def array inference --- packages/zod/src/types.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/zod/src/types.ts b/packages/zod/src/types.ts index dc6aa8578..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'; @@ -83,7 +84,7 @@ export type GetModelUpdateFieldsShape> = { [Field in GetTypeDefFields]: ZodOptionalAndNullableIf< - MapTypeDefFieldToZod, + ZodArrayIf, TypeDefFieldIsArray>, TypeDefFieldIsOptional >; }; From ceca1361ea22eeb4dd2224f046dcb2b16b1659bc Mon Sep 17 00:00:00 2001 From: Bo Lingen Date: Fri, 20 Mar 2026 19:06:40 +0000 Subject: [PATCH 7/7] update zod test schema and test cases for type defs --- packages/zod/test/factory.test.ts | 76 +++++++++++++++++++------- packages/zod/test/schema/schema.ts | 5 ++ packages/zod/test/schema/schema.zmodel | 7 ++- 3 files changed, 64 insertions(+), 24 deletions(-) diff --git a/packages/zod/test/factory.test.ts b/packages/zod/test/factory.test.ts index 036ce49a8..40160a040 100644 --- a/packages/zod/test/factory.test.ts +++ b/packages/zod/test/factory.test.ts @@ -376,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); }); @@ -385,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); }); @@ -394,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); }); @@ -403,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); }); @@ -454,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, @@ -476,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'); @@ -525,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']); @@ -534,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); }); @@ -555,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 c023eed9e..8abbf29c0 100644 --- a/packages/zod/test/schema/schema.ts +++ b/packages/zod/test/schema/schema.ts @@ -307,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 c1ab2ba44..9e0d5716a 100644 --- a/packages/zod/test/schema/schema.zmodel +++ b/packages/zod/test/schema/schema.zmodel @@ -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")