Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 117 additions & 2 deletions packages/cli/test/ts-schema-gen.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -445,7 +445,7 @@ model User {
});
});

it('supports lite schema generation', async () => {
it('strips non-lite attributes from lite schemas', async () => {
const { schemaLite } = await generateTsSchema(
`
model User {
Expand All @@ -455,6 +455,10 @@ model User {

@@map('users')
}

type Profile {
id String @id
}
`,
undefined,
undefined,
Expand All @@ -463,8 +467,119 @@ model User {
);

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();
});

it('does not strip lite 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 () => {
Expand Down
51 changes: 28 additions & 23 deletions packages/language/res/stdlib.zmodel
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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).
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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)

/**
* Specifies an attribute should not be stripped when generating lite schemas.
*/
attribute @@@lite()
7 changes: 7 additions & 0 deletions packages/language/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 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.
Expand Down
25 changes: 17 additions & 8 deletions packages/sdk/src/ts-schema-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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
Expand All @@ -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,
),
),
Expand Down Expand Up @@ -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)),
),
),
),
Expand Down
3 changes: 2 additions & 1 deletion packages/zod/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -50,6 +50,7 @@
"@zenstackhq/tsdown-config": "workspace:*",
"@zenstackhq/typescript-config": "workspace:*",
"@zenstackhq/vitest-config": "workspace:*",
"@types/node": "catalog:",
Comment thread
sanny-io marked this conversation as resolved.
"zod": "^4.1.0"
},
"peerDependencies": {
Expand Down
Loading
Loading