From b962e05f56fcc2a9c854747c22f5dba9283ee767 Mon Sep 17 00:00:00 2001 From: Yiming Date: Thu, 16 Nov 2023 20:59:27 -0800 Subject: [PATCH] chore: refactor zmodel-code-generator and move it to sdk package (#836) --- package.json | 2 +- packages/language/package.json | 2 +- packages/language/src/generated/grammar.ts | 3 +- packages/language/src/zmodel.langium | 4 +- 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/schema/src/cli/plugin-runner.ts | 11 +- .../src/plugins/prisma/schema-generator.ts | 10 +- .../plugins/prisma/zmodel-code-generator.ts | 177 ---------- .../schema/tests/schema/all-features.zmodel | 177 ++++++++++ .../zmodel-generator.test.ts | 18 +- packages/schema/tests/utils.ts | 6 +- packages/sdk/package.json | 2 +- packages/sdk/src/index.ts | 1 + packages/sdk/src/utils.ts | 2 +- packages/sdk/src/zmodel-code-generator.ts | 332 ++++++++++++++++++ packages/server/package.json | 2 +- packages/testtools/package.json | 2 +- pnpm-lock.yaml | 282 +++++++-------- 23 files changed, 691 insertions(+), 354 deletions(-) delete mode 100644 packages/schema/src/plugins/prisma/zmodel-code-generator.ts create mode 100644 packages/schema/tests/schema/all-features.zmodel rename packages/schema/tests/{generator => schema}/zmodel-generator.test.ts (83%) create mode 100644 packages/sdk/src/zmodel-code-generator.ts 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/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/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/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/schema/src/plugins/prisma/schema-generator.ts b/packages/schema/src/plugins/prisma/schema-generator.ts index 238c494e9..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'; @@ -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 deleted file mode 100644 index 5db0261e2..000000000 --- a/packages/schema/src/plugins/prisma/zmodel-code-generator.ts +++ /dev/null @@ -1,177 +0,0 @@ -import { - Argument, - ArrayExpr, - AttributeArg, - BinaryExpr, - BinaryExprOperatorPriority, - BooleanLiteral, - DataModelAttribute, - DataModelFieldAttribute, - Expression, - FieldInitializer, - InvocationExpr, - LiteralExpr, - MemberAccessExpr, - NullExpr, - NumberLiteral, - ObjectExpr, - ReferenceArg, - ReferenceExpr, - StringLiteral, - ThisExpr, - UnaryExpr, -} from '@zenstackhq/language/ast'; -import { resolved } from '@zenstackhq/sdk'; - -/** - * Options for the generator. - */ -export interface ZModelCodeOptions { - binaryExprNumberOfSpaces: number; - unaryExprNumberOfSpaces: number; -} - -export default class ZModelCodeGenerator { - private readonly options: ZModelCodeOptions; - constructor(options?: Partial) { - this.options = { - binaryExprNumberOfSpaces: options?.binaryExprNumberOfSpaces ?? 1, - unaryExprNumberOfSpaces: options?.unaryExprNumberOfSpaces ?? 0, - }; - } - generateAttribute(ast: DataModelAttribute | DataModelFieldAttribute): string { - const args = ast.args.length ? `(${ast.args.map((x) => this.generateAttributeArg(x)).join(', ')})` : ''; - return `${resolved(ast.decl).name}${args}`; - } - - generateAttributeArg(ast: AttributeArg) { - if (ast.name) { - return `${ast.name}: ${this.generateExpression(ast.value)}`; - } else { - return this.generateExpression(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}`); - } - } - - generateObjectExpr(ast: ObjectExpr) { - return `{ ${ast.fields.map((field) => this.generateObjectField(field)).join(', ')} }`; - } - - generateObjectField(field: FieldInitializer) { - return `${field.name}: ${this.generateExpression(field.value)}`; - } - - generateArrayExpr(ast: ArrayExpr) { - return `[${ast.items.map((item) => this.generateExpression(item)).join(', ')}]`; - } - - generateLiteralExpr(ast: LiteralExpr) { - return ast.$type === StringLiteral ? `'${ast.value}'` : ast.value.toString(); - } - - generateUnaryExpr(ast: UnaryExpr) { - return `${ast.operator}${this.unaryExprSpace}${this.generateExpression(ast.operand)}`; - } - - generateBinaryExpr(ast: BinaryExpr) { - const operator = ast.operator; - const isCollectionPredicate = this.isCollectionPredicateOperator(operator); - const rightExpr = this.generateExpression(ast.right); - - const { left: isLeftParenthesis, right: isRightParenthesis } = this.isParenthesesNeededForBinaryExpr(ast); - - return `${isLeftParenthesis ? '(' : ''}${this.generateExpression(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(', ')})` : ''; - return `${ast.target.ref?.name}${args}`; - } - - generateReferenceArg(ast: ReferenceArg) { - return `${ast.name}:${ast.value}`; - } - - generateMemberExpr(ast: MemberAccessExpr) { - return `${this.generateExpression(ast.operand)}.${ast.member.ref?.name}`; - } - - generateInvocationExpr(ast: InvocationExpr) { - return `${ast.function.ref?.name}(${ast.args.map((x) => this.generateArgument(x)).join(', ')})`; - } - - generateArgument(ast: Argument) { - return `${ast.name && ':'} ${this.generateExpression(ast.value)}`; - } - - private get binaryExprSpace(): string { - return ' '.repeat(this.options.binaryExprNumberOfSpaces); - } - - private get unaryExprSpace(): string { - return ' '.repeat(this.options.unaryExprNumberOfSpaces); - } - - private isParenthesesNeededForBinaryExpr(ast: BinaryExpr): { left: boolean; right: boolean } { - const result = { left: false, right: false }; - const operator = ast.operator; - const isCollectionPredicate = this.isCollectionPredicateOperator(operator); - - const currentPriority = BinaryExprOperatorPriority[operator]; - - if ( - ast.left.$type === BinaryExpr && - BinaryExprOperatorPriority[(ast.left as BinaryExpr)['operator']] < currentPriority - ) { - result.left = true; - } - /** - * 1 collection predicate operator has [] around the right operand, no need to add parenthesis. - * 2 grammar is left associative, so if the right operand has the same priority still need to add parenthesis. - **/ - if ( - !isCollectionPredicate && - ast.right.$type === BinaryExpr && - BinaryExprOperatorPriority[(ast.right as BinaryExpr)['operator']] <= currentPriority - ) { - result.right = true; - } - - return result; - } - - private isCollectionPredicateOperator(op: BinaryExpr['operator']) { - return ['?', '!', '^'].includes(op); - } -} 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/generator/zmodel-generator.test.ts b/packages/schema/tests/schema/zmodel-generator.test.ts similarity index 83% rename from packages/schema/tests/generator/zmodel-generator.test.ts rename to packages/schema/tests/schema/zmodel-generator.test.ts index 91ddacca2..a58cfd1a7 100644 --- a/packages/schema/tests/generator/zmodel-generator.test.ts +++ b/packages/schema/tests/schema/zmodel-generator.test.ts @@ -1,12 +1,24 @@ +/// + +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'; -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(); + 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 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.generateAttribute(ast); + const result = generator.generate(ast); expect(result).toBe(expected); } 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/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/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/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 { diff --git a/packages/sdk/src/zmodel-code-generator.ts b/packages/sdk/src/zmodel-code-generator.ts new file mode 100644 index 000000000..dac743f79 --- /dev/null +++ b/packages/sdk/src/zmodel-code-generator.ts @@ -0,0 +1,332 @@ +import { + Argument, + ArrayExpr, + AstNode, + AttributeArg, + BinaryExpr, + BinaryExprOperatorPriority, + BooleanLiteral, + ConfigArrayExpr, + ConfigField, + ConfigInvocationExpr, + DataModel, + DataModelAttribute, + DataModelField, + DataModelFieldAttribute, + DataModelFieldType, + DataSource, + Enum, + EnumField, + FieldInitializer, + GeneratorDecl, + InvocationExpr, + LiteralExpr, + MemberAccessExpr, + Model, + NullExpr, + NumberLiteral, + ObjectExpr, + Plugin, + PluginField, + ReferenceArg, + ReferenceExpr, + StringLiteral, + ThisExpr, + UnaryExpr, +} from './ast'; +import { resolved } from './utils'; + +/** + * Options for the generator. + */ +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; + }; +} + +/** + * Generates ZModel source code from AST. + */ +export class ZModelCodeGenerator { + private readonly options: ZModelCodeOptions; + + constructor(options?: Partial) { + this.options = { + binaryExprNumberOfSpaces: options?.binaryExprNumberOfSpaces ?? 1, + unaryExprNumberOfSpaces: options?.unaryExprNumberOfSpaces ?? 0, + indent: options?.indent ?? 4, + }; + } + + /** + * Generates ZModel source code from AST. + */ + 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(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} { +${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) { + 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) + 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.length > 0 + ? '\n\n' + ast.attributes.map((x) => this.indent + this.generate(x)).join('\n') + : '' + } +}`; + } + + @gen(DataModelField) + private _generateDataModelField(ast: DataModelField) { + 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) + 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}`; + } + + @gen(AttributeArg) + private _generateAttributeArg(ast: AttributeArg) { + if (ast.name) { + return `${ast.name}: ${this.generate(ast.value)}`; + } else { + return this.generate(ast.value); + } + } + + @gen(ObjectExpr) + private _generateObjectExpr(ast: ObjectExpr) { + return `{ ${ast.fields.map((field) => this.objectField(field)).join(', ')} }`; + } + + private objectField(field: FieldInitializer) { + return `${field.name}: ${this.generate(field.value)}`; + } + + @gen(ArrayExpr) + private _generateArrayExpr(ast: ArrayExpr) { + return `[${ast.items.map((item) => this.generate(item)).join(', ')}]`; + } + + @gen(StringLiteral) + private _generateLiteralExpr(ast: LiteralExpr) { + return `'${ast.value}'`; + } + + @gen(NumberLiteral) + private _generateNumberLiteral(ast: NumberLiteral) { + return ast.value.toString(); + } + + @gen(BooleanLiteral) + private _generateBooleanLiteral(ast: BooleanLiteral) { + return ast.value.toString(); + } + + @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.generate(ast.right); + + const { left: isLeftParenthesis, right: isRightParenthesis } = this.isParenthesesNeededForBinaryExpr(ast); + + return `${isLeftParenthesis ? '(' : ''}${this.generate(ast.left)}${isLeftParenthesis ? ')' : ''}${ + this.binaryExprSpace + }${operator}${this.binaryExprSpace}${isRightParenthesis ? '(' : ''}${ + isCollectionPredicate ? `[${rightExpr}]` : rightExpr + }${isRightParenthesis ? ')' : ''}`; + } + + @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}`; + } + + @gen(ReferenceArg) + private _generateReferenceArg(ast: ReferenceArg) { + return `${ast.name}:${ast.value}`; + } + + @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'; + } + + @gen(ThisExpr) + private _generateThisExpr() { + return 'this'; + } + + argument(ast: Argument) { + return `${ast.name ? ast.name + ': ' : ''}${this.generate(ast.value)}`; + } + + private get binaryExprSpace() { + return ' '.repeat(this.options.binaryExprNumberOfSpaces); + } + + 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; + const isCollectionPredicate = this.isCollectionPredicateOperator(operator); + + const currentPriority = BinaryExprOperatorPriority[operator]; + + if ( + ast.left.$type === BinaryExpr && + BinaryExprOperatorPriority[(ast.left as BinaryExpr)['operator']] < currentPriority + ) { + result.left = true; + } + /** + * 1 collection predicate operator has [] around the right operand, no need to add parenthesis. + * 2 grammar is left associative, so if the right operand has the same priority still need to add parenthesis. + **/ + if ( + !isCollectionPredicate && + ast.right.$type === BinaryExpr && + BinaryExprOperatorPriority[(ast.right as BinaryExpr)['operator']] <= currentPriority + ) { + result.right = true; + } + + return result; + } + + private isCollectionPredicateOperator(op: BinaryExpr['operator']) { + return ['?', '!', '^'].includes(op); + } +} 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, 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