Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 1 addition & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
packages/language/src/generated/**
**/schema.ts
**/test/**/schema.ts
9 changes: 7 additions & 2 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
- [x] validate
- [ ] format
- [ ] db seed
- [ ] ZModel
- [ ] View support
- [ ] ORM
- [x] Create
- [x] Input validation
Expand Down Expand Up @@ -56,13 +58,14 @@
- [x] Aggregate
- [x] Group by
- [x] Raw queries
- [ ] Transactions
- [x] Transactions
- [x] Interactive transaction
- [x] Sequential transaction
- [ ] Extensions
- [x] Query builder API
- [x] Computed fields
- [x] Prisma client extension
- [ ] Custom procedures
- [ ] Misc
- [x] JSDoc for CRUD methods
- [x] Cache validation schemas
Expand All @@ -71,14 +74,16 @@
- [x] Many-to-many relation
- [ ] Empty AND/OR/NOT behavior
- [?] Logging
- [ ] Error system
- [x] Error system
- [x] Custom table name
- [x] Custom field name
- [ ] Strict undefined checks
- [ ] DbNull vs JsonNull
- [ ] Benchmark
- [ ] Plugin
- [ ] Post-mutation hooks should be called after transaction is committed
- [x] TypeDef and mixin
- [ ] Strongly typed JSON
- [ ] Polymorphism
- [ ] Validation
- [ ] Access Policy
Expand Down
4 changes: 3 additions & 1 deletion packages/cli/src/actions/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ type Options = {
* CLI action for generating code from schema
*/
export async function run(options: Options) {
const start = Date.now();

const schemaFile = getSchemaFile(options.schema);

const model = await loadSchemaDocument(schemaFile);
Expand All @@ -40,7 +42,7 @@ export async function run(options: Options) {
}

if (!options.silent) {
console.log(colors.green('Generation completed successfully.'));
console.log(colors.green(`Generation completed successfully in ${Date.now() - start}ms.`));
console.log(`You can now create a ZenStack client with it.

\`\`\`ts
Expand Down
85 changes: 85 additions & 0 deletions packages/cli/test/ts-schema-gen.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,4 +181,89 @@ model Post {
},
});
});

it('merges fields and attributes from mixins', async () => {
const { schema } = await generateTsSchema(`
type Timestamped {
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}

type Named {
name String
@@unique([name])
}

model User with Timestamped Named {
id String @id @default(uuid())
email String @unique
}
`);
expect(schema).toMatchObject({
models: {
User: {
fields: {
id: { type: 'String' },
email: { type: 'String' },
createdAt: {
type: 'DateTime',
default: expect.objectContaining({ function: 'now', kind: 'call' }),
},
updatedAt: { type: 'DateTime', updatedAt: true },
name: { type: 'String' },
},
uniqueFields: expect.objectContaining({
name: { type: 'String' },
}),
},
},
});
});

it('generates type definitions', async () => {
const { schema } = await generateTsSchema(`
type Base {
name String
@@meta('foo', 'bar')
}

type Address with Base {
street String
city String
}
`);
expect(schema).toMatchObject({
typeDefs: {
Base: {
fields: {
name: { type: 'String' },
},
attributes: [
{
name: '@@meta',
args: [
{ name: 'name', value: { kind: 'literal', value: 'foo' } },
{ name: 'value', value: { kind: 'literal', value: 'bar' } },
],
},
],
},
Address: {
fields: {
street: { type: 'String' },
city: { type: 'String' },
},
attributes: [
{
name: '@@meta',
args: [
{ name: 'name', value: { kind: 'literal', value: 'foo' } },
{ name: 'value', value: { kind: 'literal', value: 'bar' } },
],
},
],
},
},
});
});
});
11 changes: 11 additions & 0 deletions packages/language/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,16 @@
"default": "./dist/ast.cjs"
}
},
"./utils": {
"import": {
"types": "./dist/utils.d.ts",
"default": "./dist/utils.js"
},
"require": {
"types": "./dist/utils.d.cts",
"default": "./dist/utils.cjs"
}
},
"./package.json": {
"import": "./package.json",
"require": "./package.json"
Expand All @@ -51,6 +61,7 @@
"@types/pluralize": "^0.0.33",
"@zenstackhq/eslint-config": "workspace:*",
"@zenstackhq/typescript-config": "workspace:*",
"@zenstackhq/common-helpers": "workspace:*",
"langium-cli": "catalog:"
},
"volta": {
Expand Down
62 changes: 36 additions & 26 deletions packages/language/res/stdlib.zmodel
Original file line number Diff line number Diff line change
Expand Up @@ -199,11 +199,6 @@ function currentOperation(casing: String?): String {
*/
attribute @@@targetField(_ targetField: AttributeTargetField[])

/**
* Marks an attribute to be applicable to type defs and fields.
*/
attribute @@@supportTypeDef()

/**
* Marks an attribute to be used for data validation.
*/
Expand Down Expand Up @@ -237,13 +232,13 @@ attribute @@@once()
* @param sort: Allows you to specify in what order the entries of the ID are stored in the database. The available options are Asc and Desc.
* @param clustered: Defines whether the ID is clustered or non-clustered. Defaults to true.
*/
attribute @id(map: String?, length: Int?, sort: SortOrder?, clustered: Boolean?) @@@prisma @@@supportTypeDef @@@once
attribute @id(map: String?, length: Int?, sort: SortOrder?, clustered: Boolean?) @@@prisma @@@once

/**
* Defines a default value for a field.
* @param value: An expression (e.g. 5, true, now(), auth()).
*/
attribute @default(_ value: ContextType, map: String?) @@@prisma @@@supportTypeDef
attribute @default(_ value: ContextType, map: String?) @@@prisma

/**
* Defines a unique constraint for this field.
Expand All @@ -264,7 +259,7 @@ attribute @unique(map: String?, length: Int?, sort: SortOrder?, clustered: Boole
* @param sort: Allows you to specify in what order the entries of the ID are stored in the database. The available options are Asc and Desc.
* @param clustered: Defines whether the ID is clustered or non-clustered. Defaults to true.
*/
attribute @@id(_ fields: FieldReference[], name: String?, map: String?, length: Int?, sort: SortOrder?, clustered: Boolean?) @@@prisma
attribute @@id(_ fields: FieldReference[], name: String?, map: String?, length: Int?, sort: SortOrder?, clustered: Boolean?) @@@prisma @@@once

/**
* Defines a compound unique constraint for the specified fields.
Expand Down Expand Up @@ -560,82 +555,82 @@ attribute @omit()
/**
* Validates length of a string field.
*/
attribute @length(_ min: Int?, _ max: Int?, _ message: String?) @@@targetField([StringField]) @@@validation @@@supportTypeDef
attribute @length(_ min: Int?, _ max: Int?, _ message: String?) @@@targetField([StringField]) @@@validation

/**
* Validates a string field value starts with the given text.
*/
attribute @startsWith(_ text: String, _ message: String?) @@@targetField([StringField]) @@@validation @@@supportTypeDef
attribute @startsWith(_ text: String, _ message: String?) @@@targetField([StringField]) @@@validation

/**
* Validates a string field value ends with the given text.
*/
attribute @endsWith(_ text: String, _ message: String?) @@@targetField([StringField]) @@@validation @@@supportTypeDef
attribute @endsWith(_ text: String, _ message: String?) @@@targetField([StringField]) @@@validation

/**
* Validates a string field value contains the given text.
*/
attribute @contains(_ text: String, _ message: String?) @@@targetField([StringField]) @@@validation @@@supportTypeDef
attribute @contains(_ text: String, _ message: String?) @@@targetField([StringField]) @@@validation

/**
* Validates a string field value matches a regex.
*/
attribute @regex(_ regex: String, _ message: String?) @@@targetField([StringField]) @@@validation @@@supportTypeDef
attribute @regex(_ regex: String, _ message: String?) @@@targetField([StringField]) @@@validation

/**
* Validates a string field value is a valid email address.
*/
attribute @email(_ message: String?) @@@targetField([StringField]) @@@validation @@@supportTypeDef
attribute @email(_ message: String?) @@@targetField([StringField]) @@@validation

/**
* Validates a string field value is a valid ISO datetime.
*/
attribute @datetime(_ message: String?) @@@targetField([StringField]) @@@validation @@@supportTypeDef
attribute @datetime(_ message: String?) @@@targetField([StringField]) @@@validation

/**
* Validates a string field value is a valid url.
*/
attribute @url(_ message: String?) @@@targetField([StringField]) @@@validation @@@supportTypeDef
attribute @url(_ message: String?) @@@targetField([StringField]) @@@validation

/**
* Trims whitespaces from the start and end of the string.
*/
attribute @trim() @@@targetField([StringField]) @@@validation @@@supportTypeDef
attribute @trim() @@@targetField([StringField]) @@@validation

/**
* Transform entire string toLowerCase.
*/
attribute @lower() @@@targetField([StringField]) @@@validation @@@supportTypeDef
attribute @lower() @@@targetField([StringField]) @@@validation

/**
* Transform entire string toUpperCase.
*/
attribute @upper() @@@targetField([StringField]) @@@validation @@@supportTypeDef
attribute @upper() @@@targetField([StringField]) @@@validation

/**
* Validates a number field is greater than the given value.
*/
attribute @gt(_ value: Int, _ message: String?) @@@targetField([IntField, FloatField, DecimalField]) @@@validation @@@supportTypeDef
attribute @gt(_ value: Int, _ message: String?) @@@targetField([IntField, FloatField, DecimalField]) @@@validation

/**
* Validates a number field is greater than or equal to the given value.
*/
attribute @gte(_ value: Int, _ message: String?) @@@targetField([IntField, FloatField, DecimalField]) @@@validation @@@supportTypeDef
attribute @gte(_ value: Int, _ message: String?) @@@targetField([IntField, FloatField, DecimalField]) @@@validation

/**
* Validates a number field is less than the given value.
*/
attribute @lt(_ value: Int, _ message: String?) @@@targetField([IntField, FloatField, DecimalField]) @@@validation @@@supportTypeDef
attribute @lt(_ value: Int, _ message: String?) @@@targetField([IntField, FloatField, DecimalField]) @@@validation

/**
* Validates a number field is less than or equal to the given value.
*/
attribute @lte(_ value: Int, _ message: String?) @@@targetField([IntField, FloatField, DecimalField]) @@@validation @@@supportTypeDef
attribute @lte(_ value: Int, _ message: String?) @@@targetField([IntField, FloatField, DecimalField]) @@@validation

/**
* Validates the entity with a complex condition.
*/
attribute @@validate(_ value: Boolean, _ message: String?, _ path: String[]?) @@@validation @@@supportTypeDef
attribute @@validate(_ value: Boolean, _ message: String?, _ path: String[]?) @@@validation

/**
* Validates length of a string field.
Expand Down Expand Up @@ -706,7 +701,7 @@ function raw(value: String): Any {
/**
* Marks a field to be strong-typed JSON.
*/
attribute @json() @@@targetField([TypeDefField])
attribute @json() @@@targetField([TypeDefField]) @@@deprecated('The "@json" attribute is not needed anymore. ZenStack will automatically use JSON to store typed fields.')

/**
* Marks a field to be computed.
Expand All @@ -723,4 +718,19 @@ function auth(): Any {
* Used to specify the model for resolving `auth()` function call in access policies. A Zmodel
* can have at most one model with this attribute. By default, the model named "User" is used.
*/
attribute @@auth() @@@supportTypeDef
attribute @@auth()

/**
* Attaches arbitrary metadata to a model or type def.
*/
attribute @@meta(_ name: String, _ value: Any)

/**
* Attaches arbitrary metadata to a field.
*/
attribute @meta(_ name: String, _ value: Any)

/**
* Marks an attribute as deprecated.
*/
attribute @@@deprecated(_ message: String)
9 changes: 2 additions & 7 deletions packages/language/src/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ declare module './ast' {
$resolvedParam?: AttributeParam;
}

interface DataModelField {
interface DataField {
$inheritedFrom?: DataModel;
}

Expand All @@ -55,15 +55,10 @@ declare module './ast' {
}

export interface DataModel {
/**
* Indicates whether the model is already merged with the base types
*/
$baseMerged?: boolean;

/**
* All fields including those marked with `@ignore`
*/
$allFields?: DataModelField[];
$allFields?: DataField[];
}
}

Expand Down
Loading
Loading