From f3d51547a77fee3fa5cc1434b9d2f9794c59e0a2 Mon Sep 17 00:00:00 2001 From: sanny-io Date: Sat, 24 Oct 2026 15:00:47 +0000 Subject: [PATCH 1/8] feat(language): make `@length` available for `Bytes` --- packages/language/res/stdlib.zmodel | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/language/res/stdlib.zmodel b/packages/language/res/stdlib.zmodel index 5ec7d7e02..55da72a5e 100644 --- a/packages/language/res/stdlib.zmodel +++ b/packages/language/res/stdlib.zmodel @@ -518,9 +518,9 @@ attribute @@schema(_ map: String) @@@prisma ////////////////////////////////////////////// /** - * Validates length of a string field or list field. + * Validates length of a string field, list field, or bytes field. */ -attribute @length(_ min: Int?, _ max: Int?, _ message: String?) @@@targetField([StringField, ListField]) @@@validation @@@lite +attribute @length(_ min: Int?, _ max: Int?, _ message: String?) @@@targetField([StringField, ListField, BytesField]) @@@validation @@@lite /** * Validates a string field value starts with the given text. From 2b0c21139e348aeaa56f7bbbf12d1415e2696c22 Mon Sep 17 00:00:00 2001 From: sanny-io Date: Sat, 24 Oct 2026 15:01:29 +0000 Subject: [PATCH 2/8] feat(zod): add `@length` validation --- packages/zod/src/factory.ts | 3 ++- packages/zod/src/utils.ts | 27 +++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/packages/zod/src/factory.ts b/packages/zod/src/factory.ts index 6d768e1ca..1ff090687 100644 --- a/packages/zod/src/factory.ts +++ b/packages/zod/src/factory.ts @@ -23,6 +23,7 @@ import type { } from './types'; import { addBigIntValidation, + addBytesValidation, addCustomValidation, addDecimalValidation, addListValidation, @@ -392,7 +393,7 @@ class SchemaFactory { schema = z.union([z.date(), z.iso.datetime()]); break; case 'Bytes': - schema = z.instanceof(Uint8Array); + schema = addBytesValidation(z.instanceof(Uint8Array), attributes); break; case 'Json': schema = this.makeJsonSchema(); diff --git a/packages/zod/src/utils.ts b/packages/zod/src/utils.ts index db773096e..43054b317 100644 --- a/packages/zod/src/utils.ts +++ b/packages/zod/src/utils.ts @@ -282,6 +282,33 @@ export function addDecimalValidation( return result; } +export function addBytesValidation( + schema: z.ZodType, + attributes: readonly AttributeApplication[] | undefined, +): z.ZodSchema { + if (!attributes || attributes.length === 0) { + return schema; + } + + let result = schema; + for (const attr of attributes) { + switch (attr.name) { + case '@length': { + const min = getArgValue(attr.args?.find((a) => a.name === 'min')?.value); + if (min !== undefined) { + result = result.refine((data) => data.length >= min); + } + const max = getArgValue(attr.args?.find((a) => a.name === 'max')?.value); + if (max !== undefined) { + result = result.refine((data) => data.length <= max); + } + break; + } + } + } + return result; +} + export function addListValidation( schema: z.ZodArray, attributes: readonly AttributeApplication[] | undefined, From 800bd6f8fbeb93fed22ef59a47add503dd32900d Mon Sep 17 00:00:00 2001 From: sanny-io Date: Sat, 24 Oct 2026 15:01:46 +0000 Subject: [PATCH 3/8] chore(zod): add tests --- packages/zod/test/factory.test.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/packages/zod/test/factory.test.ts b/packages/zod/test/factory.test.ts index 9f68154f4..32888f97a 100644 --- a/packages/zod/test/factory.test.ts +++ b/packages/zod/test/factory.test.ts @@ -436,6 +436,26 @@ describe.each([ }); }); + describe('bytes validation attributes', () => { + it('rejects avatar too short for @length(3, 5)', () => { + const userSchema = factory.makeModelSchema('User'); + const result = userSchema.safeParse({ ...validUser, avatar: new Uint8Array([1, 2]) }); + expect(result.success).toBe(false); + }); + + it('rejects avatar too long for @length(3, 5)', () => { + const userSchema = factory.makeModelSchema('User'); + const result = userSchema.safeParse({ ...validUser, avatar: new Uint8Array([1, 2, 3, 4, 5, 6]) }); + expect(result.success).toBe(false); + }); + + it('accepts avatar within @length bounds', () => { + const userSchema = factory.makeModelSchema('User'); + const result = userSchema.safeParse({ ...validUser, avatar: new Uint8Array([1, 2, 3, 4]) }); + expect(result.success).toBe(true); + }); + }); + describe('enum fields', () => { it('accepts valid enum values', () => { const userSchema = factory.makeModelSchema('User'); From d0c11a525e51997fa5353d27781bd8c62774f697 Mon Sep 17 00:00:00 2001 From: sanny-io Date: Sat, 24 Oct 2026 15:02:13 +0000 Subject: [PATCH 4/8] feat(orm): add `@length` validation --- packages/orm/src/client/zod/factory.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/orm/src/client/zod/factory.ts b/packages/orm/src/client/zod/factory.ts index 53014d4b1..736d1add5 100644 --- a/packages/orm/src/client/zod/factory.ts +++ b/packages/orm/src/client/zod/factory.ts @@ -408,7 +408,11 @@ export class ZodSchemaFactory< ]); }) .with('DateTime', () => this.makeDateTimeValueSchema()) - .with('Bytes', () => z.instanceof(Uint8Array)) + .with('Bytes', () => + this.extraValidationsEnabled + ? ZodUtils.addBytesValidation(z.instanceof(Uint8Array), attributes) + : z.instanceof(Uint8Array), + ) .with('Json', () => this.makeJsonValueSchema()) .otherwise(() => z.unknown()); } From da6d8ef63f4bb9dda8b1a5fbf2ebc792fe936579 Mon Sep 17 00:00:00 2001 From: sanny-io Date: Sat, 24 Oct 2026 15:02:28 +0000 Subject: [PATCH 5/8] chore(orm): add tests --- tests/e2e/orm/validation/toplevel.test.ts | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/e2e/orm/validation/toplevel.test.ts b/tests/e2e/orm/validation/toplevel.test.ts index 2cc3b3a8d..e6d64b0d9 100644 --- a/tests/e2e/orm/validation/toplevel.test.ts +++ b/tests/e2e/orm/validation/toplevel.test.ts @@ -229,6 +229,28 @@ describe('Toplevel field validation tests', () => { await expect(db.foo.create({ data: { int1: '3.3', int2: new Decimal(3.9) } })).toResolveTruthy(); }); + it('works with bytes fields', async () => { + const db = await createTestClient( + ` + model Foo { + id Int @id @default(autoincrement()) + bytes1 Bytes? @length(3, 5) + } + `, + ); + + // violates @length min + await expect(db.foo.create({ data: { bytes1: new Uint8Array([1, 2]) } })).toBeRejectedByValidation(); + + // violates @length max + await expect( + db.foo.create({ data: { bytes1: new Uint8Array([1, 2, 3, 4, 5, 6]) } }), + ).toBeRejectedByValidation(); + + // satisfies all + await expect(db.foo.create({ data: { bytes1: new Uint8Array([1, 2, 3, 4]) } })).toResolveTruthy(); + }); + it('works with list fields', async () => { const db = await createTestClient( ` From e94d410598b18dc12cad121f516b35df4288e4ed Mon Sep 17 00:00:00 2001 From: sanny-io Date: Sat, 24 Oct 2026 15:02:35 +0000 Subject: [PATCH 6/8] chore(zod): regenerate schema --- packages/zod/test/schema/schema-lite.ts | 3 ++- packages/zod/test/schema/schema.ts | 3 ++- packages/zod/test/schema/schema.zmodel | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/zod/test/schema/schema-lite.ts b/packages/zod/test/schema/schema-lite.ts index 71f5b069e..7477bc6b9 100644 --- a/packages/zod/test/schema/schema-lite.ts +++ b/packages/zod/test/schema/schema-lite.ts @@ -96,7 +96,8 @@ export class SchemaType implements SchemaDef { avatar: { name: "avatar", type: "Bytes", - optional: true + optional: true, + attributes: [{ name: "@length", args: [{ name: "min", value: ExpressionUtils.literal(3) }, { name: "max", value: ExpressionUtils.literal(5) }] }] as readonly AttributeApplication[] }, metadata: { name: "metadata", diff --git a/packages/zod/test/schema/schema.ts b/packages/zod/test/schema/schema.ts index c4735be9d..444fc8b3d 100644 --- a/packages/zod/test/schema/schema.ts +++ b/packages/zod/test/schema/schema.ts @@ -96,7 +96,8 @@ export class SchemaType implements SchemaDef { avatar: { name: "avatar", type: "Bytes", - optional: true + optional: true, + attributes: [{ name: "@length", args: [{ name: "min", value: ExpressionUtils.literal(3) }, { name: "max", value: ExpressionUtils.literal(5) }] }] as readonly AttributeApplication[] }, metadata: { name: "metadata", diff --git a/packages/zod/test/schema/schema.zmodel b/packages/zod/test/schema/schema.zmodel index 88d2280f2..3a68bb379 100644 --- a/packages/zod/test/schema/schema.zmodel +++ b/packages/zod/test/schema/schema.zmodel @@ -43,7 +43,7 @@ model User { localTime String? @time extId String? @uuid createdAt DateTime? - avatar Bytes? + avatar Bytes? @length(3, 5) metadata Json? status Status address Address? @json From 1106d08a0052dd9cba3a3063f4fbeb4f4d9996d8 Mon Sep 17 00:00:00 2001 From: sanny-io Date: Sat, 24 Oct 2026 15:15:20 +0000 Subject: [PATCH 7/8] chore(zod): add primitive test cases --- packages/zod/test/factory.test.ts | 58 +++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/packages/zod/test/factory.test.ts b/packages/zod/test/factory.test.ts index 32888f97a..148d935c2 100644 --- a/packages/zod/test/factory.test.ts +++ b/packages/zod/test/factory.test.ts @@ -1741,6 +1741,14 @@ describe.each([ expect(postTagSchema.safeParse('abcdefghijklm').success).toBe(false); }); + it('applies bytes `@length` on `this` field', () => { + const chunkSchema = factory.makeTypeSchema('Chunk'); + expect(chunkSchema.safeParse(new Uint8Array([])).success).toBe(false); + expect(chunkSchema.safeParse(new Uint8Array([1])).success).toBe(true); + expect(chunkSchema.safeParse(new Uint8Array([1, 2, 3, 4])).success).toBe(true); + expect(chunkSchema.safeParse(new Uint8Array([1, 2, 3, 4, 5])).success).toBe(false); + }); + it('supports `@@validate` attributes', () => { const contactSchema = factory.makeTypeSchema('Contact'); let result = contactSchema.safeParse('test@mail.com'); @@ -1827,6 +1835,56 @@ describe.each([ expect(result.success).toBe(true); }); + it('applies field attributes to the bytes list', () => { + const uploadSchema = factory.makeModelSchema('Upload'); + const chunk = new Uint8Array([1]); + let result = uploadSchema.safeParse({ id: 0, chunks: [chunk, chunk], blobs: [] }); + expect(result.success).toBe(true); + + result = uploadSchema.safeParse({ id: 0, chunks: [chunk, chunk, chunk], blobs: [] }); + expect(result.success).toBe(true); + + result = uploadSchema.safeParse({ id: 0, chunks: [chunk], blobs: [] }); + expect(result.success).toBe(false); + + result = uploadSchema.safeParse({ id: 0, chunks: [chunk, chunk, chunk, chunk], blobs: [] }); + expect(result.success).toBe(false); + }); + + it('applies both the list length and the element bytes length', () => { + const uploadSchema = factory.makeModelSchema('Upload'); + const chunk = new Uint8Array([1]); + let result = uploadSchema.safeParse({ + id: 0, + chunks: [chunk, new Uint8Array([1, 2, 3, 4])], + blobs: [], + }); + expect(result.success).toBe(true); + + result = uploadSchema.safeParse({ id: 0, chunks: [chunk, new Uint8Array([])], blobs: [] }); + expect(result.success).toBe(false); + + result = uploadSchema.safeParse({ + id: 0, + chunks: [chunk, new Uint8Array([1, 2, 3, 4, 5])], + blobs: [], + }); + expect(result.success).toBe(false); + }); + + it('does not apply bytes lengths to lists', () => { + const uploadSchema = factory.makeModelSchema('Upload'); + const chunks = [new Uint8Array([1]), new Uint8Array([1])]; + let result = uploadSchema.safeParse({ id: 0, chunks, blobs: [] }); + expect(result.success).toBe(true); + + result = uploadSchema.safeParse({ id: 0, chunks, blobs: [new Uint8Array([])] }); + expect(result.success).toBe(false); + + result = uploadSchema.safeParse({ id: 0, chunks, blobs: [new Uint8Array([1])] }); + expect(result.success).toBe(true); + }); + it('applies typedef attributes to each item', () => { const postSchema = factory.makeModelSchema('Post'); const result = postSchema.safeParse({ ...validPost, tags: ['A', 'B'] }); From e55697c84d6384714b99238c4fd7ad68d05ee63c Mon Sep 17 00:00:00 2001 From: sanny-io Date: Sat, 24 Oct 2026 15:15:30 +0000 Subject: [PATCH 8/8] chore(zod): regenerate schema --- packages/zod/test/schema/schema-lite.ts | 38 +++++++++++++++++++++++++ packages/zod/test/schema/schema.ts | 38 +++++++++++++++++++++++++ packages/zod/test/schema/schema.zmodel | 10 +++++++ 3 files changed, 86 insertions(+) diff --git a/packages/zod/test/schema/schema-lite.ts b/packages/zod/test/schema/schema-lite.ts index 7477bc6b9..b3dd02316 100644 --- a/packages/zod/test/schema/schema-lite.ts +++ b/packages/zod/test/schema/schema-lite.ts @@ -199,6 +199,33 @@ export class SchemaType implements SchemaDef { id: { type: "Int" } } }, + Upload: { + name: "Upload", + fields: { + id: { + name: "id", + type: "Int", + id: true, + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.call("autoincrement") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("autoincrement") as FieldDefault + }, + chunks: { + name: "chunks", + type: "Chunk", + array: true, + attributes: [{ name: "@length", args: [{ name: "min", value: ExpressionUtils.literal(2) }, { name: "max", value: ExpressionUtils.literal(3) }] }] as readonly AttributeApplication[] + }, + blobs: { + name: "blobs", + type: "Chunk", + array: true + } + }, + idFields: ["id"], + uniqueFields: { + id: { type: "Int" } + } + }, Product: { name: "Product", fields: { @@ -434,6 +461,17 @@ export class SchemaType implements SchemaDef { attributes: [{ name: "@lower" }, { name: "@length", args: [{ name: "min", value: ExpressionUtils.literal(1) }, { name: "max", value: ExpressionUtils.literal(12) }] }] as readonly AttributeApplication[] } } + }, + Chunk: { + name: "Chunk", + base: "Bytes", + fields: { + this: { + name: "this", + type: "Bytes", + attributes: [{ name: "@length", args: [{ name: "min", value: ExpressionUtils.literal(1) }, { name: "max", value: ExpressionUtils.literal(4) }] }] as readonly AttributeApplication[] + } + } } } as const; enums = { diff --git a/packages/zod/test/schema/schema.ts b/packages/zod/test/schema/schema.ts index 444fc8b3d..48f2144d0 100644 --- a/packages/zod/test/schema/schema.ts +++ b/packages/zod/test/schema/schema.ts @@ -201,6 +201,33 @@ export class SchemaType implements SchemaDef { id: { type: "Int" } } }, + Upload: { + name: "Upload", + fields: { + id: { + name: "id", + type: "Int", + id: true, + attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("autoincrement") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("autoincrement") as FieldDefault + }, + chunks: { + name: "chunks", + type: "Chunk", + array: true, + attributes: [{ name: "@length", args: [{ name: "min", value: ExpressionUtils.literal(2) }, { name: "max", value: ExpressionUtils.literal(3) }] }] as readonly AttributeApplication[] + }, + blobs: { + name: "blobs", + type: "Chunk", + array: true + } + }, + idFields: ["id"], + uniqueFields: { + id: { type: "Int" } + } + }, Product: { name: "Product", fields: { @@ -440,6 +467,17 @@ export class SchemaType implements SchemaDef { attributes: [{ name: "@lower" }, { name: "@length", args: [{ name: "min", value: ExpressionUtils.literal(1) }, { name: "max", value: ExpressionUtils.literal(12) }] }] as readonly AttributeApplication[] } } + }, + Chunk: { + name: "Chunk", + base: "Bytes", + fields: { + this: { + name: "this", + type: "Bytes", + attributes: [{ name: "@length", args: [{ name: "min", value: ExpressionUtils.literal(1) }, { name: "max", value: ExpressionUtils.literal(4) }] }] as readonly AttributeApplication[] + } + } } } as const; enums = { diff --git a/packages/zod/test/schema/schema.zmodel b/packages/zod/test/schema/schema.zmodel index 3a68bb379..5fd6a88a2 100644 --- a/packages/zod/test/schema/schema.zmodel +++ b/packages/zod/test/schema/schema.zmodel @@ -68,6 +68,12 @@ model Holder { labels Label[] } +model Upload { + id Int @id @default(autoincrement()) + chunks Chunk[] @length(2, 3) + blobs Chunk[] +} + // --- Computed fields --- model Product { id String @id @default(cuid()) @@ -121,3 +127,7 @@ type Contact with String { type PostTag with String { this String @lower @length(1, 12) } + +type Chunk with Bytes { + this Bytes @length(1, 4) +}