diff --git a/.vscode/launch.json b/.vscode/launch.json index 7a65620b..886089d1 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,7 +10,8 @@ "request": "launch", "skipFiles": ["/**"], "type": "node", - "args": ["generate", "--schema", "${workspaceFolder}/samples/blog/zenstack/schema.zmodel"] + "args": ["generate"], + "cwd": "${workspaceFolder}/samples/blog/zenstack" }, { "name": "Debug with TSX", diff --git a/TODO.md b/TODO.md index 3be3a477..ea880bb3 100644 --- a/TODO.md +++ b/TODO.md @@ -9,14 +9,14 @@ - [x] init - [x] validate - [ ] format - - [ ] db seed - - [ ] plugin mechanism - - [ ] built-in plugins - - [ ] ts - - [ ] prisma + - [x] plugin mechanism + - [x] built-in plugins + - [x] ts + - [x] prisma - [ ] ZModel - - [ ] Import + - [x] Import - [ ] View support + - [ ] Datasource provider-scoped attributes - [ ] ORM - [x] Create - [x] Input validation @@ -85,11 +85,12 @@ - [x] Custom field name - [ ] Strict undefined checks - [ ] DbNull vs JsonNull + - [ ] Migrate to tsdown - [ ] Benchmark - [x] Plugin - [x] Post-mutation hooks should be called after transaction is committed - [x] TypeDef and mixin -- [ ] Strongly typed JSON +- [x] Strongly typed JSON - [x] Polymorphism - [x] ZModel - [x] Runtime @@ -105,3 +106,4 @@ - [x] SQLite - [x] PostgreSQL - [ ] Multi-schema + - [ ] MySQL diff --git a/package.json b/package.json index 1c0ec9ae..8924b5da 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zenstack-v3", - "version": "3.0.0-alpha.26", + "version": "3.0.0-alpha.27", "description": "ZenStack", "packageManager": "pnpm@10.12.1", "scripts": { @@ -21,7 +21,7 @@ "license": "MIT", "devDependencies": { "@eslint/js": "^9.29.0", - "@types/node": "^20.17.24", + "@types/node": "catalog:", "eslint": "~9.29.0", "glob": "^11.0.2", "prettier": "^3.5.3", diff --git a/packages/cli/package.json b/packages/cli/package.json index 1d4c8d69..9a23af1d 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -3,7 +3,7 @@ "publisher": "zenstack", "displayName": "ZenStack CLI", "description": "FullStack database toolkit with built-in access control and automatic API generation.", - "version": "3.0.0-alpha.26", + "version": "3.0.0-alpha.27", "type": "module", "author": { "name": "ZenStack Team" diff --git a/packages/cli/src/actions/generate.ts b/packages/cli/src/actions/generate.ts index 58c2060a..b70c0cda 100644 --- a/packages/cli/src/actions/generate.ts +++ b/packages/cli/src/actions/generate.ts @@ -1,16 +1,18 @@ import { invariant } from '@zenstackhq/common-helpers'; -import { isPlugin, LiteralExpr, type Model } from '@zenstackhq/language/ast'; -import { PrismaSchemaGenerator, TsSchemaGenerator, type CliGenerator } from '@zenstackhq/sdk'; +import { isPlugin, LiteralExpr, Plugin, type Model } from '@zenstackhq/language/ast'; +import { getLiteral, getLiteralArray } from '@zenstackhq/language/utils'; +import { type CliPlugin } from '@zenstackhq/sdk'; import colors from 'colors'; -import fs from 'node:fs'; import path from 'node:path'; +import ora from 'ora'; +import { CliError } from '../cli-error'; +import * as corePlugins from '../plugins'; import { getPkgJsonConfig, getSchemaFile, loadSchemaDocument } from './action-utils'; type Options = { schema?: string; output?: string; silent?: boolean; - savePrismaSchema?: string | boolean; }; /** @@ -24,25 +26,10 @@ export async function run(options: Options) { const model = await loadSchemaDocument(schemaFile); const outputPath = getOutputPath(options, schemaFile); - // generate TS schema - const tsSchemaFile = path.join(outputPath, 'schema.ts'); - await new TsSchemaGenerator().generate(schemaFile, [], outputPath); - - await runPlugins(model, outputPath, tsSchemaFile); - - // generate Prisma schema - if (options.savePrismaSchema) { - const prismaSchema = await new PrismaSchemaGenerator(model).generate(); - let prismaSchemaFile = path.join(outputPath, 'schema.prisma'); - if (typeof options.savePrismaSchema === 'string') { - prismaSchemaFile = path.resolve(outputPath, options.savePrismaSchema); - fs.mkdirSync(path.dirname(prismaSchemaFile), { recursive: true }); - } - fs.writeFileSync(prismaSchemaFile, prismaSchema); - } + await runPlugins(schemaFile, model, outputPath); if (!options.silent) { - console.log(colors.green(`Generation completed successfully in ${Date.now() - start}ms.`)); + console.log(colors.green(`Generation completed successfully in ${Date.now() - start}ms.\n`)); console.log(`You can now create a ZenStack client with it. \`\`\`ts @@ -68,18 +55,84 @@ function getOutputPath(options: Options, schemaFile: string) { } } -async function runPlugins(model: Model, outputPath: string, tsSchemaFile: string) { +async function runPlugins(schemaFile: string, model: Model, outputPath: string) { const plugins = model.declarations.filter(isPlugin); + const processedPlugins: { cliPlugin: CliPlugin; pluginOptions: Record }[] = []; + for (const plugin of plugins) { - const providerField = plugin.fields.find((f) => f.name === 'provider'); - invariant(providerField, `Plugin ${plugin.name} does not have a provider field`); - const provider = (providerField.value as LiteralExpr).value as string; - let useProvider = provider; - if (useProvider.startsWith('@core/')) { - useProvider = `@zenstackhq/runtime/plugins/${useProvider.slice(6)}`; + const provider = getPluginProvider(plugin); + + let cliPlugin: CliPlugin; + if (provider.startsWith('@core/')) { + cliPlugin = (corePlugins as any)[provider.slice('@core/'.length)]; + if (!cliPlugin) { + throw new CliError(`Unknown core plugin: ${provider}`); + } + } else { + let moduleSpec = provider; + if (moduleSpec.startsWith('.')) { + // relative to schema's path + moduleSpec = path.resolve(path.dirname(schemaFile), moduleSpec); + } + try { + cliPlugin = (await import(moduleSpec)).default as CliPlugin; + } catch (error) { + throw new CliError(`Failed to load plugin ${provider}: ${error}`); + } + } + + processedPlugins.push({ cliPlugin, pluginOptions: getPluginOptions(plugin) }); + } + + const defaultPlugins = [corePlugins['typescript']].reverse(); + defaultPlugins.forEach((d) => { + if (!processedPlugins.some((p) => p.cliPlugin === d)) { + processedPlugins.push({ cliPlugin: d, pluginOptions: {} }); + } + }); + + for (const { cliPlugin, pluginOptions } of processedPlugins) { + invariant( + typeof cliPlugin.generate === 'function', + `Plugin ${cliPlugin.name} does not have a generate function`, + ); + + // run plugin generator + const spinner = ora(cliPlugin.statusText ?? `Running plugin ${cliPlugin.name}`).start(); + try { + await cliPlugin.generate({ + schemaFile, + model, + defaultOutputPath: outputPath, + pluginOptions, + }); + spinner.succeed(); + } catch (err) { + spinner.fail(); + console.error(err); + } + } +} + +function getPluginProvider(plugin: Plugin) { + const providerField = plugin.fields.find((f) => f.name === 'provider'); + invariant(providerField, `Plugin ${plugin.name} does not have a provider field`); + const provider = (providerField.value as LiteralExpr).value as string; + return provider; +} + +function getPluginOptions(plugin: Plugin): Record { + const result: Record = {}; + for (const field of plugin.fields) { + if (field.name === 'provider') { + continue; // skip provider + } + const value = getLiteral(field.value) ?? getLiteralArray(field.value); + if (value === undefined) { + console.warn(`Plugin "${plugin.name}" option "${field.name}" has unsupported value, skipping`); + continue; } - const generator = (await import(useProvider)).default as CliGenerator; - console.log('Running generator:', provider); - await generator({ model, outputPath, tsSchemaFile }); + result[field.name] = value; } + return result; } diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index fd5ad01e..a275800d 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -55,12 +55,6 @@ export function createProgram() { .description('Run code generation.') .addOption(schemaOption) .addOption(new Option('--silent', 'do not print any output')) - .addOption( - new Option( - '--save-prisma-schema [path]', - 'save a Prisma schema file, by default into the output directory', - ), - ) .addOption(new Option('-o, --output ', 'default output directory for core plugins')) .action(generateAction); diff --git a/packages/cli/src/plugins/index.ts b/packages/cli/src/plugins/index.ts new file mode 100644 index 00000000..1a09b07d --- /dev/null +++ b/packages/cli/src/plugins/index.ts @@ -0,0 +1,2 @@ +export { default as prisma } from './prisma'; +export { default as typescript } from './typescript'; diff --git a/packages/cli/src/plugins/prisma.ts b/packages/cli/src/plugins/prisma.ts new file mode 100644 index 00000000..b471ec36 --- /dev/null +++ b/packages/cli/src/plugins/prisma.ts @@ -0,0 +1,21 @@ +import { PrismaSchemaGenerator, type CliPlugin } from '@zenstackhq/sdk'; +import fs from 'node:fs'; +import path from 'node:path'; + +const plugin: CliPlugin = { + name: 'Prisma Schema Generator', + statusText: 'Generating Prisma schema', + async generate({ model, defaultOutputPath, pluginOptions }) { + let outDir = defaultOutputPath; + if (typeof pluginOptions['output'] === 'string') { + outDir = path.resolve(defaultOutputPath, pluginOptions['output']); + if (!fs.existsSync(outDir)) { + fs.mkdirSync(outDir, { recursive: true }); + } + } + const prismaSchema = await new PrismaSchemaGenerator(model).generate(); + fs.writeFileSync(path.join(outDir, 'schema.prisma'), prismaSchema); + }, +}; + +export default plugin; diff --git a/packages/cli/src/plugins/typescript.ts b/packages/cli/src/plugins/typescript.ts new file mode 100644 index 00000000..4fd5006f --- /dev/null +++ b/packages/cli/src/plugins/typescript.ts @@ -0,0 +1,21 @@ +import type { CliPlugin } from '@zenstackhq/sdk'; +import { TsSchemaGenerator } from '@zenstackhq/sdk'; +import fs from 'node:fs'; +import path from 'node:path'; + +const plugin: CliPlugin = { + name: 'TypeScript Schema Generator', + statusText: 'Generating TypeScript schema', + async generate({ model, defaultOutputPath, pluginOptions }) { + let outDir = defaultOutputPath; + if (typeof pluginOptions['output'] === 'string') { + outDir = path.resolve(defaultOutputPath, pluginOptions['output']); + if (!fs.existsSync(outDir)) { + fs.mkdirSync(outDir, { recursive: true }); + } + } + await new TsSchemaGenerator().generate(model, outDir); + }, +}; + +export default plugin; diff --git a/packages/cli/test/generate.test.ts b/packages/cli/test/generate.test.ts index ad9e0497..701fe4f0 100644 --- a/packages/cli/test/generate.test.ts +++ b/packages/cli/test/generate.test.ts @@ -30,18 +30,6 @@ describe('CLI generate command test', () => { expect(fs.existsSync(path.join(workDir, 'zenstack/schema.ts'))).toBe(true); }); - it('should respect save prisma schema option', () => { - const workDir = createProject(model); - runCli('generate --save-prisma-schema', workDir); - expect(fs.existsSync(path.join(workDir, 'zenstack/schema.prisma'))).toBe(true); - }); - - it('should respect save prisma schema custom path option', () => { - const workDir = createProject(model); - runCli('generate --save-prisma-schema "../prisma/schema.prisma"', workDir); - expect(fs.existsSync(path.join(workDir, 'prisma/schema.prisma'))).toBe(true); - }); - it('should respect package.json config', () => { const workDir = createProject(model); fs.mkdirSync(path.join(workDir, 'foo')); diff --git a/packages/cli/test/plugins/custom-plugin.test.ts b/packages/cli/test/plugins/custom-plugin.test.ts new file mode 100644 index 00000000..084bf9cd --- /dev/null +++ b/packages/cli/test/plugins/custom-plugin.test.ts @@ -0,0 +1,50 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { describe, expect, it } from 'vitest'; +import { createProject, runCli } from '../utils'; +import { execSync } from 'node:child_process'; + +describe('Custom plugins tests', () => { + it('runs custom plugin generator', () => { + const workDir = createProject(` +plugin custom { + provider = '../my-plugin.js' + output = '../custom-output' +} + +model User { + id String @id @default(cuid()) +} +`); + + fs.writeFileSync( + path.join(workDir, 'my-plugin.ts'), + ` +import type { CliPlugin } from '@zenstackhq/sdk'; +import fs from 'node:fs'; +import path from 'node:path'; + +const plugin: CliPlugin = { + name: 'Custom Generator', + statusText: 'Generating foo.txt', + async generate({ model, defaultOutputPath, pluginOptions }) { + let outDir = defaultOutputPath; + if (typeof pluginOptions['output'] === 'string') { + outDir = path.resolve(defaultOutputPath, pluginOptions['output']); + if (!fs.existsSync(outDir)) { + fs.mkdirSync(outDir, { recursive: true }); + } + } + fs.writeFileSync(path.join(outDir, 'foo.txt'), 'from my plugin'); + }, +}; + +export default plugin; +`, + ); + + execSync('npx tsc', { cwd: workDir }); + runCli('generate', workDir); + expect(fs.existsSync(path.join(workDir, 'custom-output/foo.txt'))).toBe(true); + }); +}); diff --git a/packages/cli/test/plugins/prisma-plugin.test.ts b/packages/cli/test/plugins/prisma-plugin.test.ts new file mode 100644 index 00000000..06f252cb --- /dev/null +++ b/packages/cli/test/plugins/prisma-plugin.test.ts @@ -0,0 +1,60 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { describe, expect, it } from 'vitest'; +import { createProject, runCli } from '../utils'; + +describe('Core plugins tests', () => { + it('can automatically generate a TypeScript schema with default output', () => { + const workDir = createProject(` +model User { + id String @id @default(cuid()) +} +`); + runCli('generate', workDir); + expect(fs.existsSync(path.join(workDir, 'zenstack/schema.ts'))).toBe(true); + }); + + it('can automatically generate a TypeScript schema with custom output', () => { + const workDir = createProject(` +plugin typescript { + provider = '@core/typescript' + output = '../generated-schema' +} + +model User { + id String @id @default(cuid()) +} +`); + runCli('generate', workDir); + expect(fs.existsSync(path.join(workDir, 'generated-schema/schema.ts'))).toBe(true); + }); + + it('can generate a Prisma schema with default output', () => { + const workDir = createProject(` +plugin prisma { + provider = '@core/prisma' +} + +model User { + id String @id @default(cuid()) +} +`); + runCli('generate', workDir); + expect(fs.existsSync(path.join(workDir, 'zenstack/schema.prisma'))).toBe(true); + }); + + it('can generate a Prisma schema with custom output', () => { + const workDir = createProject(` +plugin prisma { + provider = '@core/prisma' + output = './prisma' +} + +model User { + id String @id @default(cuid()) +} +`); + runCli('generate', workDir); + expect(fs.existsSync(path.join(workDir, 'zenstack/prisma/schema.prisma'))).toBe(true); + }); +}); diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json index bd22b363..8ef64682 100644 --- a/packages/cli/tsconfig.json +++ b/packages/cli/tsconfig.json @@ -1,7 +1,4 @@ { "extends": "@zenstackhq/typescript-config/base.json", - "compilerOptions": { - "baseUrl": "." - }, "include": ["src/**/*.ts"] } diff --git a/packages/common-helpers/package.json b/packages/common-helpers/package.json index 72c236a2..dd2fbef0 100644 --- a/packages/common-helpers/package.json +++ b/packages/common-helpers/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/common-helpers", - "version": "3.0.0-alpha.26", + "version": "3.0.0-alpha.27", "description": "ZenStack Common Helpers", "type": "module", "scripts": { diff --git a/packages/common-helpers/tsconfig.json b/packages/common-helpers/tsconfig.json index bd22b363..8ef64682 100644 --- a/packages/common-helpers/tsconfig.json +++ b/packages/common-helpers/tsconfig.json @@ -1,7 +1,4 @@ { "extends": "@zenstackhq/typescript-config/base.json", - "compilerOptions": { - "baseUrl": "." - }, "include": ["src/**/*.ts"] } diff --git a/packages/create-zenstack/package.json b/packages/create-zenstack/package.json index 6e4c5b5b..82458150 100644 --- a/packages/create-zenstack/package.json +++ b/packages/create-zenstack/package.json @@ -1,6 +1,6 @@ { "name": "create-zenstack", - "version": "3.0.0-alpha.26", + "version": "3.0.0-alpha.27", "description": "Create a new ZenStack project", "type": "module", "scripts": { diff --git a/packages/create-zenstack/tsconfig.json b/packages/create-zenstack/tsconfig.json index bd22b363..8ef64682 100644 --- a/packages/create-zenstack/tsconfig.json +++ b/packages/create-zenstack/tsconfig.json @@ -1,7 +1,4 @@ { "extends": "@zenstackhq/typescript-config/base.json", - "compilerOptions": { - "baseUrl": "." - }, "include": ["src/**/*.ts"] } diff --git a/packages/dialects/sql.js/package.json b/packages/dialects/sql.js/package.json index cd15a02a..4944fc4b 100644 --- a/packages/dialects/sql.js/package.json +++ b/packages/dialects/sql.js/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/kysely-sql-js", - "version": "3.0.0-alpha.26", + "version": "3.0.0-alpha.27", "description": "Kysely dialect for sql.js", "type": "module", "scripts": { diff --git a/packages/dialects/sql.js/tsconfig.json b/packages/dialects/sql.js/tsconfig.json index 7b457d06..41472d08 100644 --- a/packages/dialects/sql.js/tsconfig.json +++ b/packages/dialects/sql.js/tsconfig.json @@ -1,7 +1,4 @@ { "extends": "@zenstackhq/typescript-config/base.json", - "compilerOptions": { - "baseUrl": "." - }, "include": ["src/**/*"] } diff --git a/packages/eslint-config/package.json b/packages/eslint-config/package.json index 957042e4..5882e1b7 100644 --- a/packages/eslint-config/package.json +++ b/packages/eslint-config/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/eslint-config", - "version": "3.0.0-alpha.26", + "version": "3.0.0-alpha.27", "type": "module", "private": true, "license": "MIT" diff --git a/packages/ide/vscode/package.json b/packages/ide/vscode/package.json index 0359609b..6eb8ace1 100644 --- a/packages/ide/vscode/package.json +++ b/packages/ide/vscode/package.json @@ -1,7 +1,7 @@ { "name": "zenstack", "publisher": "zenstack", - "version": "3.0.0-alpha.26", + "version": "3.0.0-alpha.27", "displayName": "ZenStack Language Tools", "description": "VSCode extension for ZenStack ZModel language", "private": true, diff --git a/packages/ide/vscode/tsconfig.json b/packages/ide/vscode/tsconfig.json index bd22b363..8ef64682 100644 --- a/packages/ide/vscode/tsconfig.json +++ b/packages/ide/vscode/tsconfig.json @@ -1,7 +1,4 @@ { "extends": "@zenstackhq/typescript-config/base.json", - "compilerOptions": { - "baseUrl": "." - }, "include": ["src/**/*.ts"] } diff --git a/packages/language/package.json b/packages/language/package.json index e7797f08..92e19508 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/language", "description": "ZenStack ZModel language specification", - "version": "3.0.0-alpha.26", + "version": "3.0.0-alpha.27", "license": "MIT", "author": "ZenStack Team", "files": [ diff --git a/packages/language/src/validators/datamodel-validator.ts b/packages/language/src/validators/datamodel-validator.ts index cbcbf896..40d74dbf 100644 --- a/packages/language/src/validators/datamodel-validator.ts +++ b/packages/language/src/validators/datamodel-validator.ts @@ -443,7 +443,7 @@ export default class DataModelValidator implements AstValidator { invariant(model.baseModel.ref, 'baseModel must be resolved'); // check if the base model is a delegate model - if (!isDelegateModel(model.baseModel.ref)) { + if (!isDelegateModel(model.baseModel.ref!)) { accept('error', `Model ${model.baseModel.$refText} cannot be extended because it's not a delegate model`, { node: model, property: 'baseModel', diff --git a/packages/language/tsconfig.json b/packages/language/tsconfig.json index bd22b363..8ef64682 100644 --- a/packages/language/tsconfig.json +++ b/packages/language/tsconfig.json @@ -1,7 +1,4 @@ { "extends": "@zenstackhq/typescript-config/base.json", - "compilerOptions": { - "baseUrl": "." - }, "include": ["src/**/*.ts"] } diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 401e064f..9fa2db84 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/runtime", - "version": "3.0.0-alpha.26", + "version": "3.0.0-alpha.27", "description": "ZenStack Runtime", "type": "module", "scripts": { diff --git a/packages/runtime/src/client/client-impl.ts b/packages/runtime/src/client/client-impl.ts index d5f65a46..be1f2dff 100644 --- a/packages/runtime/src/client/client-impl.ts +++ b/packages/runtime/src/client/client-impl.ts @@ -14,7 +14,7 @@ import type { AuthType } from '../schema/auth'; import type { UnwrapTuplePromises } from '../utils/type-utils'; import type { ClientConstructor, ClientContract, ModelOperations, TransactionIsolationLevel } from './contract'; import { AggregateOperationHandler } from './crud/operations/aggregate'; -import type { CrudOperation } from './crud/operations/base'; +import type { AllCrudOperation, CoreCrudOperation } from './crud/operations/base'; import { BaseOperationHandler } from './crud/operations/base'; import { CountOperationHandler } from './crud/operations/count'; import { CreateOperationHandler } from './crud/operations/create'; @@ -351,7 +351,8 @@ function createModelCrudHandler, ): ModelOperations { const createPromise = ( - operation: CrudOperation, + operation: CoreCrudOperation, + nominalOperation: AllCrudOperation, args: unknown, handler: BaseOperationHandler, postProcess = false, @@ -383,7 +384,7 @@ function createModelCrudHandler { return createPromise( + 'findUnique', 'findUnique', args, new FindOperationHandler(client, model, inputValidator), @@ -410,6 +412,7 @@ function createModelCrudHandler { return createPromise( 'findUnique', + 'findUniqueOrThrow', args, new FindOperationHandler(client, model, inputValidator), true, @@ -419,6 +422,7 @@ function createModelCrudHandler { return createPromise( + 'findFirst', 'findFirst', args, new FindOperationHandler(client, model, inputValidator), @@ -429,6 +433,7 @@ function createModelCrudHandler { return createPromise( 'findFirst', + 'findFirstOrThrow', args, new FindOperationHandler(client, model, inputValidator), true, @@ -438,6 +443,7 @@ function createModelCrudHandler { return createPromise( + 'findMany', 'findMany', args, new FindOperationHandler(client, model, inputValidator), @@ -447,6 +453,7 @@ function createModelCrudHandler { return createPromise( + 'create', 'create', args, new CreateOperationHandler(client, model, inputValidator), @@ -456,6 +463,7 @@ function createModelCrudHandler { return createPromise( + 'createMany', 'createMany', args, new CreateOperationHandler(client, model, inputValidator), @@ -465,6 +473,7 @@ function createModelCrudHandler { return createPromise( + 'createManyAndReturn', 'createManyAndReturn', args, new CreateOperationHandler(client, model, inputValidator), @@ -474,6 +483,7 @@ function createModelCrudHandler { return createPromise( + 'update', 'update', args, new UpdateOperationHandler(client, model, inputValidator), @@ -483,6 +493,7 @@ function createModelCrudHandler { return createPromise( + 'updateMany', 'updateMany', args, new UpdateOperationHandler(client, model, inputValidator), @@ -492,6 +503,7 @@ function createModelCrudHandler { return createPromise( + 'updateManyAndReturn', 'updateManyAndReturn', args, new UpdateOperationHandler(client, model, inputValidator), @@ -501,6 +513,7 @@ function createModelCrudHandler { return createPromise( + 'upsert', 'upsert', args, new UpdateOperationHandler(client, model, inputValidator), @@ -510,6 +523,7 @@ function createModelCrudHandler { return createPromise( + 'delete', 'delete', args, new DeleteOperationHandler(client, model, inputValidator), @@ -519,6 +533,7 @@ function createModelCrudHandler { return createPromise( + 'deleteMany', 'deleteMany', args, new DeleteOperationHandler(client, model, inputValidator), @@ -528,6 +543,7 @@ function createModelCrudHandler { return createPromise( + 'count', 'count', args, new CountOperationHandler(client, model, inputValidator), @@ -537,6 +553,7 @@ function createModelCrudHandler { return createPromise( + 'aggregate', 'aggregate', args, new AggregateOperationHandler(client, model, inputValidator), @@ -546,6 +563,7 @@ function createModelCrudHandler { return createPromise( + 'groupBy', 'groupBy', args, new GroupByOperationHandler(client, model, inputValidator), diff --git a/packages/runtime/src/client/crud-types.ts b/packages/runtime/src/client/crud-types.ts index 728082d7..fd1918df 100644 --- a/packages/runtime/src/client/crud-types.ts +++ b/packages/runtime/src/client/crud-types.ts @@ -437,14 +437,6 @@ export type SelectIncludeOmit; }; -type Distinct> = { - distinct?: OrArray>; -}; - -type Cursor> = { - cursor?: WhereUniqueInput; -}; - export type SelectInput< Schema extends SchemaDef, Model extends GetModels, @@ -621,25 +613,34 @@ type OppositeRelationAndFK< //#region Find args +type FilterArgs> = { + where?: WhereInput; +}; + +type SortAndTakeArgs> = { + skip?: number; + take?: number; + orderBy?: OrArray>; + cursor?: WhereUniqueInput; +}; + export type FindArgs< Schema extends SchemaDef, Model extends GetModels, Collection extends boolean, AllowFilter extends boolean = true, -> = (Collection extends true - ? { - skip?: number; - take?: number; - orderBy?: OrArray>; - } & Distinct & - Cursor - : {}) & - (AllowFilter extends true - ? { - where?: WhereInput; - } - : {}) & - SelectIncludeOmit; +> = + ProviderSupportsDistinct extends true + ? (Collection extends true + ? SortAndTakeArgs & { + distinct?: OrArray>; + } + : {}) & + (AllowFilter extends true ? FilterArgs : {}) & + SelectIncludeOmit + : (Collection extends true ? SortAndTakeArgs : {}) & + (AllowFilter extends true ? FilterArgs : {}) & + SelectIncludeOmit; export type FindManyArgs> = FindArgs; export type FindFirstArgs> = FindArgs; @@ -1259,6 +1260,12 @@ type HasToManyRelations = Schema['provider'] extends 'postgresql' ? true : false; +type ProviderSupportsCaseSensitivity = Schema['provider']['type'] extends 'postgresql' + ? true + : false; + +type ProviderSupportsDistinct = Schema['provider']['type'] extends 'postgresql' + ? true + : false; // #endregion diff --git a/packages/runtime/src/client/crud/dialects/base.ts b/packages/runtime/src/client/crud/dialects/base.ts index 2c1738cd..34d3ffd3 100644 --- a/packages/runtime/src/client/crud/dialects/base.ts +++ b/packages/runtime/src/client/crud/dialects/base.ts @@ -21,6 +21,7 @@ import { aggregate, buildFieldRef, buildJoinPairs, + ensureArray, flattenCompoundUniqueFilters, getDelegateDescendantModels, getIdFields, @@ -58,6 +59,54 @@ export abstract class BaseCrudDialect { return result; } + buildFilterSortTake( + model: GetModels, + args: FindArgs, true>, + query: SelectQueryBuilder, + ) { + let result = query; + + // where + if (args.where) { + result = result.where((eb) => this.buildFilter(eb, model, model, args?.where)); + } + + // skip && take + let negateOrderBy = false; + const skip = args.skip; + let take = args.take; + if (take !== undefined && take < 0) { + negateOrderBy = true; + take = -take; + } + result = this.buildSkipTake(result, skip, take); + + // orderBy + result = this.buildOrderBy( + result, + model, + model, + args.orderBy, + skip !== undefined || take !== undefined, + negateOrderBy, + ); + + // distinct + if ('distinct' in args && (args as any).distinct) { + const distinct = ensureArray((args as any).distinct) as string[]; + if (this.supportsDistinctOn) { + result = result.distinctOn(distinct.map((f) => sql.ref(`${model}.${f}`))); + } else { + throw new QueryError(`"distinct" is not supported by "${this.schema.provider.type}" provider`); + } + } + + if (args.cursor) { + result = this.buildCursorFilter(model, result, args.cursor, args.orderBy, negateOrderBy); + } + return result; + } + buildFilter( eb: ExpressionBuilder, model: string, @@ -117,6 +166,47 @@ export abstract class BaseCrudDialect { return result; } + private buildCursorFilter( + model: string, + query: SelectQueryBuilder, + cursor: FindArgs, true>['cursor'], + orderBy: FindArgs, true>['orderBy'], + negateOrderBy: boolean, + ) { + const _orderBy = orderBy ?? makeDefaultOrderBy(this.schema, model); + + const orderByItems = ensureArray(_orderBy).flatMap((obj) => Object.entries(obj)); + + const eb = expressionBuilder(); + const cursorFilter = this.buildFilter(eb, model, model, cursor); + + let result = query; + const filters: ExpressionWrapper[] = []; + + for (let i = orderByItems.length - 1; i >= 0; i--) { + const andFilters: ExpressionWrapper[] = []; + + for (let j = 0; j <= i; j++) { + const [field, order] = orderByItems[j]!; + const _order = negateOrderBy ? (order === 'asc' ? 'desc' : 'asc') : order; + const op = j === i ? (_order === 'asc' ? '>=' : '<=') : '='; + andFilters.push( + eb( + eb.ref(`${model}.${field}`), + op, + eb.selectFrom(model).select(`${model}.${field}`).where(cursorFilter), + ), + ); + } + + filters.push(eb.and(andFilters)); + } + + result = result.where((eb) => eb.or(filters)); + + return result; + } + private isLogicalCombinator(key: string): key is (typeof LOGICAL_COMBINATORS)[number] { return LOGICAL_COMBINATORS.includes(key as any); } @@ -722,7 +812,7 @@ export abstract class BaseCrudDialect { // aggregations if (['_count', '_avg', '_sum', '_min', '_max'].includes(field)) { invariant(value && typeof value === 'object', `invalid orderBy value for field "${field}"`); - for (const [k, v] of Object.entries(value)) { + for (const [k, v] of Object.entries(value)) { invariant(v === 'asc' || v === 'desc', `invalid orderBy value for field "${field}"`); result = result.orderBy( (eb) => diff --git a/packages/runtime/src/client/crud/dialects/postgresql.ts b/packages/runtime/src/client/crud/dialects/postgresql.ts index 08d07950..6d10fc12 100644 --- a/packages/runtime/src/client/crud/dialects/postgresql.ts +++ b/packages/runtime/src/client/crud/dialects/postgresql.ts @@ -91,31 +91,7 @@ export class PostgresCrudDialect extends BaseCrudDiale ); if (payload && typeof payload === 'object') { - if (payload.where) { - subQuery = subQuery.where((eb) => - this.buildFilter(eb, relationModel, relationModel, payload.where), - ); - } - - // skip & take - const skip = payload.skip; - let take = payload.take; - let negateOrderBy = false; - if (take !== undefined && take < 0) { - negateOrderBy = true; - take = -take; - } - subQuery = this.buildSkipTake(subQuery, skip, take); - - // orderBy - subQuery = this.buildOrderBy( - subQuery, - relationModel, - relationModel, - payload.orderBy, - skip !== undefined || take !== undefined, - negateOrderBy, - ); + subQuery = this.buildFilterSortTake(relationModel, payload, subQuery); } // add join conditions diff --git a/packages/runtime/src/client/crud/dialects/sqlite.ts b/packages/runtime/src/client/crud/dialects/sqlite.ts index 3a2a4868..747337fe 100644 --- a/packages/runtime/src/client/crud/dialects/sqlite.ts +++ b/packages/runtime/src/client/crud/dialects/sqlite.ts @@ -85,31 +85,8 @@ export class SqliteCrudDialect extends BaseCrudDialect ); if (payload && typeof payload === 'object') { - if (payload.where) { - subQuery = subQuery.where((eb) => - this.buildFilter(eb, relationModel, relationModel, payload.where), - ); - } - - // skip & take - const skip = payload.skip; - let take = payload.take; - let negateOrderBy = false; - if (take !== undefined && take < 0) { - negateOrderBy = true; - take = -take; - } - subQuery = this.buildSkipTake(subQuery, skip, take); - - // orderBy - subQuery = this.buildOrderBy( - subQuery, - relationModel, - relationModel, - payload.orderBy, - skip !== undefined || take !== undefined, - negateOrderBy, - ); + // take care of where, orderBy, skip, take, cursor, and distinct + subQuery = this.buildFilterSortTake(relationModel, payload, subQuery); } // join conditions diff --git a/packages/runtime/src/client/crud/operations/base.ts b/packages/runtime/src/client/crud/operations/base.ts index f11ad80c..19fca142 100644 --- a/packages/runtime/src/client/crud/operations/base.ts +++ b/packages/runtime/src/client/crud/operations/base.ts @@ -3,7 +3,6 @@ import { invariant, isPlainObject } from '@zenstackhq/common-helpers'; import { DeleteResult, expressionBuilder, - ExpressionWrapper, sql, UpdateResult, type Compilable, @@ -25,7 +24,7 @@ import { enumerate } from '../../../utils/enumerate'; import { extractFields, fieldsToSelectObject } from '../../../utils/object-utils'; import { NUMERIC_FIELD_TYPES } from '../../constants'; import type { CRUD } from '../../contract'; -import type { FindArgs, SelectIncludeOmit, SortOrder, WhereInput } from '../../crud-types'; +import type { FindArgs, SelectIncludeOmit, WhereInput } from '../../crud-types'; import { InternalError, NotFoundError, QueryError } from '../../errors'; import type { ToKysely } from '../../query-builder'; import { @@ -42,16 +41,14 @@ import { isForeignKeyField, isRelationField, isScalarField, - makeDefaultOrderBy, requireField, requireModel, - safeJSONStringify, } from '../../query-utils'; import { getCrudDialect } from '../dialects'; import type { BaseCrudDialect } from '../dialects/base'; import { InputValidator } from '../validator'; -export type CrudOperation = +export type CoreCrudOperation = | 'findMany' | 'findUnique' | 'findFirst' @@ -68,7 +65,7 @@ export type CrudOperation = | 'aggregate' | 'groupBy'; -export type AllCrudOperation = CrudOperation | 'findUniqueOrThrow' | 'findFirstOrThrow'; +export type AllCrudOperation = CoreCrudOperation | 'findUniqueOrThrow' | 'findFirstOrThrow'; export type FromRelationContext = { model: GetModels; @@ -99,7 +96,7 @@ export abstract class BaseOperationHandler { return this.client.$qb; } - abstract handle(operation: CrudOperation, args: any): Promise; + abstract handle(operation: CoreCrudOperation, args: any): Promise; withClient(client: ClientContract) { return new (this.constructor as new (...args: any[]) => this)(client, this.model, this.inputValidator); @@ -150,48 +147,8 @@ export abstract class BaseOperationHandler { // table let query = this.dialect.buildSelectModel(expressionBuilder(), model); - // where - if (args?.where) { - query = query.where((eb) => this.dialect.buildFilter(eb, model, model, args?.where)); - } - - // skip && take - let negateOrderBy = false; - const skip = args?.skip; - let take = args?.take; - if (take !== undefined && take < 0) { - negateOrderBy = true; - take = -take; - } - query = this.dialect.buildSkipTake(query, skip, take); - - // orderBy - query = this.dialect.buildOrderBy( - query, - model, - model, - args?.orderBy, - skip !== undefined || take !== undefined, - negateOrderBy, - ); - - // distinct - let inMemoryDistinct: string[] | undefined = undefined; - if (args?.distinct) { - const distinct = ensureArray(args.distinct) as string[]; - if (this.dialect.supportsDistinctOn) { - query = query.distinctOn(distinct.map((f) => sql.ref(`${model}.${f}`))); - } else { - // in-memory distinct after fetching all results - inMemoryDistinct = distinct; - - // make sure distinct fields are selected - query = distinct.reduce( - (acc, field) => - acc.select((eb) => this.dialect.fieldRef(model, field, eb).as(`$distinct$${field}`)), - query, - ); - } + if (args) { + query = this.dialect.buildFilterSortTake(model, args, query); } // select @@ -209,10 +166,6 @@ export abstract class BaseOperationHandler { query = this.buildFieldSelection(model, query, args.include, model); } - if (args?.cursor) { - query = this.buildCursorFilter(model, query, args.cursor, args.orderBy, negateOrderBy); - } - query = query.modifyEnd(this.makeContextComment({ model, operation: 'read' })); let result: any[] = []; @@ -229,26 +182,6 @@ export abstract class BaseOperationHandler { throw new QueryError(message, err); } - if (inMemoryDistinct) { - const distinctResult: Record[] = []; - const seen = new Set(); - for (const r of result as any[]) { - const key = safeJSONStringify(inMemoryDistinct.map((f) => r[`$distinct$${f}`]))!; - if (!seen.has(key)) { - distinctResult.push(r); - seen.add(key); - } - } - result = distinctResult; - - // clean up distinct utility fields - for (const r of result) { - Object.keys(r) - .filter((k) => k.startsWith('$distinct$')) - .forEach((k) => delete r[k]); - } - } - return result; } @@ -314,49 +247,6 @@ export abstract class BaseOperationHandler { return query.select((eb) => this.dialect.buildCountJson(model, eb, parentAlias, payload).as('_count')); } - private buildCursorFilter( - model: string, - query: SelectQueryBuilder, - cursor: FindArgs, true>['cursor'], - orderBy: FindArgs, true>['orderBy'], - negateOrderBy: boolean, - ) { - if (!orderBy) { - orderBy = makeDefaultOrderBy(this.schema, model); - } - - const orderByItems = ensureArray(orderBy).flatMap((obj) => Object.entries(obj)); - - const eb = expressionBuilder(); - const cursorFilter = this.dialect.buildFilter(eb, model, model, cursor); - - let result = query; - const filters: ExpressionWrapper[] = []; - - for (let i = orderByItems.length - 1; i >= 0; i--) { - const andFilters: ExpressionWrapper[] = []; - - for (let j = 0; j <= i; j++) { - const [field, order] = orderByItems[j]!; - const _order = negateOrderBy ? (order === 'asc' ? 'desc' : 'asc') : order; - const op = j === i ? (_order === 'asc' ? '>=' : '<=') : '='; - andFilters.push( - eb( - eb.ref(`${model}.${field}`), - op, - eb.selectFrom(model).select(`${model}.${field}`).where(cursorFilter), - ), - ); - } - - filters.push(eb.and(andFilters)); - } - - result = result.where((eb) => eb.or(filters)); - - return result; - } - protected async create( kysely: ToKysely, model: GetModels, diff --git a/packages/runtime/src/client/crud/operations/find.ts b/packages/runtime/src/client/crud/operations/find.ts index 77bbb615..ef2b60be 100644 --- a/packages/runtime/src/client/crud/operations/find.ts +++ b/packages/runtime/src/client/crud/operations/find.ts @@ -1,9 +1,9 @@ import type { GetModels, SchemaDef } from '../../../schema'; import type { FindArgs } from '../../crud-types'; -import { BaseOperationHandler, type CrudOperation } from './base'; +import { BaseOperationHandler, type CoreCrudOperation } from './base'; export class FindOperationHandler extends BaseOperationHandler { - async handle(operation: CrudOperation, args: unknown, validateArgs = true): Promise { + async handle(operation: CoreCrudOperation, args: unknown, validateArgs = true): Promise { // normalize args to strip `undefined` fields const normalizedArgs = this.normalizeArgs(args); diff --git a/packages/runtime/src/client/crud/validator.ts b/packages/runtime/src/client/crud/validator.ts index be83d102..eb9ecf21 100644 --- a/packages/runtime/src/client/crud/validator.ts +++ b/packages/runtime/src/client/crud/validator.ts @@ -627,30 +627,32 @@ export class InputValidator { } private makeRelationSelectIncludeSchema(fieldDef: FieldDef) { - return z.union([ - z.boolean(), - z.strictObject({ - ...(fieldDef.array || fieldDef.optional - ? { - // to-many relations and optional to-one relations are filterable - where: z.lazy(() => this.makeWhereSchema(fieldDef.type, false)).optional(), - } - : {}), - select: z.lazy(() => this.makeSelectSchema(fieldDef.type)).optional(), - include: z.lazy(() => this.makeIncludeSchema(fieldDef.type)).optional(), - omit: z.lazy(() => this.makeOmitSchema(fieldDef.type)).optional(), - ...(fieldDef.array - ? { - // to-many relations can be ordered, skipped, taken, and cursor-located - orderBy: z.lazy(() => this.makeOrderBySchema(fieldDef.type, true, false)).optional(), - skip: this.makeSkipSchema().optional(), - take: this.makeTakeSchema().optional(), - cursor: this.makeCursorSchema(fieldDef.type).optional(), - distinct: this.makeDistinctSchema(fieldDef.type).optional(), - } - : {}), - }), - ]); + let objSchema: z.ZodType = z.strictObject({ + ...(fieldDef.array || fieldDef.optional + ? { + // to-many relations and optional to-one relations are filterable + where: z.lazy(() => this.makeWhereSchema(fieldDef.type, false)).optional(), + } + : {}), + select: z.lazy(() => this.makeSelectSchema(fieldDef.type)).optional(), + include: z.lazy(() => this.makeIncludeSchema(fieldDef.type)).optional(), + omit: z.lazy(() => this.makeOmitSchema(fieldDef.type)).optional(), + ...(fieldDef.array + ? { + // to-many relations can be ordered, skipped, taken, and cursor-located + orderBy: z.lazy(() => this.makeOrderBySchema(fieldDef.type, true, false)).optional(), + skip: this.makeSkipSchema().optional(), + take: this.makeTakeSchema().optional(), + cursor: this.makeCursorSchema(fieldDef.type).optional(), + distinct: this.makeDistinctSchema(fieldDef.type).optional(), + } + : {}), + }); + + objSchema = this.refineForSelectIncludeMutuallyExclusive(objSchema); + objSchema = this.refineForSelectOmitMutuallyExclusive(objSchema); + + return z.union([z.boolean(), objSchema]); } private makeOmitSchema(model: string) { @@ -742,7 +744,7 @@ export class InputValidator { private makeCreateSchema(model: string) { const dataSchema = this.makeCreateDataSchema(model, false); - const schema = z.object({ + const schema = z.strictObject({ data: dataSchema, select: this.makeSelectSchema(model).optional(), include: this.makeIncludeSchema(model).optional(), @@ -757,12 +759,10 @@ export class InputValidator { private makeCreateManyAndReturnSchema(model: string) { const base = this.makeCreateManyDataSchema(model, []); - const result = base.merge( - z.strictObject({ - select: this.makeSelectSchema(model).optional(), - omit: this.makeOmitSchema(model).optional(), - }), - ); + const result = base.extend({ + select: this.makeSelectSchema(model).optional(), + omit: this.makeOmitSchema(model).optional(), + }); return this.refineForSelectOmitMutuallyExclusive(result).optional(); } @@ -986,7 +986,7 @@ export class InputValidator { const whereSchema = this.makeWhereSchema(model, true); const createSchema = this.makeCreateDataSchema(model, false, withoutFields); return this.orArray( - z.object({ + z.strictObject({ where: whereSchema, create: createSchema, }), @@ -995,7 +995,7 @@ export class InputValidator { } private makeCreateManyDataSchema(model: string, withoutFields: string[]) { - return z.object({ + return z.strictObject({ data: this.makeCreateDataSchema(model, true, withoutFields, true), skipDuplicates: z.boolean().optional(), }); @@ -1006,7 +1006,7 @@ export class InputValidator { // #region Update private makeUpdateSchema(model: string) { - const schema = z.object({ + const schema = z.strictObject({ where: this.makeWhereSchema(model, true), data: this.makeUpdateDataSchema(model), select: this.makeSelectSchema(model).optional(), @@ -1017,7 +1017,7 @@ export class InputValidator { } private makeUpdateManySchema(model: string) { - return z.object({ + return z.strictObject({ where: this.makeWhereSchema(model, false).optional(), data: this.makeUpdateDataSchema(model, [], true), limit: z.int().nonnegative().optional(), @@ -1026,17 +1026,15 @@ export class InputValidator { private makeUpdateManyAndReturnSchema(model: string) { const base = this.makeUpdateManySchema(model); - const result = base.merge( - z.strictObject({ - select: this.makeSelectSchema(model).optional(), - omit: this.makeOmitSchema(model).optional(), - }), - ); + const result = base.extend({ + select: this.makeSelectSchema(model).optional(), + omit: this.makeOmitSchema(model).optional(), + }); return this.refineForSelectOmitMutuallyExclusive(result); } private makeUpsertSchema(model: string) { - const schema = z.object({ + const schema = z.strictObject({ where: this.makeWhereSchema(model, true), create: this.makeCreateDataSchema(model, false), update: this.makeUpdateDataSchema(model), @@ -1148,7 +1146,7 @@ export class InputValidator { // #region Delete private makeDeleteSchema(model: GetModels) { - const schema = z.object({ + const schema = z.strictObject({ where: this.makeWhereSchema(model, true), select: this.makeSelectSchema(model).optional(), include: this.makeIncludeSchema(model).optional(), @@ -1187,7 +1185,7 @@ export class InputValidator { const modelDef = requireModel(this.schema, model); return z.union([ z.literal(true), - z.object({ + z.strictObject({ _all: z.literal(true).optional(), ...Object.keys(modelDef.fields).reduce( (acc, field) => { @@ -1257,7 +1255,7 @@ export class InputValidator { const modelDef = requireModel(this.schema, model); const nonRelationFields = Object.keys(modelDef.fields).filter((field) => !modelDef.fields[field]?.relation); - let schema = z.object({ + let schema = z.strictObject({ where: this.makeWhereSchema(model, false).optional(), orderBy: this.orArray(this.makeOrderBySchema(model, false, true), true).optional(), by: this.orArray(z.enum(nonRelationFields), true), diff --git a/packages/runtime/src/client/helpers/schema-db-pusher.ts b/packages/runtime/src/client/helpers/schema-db-pusher.ts index 4978dedf..bbfa2a3e 100644 --- a/packages/runtime/src/client/helpers/schema-db-pusher.ts +++ b/packages/runtime/src/client/helpers/schema-db-pusher.ts @@ -188,6 +188,10 @@ export class SchemaDbPusher { return 'serial'; } + if (this.isCustomType(fieldDef.type)) { + return 'jsonb'; + } + const type = fieldDef.type as BuiltinType; const result = match(type) .with('String', () => 'text') @@ -211,6 +215,10 @@ export class SchemaDbPusher { } } + private isCustomType(type: string) { + return this.schema.typeDefs && Object.values(this.schema.typeDefs).some((def) => def.name === type); + } + private isAutoIncrement(fieldDef: FieldDef) { return ( fieldDef.default && diff --git a/packages/runtime/src/client/plugin.ts b/packages/runtime/src/client/plugin.ts index 99ee2d92..b8d6e314 100644 --- a/packages/runtime/src/client/plugin.ts +++ b/packages/runtime/src/client/plugin.ts @@ -46,7 +46,7 @@ export function definePlugin(plugin: RuntimePlugin { +describe.each(createClientSpecs(PG_DB_NAME))('Client find tests for $provider', ({ createClient, provider }) => { let client: ClientContract; beforeEach(async () => { @@ -241,10 +241,11 @@ describe.each(createClientSpecs(PG_DB_NAME))('Client find tests for $provider', }); it('works with distinct', async () => { - await createUser(client, 'u1@test.com', { + const user1 = await createUser(client, 'u1@test.com', { name: 'Admin1', role: 'ADMIN', profile: { create: { bio: 'Bio1' } }, + posts: { create: [{ title: 'Post1' }, { title: 'Post1' }, { title: 'Post2' }] }, }); await createUser(client, 'u3@test.com', { name: 'User', @@ -260,8 +261,13 @@ describe.each(createClientSpecs(PG_DB_NAME))('Client find tests for $provider', role: 'USER', }); + if (provider === 'sqlite') { + await expect(client.user.findMany({ distinct: ['role'] } as any)).rejects.toThrow('not supported'); + return; + } + // single field distinct - let r: any = await client.user.findMany({ distinct: ['role'] }); + let r: any = await client.user.findMany({ distinct: ['role'] } as any); expect(r).toHaveLength(2); expect(r).toEqual( expect.arrayContaining([ @@ -270,8 +276,15 @@ describe.each(createClientSpecs(PG_DB_NAME))('Client find tests for $provider', ]), ); + // distinct in relation + r = await client.user.findUnique({ + where: { id: user1.id }, + include: { posts: { distinct: ['title'] } as any }, + }); + expect(r.posts).toHaveLength(2); + // distinct with include - r = await client.user.findMany({ distinct: ['role'], include: { profile: true } }); + r = await client.user.findMany({ distinct: ['role'], include: { profile: true } } as any); expect(r).toHaveLength(2); expect(r).toEqual( expect.arrayContaining([ @@ -281,14 +294,14 @@ describe.each(createClientSpecs(PG_DB_NAME))('Client find tests for $provider', ); // distinct with select - r = await client.user.findMany({ distinct: ['role'], select: { email: true } }); + r = await client.user.findMany({ distinct: ['role'], select: { email: true } } as any); expect(r).toHaveLength(2); expect(r).toEqual(expect.arrayContaining([{ email: expect.any(String) }, { email: expect.any(String) }])); // multiple fields distinct r = await client.user.findMany({ distinct: ['role', 'name'], - }); + } as any); expect(r).toHaveLength(3); expect(r).toEqual( expect.arrayContaining([ @@ -658,26 +671,28 @@ describe.each(createClientSpecs(PG_DB_NAME))('Client find tests for $provider', posts: [expect.objectContaining({ title: 'Post1' })], }); - await expect( - client.user.findUnique({ - where: { id: user.id }, - select: { - posts: { orderBy: { title: 'asc' }, skip: 1, take: 1, distinct: ['title'] }, - }, - }), - ).resolves.toMatchObject({ - posts: [expect.objectContaining({ title: 'Post2' })], - }); - await expect( - client.user.findUnique({ - where: { id: user.id }, - include: { - posts: { orderBy: { title: 'asc' }, skip: 1, take: 1, distinct: ['title'] }, - }, - }), - ).resolves.toMatchObject({ - posts: [expect.objectContaining({ title: 'Post2' })], - }); + if (provider === 'postgresql') { + await expect( + client.user.findUnique({ + where: { id: user.id }, + select: { + posts: { orderBy: { title: 'asc' }, skip: 1, take: 1, distinct: ['title'] } as any, + }, + }), + ).resolves.toMatchObject({ + posts: [expect.objectContaining({ title: 'Post2' })], + }); + await expect( + client.user.findUnique({ + where: { id: user.id }, + include: { + posts: { orderBy: { title: 'asc' }, skip: 1, take: 1, distinct: ['title'] } as any, + }, + }), + ).resolves.toMatchObject({ + posts: [expect.objectContaining({ title: 'Post2' })], + }); + } await expect( client.post.findFirst({ @@ -895,6 +910,19 @@ describe.each(createClientSpecs(PG_DB_NAME))('Client find tests for $provider', }); expect(u.posts[0]).toMatchObject(post1); + // cursor + u = await client.user.findUniqueOrThrow({ + where: { id: user.id }, + include: { + posts: { + orderBy: { title: 'asc' }, + cursor: { id: post2.id }, + }, + }, + }); + expect(u.posts).toHaveLength(1); + expect(u.posts?.[0]?.id).toBe(post2.id); + // skip and take u = await client.user.findUniqueOrThrow({ where: { id: user.id }, diff --git a/packages/runtime/test/schemas/typing/typecheck.ts b/packages/runtime/test/schemas/typing/typecheck.ts index fe35c9a1..e90b0f82 100644 --- a/packages/runtime/test/schemas/typing/typecheck.ts +++ b/packages/runtime/test/schemas/typing/typecheck.ts @@ -84,7 +84,6 @@ async function find() { email: 'asc', name: 'desc', }, - distinct: ['name'], cursor: { id: 1 }, }); diff --git a/packages/runtime/test/scripts/generate.ts b/packages/runtime/test/scripts/generate.ts index a5cf10e9..a1393e30 100644 --- a/packages/runtime/test/scripts/generate.ts +++ b/packages/runtime/test/scripts/generate.ts @@ -1,3 +1,4 @@ +import { loadDocument } from '@zenstackhq/language'; import { TsSchemaGenerator } from '@zenstackhq/sdk'; import { glob } from 'glob'; import fs from 'node:fs'; @@ -20,7 +21,11 @@ async function generate(schemaPath: string) { const outputDir = path.dirname(schemaPath); const tsPath = path.join(outputDir, 'schema.ts'); const pluginModelFiles = glob.sync(path.resolve(dir, '../../dist/**/plugin.zmodel')); - await generator.generate(schemaPath, pluginModelFiles, outputDir); + const result = await loadDocument(schemaPath, pluginModelFiles); + if (!result.success) { + throw new Error(`Failed to load schema from ${schemaPath}: ${result.errors}`); + } + await generator.generate(result.model, outputDir); const content = fs.readFileSync(tsPath, 'utf-8'); fs.writeFileSync(tsPath, content.replace(/@zenstackhq\/runtime/g, '../../../dist')); console.log('TS schema generated at:', outputDir); diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 98105ecc..f9ea888b 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/sdk", - "version": "3.0.0-alpha.26", + "version": "3.0.0-alpha.27", "description": "ZenStack SDK", "type": "module", "scripts": { diff --git a/packages/sdk/src/cli-plugin.ts b/packages/sdk/src/cli-plugin.ts new file mode 100644 index 00000000..534e0c84 --- /dev/null +++ b/packages/sdk/src/cli-plugin.ts @@ -0,0 +1,47 @@ +import type { Model } from '@zenstackhq/language/ast'; +import type { MaybePromise } from 'langium'; + +/** + * Context passed to CLI plugins when calling `generate`. + */ +export type CliGeneratorContext = { + /** + * ZModel file path. + */ + schemaFile: string; + + /** + * ZModel AST. + */ + model: Model; + + /** + * Default output path for code generation. + */ + defaultOutputPath: string; + + /** + * Plugin options provided by the user. + */ + pluginOptions: Record; +}; + +/** + * Contract for a CLI plugin. + */ +export interface CliPlugin { + /** + * Plugin's display name. + */ + name: string; + + /** + * Text to show during generation. + */ + statusText?: string; + + /** + * Code generation callback. + */ + generate(context: CliGeneratorContext): MaybePromise; +} diff --git a/packages/sdk/src/generator.ts b/packages/sdk/src/generator.ts deleted file mode 100644 index 3868b692..00000000 --- a/packages/sdk/src/generator.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { Model } from '@zenstackhq/language/ast'; -import type { MaybePromise } from 'langium'; - -export type CliGeneratorContext = { - model: Model; - outputPath: string; - tsSchemaFile: string; -}; - -export type CliGenerator = (context: CliGeneratorContext) => MaybePromise; diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index 313d15ae..649a7201 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -1,5 +1,5 @@ import * as ModelUtils from './model-utils'; -export * from './generator'; +export * from './cli-plugin'; export { PrismaSchemaGenerator } from './prisma/prisma-schema-generator'; export * from './ts-schema-generator'; export * from './zmodel-code-generator'; diff --git a/packages/sdk/src/ts-schema-generator.ts b/packages/sdk/src/ts-schema-generator.ts index dd8fb49d..d113a3b9 100644 --- a/packages/sdk/src/ts-schema-generator.ts +++ b/packages/sdk/src/ts-schema-generator.ts @@ -1,5 +1,4 @@ import { invariant } from '@zenstackhq/common-helpers'; -import { loadDocument } from '@zenstackhq/language'; import { ArrayExpr, AttributeArg, @@ -53,14 +52,7 @@ import { } from './model-utils'; export class TsSchemaGenerator { - public async generate(schemaFile: string, pluginModelFiles: string[], outputDir: string) { - const loaded = await loadDocument(schemaFile, pluginModelFiles); - if (!loaded.success) { - throw new Error(`Error loading schema:${loaded.errors.join('\n')}`); - } - - const { model } = loaded; - + async generate(model: Model, outputDir: string) { fs.mkdirSync(outputDir, { recursive: true }); // the schema itself diff --git a/packages/sdk/tsconfig.json b/packages/sdk/tsconfig.json index 07f91d53..25cac129 100644 --- a/packages/sdk/tsconfig.json +++ b/packages/sdk/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "@zenstackhq/typescript-config/base.json", "compilerOptions": { - "baseUrl": ".", "noUnusedLocals": false }, "include": ["src/**/*.ts"] diff --git a/packages/tanstack-query/package.json b/packages/tanstack-query/package.json index 0c983361..96d6752f 100644 --- a/packages/tanstack-query/package.json +++ b/packages/tanstack-query/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/tanstack-query", - "version": "3.0.0-alpha.26", + "version": "3.0.0-alpha.27", "description": "", "main": "index.js", "type": "module", diff --git a/packages/tanstack-query/tsconfig.json b/packages/tanstack-query/tsconfig.json index a64b0eb5..e7ce31be 100644 --- a/packages/tanstack-query/tsconfig.json +++ b/packages/tanstack-query/tsconfig.json @@ -1,7 +1,4 @@ { "extends": "@zenstackhq/typescript-config/base.json", - "compilerOptions": { - "baseUrl": "." - }, "include": ["src/**/*.ts", "test/**/*.ts"] } diff --git a/packages/testtools/package.json b/packages/testtools/package.json index 216a916d..76b5fb90 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/testtools", - "version": "3.0.0-alpha.26", + "version": "3.0.0-alpha.27", "description": "ZenStack Test Tools", "type": "module", "scripts": { @@ -41,6 +41,7 @@ "pg": "^8.13.1" }, "devDependencies": { + "@types/node": "catalog:", "@types/tmp": "catalog:", "@zenstackhq/eslint-config": "workspace:*", "@zenstackhq/typescript-config": "workspace:*" diff --git a/packages/testtools/src/schema.ts b/packages/testtools/src/schema.ts index 48c43b97..788f092c 100644 --- a/packages/testtools/src/schema.ts +++ b/packages/testtools/src/schema.ts @@ -1,3 +1,4 @@ +import { loadDocument } from '@zenstackhq/language'; import { TsSchemaGenerator } from '@zenstackhq/sdk'; import type { SchemaDef } from '@zenstackhq/sdk/schema'; import { glob } from 'glob'; @@ -41,9 +42,13 @@ export async function generateTsSchema( fs.writeFileSync(zmodelPath, `${noPrelude ? '' : makePrelude(provider, dbUrl)}\n\n${schemaText}`); const pluginModelFiles = glob.sync(path.resolve(__dirname, '../../runtime/src/plugins/**/plugin.zmodel')); + const result = await loadDocument(zmodelPath, pluginModelFiles); + if (!result.success) { + throw new Error(`Failed to load schema from ${zmodelPath}: ${result.errors}`); + } const generator = new TsSchemaGenerator(); - await generator.generate(zmodelPath, pluginModelFiles, workDir); + await generator.generate(result.model, workDir); if (extraSourceFiles) { for (const [fileName, content] of Object.entries(extraSourceFiles)) { @@ -76,8 +81,11 @@ export function generateTsSchemaFromFile(filePath: string) { export async function generateTsSchemaInPlace(schemaPath: string) { const workDir = path.dirname(schemaPath); const pluginModelFiles = glob.sync(path.resolve(__dirname, '../../runtime/src/plugins/**/plugin.zmodel')); - + const result = await loadDocument(schemaPath, pluginModelFiles); + if (!result.success) { + throw new Error(`Failed to load schema from ${schemaPath}: ${result.errors}`); + } const generator = new TsSchemaGenerator(); - await generator.generate(schemaPath, pluginModelFiles, workDir); + await generator.generate(result.model, workDir); return compileAndLoad(workDir); } diff --git a/packages/testtools/tsconfig.json b/packages/testtools/tsconfig.json index a64b0eb5..e7ce31be 100644 --- a/packages/testtools/tsconfig.json +++ b/packages/testtools/tsconfig.json @@ -1,7 +1,4 @@ { "extends": "@zenstackhq/typescript-config/base.json", - "compilerOptions": { - "baseUrl": "." - }, "include": ["src/**/*.ts", "test/**/*.ts"] } diff --git a/packages/typescript-config/package.json b/packages/typescript-config/package.json index 5a51d6b9..cf54dc79 100644 --- a/packages/typescript-config/package.json +++ b/packages/typescript-config/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/typescript-config", - "version": "3.0.0-alpha.26", + "version": "3.0.0-alpha.27", "private": true, "license": "MIT" } diff --git a/packages/vitest-config/package.json b/packages/vitest-config/package.json index 516ab27c..41238acd 100644 --- a/packages/vitest-config/package.json +++ b/packages/vitest-config/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/vitest-config", "type": "module", - "version": "3.0.0-alpha.26", + "version": "3.0.0-alpha.27", "private": true, "license": "MIT", "exports": { diff --git a/packages/zod/package.json b/packages/zod/package.json index b40b2b24..eae3a50d 100644 --- a/packages/zod/package.json +++ b/packages/zod/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/zod", - "version": "3.0.0-alpha.26", + "version": "3.0.0-alpha.27", "description": "", "type": "module", "main": "index.js", diff --git a/packages/zod/src/index.ts b/packages/zod/src/index.ts index a8180b18..a7fa63e6 100644 --- a/packages/zod/src/index.ts +++ b/packages/zod/src/index.ts @@ -7,7 +7,7 @@ export function makeSelectSchema; + return z.strictObject(mapFields(schema, model)) as SelectSchema; } function mapFields(schema: Schema, model: GetModels): any { diff --git a/packages/zod/tsconfig.json b/packages/zod/tsconfig.json index a64b0eb5..e7ce31be 100644 --- a/packages/zod/tsconfig.json +++ b/packages/zod/tsconfig.json @@ -1,7 +1,4 @@ { "extends": "@zenstackhq/typescript-config/base.json", - "compilerOptions": { - "baseUrl": "." - }, "include": ["src/**/*.ts", "test/**/*.ts"] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a133573a..e7463216 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,6 +6,9 @@ settings: catalogs: default: + '@types/node': + specifier: ^20.17.24 + version: 20.17.24 '@types/tmp': specifier: ^0.2.6 version: 0.2.6 @@ -19,8 +22,8 @@ catalogs: specifier: 3.5.0 version: 3.5.0 prisma: - specifier: ^6.0.0 - version: 6.9.0 + specifier: ^6.14.0 + version: 6.14.0 tmp: specifier: ^0.2.3 version: 0.2.3 @@ -42,7 +45,7 @@ importers: specifier: ^9.29.0 version: 9.29.0 '@types/node': - specifier: ^20.17.24 + specifier: 'catalog:' version: 20.17.24 eslint: specifier: ~9.29.0 @@ -103,7 +106,7 @@ importers: version: 1.3.0 prisma: specifier: 'catalog:' - version: 6.9.0(typescript@5.8.3) + version: 6.14.0(typescript@5.8.3) ts-pattern: specifier: 'catalog:' version: 5.7.1 @@ -388,7 +391,7 @@ importers: version: 8.13.1 prisma: specifier: 'catalog:' - version: 6.9.0(typescript@5.8.3) + version: 6.14.0(typescript@5.8.3) tmp: specifier: 'catalog:' version: 0.2.3 @@ -399,6 +402,9 @@ importers: specifier: 'catalog:' version: 5.8.3 devDependencies: + '@types/node': + specifier: 'catalog:' + version: 20.17.24 '@types/tmp': specifier: 'catalog:' version: 0.2.6 @@ -455,7 +461,7 @@ importers: version: link:../../packages/typescript-config prisma: specifier: 'catalog:' - version: 6.9.0(typescript@5.8.3) + version: 6.14.0(typescript@5.8.3) tests/e2e: dependencies: @@ -888,23 +894,23 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} - '@prisma/config@6.9.0': - resolution: {integrity: sha512-Wcfk8/lN3WRJd5w4jmNQkUwhUw0eksaU/+BlAJwPQKW10k0h0LC9PD/6TQFmqKVbHQL0vG2z266r0S1MPzzhbA==} + '@prisma/config@6.14.0': + resolution: {integrity: sha512-IwC7o5KNNGhmblLs23swnfBjADkacBb7wvyDXUWLwuvUQciKJZqyecU0jw0d7JRkswrj+XTL8fdr0y2/VerKQQ==} - '@prisma/debug@6.9.0': - resolution: {integrity: sha512-bFeur/qi/Q+Mqk4JdQ3R38upSYPebv5aOyD1RKywVD+rAMLtRkmTFn28ZuTtVOnZHEdtxnNOCH+bPIeSGz1+Fg==} + '@prisma/debug@6.14.0': + resolution: {integrity: sha512-j4Lf+y+5QIJgQD4sJWSbkOD7geKx9CakaLp/TyTy/UDu9Wo0awvWCBH/BAxTHUaCpIl9USA5VS/KJhDqKJSwug==} - '@prisma/engines-version@6.9.0-10.81e4af48011447c3cc503a190e86995b66d2a28e': - resolution: {integrity: sha512-Qp9gMoBHgqhKlrvumZWujmuD7q4DV/gooEyPCLtbkc13EZdSz2RsGUJ5mHb3RJgAbk+dm6XenqG7obJEhXcJ6Q==} + '@prisma/engines-version@6.14.0-25.717184b7b35ea05dfa71a3236b7af656013e1e49': + resolution: {integrity: sha512-EgN9ODJpiX45yvwcngoStp3uQPJ3l+AEVoQ6dMMO2QvmwIlnxfApzKmJQExzdo7/hqQANrz5txHJdGYHzOnGHA==} - '@prisma/engines@6.9.0': - resolution: {integrity: sha512-im0X0bwDLA0244CDf8fuvnLuCQcBBdAGgr+ByvGfQY9wWl6EA+kRGwVk8ZIpG65rnlOwtaWIr/ZcEU5pNVvq9g==} + '@prisma/engines@6.14.0': + resolution: {integrity: sha512-LhJjqsALFEcoAtF07nSaOkVguaxw/ZsgfROIYZ8bAZDobe7y8Wy+PkYQaPOK1iLSsFgV2MhCO/eNrI1gdSOj6w==} - '@prisma/fetch-engine@6.9.0': - resolution: {integrity: sha512-PMKhJdl4fOdeE3J3NkcWZ+tf3W6rx3ht/rLU8w4SXFRcLhd5+3VcqY4Kslpdm8osca4ej3gTfB3+cSk5pGxgFg==} + '@prisma/fetch-engine@6.14.0': + resolution: {integrity: sha512-MPzYPOKMENYOaY3AcAbaKrfvXVlvTc6iHmTXsp9RiwCX+bPyfDMqMFVUSVXPYrXnrvEzhGHfyiFy0PRLHPysNg==} - '@prisma/get-platform@6.9.0': - resolution: {integrity: sha512-/B4n+5V1LI/1JQcHp+sUpyRT1bBgZVPHbsC4lt4/19Xp4jvNIVcq5KYNtQDk5e/ukTSjo9PZVAxxy9ieFtlpTQ==} + '@prisma/get-platform@6.14.0': + resolution: {integrity: sha512-7VjuxKNwjnBhKfqPpMeWiHEa2sVjYzmHdl1slW6STuUCe9QnOY0OY1ljGSvz6wpG4U8DfbDqkG1yofd/1GINww==} '@rollup/rollup-android-arm-eabi@4.44.0': resolution: {integrity: sha512-xEiEE5oDW6tK4jXCAyliuntGR+amEMO7HLtdSshVuhFnKTYoeYMyXQK7pLouAJJj5KHdwdn87bfHAR2nSdNAUA==} @@ -1006,6 +1012,9 @@ packages: cpu: [x64] os: [win32] + '@standard-schema/spec@1.0.0': + resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} + '@swc/core-darwin-arm64@1.12.5': resolution: {integrity: sha512-3WF+naP/qkt5flrTfJr+p07b522JcixKvIivM7FgvllA6LjJxf+pheoILrTS8IwrNAK/XtHfKWYcGY+3eaA4mA==} engines: {node: '>=10'} @@ -1293,6 +1302,14 @@ packages: peerDependencies: esbuild: '>=0.18' + c12@3.1.0: + resolution: {integrity: sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==} + peerDependencies: + magicast: ^0.3.5 + peerDependenciesMeta: + magicast: + optional: true + cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} @@ -1344,6 +1361,9 @@ packages: chownr@1.1.4: resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + citty@0.1.6: + resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} + cli-cursor@3.1.0: resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} engines: {node: '>=8'} @@ -1385,6 +1405,9 @@ packages: confbox@0.1.8: resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + confbox@0.2.2: + resolution: {integrity: sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==} + consola@3.4.2: resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} engines: {node: ^14.18.0 || >=16.10.0} @@ -1420,6 +1443,10 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + deepmerge-ts@7.1.5: + resolution: {integrity: sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==} + engines: {node: '>=16.0.0'} + defaults@1.0.4: resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} @@ -1427,10 +1454,20 @@ packages: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} + defu@6.1.4: + resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + + destr@2.0.5: + resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} + detect-libc@2.0.3: resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} engines: {node: '>=8'} + dotenv@16.6.1: + resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} + engines: {node: '>=12'} + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -1438,12 +1475,19 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + effect@3.16.12: + resolution: {integrity: sha512-N39iBk0K71F9nb442TLbTkjl24FLUzuvx2i1I2RsEAQsdAdUTuUoW0vlfUXgkMTUOnYqKnWcFfqw4hK4Pw27hg==} + emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + empathic@2.0.0: + resolution: {integrity: sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==} + engines: {node: '>=14'} + end-of-stream@1.4.4: resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} @@ -1529,6 +1573,13 @@ packages: resolution: {integrity: sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==} engines: {node: '>=12.0.0'} + exsolve@1.0.7: + resolution: {integrity: sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==} + + fast-check@3.23.2: + resolution: {integrity: sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==} + engines: {node: '>=8.0.0'} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -1611,6 +1662,10 @@ packages: get-tsconfig@4.10.1: resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==} + giget@2.0.0: + resolution: {integrity: sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==} + hasBin: true + github-from-package@0.0.0: resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} @@ -1900,6 +1955,14 @@ packages: resolution: {integrity: sha512-z8iYzQGBu35ZkTQ9mtR8RqugJZ9RCLn8fv3d7LsgDBzOijGQP3RdKTX4LA7LXw03ZhU5z0l4xfhIMgSES31+cg==} engines: {node: '>=10'} + node-fetch-native@1.6.7: + resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} + + nypm@0.6.1: + resolution: {integrity: sha512-hlacBiRiv1k9hZFiphPUkfSQ/ZfQzZDzC+8z0wL3lvDAOUu/2NnChkKuMoMjNur/9OpKuz2QsIeiPVN0xM5Q0w==} + engines: {node: ^14.16.0 || >=16.10.0} + hasBin: true + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -1911,6 +1974,9 @@ packages: obuf@1.1.2: resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==} + ohash@2.0.11: + resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} + once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -1967,6 +2033,9 @@ packages: resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==} engines: {node: '>= 14.16'} + perfect-debounce@1.0.0: + resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} + pg-cloudflare@1.1.1: resolution: {integrity: sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==} @@ -2027,6 +2096,9 @@ packages: pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + pkg-types@2.2.0: + resolution: {integrity: sha512-2SM/GZGAEkPp3KWORxQZns4M+WSeXbC2HEvmOIJe3Cmiv6ieAJvdVhDldtHqM5J1Y7MrR1XhkBT/rMlhh9FdqQ==} + pluralize@8.0.0: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} @@ -2102,8 +2174,8 @@ packages: engines: {node: '>=14'} hasBin: true - prisma@6.9.0: - resolution: {integrity: sha512-resJAwMyZREC/I40LF6FZ6rZTnlrlrYrb63oW37Gq+U+9xHwbyMSPJjKtM7VZf3gTO86t/Oyz+YeSXr3CmAY1Q==} + prisma@6.14.0: + resolution: {integrity: sha512-QEuCwxu+Uq9BffFw7in8In+WfbSUN0ewnaSUKloLkbJd42w6EyFckux4M0f7VwwHlM3A8ssaz4OyniCXlsn0WA==} engines: {node: '>=18.18'} hasBin: true peerDependencies: @@ -2119,12 +2191,18 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + pure-rand@6.1.0: + resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} railroad-diagrams@1.0.0: resolution: {integrity: sha512-cz93DjNeLY0idrCNOH6PviZGRN9GJhsdm9hpn1YCS879fj4W+x5IFJhhkRZcwVgMmFF7R82UA/7Oh+R8lLZg6A==} + rc9@2.1.2: + resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==} + rc@1.2.8: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true @@ -2289,6 +2367,9 @@ packages: tinyexec@0.3.2: resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + tinyexec@1.0.1: + resolution: {integrity: sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==} + tinyglobby@0.2.14: resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} engines: {node: '>=12.0.0'} @@ -2862,30 +2943,35 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true - '@prisma/config@6.9.0': + '@prisma/config@6.14.0': dependencies: - jiti: 2.4.2 + c12: 3.1.0 + deepmerge-ts: 7.1.5 + effect: 3.16.12 + empathic: 2.0.0 + transitivePeerDependencies: + - magicast - '@prisma/debug@6.9.0': {} + '@prisma/debug@6.14.0': {} - '@prisma/engines-version@6.9.0-10.81e4af48011447c3cc503a190e86995b66d2a28e': {} + '@prisma/engines-version@6.14.0-25.717184b7b35ea05dfa71a3236b7af656013e1e49': {} - '@prisma/engines@6.9.0': + '@prisma/engines@6.14.0': dependencies: - '@prisma/debug': 6.9.0 - '@prisma/engines-version': 6.9.0-10.81e4af48011447c3cc503a190e86995b66d2a28e - '@prisma/fetch-engine': 6.9.0 - '@prisma/get-platform': 6.9.0 + '@prisma/debug': 6.14.0 + '@prisma/engines-version': 6.14.0-25.717184b7b35ea05dfa71a3236b7af656013e1e49 + '@prisma/fetch-engine': 6.14.0 + '@prisma/get-platform': 6.14.0 - '@prisma/fetch-engine@6.9.0': + '@prisma/fetch-engine@6.14.0': dependencies: - '@prisma/debug': 6.9.0 - '@prisma/engines-version': 6.9.0-10.81e4af48011447c3cc503a190e86995b66d2a28e - '@prisma/get-platform': 6.9.0 + '@prisma/debug': 6.14.0 + '@prisma/engines-version': 6.14.0-25.717184b7b35ea05dfa71a3236b7af656013e1e49 + '@prisma/get-platform': 6.14.0 - '@prisma/get-platform@6.9.0': + '@prisma/get-platform@6.14.0': dependencies: - '@prisma/debug': 6.9.0 + '@prisma/debug': 6.14.0 '@rollup/rollup-android-arm-eabi@4.44.0': optional: true @@ -2947,6 +3033,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.44.0': optional: true + '@standard-schema/spec@1.0.0': {} + '@swc/core-darwin-arm64@1.12.5': optional: true @@ -3257,6 +3345,21 @@ snapshots: esbuild: 0.25.5 load-tsconfig: 0.2.5 + c12@3.1.0: + dependencies: + chokidar: 4.0.3 + confbox: 0.2.2 + defu: 6.1.4 + dotenv: 16.6.1 + exsolve: 1.0.7 + giget: 2.0.0 + jiti: 2.4.2 + ohash: 2.0.11 + pathe: 2.0.3 + perfect-debounce: 1.0.0 + pkg-types: 2.2.0 + rc9: 2.1.2 + cac@6.7.14: {} call-bind-apply-helpers@1.0.2: @@ -3315,6 +3418,10 @@ snapshots: chownr@1.1.4: {} + citty@0.1.6: + dependencies: + consola: 3.4.2 + cli-cursor@3.1.0: dependencies: restore-cursor: 3.1.0 @@ -3341,6 +3448,8 @@ snapshots: confbox@0.1.8: {} + confbox@0.2.2: {} + consola@3.4.2: {} cross-spawn@7.0.6: @@ -3365,6 +3474,8 @@ snapshots: deep-is@0.1.4: {} + deepmerge-ts@7.1.5: {} + defaults@1.0.4: dependencies: clone: 1.0.4 @@ -3375,8 +3486,14 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 + defu@6.1.4: {} + + destr@2.0.5: {} + detect-libc@2.0.3: {} + dotenv@16.6.1: {} + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -3385,10 +3502,17 @@ snapshots: eastasianwidth@0.2.0: {} + effect@3.16.12: + dependencies: + '@standard-schema/spec': 1.0.0 + fast-check: 3.23.2 + emoji-regex@8.0.0: {} emoji-regex@9.2.2: {} + empathic@2.0.0: {} + end-of-stream@1.4.4: dependencies: once: 1.4.0 @@ -3537,6 +3661,12 @@ snapshots: expect-type@1.2.1: {} + exsolve@1.0.7: {} + + fast-check@3.23.2: + dependencies: + pure-rand: 6.1.0 + fast-deep-equal@3.1.3: {} fast-glob@3.3.3: @@ -3631,6 +3761,15 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 + giget@2.0.0: + dependencies: + citty: 0.1.6 + consola: 3.4.2 + defu: 6.1.4 + node-fetch-native: 1.6.7 + nypm: 0.6.1 + pathe: 2.0.3 + github-from-package@0.0.0: {} glob-parent@5.1.2: @@ -3887,12 +4026,24 @@ snapshots: dependencies: semver: 7.7.2 + node-fetch-native@1.6.7: {} + + nypm@0.6.1: + dependencies: + citty: 0.1.6 + consola: 3.4.2 + pathe: 2.0.3 + pkg-types: 2.2.0 + tinyexec: 1.0.1 + object-assign@4.1.1: {} object-keys@1.1.1: {} obuf@1.1.2: {} + ohash@2.0.11: {} + once@1.4.0: dependencies: wrappy: 1.0.2 @@ -3956,6 +4107,8 @@ snapshots: pathval@2.0.0: {} + perfect-debounce@1.0.0: {} + pg-cloudflare@1.1.1: optional: true @@ -4017,6 +4170,12 @@ snapshots: mlly: 1.7.4 pathe: 2.0.3 + pkg-types@2.2.0: + dependencies: + confbox: 0.2.2 + exsolve: 1.0.7 + pathe: 2.0.3 + pluralize@8.0.0: {} postcss-load-config@6.0.1(jiti@2.4.2)(postcss@8.5.6)(tsx@4.20.3)(yaml@2.8.0): @@ -4075,12 +4234,14 @@ snapshots: prettier@3.5.3: {} - prisma@6.9.0(typescript@5.8.3): + prisma@6.14.0(typescript@5.8.3): dependencies: - '@prisma/config': 6.9.0 - '@prisma/engines': 6.9.0 + '@prisma/config': 6.14.0 + '@prisma/engines': 6.14.0 optionalDependencies: typescript: 5.8.3 + transitivePeerDependencies: + - magicast pump@3.0.2: dependencies: @@ -4089,10 +4250,17 @@ snapshots: punycode@2.3.1: {} + pure-rand@6.1.0: {} + queue-microtask@1.2.3: {} railroad-diagrams@1.0.0: {} + rc9@2.1.2: + dependencies: + defu: 6.1.4 + destr: 2.0.5 + rc@1.2.8: dependencies: deep-extend: 0.6.0 @@ -4275,6 +4443,8 @@ snapshots: tinyexec@0.3.2: {} + tinyexec@1.0.1: {} + tinyglobby@0.2.14: dependencies: fdir: 6.4.6(picomatch@4.0.2) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index de6cfe1b..4b745954 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -5,10 +5,11 @@ packages: catalog: kysely: ^0.27.6 zod: ^4.0.0 - prisma: ^6.0.0 + prisma: ^6.14.0 langium: 3.5.0 langium-cli: 3.5.0 ts-pattern: ^5.7.1 typescript: ^5.0.0 + '@types/node': ^20.17.24 tmp: ^0.2.3 '@types/tmp': ^0.2.6 diff --git a/samples/blog/package.json b/samples/blog/package.json index 8dfec300..68f964cf 100644 --- a/samples/blog/package.json +++ b/samples/blog/package.json @@ -1,11 +1,12 @@ { "name": "sample-blog", - "version": "3.0.0-alpha.26", + "version": "3.0.0-alpha.27", "description": "", "main": "index.js", "scripts": { - "generate": "zenstack generate", - "db:migrate": "zenstack migrate dev", + "generate": "zen generate", + "db:push": "zen db push", + "db:migrate": "zen migrate dev", "build": "pnpm generate && tsc --noEmit", "dev": "tsx main.ts" }, diff --git a/tests/e2e/package.json b/tests/e2e/package.json index 928f1127..1efee3ca 100644 --- a/tests/e2e/package.json +++ b/tests/e2e/package.json @@ -1,6 +1,6 @@ { "name": "e2e", - "version": "3.0.0-alpha.26", + "version": "3.0.0-alpha.27", "private": true, "type": "module", "scripts": {