diff --git a/package.json b/package.json index 788c28e6e..743d486c4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zenstack-monorepo", - "version": "1.0.0-alpha.60", + "version": "1.0.0-alpha.62", "description": "", "scripts": { "build": "pnpm -r build", diff --git a/packages/language/package.json b/packages/language/package.json index 4f322d2af..c2aaa08e0 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/language", - "version": "1.0.0-alpha.60", + "version": "1.0.0-alpha.62", "displayName": "ZenStack modeling language compiler", "description": "ZenStack modeling language compiler", "homepage": "https://zenstack.dev", diff --git a/packages/language/src/generated/ast.ts b/packages/language/src/generated/ast.ts index eed3c050f..226da0c0a 100644 --- a/packages/language/src/generated/ast.ts +++ b/packages/language/src/generated/ast.ts @@ -248,7 +248,7 @@ export interface DataSourceField extends AstNode { readonly $container: DataSource; readonly $type: 'DataSourceField'; name: string - value: InvocationExpr | LiteralExpr + value: ArrayExpr | InvocationExpr | LiteralExpr } export const DataSourceField = 'DataSourceField'; diff --git a/packages/language/src/generated/grammar.ts b/packages/language/src/generated/grammar.ts index 93a60d292..ad937f881 100644 --- a/packages/language/src/generated/grammar.ts +++ b/packages/language/src/generated/grammar.ts @@ -208,6 +208,13 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "$ref": "#/rules@16" }, "arguments": [] + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@10" + }, + "arguments": [] } ] } diff --git a/packages/language/src/zmodel.langium b/packages/language/src/zmodel.langium index e807c942d..aa6ff4735 100644 --- a/packages/language/src/zmodel.langium +++ b/packages/language/src/zmodel.langium @@ -13,7 +13,7 @@ DataSource: TRIPLE_SLASH_COMMENT* 'datasource' name=ID '{' (fields+=DataSourceField)* '}'; DataSourceField: - TRIPLE_SLASH_COMMENT* name=ID '=' value=(LiteralExpr|InvocationExpr); + TRIPLE_SLASH_COMMENT* name=ID '=' value=(LiteralExpr | InvocationExpr | ArrayExpr); // generator GeneratorDecl: diff --git a/packages/next/package.json b/packages/next/package.json index 0a525b995..9bb209369 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/next", - "version": "1.0.0-alpha.60", + "version": "1.0.0-alpha.62", "displayName": "ZenStack Next.js integration", "description": "ZenStack Next.js integration", "homepage": "https://zenstack.dev", diff --git a/packages/plugins/react/package.json b/packages/plugins/react/package.json index f3a068f97..8f959a7f5 100644 --- a/packages/plugins/react/package.json +++ b/packages/plugins/react/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/react", "displayName": "ZenStack plugin and runtime for ReactJS", - "version": "1.0.0-alpha.60", + "version": "1.0.0-alpha.62", "description": "ZenStack plugin and runtime for ReactJS", "main": "index.js", "repository": { diff --git a/packages/plugins/trpc/package.json b/packages/plugins/trpc/package.json index 935438cd4..f0d7015e2 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.0.0-alpha.60", + "version": "1.0.0-alpha.62", "description": "ZenStack plugin for tRPC", "main": "index.js", "repository": { diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 2557c8b8b..388d235c1 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/runtime", "displayName": "ZenStack Runtime Library", - "version": "1.0.0-alpha.60", + "version": "1.0.0-alpha.62", "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 e0053b6c4..0490147ab 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -3,7 +3,7 @@ "publisher": "zenstack", "displayName": "ZenStack Language Tools", "description": "A toolkit for building secure CRUD apps with Next.js + Typescript", - "version": "1.0.0-alpha.60", + "version": "1.0.0-alpha.62", "author": { "name": "ZenStack Team" }, diff --git a/packages/schema/src/language-server/validator/datasource-validator.ts b/packages/schema/src/language-server/validator/datasource-validator.ts index bbb08fa8e..f24fed08b 100644 --- a/packages/schema/src/language-server/validator/datasource-validator.ts +++ b/packages/schema/src/language-server/validator/datasource-validator.ts @@ -4,28 +4,17 @@ import { ValidationAcceptor } from 'langium'; import { getStringLiteral, validateDuplicatedDeclarations } from './utils'; import { SUPPORTED_PROVIDERS } from '../constants'; -const supportedFields = ['provider', 'url', 'shadowDatabaseUrl', 'relationMode']; - /** * Validates data source declarations. */ export default class DataSourceValidator implements AstValidator { validate(ds: DataSource, accept: ValidationAcceptor): void { validateDuplicatedDeclarations(ds.fields, accept); - this.validateFields(ds, accept); this.validateProvider(ds, accept); this.validateUrl(ds, accept); this.validateRelationMode(ds, accept); } - private validateFields(ds: DataSource, accept: ValidationAcceptor) { - ds.fields.forEach((f) => { - if (!supportedFields.includes(f.name)) { - accept('error', `Unexpected field "${f.name}"`, { node: f }); - } - }); - } - private validateProvider(ds: DataSource, accept: ValidationAcceptor) { const provider = ds.fields.find((f) => f.name === 'provider'); if (!provider) { diff --git a/packages/schema/src/plugins/plugin-utils.ts b/packages/schema/src/plugins/plugin-utils.ts index 95cfe1ab2..c70364438 100644 --- a/packages/schema/src/plugins/plugin-utils.ts +++ b/packages/schema/src/plugins/plugin-utils.ts @@ -6,7 +6,7 @@ export const RUNTIME_PACKAGE = '@zenstackhq/runtime'; export const ALL_OPERATION_KINDS: PolicyOperationKind[] = ['create', 'update', 'postUpdate', 'read', 'delete']; /** - * Gets the nearest "node_modules" folder by walking up froma start path. + * Gets the nearest "node_modules" folder by walking up from start path. */ export function getNodeModulesFolder(startPath?: string): string | undefined { startPath = startPath ?? process.cwd(); @@ -25,7 +25,9 @@ export function getNodeModulesFolder(startPath?: string): string | undefined { * @returns */ export function getDefaultOutputFolder() { - const modulesFolder = getNodeModulesFolder(); + // Find the real runtime module path, it might be a symlink in pnpm + const runtimeModulePath = require.resolve('@zenstackhq/runtime'); + const modulesFolder = getNodeModulesFolder(runtimeModulePath); return modulesFolder ? path.join(modulesFolder, '.zenstack') : undefined; } diff --git a/packages/schema/src/plugins/prisma/prisma-builder.ts b/packages/schema/src/plugins/prisma/prisma-builder.ts index 88190334d..3ec0a47ca 100644 --- a/packages/schema/src/plugins/prisma/prisma-builder.ts +++ b/packages/schema/src/plugins/prisma/prisma-builder.ts @@ -1,5 +1,10 @@ import indentString from './indent-string'; +/** + * Field used by datasource and generator declarations. + */ +export type SimpleField = { name: string; value: string | string[] }; + /** * Prisma schema builder */ @@ -9,13 +14,19 @@ export class PrismaModel { private models: Model[] = []; private enums: Enum[] = []; - addDataSource(name: string, provider: string, url: DataSourceUrl, shadowDatabaseUrl?: DataSourceUrl): DataSource { - const ds = new DataSource(name, provider, url, shadowDatabaseUrl); + addDataSource( + name: string, + provider: string, + url: DataSourceUrl, + shadowDatabaseUrl?: DataSourceUrl, + restFields: SimpleField[] = [] + ): DataSource { + const ds = new DataSource(name, provider, url, shadowDatabaseUrl, restFields); this.datasources.push(ds); return ds; } - addGenerator(name: string, fields: Array<{ name: string; value: string | string[] }>): Generator { + addGenerator(name: string, fields: SimpleField[]): Generator { const generator = new Generator(name, fields); this.generators.push(generator); return generator; @@ -45,15 +56,21 @@ export class DataSource { public name: string, public provider: string, public url: DataSourceUrl, - public shadowDatabaseUrl?: DataSourceUrl + public shadowDatabaseUrl?: DataSourceUrl, + public restFields: SimpleField[] = [] ) {} toString(): string { + const restFields = + this.restFields.length > 0 + ? this.restFields.map((f) => indentString(`${f.name} = ${JSON.stringify(f.value)}`)).join('\n') + : ''; return ( `datasource ${this.name} {\n` + indentString(`provider="${this.provider}"\n`) + indentString(`url=${this.url}\n`) + (this.shadowDatabaseUrl ? indentString(`shadowDatabaseurl=${this.shadowDatabaseUrl}\n`) : '') + + (restFields ? restFields + '\n' : '') + `}` ); } @@ -68,7 +85,7 @@ export class DataSourceUrl { } export class Generator { - constructor(public name: string, public fields: Array<{ name: string; value: string | string[] }>) {} + constructor(public name: string, public fields: SimpleField[]) {} toString(): string { return ( diff --git a/packages/schema/src/plugins/prisma/schema-generator.ts b/packages/schema/src/plugins/prisma/schema-generator.ts index 1820602da..4066270e3 100644 --- a/packages/schema/src/plugins/prisma/schema-generator.ts +++ b/packages/schema/src/plugins/prisma/schema-generator.ts @@ -1,4 +1,5 @@ import { + ArrayExpr, AstNode, AttributeArg, DataModel, @@ -48,6 +49,7 @@ import { ModelFieldType, PassThroughAttribute as PrismaPassThroughAttribute, PrismaModel, + SimpleField, } from './prisma-builder'; import ZModelCodeGenerator from './zmodel-code-generator'; @@ -96,14 +98,19 @@ export default class PrismaSchemaGenerator { } await writeFile(outFile, this.PRELUDE + prisma.toString()); - // run 'prisma generate' - await execSync(`npx prisma generate --schema ${outFile}`); + const generateClient = options.generateClient !== false; + + if (generateClient) { + // run 'prisma generate' + await execSync(`npx prisma generate --schema ${outFile}`); + } } private generateDataSource(prisma: PrismaModel, dataSource: DataSource) { let provider: string | undefined = undefined; let url: PrismaDataSourceUrl | undefined = undefined; let shadowDatabaseUrl: PrismaDataSourceUrl | undefined = undefined; + const restFields: SimpleField[] = []; for (const f of dataSource.fields) { switch (f.name) { @@ -133,6 +140,19 @@ export default class PrismaSchemaGenerator { shadowDatabaseUrl = r; break; } + + default: { + // rest fields + const value = isArrayExpr(f.value) ? getLiteralArray(f.value) : getLiteral(f.value); + if (value === undefined) { + throw new PluginError( + `Invalid value for datasource field ${f.name}: value must be a string or an array of strings` + ); + } else { + restFields.push({ name: f.name, value }); + } + break; + } } } @@ -143,10 +163,10 @@ export default class PrismaSchemaGenerator { throw new PluginError('Datasource is missing "url" field'); } - prisma.addDataSource(dataSource.name, provider, url, shadowDatabaseUrl); + prisma.addDataSource(dataSource.name, provider, url, shadowDatabaseUrl, restFields); } - private extractDataSourceUrl(fieldValue: LiteralExpr | InvocationExpr) { + private extractDataSourceUrl(fieldValue: LiteralExpr | InvocationExpr | ArrayExpr) { if (this.isStringLiteral(fieldValue)) { return new PrismaDataSourceUrl(fieldValue.value as string, false); } else if ( diff --git a/packages/schema/src/res/stdlib.zmodel b/packages/schema/src/res/stdlib.zmodel index 940a348b7..52f354749 100644 --- a/packages/schema/src/res/stdlib.zmodel +++ b/packages/schema/src/res/stdlib.zmodel @@ -246,6 +246,11 @@ attribute @db.Blob() @@@targetField([BytesField]) @@@prisma attribute @db.MediumBlob() @@@targetField([BytesField]) @@@prisma attribute @db.Image() @@@targetField([BytesField]) @@@prisma +/* + * Specifies the schema to use in a multi-schema database. https://www.prisma.io/docs/guides/database/multi-schema. + */ +attribute @@schema(_ name: String) @@@prisma + /* * Defines an access policy that allows a set of operations when the given condition is true. */ diff --git a/packages/schema/src/utils/pkg-utils.ts b/packages/schema/src/utils/pkg-utils.ts index dedef31d7..84b8d4553 100644 --- a/packages/schema/src/utils/pkg-utils.ts +++ b/packages/schema/src/utils/pkg-utils.ts @@ -4,16 +4,36 @@ import { execSync } from './exec-utils'; export type PackageManagers = 'npm' | 'yarn' | 'pnpm'; +function findUp(names: string[], cwd: string): string | undefined { + let dir = cwd; + // eslint-disable-next-line no-constant-condition + while (true) { + const target = names.find((name) => fs.existsSync(path.join(dir, name))); + if (target) return target; + + const up = path.resolve(dir, '..'); + if (up === dir) return undefined; // it'll fail anyway + dir = up; + } +} + function getPackageManager(projectPath = '.'): PackageManagers { - if (fs.existsSync(path.join(projectPath, 'yarn.lock'))) { - return 'yarn'; - } else if (fs.existsSync(path.join(projectPath, 'pnpm-lock.yaml'))) { - return 'pnpm'; - } else { + const lockFile = findUp(['yarn.lock', 'pnpm-lock.yaml', 'package-lock.json'], projectPath); + + if (!lockFile) { + // default use npm return 'npm'; } -} + switch (path.basename(lockFile)) { + case 'yarn.lock': + return 'yarn'; + case 'pnpm-lock.yaml': + return 'pnpm'; + default: + return 'npm'; + } +} export function installPackage( pkg: string, dev: boolean, diff --git a/packages/schema/tests/generator/prisma-generator.test.ts b/packages/schema/tests/generator/prisma-generator.test.ts index a132a7af8..aff1db9fa 100644 --- a/packages/schema/tests/generator/prisma-generator.test.ts +++ b/packages/schema/tests/generator/prisma-generator.test.ts @@ -1,3 +1,5 @@ +/// + import { getDMMF } from '@prisma/internals'; import fs from 'fs'; import tmp from 'tmp'; @@ -137,4 +139,56 @@ describe('Prisma generator test', () => { expect(content).toContain('@unique()'); expect(content).toContain('@@index([x, y])'); }); + + it('multi schema', async () => { + const model = await loadModel(` + datasource db { + provider = 'postgresql' + url = env('URL') + schemas = ['base', 'transactional'] + } + + generator client { + provider = "prisma-client-js" + previewFeatures = ["multiSchema"] + } + + model User { + id Int @id + orders Order[] + + @@schema("base") + } + + model Order { + id Int @id + user User @relation(fields: [id], references: [id]) + user_id Int + + @@schema("transactional") + } + + enum Size { + Small + Medium + Large + + @@schema("transactional") + } + `); + + const { name } = tmp.fileSync({ postfix: '.prisma' }); + await new PrismaSchemaGenerator().generate(model, { + provider: '@zenstack/prisma', + schemaPath: 'schema.zmodel', + output: name, + generateClient: false, + }); + + const content = fs.readFileSync(name, 'utf-8'); + await getDMMF({ datamodel: content }); + expect(content).toContain('@@schema("base")'); + expect(content).toContain('@@schema("base")'); + expect(content).toContain('schemas = ["base","transactional"]'); + }); }); diff --git a/packages/schema/tests/schema/validation/datasource-validation.test.ts b/packages/schema/tests/schema/validation/datasource-validation.test.ts index bfd8dea13..19be1f076 100644 --- a/packages/schema/tests/schema/validation/datasource-validation.test.ts +++ b/packages/schema/tests/schema/validation/datasource-validation.test.ts @@ -26,16 +26,6 @@ describe('Datasource Validation Tests', () => { ).toContain('Duplicated declaration name "provider"'); }); - it('unknown fields', async () => { - expect( - await loadModelWithError(` - datasource db { - x = 1 - } - `) - ).toContain('Unexpected field "x"'); - }); - it('invalid provider value', async () => { expect( await loadModelWithError(` diff --git a/packages/sdk/package.json b/packages/sdk/package.json index cc50ca25b..36a09fd0f 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/sdk", - "version": "1.0.0-alpha.60", + "version": "1.0.0-alpha.62", "description": "ZenStack plugin development SDK", "main": "index.js", "scripts": { diff --git a/packages/testtools/package.json b/packages/testtools/package.json index ca9ffc57a..7242aea6e 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/testtools", - "version": "1.0.0-alpha.60", + "version": "1.0.0-alpha.62", "description": "ZenStack Test Tools", "main": "index.js", "publishConfig": { diff --git a/tests/integration/test-run/package-lock.json b/tests/integration/test-run/package-lock.json index be0159d26..ff46da152 100644 --- a/tests/integration/test-run/package-lock.json +++ b/tests/integration/test-run/package-lock.json @@ -126,7 +126,7 @@ }, "../../../packages/runtime/dist": { "name": "@zenstackhq/runtime", - "version": "1.0.0-alpha.60", + "version": "1.0.0-alpha.62", "license": "MIT", "dependencies": { "@types/bcryptjs": "^2.4.2", @@ -156,7 +156,7 @@ }, "../../../packages/schema/dist": { "name": "zenstack", - "version": "1.0.0-alpha.60", + "version": "1.0.0-alpha.62", "hasInstallScript": true, "license": "MIT", "dependencies": {