From 7cfe797b0e95f04e5f21488d5c563a5078d139c7 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Thu, 16 Nov 2023 19:15:22 -0800 Subject: [PATCH 1/5] chore: let plugin loader recognize absolute/relative path --- packages/schema/src/cli/plugin-runner.ts | 11 +++++++---- packages/sdk/src/utils.ts | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/schema/src/cli/plugin-runner.ts b/packages/schema/src/cli/plugin-runner.ts index 2b0b64845..f2a3f92a1 100644 --- a/packages/schema/src/cli/plugin-runner.ts +++ b/packages/schema/src/cli/plugin-runner.ts @@ -54,7 +54,7 @@ export class PluginRunner { const plugins: PluginInfo[] = []; const pluginDecls = options.schema.declarations.filter((d): d is Plugin => isPlugin(d)); - let prismaOutput = resolvePath('./prisma/schema.prisma', { schemaPath: options.schemaPath, name: '' }); + let prismaOutput = resolvePath('./prisma/schema.prisma', { schemaPath: options.schemaPath }); for (const pluginDecl of pluginDecls) { const pluginProvider = this.getPluginProvider(pluginDecl); @@ -62,7 +62,7 @@ export class PluginRunner { console.error(`Plugin ${pluginDecl.name} has invalid provider option`); throw new PluginError('', `Plugin ${pluginDecl.name} has invalid provider option`); } - const pluginModulePath = this.getPluginModulePath(pluginProvider); + const pluginModulePath = this.getPluginModulePath(pluginProvider, options); // eslint-disable-next-line @typescript-eslint/no-explicit-any let pluginModule: any; try { @@ -117,7 +117,7 @@ export class PluginRunner { plugins.unshift(existing); } else { // synthesize a plugin and insert front - const pluginModule = require(this.getPluginModulePath(corePlugin.provider)); + const pluginModule = require(this.getPluginModulePath(corePlugin.provider, options)); const pluginName = this.getPluginName(pluginModule, corePlugin.provider); plugins.unshift({ name: pluginName, @@ -300,7 +300,10 @@ export class PluginRunner { } } - private getPluginModulePath(provider: string) { + private getPluginModulePath(provider: string, options: Pick) { + if (path.isAbsolute(provider) || provider.startsWith('.')) { + return resolvePath(provider, options); + } let pluginModulePath = provider; if (pluginModulePath.startsWith('@core/')) { pluginModulePath = pluginModulePath.replace(/^@core/, path.join(__dirname, '../plugins')); diff --git a/packages/sdk/src/utils.ts b/packages/sdk/src/utils.ts index 6d726d091..e246461d0 100644 --- a/packages/sdk/src/utils.ts +++ b/packages/sdk/src/utils.ts @@ -267,7 +267,7 @@ export function isForeignKeyField(field: DataModelField) { }); } -export function resolvePath(_path: string, options: PluginOptions) { +export function resolvePath(_path: string, options: Pick) { if (path.isAbsolute(_path)) { return _path; } else { From 1918104f9b28ce75b82ea18b54bbd951b346de0a Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Thu, 16 Nov 2023 19:16:27 -0800 Subject: [PATCH 2/5] refactor: zmodel code generator to dispatcher pattern --- .../src/plugins/prisma/schema-generator.ts | 8 +- .../plugins/prisma/zmodel-code-generator.ts | 226 +++++++++++++----- 2 files changed, 174 insertions(+), 60 deletions(-) diff --git a/packages/schema/src/plugins/prisma/schema-generator.ts b/packages/schema/src/plugins/prisma/schema-generator.ts index 238c494e9..b9cdbc4a1 100644 --- a/packages/schema/src/plugins/prisma/schema-generator.ts +++ b/packages/schema/src/plugins/prisma/schema-generator.ts @@ -266,7 +266,7 @@ export default class PrismaSchemaGenerator { decl.attributes .filter((attr) => attr.decl.ref && !this.isPrismaAttribute(attr)) - .forEach((attr) => model.addComment('/// ' + this.zModelGenerator.generateAttribute(attr))); + .forEach((attr) => model.addComment('/// ' + this.zModelGenerator.generate(attr))); // user defined comments pass-through decl.comments.forEach((c) => model.addComment(c)); @@ -313,7 +313,7 @@ export default class PrismaSchemaGenerator { const nonPrismaAttributes = field.attributes.filter((attr) => attr.decl.ref && !this.isPrismaAttribute(attr)); - const documentations = nonPrismaAttributes.map((attr) => '/// ' + this.zModelGenerator.generateAttribute(attr)); + const documentations = nonPrismaAttributes.map((attr) => '/// ' + this.zModelGenerator.generate(attr)); const result = model.addField(field.name, type, attributes, documentations); @@ -413,7 +413,7 @@ export default class PrismaSchemaGenerator { decl.attributes .filter((attr) => attr.decl.ref && !this.isPrismaAttribute(attr)) - .forEach((attr) => _enum.addComment('/// ' + this.zModelGenerator.generateAttribute(attr))); + .forEach((attr) => _enum.addComment('/// ' + this.zModelGenerator.generate(attr))); // user defined comments pass-through decl.comments.forEach((c) => _enum.addComment(c)); @@ -426,7 +426,7 @@ export default class PrismaSchemaGenerator { const nonPrismaAttributes = field.attributes.filter((attr) => attr.decl.ref && !this.isPrismaAttribute(attr)); - const documentations = nonPrismaAttributes.map((attr) => '/// ' + this.zModelGenerator.generateAttribute(attr)); + const documentations = nonPrismaAttributes.map((attr) => '/// ' + this.zModelGenerator.generate(attr)); _enum.addField(field.name, attributes, documentations); } } diff --git a/packages/schema/src/plugins/prisma/zmodel-code-generator.ts b/packages/schema/src/plugins/prisma/zmodel-code-generator.ts index 5db0261e2..25eba6c05 100644 --- a/packages/schema/src/plugins/prisma/zmodel-code-generator.ts +++ b/packages/schema/src/plugins/prisma/zmodel-code-generator.ts @@ -1,20 +1,30 @@ import { Argument, ArrayExpr, + AstNode, AttributeArg, BinaryExpr, BinaryExprOperatorPriority, BooleanLiteral, + ConfigArrayExpr, + ConfigField, + ConfigInvocationExpr, + DataModel, DataModelAttribute, + DataModelField, DataModelFieldAttribute, - Expression, + DataSource, FieldInitializer, + GeneratorDecl, InvocationExpr, LiteralExpr, MemberAccessExpr, + Model, NullExpr, NumberLiteral, ObjectExpr, + Plugin, + PluginField, ReferenceArg, ReferenceExpr, StringLiteral, @@ -29,120 +39,224 @@ import { resolved } from '@zenstackhq/sdk'; export interface ZModelCodeOptions { binaryExprNumberOfSpaces: number; unaryExprNumberOfSpaces: number; + indent: number; +} + +// a registry of generation handlers marked with @gen +const generationHandlers = new Map(); + +// generation handler decorator +function gen(name: string) { + return function (target: unknown, propertyKey: string, descriptor: PropertyDescriptor) { + if (!generationHandlers.get(name)) { + generationHandlers.set(name, descriptor); + } + return descriptor; + }; } export default class ZModelCodeGenerator { private readonly options: ZModelCodeOptions; + constructor(options?: Partial) { this.options = { binaryExprNumberOfSpaces: options?.binaryExprNumberOfSpaces ?? 1, unaryExprNumberOfSpaces: options?.unaryExprNumberOfSpaces ?? 0, + indent: options?.indent ?? 4, }; } - generateAttribute(ast: DataModelAttribute | DataModelFieldAttribute): string { - const args = ast.args.length ? `(${ast.args.map((x) => this.generateAttributeArg(x)).join(', ')})` : ''; + + generate(ast: AstNode): string { + const handler = generationHandlers.get(ast.$type); + if (!handler) { + throw new Error(`No generation handler found for ${ast.$type}`); + } + return handler.value.call(this, ast); + } + + @gen(Model) + private _generateModel(ast: Model) { + return ast.declarations.map((d) => this.generate(d)).join('\n\n'); + } + + @gen(DataSource) + private _generateDataSource(ast: DataSource) { + return `datasource ${ast.name} { +${ast.fields.map((x) => this.indent + this.generate(x)).join('\n')} +}`; + } + + @gen(GeneratorDecl) + private _generateGenerator(ast: GeneratorDecl) { + return `generator ${ast.name} { +${ast.fields.map((x) => this.indent + this.generate(x)).join('\n')} +}`; + } + + @gen(ConfigField) + private _generateConfigField(ast: ConfigField) { + return `${ast.name} = ${this.generate(ast.value)}`; + } + + @gen(ConfigArrayExpr) + private _generateConfigArrayExpr(ast: ConfigArrayExpr) { + return `[${ast.items.map((x) => this.generate(x)).join(', ')}]`; + } + + @gen(ConfigInvocationExpr) + private _generateConfigInvocationExpr(ast: ConfigInvocationExpr) { + return `${ast.name}(${ast.args.map((x) => x.name + ': ' + this.generate(x.value)).join(', ')})`; + } + + @gen(Plugin) + private _generatePlugin(ast: Plugin) { + return `plugin ${ast.name} { +${ast.fields.map((x) => this.indent + this.generate(x)).join('\n')} +}`; + } + + @gen(PluginField) + private _generatePluginField(ast: PluginField) { + return `${ast.name} = ${this.generate(ast.value)}`; + } + + @gen(DataModel) + private _generateDataModel(ast: DataModel) { + return `${ast.isAbstract ? 'abstract ' : ''}${ast.isView ? 'view' : 'model'} ${ast.name}${ + ast.superTypes.length > 0 ? ' extends ' + ast.superTypes.map((x) => x.ref?.name).join(', ') : '' + }} { +${ast.fields.map((x) => this.indent + this.generate(x)).join('\n')} +${ast.attributes.map((x) => this.indent + this.generate(x)).join('\n')} +}`; + } + + @gen(DataModelField) + private _generateDataModelField(ast: DataModelField) { + return `${ast.name} ${ast.type.type ?? ast.type.reference?.$refText}${ast.type.array ? '[]' : ''}${ + ast.type.optional ? '?' : '' + } ${ast.attributes.length ? ' ' + ast.attributes.map((x) => this.generate(x)).join(' ') : ''}`; + } + + @gen(DataModelAttribute) + private _generateDataModelAttribute(ast: DataModelAttribute) { + return this.attribute(ast); + } + + @gen(DataModelFieldAttribute) + private _generateDataModelFieldAttribute(ast: DataModelFieldAttribute) { + return this.attribute(ast); + } + + private attribute(ast: DataModelAttribute | DataModelFieldAttribute) { + const args = ast.args.length ? `(${ast.args.map((x) => this.generate(x)).join(', ')})` : ''; return `${resolved(ast.decl).name}${args}`; } - generateAttributeArg(ast: AttributeArg) { + @gen(AttributeArg) + private _generateAttributeArg(ast: AttributeArg) { if (ast.name) { - return `${ast.name}: ${this.generateExpression(ast.value)}`; + return `${ast.name}: ${this.generate(ast.value)}`; } else { - return this.generateExpression(ast.value); + return this.generate(ast.value); } } - generateExpression(ast: Expression): string { - switch (ast.$type) { - case StringLiteral: - case NumberLiteral: - case BooleanLiteral: - return this.generateLiteralExpr(ast as LiteralExpr); - case UnaryExpr: - return this.generateUnaryExpr(ast as UnaryExpr); - case ArrayExpr: - return this.generateArrayExpr(ast as ArrayExpr); - case BinaryExpr: - return this.generateBinaryExpr(ast as BinaryExpr); - case ReferenceExpr: - return this.generateReferenceExpr(ast as ReferenceExpr); - case MemberAccessExpr: - return this.generateMemberExpr(ast as MemberAccessExpr); - case InvocationExpr: - return this.generateInvocationExpr(ast as InvocationExpr); - case ObjectExpr: - return this.generateObjectExpr(ast as ObjectExpr); - case NullExpr: - case ThisExpr: - return (ast as NullExpr | ThisExpr).value; - default: - throw new Error(`Not implemented: ${ast}`); - } + @gen(ObjectExpr) + private _generateObjectExpr(ast: ObjectExpr) { + return `{ ${ast.fields.map((field) => this.objectField(field)).join(', ')} }`; } - generateObjectExpr(ast: ObjectExpr) { - return `{ ${ast.fields.map((field) => this.generateObjectField(field)).join(', ')} }`; + private objectField(field: FieldInitializer) { + return `${field.name}: ${this.generate(field.value)}`; } - generateObjectField(field: FieldInitializer) { - return `${field.name}: ${this.generateExpression(field.value)}`; + @gen(ArrayExpr) + private _generateArrayExpr(ast: ArrayExpr) { + return `[${ast.items.map((item) => this.generate(item)).join(', ')}]`; } - generateArrayExpr(ast: ArrayExpr) { - return `[${ast.items.map((item) => this.generateExpression(item)).join(', ')}]`; + @gen(StringLiteral) + private _generateLiteralExpr(ast: LiteralExpr) { + return `'${ast.value}`; } - generateLiteralExpr(ast: LiteralExpr) { - return ast.$type === StringLiteral ? `'${ast.value}'` : ast.value.toString(); + @gen(NumberLiteral) + private _generateNumberLiteral(ast: NumberLiteral) { + return ast.value.toString(); } - generateUnaryExpr(ast: UnaryExpr) { - return `${ast.operator}${this.unaryExprSpace}${this.generateExpression(ast.operand)}`; + @gen(BooleanLiteral) + private _generateBooleanLiteral(ast: BooleanLiteral) { + return ast.value.toString(); } - generateBinaryExpr(ast: BinaryExpr) { + @gen(UnaryExpr) + private _generateUnaryExpr(ast: UnaryExpr) { + return `${ast.operator}${this.unaryExprSpace}${this.generate(ast.operand)}`; + } + + @gen(BinaryExpr) + private _generateBinaryExpr(ast: BinaryExpr) { const operator = ast.operator; const isCollectionPredicate = this.isCollectionPredicateOperator(operator); - const rightExpr = this.generateExpression(ast.right); + const rightExpr = this.generate(ast.right); const { left: isLeftParenthesis, right: isRightParenthesis } = this.isParenthesesNeededForBinaryExpr(ast); - return `${isLeftParenthesis ? '(' : ''}${this.generateExpression(ast.left)}${isLeftParenthesis ? ')' : ''}${ + return `${isLeftParenthesis ? '(' : ''}${this.generate(ast.left)}${isLeftParenthesis ? ')' : ''}${ this.binaryExprSpace }${operator}${this.binaryExprSpace}${isRightParenthesis ? '(' : ''}${ isCollectionPredicate ? `[${rightExpr}]` : rightExpr }${isRightParenthesis ? ')' : ''}`; } - generateReferenceExpr(ast: ReferenceExpr) { - const args = ast.args.length ? `(${ast.args.map((x) => this.generateReferenceArg(x)).join(', ')})` : ''; + @gen(ReferenceExpr) + private _generateReferenceExpr(ast: ReferenceExpr) { + const args = ast.args.length ? `(${ast.args.map((x) => this.generate(x)).join(', ')})` : ''; return `${ast.target.ref?.name}${args}`; } - generateReferenceArg(ast: ReferenceArg) { + @gen(ReferenceArg) + private _generateReferenceArg(ast: ReferenceArg) { return `${ast.name}:${ast.value}`; } - generateMemberExpr(ast: MemberAccessExpr) { - return `${this.generateExpression(ast.operand)}.${ast.member.ref?.name}`; + @gen(MemberAccessExpr) + private _generateMemberExpr(ast: MemberAccessExpr) { + return `${this.generate(ast.operand)}.${ast.member.ref?.name}`; + } + + @gen(InvocationExpr) + private _generateInvocationExpr(ast: InvocationExpr) { + return `${ast.function.ref?.name}(${ast.args.map((x) => this.argument(x)).join(', ')})`; + } + + @gen(NullExpr) + private _generateNullExpr() { + return 'null'; } - generateInvocationExpr(ast: InvocationExpr) { - return `${ast.function.ref?.name}(${ast.args.map((x) => this.generateArgument(x)).join(', ')})`; + @gen(ThisExpr) + private _generateThisExpr() { + return 'this'; } - generateArgument(ast: Argument) { - return `${ast.name && ':'} ${this.generateExpression(ast.value)}`; + argument(ast: Argument) { + return `${ast.name && ':'} ${this.generate(ast.value)}`; } - private get binaryExprSpace(): string { + private get binaryExprSpace() { return ' '.repeat(this.options.binaryExprNumberOfSpaces); } - private get unaryExprSpace(): string { + private get unaryExprSpace() { return ' '.repeat(this.options.unaryExprNumberOfSpaces); } + private get indent() { + return ' '.repeat(this.options.indent); + } + private isParenthesesNeededForBinaryExpr(ast: BinaryExpr): { left: boolean; right: boolean } { const result = { left: false, right: false }; const operator = ast.operator; From 2236c1e165f5a3f1f0b64f969441a97bdda61a30 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Thu, 16 Nov 2023 20:14:26 -0800 Subject: [PATCH 3/5] chore: refactor zmodel-code-generator and move it to sdk --- packages/language/src/generated/grammar.ts | 3 +- packages/language/src/zmodel.langium | 4 +- .../src/plugins/prisma/schema-generator.ts | 2 +- .../schema/tests/schema/all-features.zmodel | 177 +++++++++++ .../tests/schema/zmodel-generator.test.ts | 17 ++ packages/schema/tests/utils.ts | 6 +- packages/sdk/src/index.ts | 1 + .../src}/zmodel-code-generator.ts | 65 +++- pnpm-lock.yaml | 282 +++++++++--------- 9 files changed, 391 insertions(+), 166 deletions(-) create mode 100644 packages/schema/tests/schema/all-features.zmodel create mode 100644 packages/schema/tests/schema/zmodel-generator.test.ts rename packages/{schema/src/plugins/prisma => sdk/src}/zmodel-code-generator.ts (83%) diff --git a/packages/language/src/generated/grammar.ts b/packages/language/src/generated/grammar.ts index 8cad3d2cf..9ae762b6a 100644 --- a/packages/language/src/generated/grammar.ts +++ b/packages/language/src/generated/grammar.ts @@ -665,7 +665,8 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "rule": { "$ref": "#/rules@15" }, - "arguments": [] + "arguments": [], + "cardinality": "?" }, { "$type": "Keyword", diff --git a/packages/language/src/zmodel.langium b/packages/language/src/zmodel.langium index 47dfa1866..6760bb87d 100644 --- a/packages/language/src/zmodel.langium +++ b/packages/language/src/zmodel.langium @@ -51,7 +51,7 @@ ArrayExpr: '[' (items+=Expression (',' items+=Expression)*)? ']'; ConfigInvocationExpr: - name=ID ('(' ConfigInvocationArgList ')')?; + name=ID ('(' ConfigInvocationArgList? ')')?; fragment ConfigInvocationArgList: args+=ConfigInvocationArg (',' args+=ConfigInvocationArg)*; @@ -67,7 +67,6 @@ ConfigExpr: LiteralExpr | InvocationExpr | ConfigArrayExpr; type ReferenceTarget = FunctionParam | DataModelField | EnumField; - ThisExpr: value=THIS; @@ -259,7 +258,6 @@ AttributeParamType: (type=(ExpressionType | 'FieldReference' | 'TransitiveFieldReference' | 'ContextType') | reference=[TypeDeclaration:RegularID]) (array?='[' ']')? (optional?='?')?; type TypeDeclaration = DataModel | Enum; - DataModelFieldAttribute: decl=[Attribute:DataModelFieldAttributeName] ('(' AttributeArgList? ')')?; diff --git a/packages/schema/src/plugins/prisma/schema-generator.ts b/packages/schema/src/plugins/prisma/schema-generator.ts index b9cdbc4a1..fc7cd1ca4 100644 --- a/packages/schema/src/plugins/prisma/schema-generator.ts +++ b/packages/schema/src/plugins/prisma/schema-generator.ts @@ -62,7 +62,7 @@ import { PassThroughAttribute as PrismaPassThroughAttribute, SimpleField, } from './prisma-builder'; -import ZModelCodeGenerator from './zmodel-code-generator'; +import { ZModelCodeGenerator } from '@zenstackhq/sdk'; const MODEL_PASSTHROUGH_ATTR = '@@prisma.passthrough'; const FIELD_PASSTHROUGH_ATTR = '@prisma.passthrough'; diff --git a/packages/schema/tests/schema/all-features.zmodel b/packages/schema/tests/schema/all-features.zmodel new file mode 100644 index 000000000..c47a7cf79 --- /dev/null +++ b/packages/schema/tests/schema/all-features.zmodel @@ -0,0 +1,177 @@ +datasource db { + provider = 'postgresql' + url = env('DATABASE_URL') + extensions = [pg_trgm, postgis(version: "3.3.2"), uuid_ossp(map: "uuid-ossp", schema: "extensions")] +} + +generator client { + provider = "prisma-client-js" + previewFeatures = ["multiSchema", "postgresqlExtensions"] +} + +plugin openapi { + provider = '@zenstackhq/openapi' + output = 'openapi.json' + securitySchemes = { + basic: { type: 'http', scheme: 'basic' }, + bearer: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' }, + apiKey: { type: 'apiKey', in: 'header', name: 'X-API-KEY' } + } +} + +/* +* Sample model for a collaborative Todo app +*/ + +enum UserRole { + ADMIN + USER +} + +abstract model Base { + tag String +} + +/* + * Model for a space in which users can collaborate on Lists and Todos + */ +model Space extends Base { + id String @id @default(uuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + name String @length(4, 50) + slug String @unique @length(4, 16) + owner User? @relation(fields: [ownerId], references: [id]) + ownerId String? + members SpaceUser[] + lists List[] + unsupported Unsupported('foo') + + // require login + @@deny('all', auth() == null) + + // everyone can create a space + @@allow('create', true) + + // any user in the space can read the space + @@allow('read', members?[user == auth()]) + + // space admin can update and delete + @@allow('update,delete', members?[user == auth() && role == ADMIN]) +} + +/* + * Model representing membership of a user in a space + */ +model SpaceUser { + id String @id @default(uuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + space Space @relation(fields: [spaceId], references: [id], onDelete: Cascade) + spaceId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + userId String + role UserRole + @@unique([userId, spaceId]) + + // require login + @@deny('all', auth() == null) + + // space admin can create/update/delete + @@allow('create,update,delete', space.members?[user == auth() && role == ADMIN]) + + // user can read entries for spaces which he's a member of + @@allow('read', space.members?[user == auth()]) + + @@allow('read', role in [ADMIN, USER]) +} + +/* + * Model for a user + */ +model User { + id String @id @default(uuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + email String @unique @email + password String? @password @omit + emailVerified DateTime? + name String? + ownedSpaces Space[] + spaces SpaceUser[] + image String? @url + lists List[] + todos Todo[] + + // can be created by anyone, even not logged in + @@allow('create', true) + + // can be read by users sharing any space + @@allow('read', spaces?[space.members?[user == auth()]]) + + // full access by oneself + @@allow('all', auth() == this) +} + +/* + * Model for a Todo list + */ +model List { + id String @id @default(uuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + space Space @relation(fields: [spaceId], references: [id], onDelete: Cascade) + spaceId String + owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade) + ownerId String + title String @length(1, 100) + private Boolean @default(false) + todos Todo[] + + // require login + @@deny('all', auth() == null) + + // can be read by owner or space members (only if not private) + @@allow('read', owner == auth() || (space.members?[user == auth()] && !private)) + + // when create, owner must be set to current user, and user must be in the space + @@allow('create', owner == auth() && space.members?[user == auth()]) + + // when create, owner must be set to current user, and user must be in the space + // update is not allowed to change owner + @@allow('update', owner == auth()&& space.members?[user == auth()] && future().owner == owner) + + // can be deleted by owner + @@allow('delete', owner == auth()) +} + +/* + * Model for a single Todo + */ +model Todo { + id String @id @default(uuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade) + ownerId String + list List @relation(fields: [listId], references: [id], onDelete: Cascade) + listId String + title String @length(1, 100) + completedAt DateTime? + + // require login + @@deny('all', auth() == null) + + // owner has full access, also space members have full access (if the parent List is not private) + @@allow('all', list.owner == auth()) + @@allow('all', list.space.members?[user == auth()] && !list.private) + + // update is not allowed to change owner + @@deny('update', future().owner != owner) +} + +view SpaceWithMembers { + id String @unique + name String + slug String +} diff --git a/packages/schema/tests/schema/zmodel-generator.test.ts b/packages/schema/tests/schema/zmodel-generator.test.ts new file mode 100644 index 000000000..b477a6925 --- /dev/null +++ b/packages/schema/tests/schema/zmodel-generator.test.ts @@ -0,0 +1,17 @@ +/// + +import { ZModelCodeGenerator } from '@zenstackhq/sdk'; +import fs from 'fs'; +import path from 'path'; +import { loadModel } from '../utils'; + +describe('ZModel Generator Tests', () => { + it('run generator', async () => { + const content = fs.readFileSync(path.join(__dirname, './all-features.zmodel'), 'utf-8'); + const model = await loadModel(content, true, false, false); + const generator = new ZModelCodeGenerator(); + const generated = generator.generate(model); + // fs.writeFileSync(path.join(__dirname, './all-features-baseline.zmodel'), generated, 'utf-8'); + await loadModel(generated); + }); +}); diff --git a/packages/schema/tests/utils.ts b/packages/schema/tests/utils.ts index fade65628..f88aae6e2 100644 --- a/packages/schema/tests/utils.ts +++ b/packages/schema/tests/utils.ts @@ -13,7 +13,7 @@ export class SchemaLoadingError extends Error { } } -export async function loadModel(content: string, validate = true, verbose = true) { +export async function loadModel(content: string, validate = true, verbose = true, mergeBase = true) { const { name: docPath } = tmp.fileSync({ postfix: '.zmodel' }); fs.writeFileSync(docPath, content); const { shared } = createZModelServices(NodeFileSystem); @@ -51,7 +51,9 @@ export async function loadModel(content: string, validate = true, verbose = true const model = (await doc.parseResult.value) as Model; - mergeBaseModel(model); + if (mergeBase) { + mergeBaseModel(model); + } return model; } diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index 43accf73e..64060390e 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -6,3 +6,4 @@ export * from './prisma'; export * from './types'; export * from './utils'; export * from './validation'; +export * from './zmodel-code-generator'; diff --git a/packages/schema/src/plugins/prisma/zmodel-code-generator.ts b/packages/sdk/src/zmodel-code-generator.ts similarity index 83% rename from packages/schema/src/plugins/prisma/zmodel-code-generator.ts rename to packages/sdk/src/zmodel-code-generator.ts index 25eba6c05..dac743f79 100644 --- a/packages/schema/src/plugins/prisma/zmodel-code-generator.ts +++ b/packages/sdk/src/zmodel-code-generator.ts @@ -13,7 +13,10 @@ import { DataModelAttribute, DataModelField, DataModelFieldAttribute, + DataModelFieldType, DataSource, + Enum, + EnumField, FieldInitializer, GeneratorDecl, InvocationExpr, @@ -30,8 +33,8 @@ import { StringLiteral, ThisExpr, UnaryExpr, -} from '@zenstackhq/language/ast'; -import { resolved } from '@zenstackhq/sdk'; +} from './ast'; +import { resolved } from './utils'; /** * Options for the generator. @@ -55,7 +58,10 @@ function gen(name: string) { }; } -export default class ZModelCodeGenerator { +/** + * Generates ZModel source code from AST. + */ +export class ZModelCodeGenerator { private readonly options: ZModelCodeOptions; constructor(options?: Partial) { @@ -66,6 +72,9 @@ export default class ZModelCodeGenerator { }; } + /** + * Generates ZModel source code from AST. + */ generate(ast: AstNode): string { const handler = generationHandlers.get(ast.$type); if (!handler) { @@ -86,6 +95,20 @@ ${ast.fields.map((x) => this.indent + this.generate(x)).join('\n')} }`; } + @gen(Enum) + private _generateEnum(ast: Enum) { + return `enum ${ast.name} { +${ast.fields.map((x) => this.indent + this.generate(x)).join('\n')} +}`; + } + + @gen(EnumField) + private _generateEnumField(ast: EnumField) { + return `${ast.name}${ + ast.attributes.length > 0 ? ' ' + ast.attributes.map((x) => this.generate(x)).join(' ') : '' + }`; + } + @gen(GeneratorDecl) private _generateGenerator(ast: GeneratorDecl) { return `generator ${ast.name} { @@ -105,7 +128,13 @@ ${ast.fields.map((x) => this.indent + this.generate(x)).join('\n')} @gen(ConfigInvocationExpr) private _generateConfigInvocationExpr(ast: ConfigInvocationExpr) { - return `${ast.name}(${ast.args.map((x) => x.name + ': ' + this.generate(x.value)).join(', ')})`; + if (ast.args.length === 0) { + return ast.name; + } else { + return `${ast.name}(${ast.args + .map((x) => (x.name ? x.name + ': ' : '') + this.generate(x.value)) + .join(', ')})`; + } } @gen(Plugin) @@ -124,17 +153,29 @@ ${ast.fields.map((x) => this.indent + this.generate(x)).join('\n')} private _generateDataModel(ast: DataModel) { return `${ast.isAbstract ? 'abstract ' : ''}${ast.isView ? 'view' : 'model'} ${ast.name}${ ast.superTypes.length > 0 ? ' extends ' + ast.superTypes.map((x) => x.ref?.name).join(', ') : '' - }} { -${ast.fields.map((x) => this.indent + this.generate(x)).join('\n')} -${ast.attributes.map((x) => this.indent + this.generate(x)).join('\n')} + } { +${ast.fields.map((x) => this.indent + this.generate(x)).join('\n')}${ + ast.attributes.length > 0 + ? '\n\n' + ast.attributes.map((x) => this.indent + this.generate(x)).join('\n') + : '' + } }`; } @gen(DataModelField) private _generateDataModelField(ast: DataModelField) { - return `${ast.name} ${ast.type.type ?? ast.type.reference?.$refText}${ast.type.array ? '[]' : ''}${ - ast.type.optional ? '?' : '' - } ${ast.attributes.length ? ' ' + ast.attributes.map((x) => this.generate(x)).join(' ') : ''}`; + return `${ast.name} ${this.fieldType(ast.type)}${ + ast.attributes.length > 0 ? ' ' + ast.attributes.map((x) => this.generate(x)).join(' ') : '' + }`; + } + + private fieldType(type: DataModelFieldType) { + const baseType = type.type + ? type.type + : type.unsupported + ? 'Unsupported(' + this.generate(type.unsupported.value) + ')' + : type.reference?.$refText; + return `${baseType}${type.array ? '[]' : ''}${type.optional ? '?' : ''}`; } @gen(DataModelAttribute) @@ -177,7 +218,7 @@ ${ast.attributes.map((x) => this.indent + this.generate(x)).join('\n')} @gen(StringLiteral) private _generateLiteralExpr(ast: LiteralExpr) { - return `'${ast.value}`; + return `'${ast.value}'`; } @gen(NumberLiteral) @@ -242,7 +283,7 @@ ${ast.attributes.map((x) => this.indent + this.generate(x)).join('\n')} } argument(ast: Argument) { - return `${ast.name && ':'} ${this.generate(ast.value)}`; + return `${ast.name ? ast.name + ': ' : ''}${this.generate(ast.value)}`; } private get binaryExprSpace() { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9d0fddcc5..0482288f7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -124,7 +124,7 @@ importers: version: 0.2.1 ts-jest: specifier: ^29.0.5 - version: 29.0.5(@babel/core@7.22.5)(esbuild@0.18.13)(jest@29.5.0)(typescript@4.9.5) + version: 29.0.5(@babel/core@7.23.2)(esbuild@0.18.13)(jest@29.5.0)(typescript@4.9.5) typescript: specifier: ^4.9.5 version: 4.9.5 @@ -1084,16 +1084,6 @@ packages: jsesc: 2.5.2 dev: true - /@babel/generator@7.22.9: - resolution: {integrity: sha512-KtLMbmicyuK2Ak/FTCJVbDnkN1SlT8/kceFTiuDiiRUUSMnHMidxSCdG4ndkTOHHpoomWe/4xkvHkEOncwjYIw==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.23.0 - '@jridgewell/gen-mapping': 0.3.3 - '@jridgewell/trace-mapping': 0.3.18 - jsesc: 2.5.2 - dev: true - /@babel/generator@7.23.0: resolution: {integrity: sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==} engines: {node: '>=6.9.0'} @@ -1680,7 +1670,7 @@ packages: engines: {node: '>=6.9.0'} dependencies: '@babel/code-frame': 7.22.13 - '@babel/generator': 7.22.9 + '@babel/generator': 7.23.0 '@babel/helper-environment-visitor': 7.22.5 '@babel/helper-function-name': 7.22.5 '@babel/helper-hoist-variables': 7.22.5 @@ -2894,11 +2884,11 @@ packages: resolution: {integrity: sha512-NEpkObxPwyw/XxZVLPmAGKE89IQRp4puc6IQRPru6JKd1M3fW9v1xM1AnzIJE65hbCkzQAdnL8P47e9hzhiYLQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 29.5.0 + '@jest/types': 29.6.3 '@types/node': 18.0.0 chalk: 4.1.2 - jest-message-util: 29.5.0 - jest-util: 29.5.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 slash: 3.0.0 dev: true @@ -2927,7 +2917,7 @@ packages: '@jest/reporters': 29.5.0 '@jest/test-result': 29.5.0 '@jest/transform': 29.5.0 - '@jest/types': 29.5.0 + '@jest/types': 29.6.3 '@types/node': 18.0.0 ansi-escapes: 4.3.2 chalk: 4.1.2 @@ -2948,7 +2938,7 @@ packages: jest-validate: 29.5.0 jest-watcher: 29.5.0 micromatch: 4.0.5 - pretty-format: 29.5.0 + pretty-format: 29.7.0 slash: 3.0.0 strip-ansi: 6.0.1 transitivePeerDependencies: @@ -2956,7 +2946,7 @@ packages: - ts-node dev: true - /@jest/core@29.7.0: + /@jest/core@29.7.0(ts-node@10.9.1): resolution: {integrity: sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: @@ -2977,7 +2967,7 @@ packages: exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@18.0.0) + jest-config: 29.7.0(@types/node@18.0.0)(ts-node@10.9.1) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -3003,10 +2993,10 @@ packages: resolution: {integrity: sha512-5FXw2+wD29YU1d4I2htpRX7jYnAyTRjP2CsXQdo9SAM8g3ifxWPSV0HnClSn71xwctr0U3oZIIH+dtbfmnbXVQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/fake-timers': 29.5.0 - '@jest/types': 29.5.0 + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 '@types/node': 18.0.0 - jest-mock: 29.5.0 + jest-mock: 29.7.0 dev: true /@jest/environment@29.7.0: @@ -3023,7 +3013,7 @@ packages: resolution: {integrity: sha512-fmKzsidoXQT2KwnrwE0SQq3uj8Z763vzR8LnLBwC2qYWEFpjX8daRsk6rHUM1QvNlEW/UJXNXm59ztmJJWs2Mg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - jest-get-type: 29.4.3 + jest-get-type: 29.6.3 dev: true /@jest/expect-utils@29.7.0: @@ -3037,8 +3027,8 @@ packages: resolution: {integrity: sha512-PueDR2HGihN3ciUNGr4uelropW7rqUfTiOn+8u0leg/42UhblPxHkfoh0Ruu3I9Y1962P3u2DY4+h7GVTSVU6g==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - expect: 29.5.0 - jest-snapshot: 29.5.0 + expect: 29.7.0 + jest-snapshot: 29.7.0 transitivePeerDependencies: - supports-color dev: true @@ -3057,12 +3047,12 @@ packages: resolution: {integrity: sha512-9ARvuAAQcBwDAqOnglWq2zwNIRUDtk/SCkp/ToGEhFv5r86K21l+VEs0qNTaXtyiY0lEePl3kylijSYJQqdbDg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 29.5.0 + '@jest/types': 29.6.3 '@sinonjs/fake-timers': 10.3.0 '@types/node': 18.0.0 - jest-message-util: 29.5.0 - jest-mock: 29.5.0 - jest-util: 29.5.0 + jest-message-util: 29.7.0 + jest-mock: 29.7.0 + jest-util: 29.7.0 dev: true /@jest/fake-timers@29.7.0: @@ -3081,10 +3071,10 @@ packages: resolution: {integrity: sha512-S02y0qMWGihdzNbUiqSAiKSpSozSuHX5UYc7QbnHP+D9Lyw8DgGGCinrN9uSuHPeKgSSzvPom2q1nAtBvUsvPQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/environment': 29.5.0 + '@jest/environment': 29.7.0 '@jest/expect': 29.5.0 - '@jest/types': 29.5.0 - jest-mock: 29.5.0 + '@jest/types': 29.6.3 + jest-mock: 29.7.0 transitivePeerDependencies: - supports-color dev: true @@ -3111,10 +3101,10 @@ packages: optional: true dependencies: '@bcoe/v8-coverage': 0.2.3 - '@jest/console': 29.5.0 - '@jest/test-result': 29.5.0 - '@jest/transform': 29.5.0 - '@jest/types': 29.5.0 + '@jest/console': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 '@jridgewell/trace-mapping': 0.3.18 '@types/node': 18.0.0 chalk: 4.1.2 @@ -3127,8 +3117,8 @@ packages: istanbul-lib-report: 3.0.0 istanbul-lib-source-maps: 4.0.1 istanbul-reports: 3.1.5 - jest-message-util: 29.5.0 - jest-util: 29.5.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 jest-worker: 29.5.0 slash: 3.0.0 string-length: 4.0.2 @@ -3211,8 +3201,8 @@ packages: resolution: {integrity: sha512-fGl4rfitnbfLsrfx1uUpDEESS7zM8JdgZgOCQuxQvL1Sn/I6ijeAVQWGfXI9zb1i9Mzo495cIpVZhA0yr60PkQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/console': 29.5.0 - '@jest/types': 29.5.0 + '@jest/console': 29.7.0 + '@jest/types': 29.6.3 '@types/istanbul-lib-coverage': 2.0.4 collect-v8-coverage: 1.0.1 dev: true @@ -3231,9 +3221,9 @@ packages: resolution: {integrity: sha512-yPafQEcKjkSfDXyvtgiV4pevSeyuA6MQr6ZIdVkWJly9vkqjnFfcfhRQqpD5whjoU8EORki752xQmjaqoFjzMQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/test-result': 29.5.0 + '@jest/test-result': 29.7.0 graceful-fs: 4.2.11 - jest-haste-map: 29.5.0 + jest-haste-map: 29.7.0 slash: 3.0.0 dev: true @@ -3252,16 +3242,16 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@babel/core': 7.22.5 - '@jest/types': 29.5.0 + '@jest/types': 29.6.3 '@jridgewell/trace-mapping': 0.3.18 babel-plugin-istanbul: 6.1.1 chalk: 4.1.2 convert-source-map: 2.0.0 fast-json-stable-stringify: 2.1.0 graceful-fs: 4.2.11 - jest-haste-map: 29.5.0 - jest-regex-util: 29.4.3 - jest-util: 29.5.0 + jest-haste-map: 29.7.0 + jest-regex-util: 29.6.3 + jest-util: 29.7.0 micromatch: 4.0.5 pirates: 4.0.6 slash: 3.0.0 @@ -6260,7 +6250,7 @@ packages: '@babel/core': ^7.8.0 dependencies: '@babel/core': 7.22.5 - '@jest/transform': 29.5.0 + '@jest/transform': 29.7.0 '@types/babel__core': 7.20.1 babel-plugin-istanbul: 6.1.1 babel-preset-jest: 29.5.0(@babel/core@7.22.5) @@ -7188,7 +7178,7 @@ packages: chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@18.0.0) + jest-config: 29.7.0(@types/node@18.0.0)(ts-node@10.9.1) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -7634,11 +7624,6 @@ packages: wrappy: 1.0.2 dev: true - /diff-sequences@29.4.3: - resolution: {integrity: sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dev: true - /diff-sequences@29.6.3: resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -10004,7 +9989,7 @@ packages: resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==} engines: {node: '>=8'} dependencies: - '@babel/core': 7.22.5 + '@babel/core': 7.23.2 '@babel/parser': 7.23.0 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.0 @@ -10075,23 +10060,23 @@ packages: resolution: {integrity: sha512-gq/ongqeQKAplVxqJmbeUOJJKkW3dDNPY8PjhJ5G0lBRvu0e3EWGxGy5cI4LAGA7gV2UHCtWBI4EMXK8c9nQKA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/environment': 29.5.0 + '@jest/environment': 29.7.0 '@jest/expect': 29.5.0 - '@jest/test-result': 29.5.0 - '@jest/types': 29.5.0 + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 '@types/node': 18.0.0 chalk: 4.1.2 co: 4.6.0 dedent: 0.7.0 is-generator-fn: 2.1.0 jest-each: 29.5.0 - jest-matcher-utils: 29.5.0 - jest-message-util: 29.5.0 - jest-runtime: 29.5.0 - jest-snapshot: 29.5.0 - jest-util: 29.5.0 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 p-limit: 3.1.0 - pretty-format: 29.5.0 + pretty-format: 29.7.0 pure-rand: 6.0.2 slash: 3.0.0 stack-utils: 2.0.6 @@ -10138,9 +10123,9 @@ packages: node-notifier: optional: true dependencies: - '@jest/core': 29.5.0(ts-node@10.9.1) + '@jest/core': 29.7.0(ts-node@10.9.1) '@jest/test-result': 29.5.0 - '@jest/types': 29.5.0 + '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 @@ -10152,6 +10137,7 @@ packages: yargs: 17.7.2 transitivePeerDependencies: - '@types/node' + - babel-plugin-macros - supports-color - ts-node dev: true @@ -10166,14 +10152,14 @@ packages: node-notifier: optional: true dependencies: - '@jest/core': 29.7.0 + '@jest/core': 29.7.0(ts-node@10.9.1) '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 create-jest: 29.7.0(@types/node@18.0.0) exit: 0.1.2 import-local: 3.1.0 - jest-config: 29.7.0(@types/node@18.0.0) + jest-config: 29.7.0(@types/node@18.0.0)(ts-node@10.9.1) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -10198,7 +10184,7 @@ packages: dependencies: '@babel/core': 7.22.5 '@jest/test-sequencer': 29.5.0 - '@jest/types': 29.5.0 + '@jest/types': 29.6.3 '@types/node': 18.0.0 babel-jest: 29.5.0(@babel/core@7.22.5) chalk: 4.1.2 @@ -10208,15 +10194,15 @@ packages: graceful-fs: 4.2.11 jest-circus: 29.5.0 jest-environment-node: 29.5.0 - jest-get-type: 29.4.3 - jest-regex-util: 29.4.3 - jest-resolve: 29.5.0 - jest-runner: 29.5.0 - jest-util: 29.5.0 - jest-validate: 29.5.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 micromatch: 4.0.5 parse-json: 5.2.0 - pretty-format: 29.5.0 + pretty-format: 29.7.0 slash: 3.0.0 strip-json-comments: 3.1.1 ts-node: 10.9.1(@types/node@18.0.0)(typescript@4.8.4) @@ -10224,7 +10210,7 @@ packages: - supports-color dev: true - /jest-config@29.7.0(@types/node@18.0.0): + /jest-config@29.7.0(@types/node@18.0.0)(ts-node@10.9.1): resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: @@ -10259,6 +10245,7 @@ packages: pretty-format: 29.7.0 slash: 3.0.0 strip-json-comments: 3.1.1 + ts-node: 10.9.1(@types/node@18.0.0)(typescript@4.8.4) transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -10269,9 +10256,9 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: chalk: 4.1.2 - diff-sequences: 29.4.3 - jest-get-type: 29.4.3 - pretty-format: 29.5.0 + diff-sequences: 29.6.3 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 dev: true /jest-diff@29.7.0: @@ -10302,11 +10289,11 @@ packages: resolution: {integrity: sha512-HM5kIJ1BTnVt+DQZ2ALp3rzXEl+g726csObrW/jpEGl+CDSSQpOJJX2KE/vEg8cxcMXdyEPu6U4QX5eruQv5hA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 29.5.0 + '@jest/types': 29.6.3 chalk: 4.1.2 - jest-get-type: 29.4.3 - jest-util: 29.5.0 - pretty-format: 29.5.0 + jest-get-type: 29.6.3 + jest-util: 29.7.0 + pretty-format: 29.7.0 dev: true /jest-each@29.7.0: @@ -10347,12 +10334,12 @@ packages: resolution: {integrity: sha512-ExxuIK/+yQ+6PRGaHkKewYtg6hto2uGCgvKdb2nfJfKXgZ17DfXjvbZ+jA1Qt9A8EQSfPnt5FKIfnOO3u1h9qw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/environment': 29.5.0 - '@jest/fake-timers': 29.5.0 - '@jest/types': 29.5.0 + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 '@types/node': 18.0.0 - jest-mock: 29.5.0 - jest-util: 29.5.0 + jest-mock: 29.7.0 + jest-util: 29.7.0 dev: true /jest-environment-node@29.7.0: @@ -10390,14 +10377,14 @@ packages: resolution: {integrity: sha512-IspOPnnBro8YfVYSw6yDRKh/TiCdRngjxeacCps1cQ9cgVN6+10JUcuJ1EabrgYLOATsIAigxA0rLR9x/YlrSA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 29.5.0 + '@jest/types': 29.6.3 '@types/graceful-fs': 4.1.6 '@types/node': 18.0.0 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 - jest-regex-util: 29.4.3 - jest-util: 29.5.0 + jest-regex-util: 29.6.3 + jest-util: 29.7.0 jest-worker: 29.5.0 micromatch: 4.0.5 walker: 1.0.8 @@ -10428,8 +10415,8 @@ packages: resolution: {integrity: sha512-u9YdeeVnghBUtpN5mVxjID7KbkKE1QU4f6uUwuxiY0vYRi9BUCLKlPEZfDGR67ofdFmDz9oPAy2G92Ujrntmow==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - jest-get-type: 29.4.3 - pretty-format: 29.5.0 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 dev: true /jest-leak-detector@29.7.0: @@ -10446,8 +10433,8 @@ packages: dependencies: chalk: 4.1.2 jest-diff: 29.5.0 - jest-get-type: 29.4.3 - pretty-format: 29.5.0 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 dev: true /jest-matcher-utils@29.7.0: @@ -10465,12 +10452,12 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@babel/code-frame': 7.22.13 - '@jest/types': 29.5.0 + '@jest/types': 29.6.3 '@types/stack-utils': 2.0.1 chalk: 4.1.2 graceful-fs: 4.2.11 micromatch: 4.0.5 - pretty-format: 29.5.0 + pretty-format: 29.7.0 slash: 3.0.0 stack-utils: 2.0.6 dev: true @@ -10494,9 +10481,9 @@ packages: resolution: {integrity: sha512-GqOzvdWDE4fAV2bWQLQCkujxYWL7RxjCnj71b5VhDAGOevB3qj3Ovg26A5NI84ZpODxyzaozXLOh2NCgkbvyaw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 29.5.0 + '@jest/types': 29.6.3 '@types/node': 18.0.0 - jest-util: 29.5.0 + jest-util: 29.7.0 dev: true /jest-mock@29.7.0: @@ -10546,8 +10533,8 @@ packages: resolution: {integrity: sha512-sjV3GFr0hDJMBpYeUuGduP+YeCRbd7S/ck6IvL3kQ9cpySYKqcqhdLLC2rFwrcL7tz5vYibomBrsFYWkIGGjOg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - jest-regex-util: 29.4.3 - jest-snapshot: 29.5.0 + jest-regex-util: 29.6.3 + jest-snapshot: 29.7.0 transitivePeerDependencies: - supports-color dev: true @@ -10568,10 +10555,10 @@ packages: dependencies: chalk: 4.1.2 graceful-fs: 4.2.11 - jest-haste-map: 29.5.0 + jest-haste-map: 29.7.0 jest-pnp-resolver: 1.2.3(jest-resolve@29.5.0) - jest-util: 29.5.0 - jest-validate: 29.5.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 resolve: 1.22.2 resolve.exports: 2.0.2 slash: 3.0.0 @@ -10596,24 +10583,24 @@ packages: resolution: {integrity: sha512-m7b6ypERhFghJsslMLhydaXBiLf7+jXy8FwGRHO3BGV1mcQpPbwiqiKUR2zU2NJuNeMenJmlFZCsIqzJCTeGLQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/console': 29.5.0 + '@jest/console': 29.7.0 '@jest/environment': 29.5.0 - '@jest/test-result': 29.5.0 - '@jest/transform': 29.5.0 - '@jest/types': 29.5.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 '@types/node': 18.0.0 chalk: 4.1.2 emittery: 0.13.1 graceful-fs: 4.2.11 jest-docblock: 29.4.3 jest-environment-node: 29.5.0 - jest-haste-map: 29.5.0 + jest-haste-map: 29.7.0 jest-leak-detector: 29.5.0 - jest-message-util: 29.5.0 - jest-resolve: 29.5.0 - jest-runtime: 29.5.0 - jest-util: 29.5.0 - jest-watcher: 29.5.0 + jest-message-util: 29.7.0 + jest-resolve: 29.7.0 + jest-runtime: 29.7.0 + jest-util: 29.7.0 + jest-watcher: 29.7.0 jest-worker: 29.5.0 p-limit: 3.1.0 source-map-support: 0.5.13 @@ -10658,22 +10645,22 @@ packages: '@jest/fake-timers': 29.5.0 '@jest/globals': 29.5.0 '@jest/source-map': 29.4.3 - '@jest/test-result': 29.5.0 - '@jest/transform': 29.5.0 - '@jest/types': 29.5.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 '@types/node': 18.0.0 chalk: 4.1.2 cjs-module-lexer: 1.2.3 collect-v8-coverage: 1.0.1 glob: 7.2.3 graceful-fs: 4.2.11 - jest-haste-map: 29.5.0 - jest-message-util: 29.5.0 + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 jest-mock: 29.5.0 - jest-regex-util: 29.4.3 - jest-resolve: 29.5.0 - jest-snapshot: 29.5.0 - jest-util: 29.5.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 slash: 3.0.0 strip-bom: 4.0.0 transitivePeerDependencies: @@ -10720,22 +10707,22 @@ packages: '@babel/plugin-syntax-typescript': 7.22.5(@babel/core@7.22.5) '@babel/traverse': 7.22.5 '@babel/types': 7.22.5 - '@jest/expect-utils': 29.5.0 - '@jest/transform': 29.5.0 - '@jest/types': 29.5.0 + '@jest/expect-utils': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 '@types/babel__traverse': 7.20.1 '@types/prettier': 2.7.3 babel-preset-current-node-syntax: 1.0.1(@babel/core@7.22.5) chalk: 4.1.2 - expect: 29.5.0 + expect: 29.7.0 graceful-fs: 4.2.11 - jest-diff: 29.5.0 - jest-get-type: 29.4.3 - jest-matcher-utils: 29.5.0 - jest-message-util: 29.5.0 - jest-util: 29.5.0 + jest-diff: 29.7.0 + jest-get-type: 29.6.3 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 natural-compare: 1.4.0 - pretty-format: 29.5.0 + pretty-format: 29.7.0 semver: 7.5.4 transitivePeerDependencies: - supports-color @@ -10773,7 +10760,7 @@ packages: resolution: {integrity: sha512-RYMgG/MTadOr5t8KdhejfvUU82MxsCu5MF6KuDUHl+NuwzUt+Sm6jJWxTJVrDR1j5M/gJVCPKQEpWXY+yIQ6lQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 29.5.0 + '@jest/types': 29.6.3 '@types/node': 18.0.0 chalk: 4.1.2 ci-info: 3.8.0 @@ -10797,12 +10784,12 @@ packages: resolution: {integrity: sha512-pC26etNIi+y3HV8A+tUGr/lph9B18GnzSRAkPaaZJIE1eFdiYm6/CewuiJQ8/RlfHd1u/8Ioi8/sJ+CmbA+zAQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/types': 29.5.0 + '@jest/types': 29.6.3 camelcase: 6.3.0 chalk: 4.1.2 - jest-get-type: 29.4.3 + jest-get-type: 29.6.3 leven: 3.1.0 - pretty-format: 29.5.0 + pretty-format: 29.7.0 dev: true /jest-validate@29.7.0: @@ -10821,13 +10808,13 @@ packages: resolution: {integrity: sha512-KmTojKcapuqYrKDpRwfqcQ3zjMlwu27SYext9pt4GlF5FUgB+7XE1mcCnSm6a4uUpFyQIkb6ZhzZvHl+jiBCiA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/test-result': 29.5.0 - '@jest/types': 29.5.0 + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 '@types/node': 18.0.0 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.13.1 - jest-util: 29.5.0 + jest-util: 29.7.0 string-length: 4.0.2 dev: true @@ -10850,7 +10837,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@types/node': 18.0.0 - jest-util: 29.5.0 + jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 dev: true @@ -10881,6 +10868,7 @@ packages: jest-cli: 29.5.0(@types/node@18.0.0)(ts-node@10.9.1) transitivePeerDependencies: - '@types/node' + - babel-plugin-macros - supports-color - ts-node dev: true @@ -10895,7 +10883,7 @@ packages: node-notifier: optional: true dependencies: - '@jest/core': 29.7.0 + '@jest/core': 29.7.0(ts-node@10.9.1) '@jest/types': 29.6.3 import-local: 3.1.0 jest-cli: 29.7.0(@types/node@18.0.0) @@ -14857,7 +14845,7 @@ packages: yargs-parser: 21.1.1 dev: true - /ts-jest@29.0.5(@babel/core@7.22.5)(esbuild@0.18.13)(jest@29.5.0)(typescript@4.9.5): + /ts-jest@29.0.5(@babel/core@7.23.2)(esbuild@0.18.13)(jest@29.5.0)(typescript@4.9.4): resolution: {integrity: sha512-PL3UciSgIpQ7f6XjVOmbi96vmDHUqAyqDr8YxzopDqX3kfgYtX1cuNeBjP+L9sFXi6nzsGGA6R3fP3DDDJyrxA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -14878,7 +14866,7 @@ packages: esbuild: optional: true dependencies: - '@babel/core': 7.22.5 + '@babel/core': 7.23.2 bs-logger: 0.2.6 esbuild: 0.18.13 fast-json-stable-stringify: 2.1.0 @@ -14888,11 +14876,11 @@ packages: lodash.memoize: 4.1.2 make-error: 1.3.6 semver: 7.5.3 - typescript: 4.9.5 + typescript: 4.9.4 yargs-parser: 21.1.1 dev: true - /ts-jest@29.0.5(@babel/core@7.23.2)(esbuild@0.18.13)(jest@29.5.0)(typescript@4.9.4): + /ts-jest@29.0.5(@babel/core@7.23.2)(esbuild@0.18.13)(jest@29.5.0)(typescript@4.9.5): resolution: {integrity: sha512-PL3UciSgIpQ7f6XjVOmbi96vmDHUqAyqDr8YxzopDqX3kfgYtX1cuNeBjP+L9sFXi6nzsGGA6R3fP3DDDJyrxA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -14923,7 +14911,7 @@ packages: lodash.memoize: 4.1.2 make-error: 1.3.6 semver: 7.5.3 - typescript: 4.9.4 + typescript: 4.9.5 yargs-parser: 21.1.1 dev: true From 141ef18435b3b2435fa168ac5995ecd3e768082e Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Thu, 16 Nov 2023 20:14:56 -0800 Subject: [PATCH 4/5] bump version --- package.json | 2 +- packages/language/package.json | 2 +- packages/plugins/openapi/package.json | 2 +- packages/plugins/swr/package.json | 2 +- packages/plugins/tanstack-query/package.json | 2 +- packages/plugins/trpc/package.json | 2 +- packages/runtime/package.json | 2 +- packages/schema/package.json | 2 +- packages/sdk/package.json | 2 +- packages/server/package.json | 2 +- packages/testtools/package.json | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index eca80a58e..6955bbee9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zenstack-monorepo", - "version": "1.3.0", + "version": "1.3.1", "description": "", "scripts": { "build": "pnpm -r build", diff --git a/packages/language/package.json b/packages/language/package.json index 0558cc275..f455a7a2e 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/language", - "version": "1.3.0", + "version": "1.3.1", "displayName": "ZenStack modeling language compiler", "description": "ZenStack modeling language compiler", "homepage": "https://zenstack.dev", diff --git a/packages/plugins/openapi/package.json b/packages/plugins/openapi/package.json index 905182204..e09b5bc3a 100644 --- a/packages/plugins/openapi/package.json +++ b/packages/plugins/openapi/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/openapi", "displayName": "ZenStack Plugin and Runtime for OpenAPI", - "version": "1.3.0", + "version": "1.3.1", "description": "ZenStack plugin and runtime supporting OpenAPI", "main": "index.js", "repository": { diff --git a/packages/plugins/swr/package.json b/packages/plugins/swr/package.json index 331db27a6..e8cbc8403 100644 --- a/packages/plugins/swr/package.json +++ b/packages/plugins/swr/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/swr", "displayName": "ZenStack plugin for generating SWR hooks", - "version": "1.3.0", + "version": "1.3.1", "description": "ZenStack plugin for generating SWR hooks", "main": "index.js", "repository": { diff --git a/packages/plugins/tanstack-query/package.json b/packages/plugins/tanstack-query/package.json index 00c6174af..98a2b7330 100644 --- a/packages/plugins/tanstack-query/package.json +++ b/packages/plugins/tanstack-query/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/tanstack-query", "displayName": "ZenStack plugin for generating tanstack-query hooks", - "version": "1.3.0", + "version": "1.3.1", "description": "ZenStack plugin for generating tanstack-query hooks", "main": "index.js", "exports": { diff --git a/packages/plugins/trpc/package.json b/packages/plugins/trpc/package.json index 04ba0ded9..0ee9d3068 100644 --- a/packages/plugins/trpc/package.json +++ b/packages/plugins/trpc/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/trpc", "displayName": "ZenStack plugin for tRPC", - "version": "1.3.0", + "version": "1.3.1", "description": "ZenStack plugin for tRPC", "main": "index.js", "repository": { diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 21e7e1851..60b5190d0 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/runtime", "displayName": "ZenStack Runtime Library", - "version": "1.3.0", + "version": "1.3.1", "description": "Runtime of ZenStack for both client-side and server-side environments.", "repository": { "type": "git", diff --git a/packages/schema/package.json b/packages/schema/package.json index 653b94d59..c08481804 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -3,7 +3,7 @@ "publisher": "zenstack", "displayName": "ZenStack Language Tools", "description": "Build scalable web apps with minimum code by defining authorization and validation rules inside the data schema that closer to the database", - "version": "1.3.0", + "version": "1.3.1", "author": { "name": "ZenStack Team" }, diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 9d6f58876..214900de0 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/sdk", - "version": "1.3.0", + "version": "1.3.1", "description": "ZenStack plugin development SDK", "main": "index.js", "scripts": { diff --git a/packages/server/package.json b/packages/server/package.json index 8b8744f71..89fc51fa8 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/server", - "version": "1.3.0", + "version": "1.3.1", "displayName": "ZenStack Server-side Adapters", "description": "ZenStack server-side adapters", "homepage": "https://zenstack.dev", diff --git a/packages/testtools/package.json b/packages/testtools/package.json index 15bcf8b41..c29a441a2 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/testtools", - "version": "1.3.0", + "version": "1.3.1", "description": "ZenStack Test Tools", "main": "index.js", "private": true, From acf75380ef45f23b36a47d710686343875af6ea4 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Thu, 16 Nov 2023 20:32:36 -0800 Subject: [PATCH 5/5] fix tests --- .../tests/generator/zmodel-generator.test.ts | 105 ------------------ .../tests/schema/zmodel-generator.test.ts | 102 ++++++++++++++++- 2 files changed, 101 insertions(+), 106 deletions(-) delete mode 100644 packages/schema/tests/generator/zmodel-generator.test.ts diff --git a/packages/schema/tests/generator/zmodel-generator.test.ts b/packages/schema/tests/generator/zmodel-generator.test.ts deleted file mode 100644 index 91ddacca2..000000000 --- a/packages/schema/tests/generator/zmodel-generator.test.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { loadModel } from '../utils'; -import ZModelCodeGenerator from '../../src/plugins/prisma/zmodel-code-generator'; -import { DataModel, DataModelAttribute, DataModelFieldAttribute } from '@zenstackhq/language/ast'; - -describe('ZModel Generator Tests', () => { - const generator = new ZModelCodeGenerator(); - - function checkAttribute(ast: DataModelAttribute | DataModelFieldAttribute, expected: string) { - const result = generator.generateAttribute(ast); - expect(result).toBe(expected); - } - - async function getModule(schema: string) { - if (!schema.includes('datasource ')) { - schema = - ` - datasource db { - provider = 'postgresql' - url = 'dummy' - } - ` + schema; - } - - return loadModel(schema); - } - - async function getModelDeclaration(schema: string, name: string) { - const module = await getModule(schema); - return module.declarations.find((d) => d.name === name) as DataModel; - } - - it('check field attribute', async () => { - const model = await getModelDeclaration( - ` - model Test{ - id String @id @length(4, 50) @regex('^[0-9a-zA-Z]{4,16}$') - } - `, - 'Test' - ); - - checkAttribute(model.fields[0].attributes[0], '@id'); - checkAttribute(model.fields[0].attributes[1], '@length(4, 50)'); - checkAttribute(model.fields[0].attributes[2], "@regex('^[0-9a-zA-Z]{4,16}$')"); - }); - - it('check basic model attribute', async () => { - const model = await getModelDeclaration( - ` - model User { - id String @id - - @@deny('all', auth() == null) - @@allow('create', true) - } - `, - 'User' - ); - - checkAttribute(model.attributes[0], `@@deny('all', auth() == null)`); - checkAttribute(model.attributes[1], `@@allow('create', true)`); - }); - - it('check collection expression', async () => { - const model = await getModelDeclaration( - ` - enum UserRole { - USER - ADMIN - } - - model User { - id String @id - name String - role UserRole - deleted Boolean - level Int - - - posts Post[] - - @@allow('read', posts ? [author == auth()]) - - @@deny('read', name == '123' && (role == USER || name == '456')) - - @@allow('delete', posts?[author == auth() && ( level <10 || author.role == USER) && !author.deleted]) - } - - model Post { - id String @id - author User? @relation(fields: [authorId], references: [id]) - authorId String? - } - `, - 'User' - ); - - checkAttribute(model.attributes[0], `@@allow('read', posts ? [author == auth()])`); - checkAttribute(model.attributes[1], `@@deny('read', name == '123' && (role == USER || name == '456'))`); - checkAttribute( - model.attributes[2], - `@@allow('delete', posts ? [author == auth() && (level < 10 || author.role == USER) && !author.deleted])` - ); - }); -}); diff --git a/packages/schema/tests/schema/zmodel-generator.test.ts b/packages/schema/tests/schema/zmodel-generator.test.ts index b477a6925..a58cfd1a7 100644 --- a/packages/schema/tests/schema/zmodel-generator.test.ts +++ b/packages/schema/tests/schema/zmodel-generator.test.ts @@ -1,17 +1,117 @@ /// import { ZModelCodeGenerator } from '@zenstackhq/sdk'; +import { DataModel, DataModelAttribute, DataModelFieldAttribute } from '@zenstackhq/sdk/ast'; import fs from 'fs'; import path from 'path'; import { loadModel } from '../utils'; describe('ZModel Generator Tests', () => { + const generator = new ZModelCodeGenerator(); + it('run generator', async () => { const content = fs.readFileSync(path.join(__dirname, './all-features.zmodel'), 'utf-8'); const model = await loadModel(content, true, false, false); - const generator = new ZModelCodeGenerator(); const generated = generator.generate(model); // fs.writeFileSync(path.join(__dirname, './all-features-baseline.zmodel'), generated, 'utf-8'); await loadModel(generated); }); + + function checkAttribute(ast: DataModelAttribute | DataModelFieldAttribute, expected: string) { + const result = generator.generate(ast); + expect(result).toBe(expected); + } + + async function getModule(schema: string) { + if (!schema.includes('datasource ')) { + schema = + ` + datasource db { + provider = 'postgresql' + url = 'dummy' + } + ` + schema; + } + + return loadModel(schema); + } + + async function getModelDeclaration(schema: string, name: string) { + const module = await getModule(schema); + return module.declarations.find((d) => d.name === name) as DataModel; + } + + it('check field attribute', async () => { + const model = await getModelDeclaration( + ` + model Test{ + id String @id @length(4, 50) @regex('^[0-9a-zA-Z]{4,16}$') + } + `, + 'Test' + ); + + checkAttribute(model.fields[0].attributes[0], '@id'); + checkAttribute(model.fields[0].attributes[1], '@length(4, 50)'); + checkAttribute(model.fields[0].attributes[2], "@regex('^[0-9a-zA-Z]{4,16}$')"); + }); + + it('check basic model attribute', async () => { + const model = await getModelDeclaration( + ` + model User { + id String @id + + @@deny('all', auth() == null) + @@allow('create', true) + } + `, + 'User' + ); + + checkAttribute(model.attributes[0], `@@deny('all', auth() == null)`); + checkAttribute(model.attributes[1], `@@allow('create', true)`); + }); + + it('check collection expression', async () => { + const model = await getModelDeclaration( + ` + enum UserRole { + USER + ADMIN + } + + model User { + id String @id + name String + role UserRole + deleted Boolean + level Int + + + posts Post[] + + @@allow('read', posts ? [author == auth()]) + + @@deny('read', name == '123' && (role == USER || name == '456')) + + @@allow('delete', posts?[author == auth() && ( level <10 || author.role == USER) && !author.deleted]) + } + + model Post { + id String @id + author User? @relation(fields: [authorId], references: [id]) + authorId String? + } + `, + 'User' + ); + + checkAttribute(model.attributes[0], `@@allow('read', posts ? [author == auth()])`); + checkAttribute(model.attributes[1], `@@deny('read', name == '123' && (role == USER || name == '456'))`); + checkAttribute( + model.attributes[2], + `@@allow('delete', posts ? [author == auth() && (level < 10 || author.role == USER) && !author.deleted])` + ); + }); });