From 12306400c2b0d42ad63c63b568c6f8f7d078393d Mon Sep 17 00:00:00 2001 From: sanny-io Date: Mon, 10 Aug 2026 03:25:11 +0000 Subject: [PATCH 1/8] fix: lite schema validation --- packages/language/res/stdlib.zmodel | 47 ++++++++++++++----------- packages/language/src/utils.ts | 7 ++++ packages/sdk/src/ts-schema-generator.ts | 25 ++++++++----- 3 files changed, 50 insertions(+), 29 deletions(-) diff --git a/packages/language/res/stdlib.zmodel b/packages/language/res/stdlib.zmodel index aa62891de..cd21c3ea1 100644 --- a/packages/language/res/stdlib.zmodel +++ b/packages/language/res/stdlib.zmodel @@ -520,97 +520,97 @@ attribute @@schema(_ map: String) @@@prisma /** * Validates length of a string field or list field. */ -attribute @length(_ min: Int?, _ max: Int?, _ message: String?) @@@targetField([StringField, ListField]) @@@validation +attribute @length(_ min: Int?, _ max: Int?, _ message: String?) @@@targetField([StringField, ListField]) @@@validation @@@lite /** * Validates a string field value starts with the given text. */ -attribute @startsWith(_ text: String, _ message: String?) @@@targetField([StringField]) @@@validation +attribute @startsWith(_ text: String, _ message: String?) @@@targetField([StringField]) @@@validation @@@lite /** * Validates a string field value ends with the given text. */ -attribute @endsWith(_ text: String, _ message: String?) @@@targetField([StringField]) @@@validation +attribute @endsWith(_ text: String, _ message: String?) @@@targetField([StringField]) @@@validation @@@lite /** * Validates a string field value contains the given text. */ -attribute @contains(_ text: String, _ message: String?) @@@targetField([StringField]) @@@validation +attribute @contains(_ text: String, _ message: String?) @@@targetField([StringField]) @@@validation @@@lite /** * Validates a string field value matches a regex. */ -attribute @regex(_ regex: String, _ message: String?) @@@targetField([StringField]) @@@validation +attribute @regex(_ regex: String, _ message: String?) @@@targetField([StringField]) @@@validation @@@lite /** * Validates a string field value is a valid email address. */ -attribute @email(_ message: String?) @@@targetField([StringField]) @@@validation +attribute @email(_ message: String?) @@@targetField([StringField]) @@@validation @@@lite /** * Validates a string field value is a valid ISO datetime. */ -attribute @datetime(_ message: String?) @@@targetField([StringField]) @@@validation +attribute @datetime(_ message: String?) @@@targetField([StringField]) @@@validation @@@lite /** * Validates a string field value is a valid ISO date. */ -attribute @date(_ message: String?) @@@targetField([StringField]) @@@validation +attribute @date(_ message: String?) @@@targetField([StringField]) @@@validation @@@lite /** * Validates a string field value is a valid ISO time. */ -attribute @time(_ precision: Int?, _ message: String?) @@@targetField([StringField]) @@@validation +attribute @time(_ precision: Int?, _ message: String?) @@@targetField([StringField]) @@@validation @@@lite /** * Validates a string field value is a valid url. */ -attribute @url(_ message: String?) @@@targetField([StringField]) @@@validation +attribute @url(_ message: String?) @@@targetField([StringField]) @@@validation @@@lite /** * Validates a string field value is a valid E.164 phone number. */ -attribute @phone(_ message: String?) @@@targetField([StringField]) @@@validation +attribute @phone(_ message: String?) @@@targetField([StringField]) @@@validation @@@lite /** * Trims whitespaces from the start and end of the string. */ -attribute @trim() @@@targetField([StringField]) @@@validation +attribute @trim() @@@targetField([StringField]) @@@validation @@@lite /** * Transform entire string toLowerCase. */ -attribute @lower() @@@targetField([StringField]) @@@validation +attribute @lower() @@@targetField([StringField]) @@@validation @@@lite /** * Transform entire string toUpperCase. */ -attribute @upper() @@@targetField([StringField]) @@@validation +attribute @upper() @@@targetField([StringField]) @@@validation @@@lite /** * Validates a number field is greater than the given value. */ -attribute @gt(_ value: Any, _ message: String?) @@@targetField([IntField, FloatField, DecimalField, BigIntField]) @@@validation +attribute @gt(_ value: Any, _ message: String?) @@@targetField([IntField, FloatField, DecimalField, BigIntField]) @@@validation @@@lite /** * Validates a number field is greater than or equal to the given value. */ -attribute @gte(_ value: Any, _ message: String?) @@@targetField([IntField, FloatField, DecimalField, BigIntField]) @@@validation +attribute @gte(_ value: Any, _ message: String?) @@@targetField([IntField, FloatField, DecimalField, BigIntField]) @@@validation @@@lite /** * Validates a number field is less than the given value. */ -attribute @lt(_ value: Any, _ message: String?) @@@targetField([IntField, FloatField, DecimalField, BigIntField]) @@@validation +attribute @lt(_ value: Any, _ message: String?) @@@targetField([IntField, FloatField, DecimalField, BigIntField]) @@@validation @@@lite /** * Validates a number field is less than or equal to the given value. */ -attribute @lte(_ value: Any, _ message: String?) @@@targetField([IntField, FloatField, DecimalField, BigIntField]) @@@validation +attribute @lte(_ value: Any, _ message: String?) @@@targetField([IntField, FloatField, DecimalField, BigIntField]) @@@validation @@@lite /** * Validates the entity with a complex condition. */ -attribute @@validate(_ value: Boolean, _ message: String?, _ path: String[]?) @@@validation +attribute @@validate(_ value: Boolean, _ message: String?, _ path: String[]?) @@@validation @@@lite /** * Returns the length of a string field or a list field. @@ -718,14 +718,19 @@ attribute @@auth() /** * Attaches arbitrary metadata to a model or type def. */ -attribute @@meta(_ name: String, _ value: Any) +attribute @@meta(_ name: String, _ value: Any) @@@lite /** * Attaches arbitrary metadata to a field. */ -attribute @meta(_ name: String, _ value: Any) +attribute @meta(_ name: String, _ value: Any) @@@lite /** * Marks an attribute as deprecated. */ attribute @@@deprecated(_ message: String) + +/** + * Marks an attribute as being compatible with lite schemas. + */ +attribute @@@lite() diff --git a/packages/language/src/utils.ts b/packages/language/src/utils.ts index 4fa380599..e56e02d8f 100644 --- a/packages/language/src/utils.ts +++ b/packages/language/src/utils.ts @@ -186,6 +186,13 @@ export function isNativeTypeMappingAttribute(node: AstNode): node is Attribute { return isPrismaAttribute(node) && node.name.startsWith('@db.'); } +/** + * Returns if the given node is a lite-compatible attribute. + */ +export function isLiteAttribute(node: AstNode): node is Attribute { + return isAttribute(node) && hasAttribute(node, '@@@lite'); +} + /** * Returns the datasource provider literal (e.g. `'postgresql'`) declared in the schema, or undefined * if no datasource is found or its provider is not a literal. diff --git a/packages/sdk/src/ts-schema-generator.ts b/packages/sdk/src/ts-schema-generator.ts index cfa261ad5..bb45b1441 100644 --- a/packages/sdk/src/ts-schema-generator.ts +++ b/packages/sdk/src/ts-schema-generator.ts @@ -38,7 +38,13 @@ import { UnaryExpr, type Model, } from '@zenstackhq/language/ast'; -import { getAllAttributes, getAllFields, getAttributeArg, isDataFieldReference } from '@zenstackhq/language/utils'; +import { + getAllAttributes, + getAllFields, + getAttributeArg, + isDataFieldReference, + isLiteAttribute, +} from '@zenstackhq/language/utils'; import fs from 'node:fs'; import path from 'node:path'; import { match } from 'ts-pattern'; @@ -374,7 +380,7 @@ export class TsSchemaGenerator { private createDataModelObject(dm: DataModel, lite: boolean) { const allFields = getAllFields(dm); const allAttributes = lite - ? [] // in lite mode, skip all model-level attributes + ? getAllAttributes(dm).filter((attr) => isLiteAttribute(attr.decl.ref!)) : getAllAttributes(dm).filter((attr) => { // exclude `@@delegate` attribute from base model if (attr.decl.$refText === '@@delegate' && attr.$container !== dm) { @@ -502,7 +508,9 @@ export class TsSchemaGenerator { private createTypeDefObject(td: TypeDef, lite: boolean): ts.Expression { const allFields = getAllFields(td); - const allAttributes = getAllAttributes(td); + const attributes = lite + ? getAllAttributes(td).filter((attr) => isLiteAttribute(attr.decl.ref!)) + : getAllAttributes(td); const fields: ts.PropertyAssignment[] = [ // name @@ -523,13 +531,13 @@ export class TsSchemaGenerator { ), // attributes - ...(allAttributes.length > 0 + ...(attributes.length > 0 ? [ ts.factory.createPropertyAssignment( 'attributes', this.createAttributesTypeAssertion( ts.factory.createArrayLiteralExpression( - allAttributes.map((attr) => this.createAttributeObject(attr)), + attributes.map((attr) => this.createAttributeObject(attr)), true, ), ), @@ -760,14 +768,15 @@ export class TsSchemaGenerator { objectFields.push(ts.factory.createPropertyAssignment('isDiscriminator', ts.factory.createTrue())); } - // attributes, only when not in lite mode - if (!lite && field.attributes.length > 0) { + const attributes = lite ? field.attributes.filter((attr) => isLiteAttribute(attr.decl.ref!)) : field.attributes; + + if (attributes.length > 0) { objectFields.push( ts.factory.createPropertyAssignment( 'attributes', this.createAttributesTypeAssertion( ts.factory.createArrayLiteralExpression( - field.attributes.map((attr) => this.createAttributeObject(attr)), + attributes.map((attr) => this.createAttributeObject(attr)), ), ), ), From 9244d937ecd39561576a22ed4b59233dffe64f8d Mon Sep 17 00:00:00 2001 From: sanny-io Date: Mon, 10 Aug 2026 03:25:29 +0000 Subject: [PATCH 2/8] chore: add schema gen tests --- packages/cli/test/ts-schema-gen.test.ts | 101 +++++++++++++++++++++++- 1 file changed, 100 insertions(+), 1 deletion(-) diff --git a/packages/cli/test/ts-schema-gen.test.ts b/packages/cli/test/ts-schema-gen.test.ts index 38a0e5cc6..7fcf88807 100644 --- a/packages/cli/test/ts-schema-gen.test.ts +++ b/packages/cli/test/ts-schema-gen.test.ts @@ -445,7 +445,7 @@ model User { }); }); - it('supports lite schema generation', async () => { + it('strips lite-incompatible attributes from lite schemas', async () => { const { schemaLite } = await generateTsSchema( ` model User { @@ -455,6 +455,10 @@ model User { @@map('users') } + +type Profile { + id String @id +} `, undefined, undefined, @@ -465,6 +469,101 @@ model User { expect(schemaLite!.models['User']!.attributes).toBeUndefined(); expect(schemaLite!.models['User']!.fields['id']!.attributes).toBeUndefined(); expect(schemaLite!.models['User']!.fields['email']!.attributes).toBeUndefined(); + expect(schemaLite!.typeDefs!['Profile']!.fields['id']!.attributes).toBeUndefined(); + }); + + it('does not strip lite-compatible attributes from lite schemas', async () => { + const { schemaLite } = await generateTsSchema( + ` +model User { + id String @id @default(uuid()) + name String + email String @unique @email @meta('description', 'HTML email address.') + + @@map('users') + @@meta('description', 'A registered user.') +} + +type Profile { + bio String + + @@meta('description', 'The profile of a user.') +} + `, + undefined, + undefined, + undefined, + true, + ); + + expect(schemaLite!.models['User']!.fields['email']?.attributes).toMatchObject([ + { + name: '@email', + }, + { + name: '@meta', + args: [ + { + name: 'name', + value: { + kind: 'literal', + value: 'description', + }, + }, + { + name: 'value', + value: { + kind: 'literal', + value: 'HTML email address.', + }, + }, + ], + }, + ]); + + expect(schemaLite!.models['User']!.attributes).toMatchObject([ + { + name: '@@meta', + args: [ + { + name: 'name', + value: { + kind: 'literal', + value: 'description', + }, + }, + { + name: 'value', + value: { + kind: 'literal', + value: 'A registered user.', + }, + }, + ], + }, + ]); + + expect(schemaLite!.typeDefs!['Profile']!.attributes).toMatchObject([ + { + name: '@@meta', + args: [ + { + name: 'name', + value: { + kind: 'literal', + value: 'description', + }, + }, + { + name: 'value', + value: { + kind: 'literal', + value: 'The profile of a user.', + }, + }, + ], + }, + ]); }); it('supports ignorable fields for @updatedAt', async () => { From 20711b2ffdc792d2d30ab056aa80472a79db9947 Mon Sep 17 00:00:00 2001 From: sanny-io Date: Mon, 10 Aug 2026 03:26:31 +0000 Subject: [PATCH 3/8] chore: add zod tests --- packages/zod/package.json | 3 +- packages/zod/test/factory.test.ts | 6 +- packages/zod/test/schema/schema-lite.ts | 354 ++++++++++++++++++++++++ packages/zod/tsconfig.json | 5 +- packages/zod/vitest.config.ts | 24 +- 5 files changed, 388 insertions(+), 4 deletions(-) create mode 100644 packages/zod/test/schema/schema-lite.ts diff --git a/packages/zod/package.json b/packages/zod/package.json index f7b90702e..0a6fe7efe 100644 --- a/packages/zod/package.json +++ b/packages/zod/package.json @@ -20,7 +20,7 @@ "lint": "eslint src --ext ts", "test": "vitest run", "pack": "pnpm pack", - "test:generate": "tsx ../../scripts/test-generate.ts ." + "test:generate": "tsx ../../scripts/test-generate.ts . --lite" }, "keywords": [ "zenstack", @@ -50,6 +50,7 @@ "@zenstackhq/tsdown-config": "workspace:*", "@zenstackhq/typescript-config": "workspace:*", "@zenstackhq/vitest-config": "workspace:*", + "@types/node": "catalog:", "zod": "^4.1.0" }, "peerDependencies": { diff --git a/packages/zod/test/factory.test.ts b/packages/zod/test/factory.test.ts index a0bc7592c..6c8ca8c43 100644 --- a/packages/zod/test/factory.test.ts +++ b/packages/zod/test/factory.test.ts @@ -2,10 +2,14 @@ import Decimal from 'decimal.js'; import { describe, expect, expectTypeOf, it } from 'vitest'; import { createSchemaFactory } from '../src/index'; import { schema } from './schema/schema'; +import { schema as schemaLite } from './schema/schema-lite'; import z from 'zod'; import type { JsonValue } from '../src/index'; -const factory = createSchemaFactory(schema); +const factory = + process.env['ZENSTACK_TEST_SCHEMA_TARGET'] === 'lite' + ? createSchemaFactory(schemaLite) + : createSchemaFactory(schema); // A fully valid User object (without relations) const validUser = { diff --git a/packages/zod/test/schema/schema-lite.ts b/packages/zod/test/schema/schema-lite.ts new file mode 100644 index 000000000..c1f44d019 --- /dev/null +++ b/packages/zod/test/schema/schema-lite.ts @@ -0,0 +1,354 @@ +////////////////////////////////////////////////////////////////////////////////////////////// +// DO NOT MODIFY THIS FILE // +// This file is automatically generated by ZenStack CLI and should not be manually updated. // +////////////////////////////////////////////////////////////////////////////////////////////// + +/* eslint-disable */ + +import { type SchemaDef, type AttributeApplication, type FieldDefault, ExpressionUtils } from "@zenstackhq/schema"; +export class SchemaType implements SchemaDef { + provider = { + type: "postgresql" + } as const; + models = { + User: { + name: "User", + fields: { + id: { + name: "id", + type: "String", + id: true, + default: ExpressionUtils.call("cuid") as FieldDefault + }, + email: { + name: "email", + type: "String", + attributes: [{ name: "@email" }, { name: "@meta", args: [{ name: "name", value: ExpressionUtils.literal("description") }, { name: "value", value: ExpressionUtils.literal("The user's email address") }] }] as readonly AttributeApplication[] + }, + phone: { + name: "phone", + type: "String", + attributes: [{ name: "@phone" }] as readonly AttributeApplication[] + }, + username: { + name: "username", + type: "String", + attributes: [{ name: "@length", args: [{ name: "min", value: ExpressionUtils.literal(3) }, { name: "max", value: ExpressionUtils.literal(50) }] }] as readonly AttributeApplication[] + }, + website: { + name: "website", + type: "String", + optional: true, + attributes: [{ name: "@url" }] as readonly AttributeApplication[] + }, + code: { + name: "code", + type: "String", + attributes: [{ name: "@startsWith", args: [{ name: "text", value: ExpressionUtils.literal("USR") }] }] as readonly AttributeApplication[] + }, + age: { + name: "age", + type: "Int", + attributes: [{ name: "@gt", args: [{ name: "value", value: ExpressionUtils.literal(0) }] }, { name: "@lte", args: [{ name: "value", value: ExpressionUtils.literal(150) }] }] as readonly AttributeApplication[] + }, + score: { + name: "score", + type: "Float", + attributes: [{ name: "@gte", args: [{ name: "value", value: ExpressionUtils.literal(0.0) }] }, { name: "@lt", args: [{ name: "value", value: ExpressionUtils.literal(100.0) }] }] as readonly AttributeApplication[] + }, + bigNum: { + name: "bigNum", + type: "BigInt", + attributes: [{ name: "@gte", args: [{ name: "value", value: ExpressionUtils.literal(0) }] }] as readonly AttributeApplication[] + }, + balance: { + name: "balance", + type: "Decimal", + attributes: [{ name: "@gt", args: [{ name: "value", value: ExpressionUtils.literal(0) }] }] as readonly AttributeApplication[] + }, + active: { + name: "active", + type: "Boolean" + }, + birthdate: { + name: "birthdate", + type: "String", + optional: true, + attributes: [{ name: "@date" }] as readonly AttributeApplication[] + }, + localTime: { + name: "localTime", + type: "String", + optional: true, + attributes: [{ name: "@time" }] as readonly AttributeApplication[] + }, + createdAt: { + name: "createdAt", + type: "DateTime", + optional: true + }, + avatar: { + name: "avatar", + type: "Bytes", + optional: true + }, + metadata: { + name: "metadata", + type: "Json", + optional: true + }, + status: { + name: "status", + type: "Status" + }, + address: { + name: "address", + type: "Address", + optional: true + }, + posts: { + name: "posts", + type: "Post", + array: true, + relation: { opposite: "author" } + } + }, + attributes: [ + { name: "@@validate", args: [{ name: "value", value: ExpressionUtils.binary(ExpressionUtils.field("age"), ">=", ExpressionUtils.literal(18)) }, { name: "message", value: ExpressionUtils.literal("Must be adult") }, { name: "path", value: ExpressionUtils.array("String", [ExpressionUtils.literal("age")]) }] }, + { name: "@@meta", args: [{ name: "name", value: ExpressionUtils.literal("description") }, { name: "value", value: ExpressionUtils.literal("A user of the system") }] } + ] as readonly AttributeApplication[], + idFields: ["id"], + uniqueFields: { + id: { type: "String" } + } + }, + Post: { + name: "Post", + fields: { + id: { + name: "id", + type: "String", + id: true, + default: ExpressionUtils.call("cuid") as FieldDefault + }, + title: { + name: "title", + type: "String" + }, + published: { + name: "published", + type: "Boolean" + }, + tags: { + name: "tags", + type: "String", + array: true + }, + author: { + name: "author", + type: "User", + optional: true, + relation: { opposite: "posts", fields: ["authorId"], references: ["id"] } + }, + authorId: { + name: "authorId", + type: "String", + optional: true, + foreignKeyFor: [ + "author" + ] as readonly string[] + } + }, + idFields: ["id"], + uniqueFields: { + id: { type: "String" } + } + }, + Product: { + name: "Product", + fields: { + id: { + name: "id", + type: "String", + id: true, + default: ExpressionUtils.call("cuid") as FieldDefault + }, + name: { + name: "name", + type: "String" + }, + price: { + name: "price", + type: "Float" + }, + discount: { + name: "discount", + type: "Float", + default: 0 as FieldDefault + }, + finalPrice: { + name: "finalPrice", + type: "Float", + computed: true + } + }, + idFields: ["id"], + uniqueFields: { + id: { type: "String" } + }, + computedFields: { + finalPrice(_context: { + modelAlias: string; + }): number { + throw new Error("This is a stub for computed field"); + } + } + }, + Asset: { + name: "Asset", + fields: { + id: { + name: "id", + type: "Int", + id: true, + default: ExpressionUtils.call("autoincrement") as FieldDefault + }, + createdAt: { + name: "createdAt", + type: "DateTime", + default: ExpressionUtils.call("now") as FieldDefault + }, + assetType: { + name: "assetType", + type: "String", + isDiscriminator: true + } + }, + idFields: ["id"], + uniqueFields: { + id: { type: "Int" } + }, + isDelegate: true, + subModels: ["Video", "Image"] + }, + Video: { + name: "Video", + baseModel: "Asset", + fields: { + id: { + name: "id", + type: "Int", + id: true, + default: ExpressionUtils.call("autoincrement") as FieldDefault + }, + createdAt: { + name: "createdAt", + type: "DateTime", + originModel: "Asset", + default: ExpressionUtils.call("now") as FieldDefault + }, + assetType: { + name: "assetType", + type: "String", + originModel: "Asset", + isDiscriminator: true + }, + duration: { + name: "duration", + type: "Int" + }, + url: { + name: "url", + type: "String" + } + }, + idFields: ["id"], + uniqueFields: { + id: { type: "Int" } + } + }, + Image: { + name: "Image", + baseModel: "Asset", + fields: { + id: { + name: "id", + type: "Int", + id: true, + default: ExpressionUtils.call("autoincrement") as FieldDefault + }, + createdAt: { + name: "createdAt", + type: "DateTime", + originModel: "Asset", + default: ExpressionUtils.call("now") as FieldDefault + }, + assetType: { + name: "assetType", + type: "String", + originModel: "Asset", + isDiscriminator: true + }, + format: { + name: "format", + type: "String" + }, + width: { + name: "width", + type: "Int" + } + }, + idFields: ["id"], + uniqueFields: { + id: { type: "Int" } + } + } + } as const; + typeDefs = { + Address: { + name: "Address", + fields: { + residents: { + name: "residents", + type: "String", + array: true + }, + street: { + name: "street", + type: "String", + attributes: [{ name: "@meta", args: [{ name: "name", value: ExpressionUtils.literal("description") }, { name: "value", value: ExpressionUtils.literal("Street address line") }] }] as readonly AttributeApplication[] + }, + city: { + name: "city", + type: "String", + attributes: [{ name: "@length", args: [{ name: "min", value: ExpressionUtils.literal(2) }] }] as readonly AttributeApplication[] + }, + zip: { + name: "zip", + type: "String", + optional: true + } + }, + attributes: [ + { name: "@@validate", args: [{ name: "value", value: ExpressionUtils.binary(ExpressionUtils.binary(ExpressionUtils.field("zip"), "==", ExpressionUtils._null()), "||", ExpressionUtils.binary(ExpressionUtils.call("length", [ExpressionUtils.field("zip")]), "==", ExpressionUtils.literal(5))) }, { name: "message", value: ExpressionUtils.literal("Zip code must be exactly 5 characters") }, { name: "path", value: ExpressionUtils.array("String", [ExpressionUtils.literal("zip")]) }] }, + { name: "@@meta", args: [{ name: "name", value: ExpressionUtils.literal("description") }, { name: "value", value: ExpressionUtils.literal("A mailing address") }] } + ] as readonly AttributeApplication[] + } + } as const; + enums = { + Status: { + name: "Status", + values: { + ACTIVE: "ACTIVE", + INACTIVE: "INACTIVE", + PENDING: "PENDING" + }, + attributes: [ + { name: "@@meta", args: [{ name: "name", value: ExpressionUtils.literal("description") }, { name: "value", value: ExpressionUtils.literal("User account status") }] } + ] as readonly AttributeApplication[] + } + } as const; + authType = "User" as const; + plugins = {}; +} +export const schema = new SchemaType(); diff --git a/packages/zod/tsconfig.json b/packages/zod/tsconfig.json index e7ce31be8..6aa4df997 100644 --- a/packages/zod/tsconfig.json +++ b/packages/zod/tsconfig.json @@ -1,4 +1,7 @@ { "extends": "@zenstackhq/typescript-config/base.json", - "include": ["src/**/*.ts", "test/**/*.ts"] + "include": ["src/**/*.ts", "test/**/*.ts"], + "compilerOptions": { + "types": ["node"] + } } diff --git a/packages/zod/vitest.config.ts b/packages/zod/vitest.config.ts index 96478c06d..904aca477 100644 --- a/packages/zod/vitest.config.ts +++ b/packages/zod/vitest.config.ts @@ -1,5 +1,25 @@ import base from '@zenstackhq/vitest-config/base'; -import { defineConfig, mergeConfig } from 'vitest/config'; +import { defineConfig, mergeConfig, TestProjectConfiguration } from 'vitest/config'; + +const fullSchemaConfig: TestProjectConfiguration = { + test: { + name: 'full', + include: ['test/**/*.test.ts'], + env: { + ZENSTACK_TEST_SCHEMA_TARGET: 'full', + }, + }, +}; + +const liteSchemaConfig: TestProjectConfiguration = { + test: { + name: 'lite', + include: ['test/**/*.test.ts'], + env: { + ZENSTACK_TEST_SCHEMA_TARGET: 'lite', + }, + }, +}; export default mergeConfig( base, @@ -9,6 +29,8 @@ export default mergeConfig( enabled: true, include: ['test/**/*.ts'], }, + + projects: [fullSchemaConfig, liteSchemaConfig], }, }), ); From 7d3213fa68f764e397a4c70efd322ac7cb1c3c33 Mon Sep 17 00:00:00 2001 From: sanny-io Date: Mon, 10 Aug 2026 03:38:18 +0000 Subject: [PATCH 4/8] chore: lock file --- pnpm-lock.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fa83e04c3..ad8a7ca3d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1028,6 +1028,9 @@ importers: specifier: 'catalog:' version: 10.6.0 devDependencies: + '@types/node': + specifier: 'catalog:' + version: 20.19.24 '@zenstackhq/eslint-config': specifier: workspace:* version: link:../config/eslint-config From 06f6ab8b7c1a238556774ab8289b9e35e636eafd Mon Sep 17 00:00:00 2001 From: sanny-io Date: Mon, 10 Aug 2026 04:54:17 +0000 Subject: [PATCH 5/8] format: inline project config --- packages/zod/vitest.config.ts | 44 +++++++++++++++++------------------ 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/packages/zod/vitest.config.ts b/packages/zod/vitest.config.ts index 904aca477..8b4bbd74f 100644 --- a/packages/zod/vitest.config.ts +++ b/packages/zod/vitest.config.ts @@ -1,25 +1,5 @@ import base from '@zenstackhq/vitest-config/base'; -import { defineConfig, mergeConfig, TestProjectConfiguration } from 'vitest/config'; - -const fullSchemaConfig: TestProjectConfiguration = { - test: { - name: 'full', - include: ['test/**/*.test.ts'], - env: { - ZENSTACK_TEST_SCHEMA_TARGET: 'full', - }, - }, -}; - -const liteSchemaConfig: TestProjectConfiguration = { - test: { - name: 'lite', - include: ['test/**/*.test.ts'], - env: { - ZENSTACK_TEST_SCHEMA_TARGET: 'lite', - }, - }, -}; +import { defineConfig, mergeConfig } from 'vitest/config'; export default mergeConfig( base, @@ -30,7 +10,27 @@ export default mergeConfig( include: ['test/**/*.ts'], }, - projects: [fullSchemaConfig, liteSchemaConfig], + projects: [ + { + test: { + name: 'full', + include: ['test/**/*.test.ts'], + env: { + ZENSTACK_TEST_SCHEMA_TARGET: 'full', + }, + }, + }, + + { + test: { + name: 'lite', + include: ['test/**/*.test.ts'], + env: { + ZENSTACK_TEST_SCHEMA_TARGET: 'lite', + }, + }, + }, + ], }, }), ); From fa3503f4bc19286b0664aee86d9d8e1463ca7841 Mon Sep 17 00:00:00 2001 From: sanny-io Date: Mon, 10 Aug 2026 20:42:40 +0000 Subject: [PATCH 6/8] rephrase comment docs and test names --- packages/cli/test/ts-schema-gen.test.ts | 4 ++-- packages/language/res/stdlib.zmodel | 2 +- packages/language/src/utils.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/cli/test/ts-schema-gen.test.ts b/packages/cli/test/ts-schema-gen.test.ts index 7fcf88807..03d233fcd 100644 --- a/packages/cli/test/ts-schema-gen.test.ts +++ b/packages/cli/test/ts-schema-gen.test.ts @@ -445,7 +445,7 @@ model User { }); }); - it('strips lite-incompatible attributes from lite schemas', async () => { + it('strips non-lite attributes from lite schemas', async () => { const { schemaLite } = await generateTsSchema( ` model User { @@ -472,7 +472,7 @@ type Profile { expect(schemaLite!.typeDefs!['Profile']!.fields['id']!.attributes).toBeUndefined(); }); - it('does not strip lite-compatible attributes from lite schemas', async () => { + it('does not strip lite attributes from lite schemas', async () => { const { schemaLite } = await generateTsSchema( ` model User { diff --git a/packages/language/res/stdlib.zmodel b/packages/language/res/stdlib.zmodel index cd21c3ea1..e46bb4e6a 100644 --- a/packages/language/res/stdlib.zmodel +++ b/packages/language/res/stdlib.zmodel @@ -731,6 +731,6 @@ attribute @meta(_ name: String, _ value: Any) @@@lite attribute @@@deprecated(_ message: String) /** - * Marks an attribute as being compatible with lite schemas. + * Specifies an attribute should not be stripped when generating lite schemas. */ attribute @@@lite() diff --git a/packages/language/src/utils.ts b/packages/language/src/utils.ts index e56e02d8f..f733cb0d4 100644 --- a/packages/language/src/utils.ts +++ b/packages/language/src/utils.ts @@ -187,7 +187,7 @@ export function isNativeTypeMappingAttribute(node: AstNode): node is Attribute { } /** - * Returns if the given node is a lite-compatible attribute. + * Returns if the given node is a lite attribute. */ export function isLiteAttribute(node: AstNode): node is Attribute { return isAttribute(node) && hasAttribute(node, '@@@lite'); From 0a915d75b76882435cdd5ecf222f7ec4481ec1af Mon Sep 17 00:00:00 2001 From: sanny-io Date: Mon, 10 Aug 2026 23:07:32 +0000 Subject: [PATCH 7/8] fix: mark `@default` and `@updatedAt` as lite for frontend --- 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 e46bb4e6a..d83159d58 100644 --- a/packages/language/res/stdlib.zmodel +++ b/packages/language/res/stdlib.zmodel @@ -224,7 +224,7 @@ attribute @id(map: String?, length: Int?, sort: SortOrder?, clustered: Boolean?) * Defines a default value for a field. * @param value: An expression (e.g. 5, true, now(), auth()). */ -attribute @default(_ value: ContextType, map: String?) @@@prisma @@@once +attribute @default(_ value: ContextType, map: String?) @@@prisma @@@once @@@lite /** * Defines a unique constraint for this field. @@ -427,7 +427,7 @@ attribute @fullText() @@@targetField([StringField]) @@@once * updates have been made to a record. An update that only contains ignored fields does not change the * timestamp. */ -attribute @updatedAt(ignore: FieldReference[]?) @@@targetField([DateTimeField]) @@@prisma +attribute @updatedAt(ignore: FieldReference[]?) @@@targetField([DateTimeField]) @@@prisma @@@lite /** * Add full text index (MySQL only). From 08c729830519c98534ecbb1a8a5988a22fe7371a Mon Sep 17 00:00:00 2001 From: sanny-io Date: Mon, 10 Aug 2026 23:31:42 +0000 Subject: [PATCH 8/8] chore: fix failing test --- packages/cli/test/ts-schema-gen.test.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/cli/test/ts-schema-gen.test.ts b/packages/cli/test/ts-schema-gen.test.ts index 03d233fcd..4d4c48d82 100644 --- a/packages/cli/test/ts-schema-gen.test.ts +++ b/packages/cli/test/ts-schema-gen.test.ts @@ -467,7 +467,23 @@ type Profile { ); expect(schemaLite!.models['User']!.attributes).toBeUndefined(); - expect(schemaLite!.models['User']!.fields['id']!.attributes).toBeUndefined(); + + expect(schemaLite!.models['User']!.fields['id']!.attributes).toMatchObject([ + { + name: '@default', + args: [ + { + name: 'value', + value: { + kind: 'call', + function: 'uuid', + args: undefined, + }, + }, + ], + }, + ]); + expect(schemaLite!.models['User']!.fields['email']!.attributes).toBeUndefined(); expect(schemaLite!.typeDefs!['Profile']!.fields['id']!.attributes).toBeUndefined(); });