diff --git a/README.md b/README.md index 40a93725..949162d8 100644 --- a/README.md +++ b/README.md @@ -118,7 +118,7 @@ npm install -D @types/pg Run the following command to sync schema to the database for local development: ```bash -npx zenstack db push +npx zen db push ``` > Under the hood, the command uses `prisma db push` to do the job. @@ -127,17 +127,17 @@ See [database migration](#database-migration) for how to use migration to manage ## Compiling ZModel schema -ZModel needs to be compiled to TypeScript before being used to create a database client. Simply run the following command: +ZModel needs to be compiled to TypeScript before being used to create a database db. Simply run the following command: ```bash -npx zenstack generate +npx zen generate ``` A `schema.ts` file will be created inside the `zenstack` folder. The file should be included as part of your source tree for compilation/bundling. You may choose to include or ignore it in source control (and generate on the fly during build). Just remember to rerun the "generate" command whenever you make changes to the ZModel schema. ## Creating ZenStack client -Now you can use the compiled TypeScript schema to instantiate a database client. +Now you can use the compiled TypeScript schema to instantiate a database db. ### SQLite @@ -147,7 +147,7 @@ import { schema } from './zenstack/schema'; import SQLite from 'better-sqlite3'; import { SqliteDialect } from 'kysely'; -const client = new ZenStackClient(schema, { +const db = new ZenStackClient(schema, { dialect: new SqliteDialect({ database: new SQLite('./dev.db') }), }); ``` @@ -159,11 +159,10 @@ import { ZenStackClient } from '@zenstackhq/runtime'; import { schema } from './zenstack/schema'; import { PostgresDialect } from 'kysely'; import { Pool } from 'pg'; -import { parseIntoClientConfig } from 'pg-connection-string'; -const client = new ZenStackClient(schema, { +const db = new ZenStackClient(schema, { dialect: new PostgresDialect({ - pool: new Pool(parseIntoClientConfig(process.env.DATABASE_URL)), + pool: new Pool({ connectionString: process.env.DATABASE_URL }), }), }); ``` @@ -177,7 +176,7 @@ const client = new ZenStackClient(schema, { A few quick examples: ```ts -const user = await client.user.create({ +const user = await db.user.create({ data: { name: 'Alex', email: 'alex@zenstack.dev', @@ -185,12 +184,12 @@ const user = await client.user.create({ }, }); -const userWithPosts = await client.user.findUnique({ +const userWithPosts = await db.user.findUnique({ where: { id: user.id }, include: { posts: true }, }); -const groupedPosts = await client.post.groupBy({ +const groupedPosts = await db.post.groupBy({ by: 'published', _count: true, }); @@ -205,7 +204,7 @@ ZenStack uses Kysely to handle database operations, and it also directly exposes Please check [Kysely documentation](https://kysely.dev/docs/intro) for more details. Here're a few quick examples: ```ts -await client.$qb +await db.$qb .selectFrom('User') .leftJoin('Post', 'Post.authorId', 'User.id') .select(['User.id', 'User.email', 'Post.title']) @@ -215,7 +214,7 @@ await client.$qb Query builder can also be "blended" into ORM API calls as a local escape hatch for building complex filter conditions. It allows for greater flexibility without forcing you to entirely resort to the query builder API. ```ts -await client.user.findMany({ +await db.user.findMany({ where: { age: { gt: 18 }, // "eb" is a Kysely expression builder @@ -243,7 +242,7 @@ ZenStack v3 allows you to define database-evaluated computed fields with the fol 2. Provide its implementation using query builder when constructing `ZenStackClient` ```ts - const client = new ZenStackClient(schema, { + const db = new ZenStackClient(schema, { ... computedFields: { User: { @@ -279,7 +278,7 @@ _Coming soon..._ ### Runtime plugins -V3 introduces a new runtime plugin mechanism that allows you to tap into the ORM's query execution in various ways. A plugin implements the [RuntimePlugin](./packages/runtime/src/client/plugin.ts#L121) interface, and can be installed with the `ZenStackClient.$use` API. +V3 introduces a new runtime plugin mechanism that allows you to tap into the ORM's query execution in various ways. A plugin implements the [RuntimePlugin](./packages/runtime/src/client/plugin.ts#L121) interface, and can be installed with the `ZenStackdb.$use` API. You can use a plugin to achieve the following goals: @@ -288,7 +287,7 @@ You can use a plugin to achieve the following goals: ORM query interception allows you to intercept the high-level ORM API calls. The interceptor's configuration is compatible with Prisma's [query client extension](https://www.prisma.io/docs/orm/prisma-client/client-extensions/query). ```ts -client.$use({ +db.$use({ id: 'cost-logger', onQuery: { $allModels: { @@ -312,7 +311,7 @@ Kysely query interception allows you to intercept the low-level query builder AP Kysely query interception works against the low-level Kysely `OperationNode` structures. It's harder to implement but can guarantee intercepting all CRUD operations. ```ts -client.$use({ +db.$use({ id: 'insert-interception-plugin', onKyselyQuery({query, proceed}) { if (query.kind === 'InsertQueryNode') { @@ -332,7 +331,7 @@ function sanitizeInsertData(query: InsertQueryNode) { Another popular interception use case is, instead of intercepting calls, "listening on" entity changes. ```ts -client.$use({ +db.$use({ id: 'mutation-hook-plugin', beforeEntityMutation({ model, action }) { console.log(`Before ${model} ${action}`); @@ -346,7 +345,7 @@ client.$use({ You can provide an extra `mutationInterceptionFilter` to control what to intercept, and opt in for loading the affected entities before and/or after the mutation. ```ts -client.$use({ +db.$use({ id: 'mutation-hook-plugin', mutationInterceptionFilter: ({ model }) => { return { @@ -375,19 +374,19 @@ ZenStack v3 delegates database schema migration to Prisma. The CLI provides Pris - Sync schema to dev database and create a migration record: ```bash - npx zenstack migrate dev + npx zen migrate dev ``` - Deploy new migrations: ```bash - npx zenstack migrate deploy + npx zen migrate deploy ``` - Reset dev database ```bash - npx zenstack migrate reset + npx zen migrate reset ``` See [Prisma Migrate](https://www.prisma.io/docs/orm/prisma-migrate) documentation for more details. @@ -398,7 +397,7 @@ See [Prisma Migrate](https://www.prisma.io/docs/orm/prisma-migrate) documentatio 1. Remove "@prisma/client" dependency 1. Install "better-sqlite3" or "pg" based on database type 1. Move "schema.prisma" to "zenstack" folder and rename it to "schema.zmodel" -1. Run `npx zenstack generate` +1. Run `npx zen generate` 1. Replace `new PrismaClient()` with `new ZenStackClient(schema, { ... })` # Limitations diff --git a/TODO.md b/TODO.md index 270a15aa..7245aef9 100644 --- a/TODO.md +++ b/TODO.md @@ -10,6 +10,10 @@ - [x] validate - [ ] format - [ ] db seed + - [ ] plugin mechanism + - [ ] built-in plugins + - [ ] ts + - [ ] prisma - [ ] ZModel - [ ] Import - [ ] View support @@ -73,6 +77,7 @@ - [x] Compound ID - [ ] Cross field comparison - [x] Many-to-many relation + - [ ] Self relation - [ ] Empty AND/OR/NOT behavior - [x] Logging - [x] Error system diff --git a/packages/cli/package.json b/packages/cli/package.json index ef955248..cf3344e1 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -49,6 +49,7 @@ "@zenstackhq/runtime": "workspace:*", "@zenstackhq/testtools": "workspace:*", "@zenstackhq/typescript-config": "workspace:*", + "@zenstackhq/vitest-config": "workspace:*", "better-sqlite3": "^11.8.1", "tmp": "catalog:" } diff --git a/packages/cli/src/actions/action-utils.ts b/packages/cli/src/actions/action-utils.ts index a6e4ec2d..287c5593 100644 --- a/packages/cli/src/actions/action-utils.ts +++ b/packages/cli/src/actions/action-utils.ts @@ -36,11 +36,10 @@ export function getSchemaFile(file?: string) { export async function loadSchemaDocument(schemaFile: string) { const loadResult = await loadDocument(schemaFile); if (!loadResult.success) { - console.error(colors.red('Error loading schema:')); loadResult.errors.forEach((err) => { console.error(colors.red(err)); }); - throw new CliError('Failed to load schema'); + throw new CliError('Schema contains errors. See above for details.'); } loadResult.warnings.forEach((warn) => { console.warn(colors.yellow(warn)); diff --git a/packages/cli/src/actions/generate.ts b/packages/cli/src/actions/generate.ts index 12d58d82..58c2060a 100644 --- a/packages/cli/src/actions/generate.ts +++ b/packages/cli/src/actions/generate.ts @@ -52,8 +52,7 @@ import { schema } from '${outputPath}/schema'; const client = new ZenStackClient(schema, { dialect: { ... } }); -\`\`\` -`); +\`\`\``); } } diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 5e16d0b2..fc154121 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -1,7 +1,8 @@ import { ZModelLanguageMetaData } from '@zenstackhq/language'; import colors from 'colors'; -import { Command, Option } from 'commander'; +import { Command, CommanderError, Option } from 'commander'; import * as actions from './actions'; +import { CliError } from './cli-error'; import { getVersion } from './utils/version-utils'; const generateAction = async (options: Parameters[0]): Promise => { @@ -125,4 +126,17 @@ export function createProgram() { } const program = createProgram(); -program.parse(process.argv); + +program.parseAsync().catch((err) => { + if (err instanceof CliError) { + console.error(colors.red(err.message)); + process.exit(1); + } else if (err instanceof CommanderError) { + // errors are already reported, just exit + process.exit(err.exitCode); + } else { + console.error(colors.red('An unexpected error occurred:')); + console.error(err); + process.exit(1); + } +}); diff --git a/packages/cli/vitest.config.ts b/packages/cli/vitest.config.ts index 04655403..75a9f709 100644 --- a/packages/cli/vitest.config.ts +++ b/packages/cli/vitest.config.ts @@ -1,4 +1,4 @@ +import base from '@zenstackhq/vitest-config/base'; import { defineConfig, mergeConfig } from 'vitest/config'; -import base from '../../vitest.base.config'; export default mergeConfig(base, defineConfig({})); diff --git a/packages/language/package.json b/packages/language/package.json index 110852e0..1524ca3c 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -62,6 +62,7 @@ "@zenstackhq/eslint-config": "workspace:*", "@zenstackhq/typescript-config": "workspace:*", "@zenstackhq/common-helpers": "workspace:*", + "@zenstackhq/vitest-config": "workspace:*", "langium-cli": "catalog:", "tmp": "catalog:", "@types/tmp": "catalog:" diff --git a/packages/language/src/index.ts b/packages/language/src/index.ts index fdd3b544..6b2cb56a 100644 --- a/packages/language/src/index.ts +++ b/packages/language/src/index.ts @@ -186,8 +186,12 @@ function linkContentToContainer(node: AstNode): void { function validationAfterImportMerge(model: Model) { const errors: string[] = []; const dataSources = model.declarations.filter((d) => isDataSource(d)); - if (dataSources.length > 1) { - errors.push('Validation error: Multiple datasource declarations are not allowed'); + if (dataSources.length === 0) { + errors.push('Validation error: schema must have a datasource declaration'); + } else { + if (dataSources.length > 1) { + errors.push('Validation error: multiple datasource declarations are not allowed'); + } } // at most one `@@auth` model diff --git a/packages/language/vitest.config.ts b/packages/language/vitest.config.ts index 04655403..75a9f709 100644 --- a/packages/language/vitest.config.ts +++ b/packages/language/vitest.config.ts @@ -1,4 +1,4 @@ +import base from '@zenstackhq/vitest-config/base'; import { defineConfig, mergeConfig } from 'vitest/config'; -import base from '../../vitest.base.config'; export default mergeConfig(base, defineConfig({})); diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 8faf7e9b..90a236c4 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -96,6 +96,7 @@ "@zenstackhq/sdk": "workspace:*", "@zenstackhq/testtools": "workspace:*", "@zenstackhq/typescript-config": "workspace:*", + "@zenstackhq/vitest-config": "workspace:*", "tsx": "^4.19.2" } } diff --git a/packages/runtime/vitest.config.ts b/packages/runtime/vitest.config.ts index c9f6cc7c..ecf30fd7 100644 --- a/packages/runtime/vitest.config.ts +++ b/packages/runtime/vitest.config.ts @@ -1,6 +1,6 @@ -import { defineConfig, mergeConfig } from 'vitest/config'; -import base from '../../vitest.base.config'; +import base from '@zenstackhq/vitest-config/base'; import path from 'node:path'; +import { defineConfig, mergeConfig } from 'vitest/config'; export default mergeConfig( base, diff --git a/packages/vitest-config/base.config.js b/packages/vitest-config/base.config.js new file mode 100644 index 00000000..f7b36f73 --- /dev/null +++ b/packages/vitest-config/base.config.js @@ -0,0 +1,12 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + deps: { + interopDefault: true, + }, + include: ['**/*.test.ts'], + testTimeout: 100000, + hookTimeout: 100000, + }, +}); diff --git a/packages/vitest-config/package.json b/packages/vitest-config/package.json new file mode 100644 index 00000000..bd160a6e --- /dev/null +++ b/packages/vitest-config/package.json @@ -0,0 +1,10 @@ +{ + "name": "@zenstackhq/vitest-config", + "type": "module", + "version": "3.0.0-alpha.16", + "private": true, + "license": "MIT", + "exports": { + "./base": "./base.config.js" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e9f0b6f7..3098cc6d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -126,6 +126,9 @@ importers: '@zenstackhq/typescript-config': specifier: workspace:* version: link:../typescript-config + '@zenstackhq/vitest-config': + specifier: workspace:* + version: link:../vitest-config better-sqlite3: specifier: ^11.8.1 version: 11.8.1 @@ -161,6 +164,27 @@ importers: specifier: workspace:* version: link:../typescript-config + packages/dialects/sql.js: + devDependencies: + '@types/sql.js': + specifier: ^1.4.9 + version: 1.4.9 + '@zenstackhq/eslint-config': + specifier: workspace:* + version: link:../../eslint-config + '@zenstackhq/typescript-config': + specifier: workspace:* + version: link:../../typescript-config + '@zenstackhq/vitest-config': + specifier: workspace:* + version: link:../../vitest-config + kysely: + specifier: 'catalog:' + version: 0.27.6 + sql.js: + specifier: ^1.13.0 + version: 1.13.0 + packages/eslint-config: {} packages/ide/vscode: @@ -215,6 +239,9 @@ importers: '@zenstackhq/typescript-config': specifier: workspace:* version: link:../typescript-config + '@zenstackhq/vitest-config': + specifier: workspace:* + version: link:../vitest-config langium-cli: specifier: 'catalog:' version: 3.5.0 @@ -282,6 +309,9 @@ importers: '@zenstackhq/typescript-config': specifier: workspace:* version: link:../typescript-config + '@zenstackhq/vitest-config': + specifier: workspace:* + version: link:../vitest-config tsx: specifier: ^4.19.2 version: 4.19.2 @@ -375,6 +405,8 @@ importers: packages/typescript-config: {} + packages/vitest-config: {} + packages/zod: dependencies: '@zenstackhq/runtime': @@ -428,6 +460,9 @@ importers: '@zenstackhq/cli': specifier: workspace:* version: link:../../packages/cli + '@zenstackhq/vitest-config': + specifier: workspace:* + version: link:../../packages/vitest-config packages: @@ -1060,6 +1095,9 @@ packages: '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/emscripten@1.40.1': + resolution: {integrity: sha512-sr53lnYkQNhjHNN0oJDdUm5564biioI5DuOpycufDVK7D3y+GR3oUswe2rlwY1nPNyusHbrJ9WoTyIHl4/Bpwg==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -1075,6 +1113,9 @@ packages: '@types/pluralize@0.0.33': resolution: {integrity: sha512-JOqsl+ZoCpP4e8TDke9W79FDcSgPAR0l6pixx2JHkhnRjvShyYiAYw2LVsnA7K08Y6DeOnaU6ujmENO4os/cYg==} + '@types/sql.js@1.4.9': + resolution: {integrity: sha512-ep8b36RKHlgWPqjNG9ToUrPiwkhwh0AEzy883mO5Xnd+cL6VBH1EvSjBAAuxLUFF2Vn/moE3Me6v9E1Lo+48GQ==} + '@types/tmp@0.2.6': resolution: {integrity: sha512-chhaNf2oKHlRkDGt+tiKE2Z5aJ6qalm7Z9rlLdBwmOiAAf09YQvvoLXjWK4HWPF1xU/fqvMgfNfpVoBscA/tKA==} @@ -2173,6 +2214,9 @@ packages: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} + sql.js@1.13.0: + resolution: {integrity: sha512-RJbVP1HRDlUUXahJ7VMTcu9Rm1Nzw+EBpoPr94vnbD4LwR715F3CcxE2G2k45PewcaZ57pjetYa+LoSJLAASgA==} + stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} @@ -2969,6 +3013,8 @@ snapshots: '@types/deep-eql@4.0.2': {} + '@types/emscripten@1.40.1': {} + '@types/estree@1.0.8': {} '@types/json-schema@7.0.15': {} @@ -2985,6 +3031,11 @@ snapshots: '@types/pluralize@0.0.33': {} + '@types/sql.js@1.4.9': + dependencies: + '@types/emscripten': 1.40.1 + '@types/node': 20.17.24 + '@types/tmp@0.2.6': {} '@types/vscode@1.101.0': {} @@ -4137,6 +4188,8 @@ snapshots: split2@4.2.0: {} + sql.js@1.13.0: {} + stackback@0.0.2: {} std-env@3.9.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 0c30abb5..de6cfe1b 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,6 +1,5 @@ packages: - packages/** - - packages/ide/** - samples/** - tests/** catalog: diff --git a/tests/e2e/package.json b/tests/e2e/package.json index d3bcf2bc..7c96b516 100644 --- a/tests/e2e/package.json +++ b/tests/e2e/package.json @@ -2,6 +2,7 @@ "name": "e2e", "version": "3.0.0-alpha.16", "private": true, + "type": "module", "scripts": { "test": "vitest run" }, @@ -9,6 +10,7 @@ "@zenstackhq/testtools": "workspace:*" }, "devDependencies": { - "@zenstackhq/cli": "workspace:*" + "@zenstackhq/cli": "workspace:*", + "@zenstackhq/vitest-config": "workspace:*" } } diff --git a/tests/e2e/prisma-consistency/datasource.test.ts b/tests/e2e/prisma-consistency/datasource.test.ts index 2c02c0de..123feb84 100644 --- a/tests/e2e/prisma-consistency/datasource.test.ts +++ b/tests/e2e/prisma-consistency/datasource.test.ts @@ -1,11 +1,5 @@ -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { - ZenStackValidationTester, - createTestDir, - expectValidationSuccess, - expectValidationFailure, - baseSchema, -} from './test-utils'; +import { afterEach, beforeEach, describe, it } from 'vitest'; +import { ZenStackValidationTester, createTestDir, expectValidationFailure } from './test-utils'; describe('Datasource Validation', () => { let tester: ZenStackValidationTester; diff --git a/tests/e2e/prisma-consistency/relations-one-to-many.test.ts b/tests/e2e/prisma-consistency/relations-one-to-many.test.ts index 506fc2d4..ca52d84c 100644 --- a/tests/e2e/prisma-consistency/relations-one-to-many.test.ts +++ b/tests/e2e/prisma-consistency/relations-one-to-many.test.ts @@ -1,10 +1,10 @@ -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { afterEach, beforeEach, describe, it } from 'vitest'; import { ZenStackValidationTester, + baseSchema, createTestDir, - expectValidationSuccess, expectValidationFailure, - baseSchema, + expectValidationSuccess, } from './test-utils'; describe('One-to-Many Relations Validation', () => { diff --git a/tests/e2e/prisma-consistency/test-utils.ts b/tests/e2e/prisma-consistency/test-utils.ts index ba6ba3a2..5a53fead 100644 --- a/tests/e2e/prisma-consistency/test-utils.ts +++ b/tests/e2e/prisma-consistency/test-utils.ts @@ -8,7 +8,6 @@ import { expect } from 'vitest'; export interface ValidationResult { success: boolean; - errors: string[]; } export class ZenStackValidationTester { @@ -60,29 +59,14 @@ export class ZenStackValidationTester { return { success: true, - errors: [], }; } catch (error: any) { return { success: false, - errors: this.extractErrors(error.stderr), }; } } - private extractErrors(output: string): string[] { - const lines = output.split('\n'); - const errors: string[] = []; - - for (const line of lines) { - if (line.includes('Error:') || line.includes('error:') || line.includes('✖')) { - errors.push(line.trim()); - } - } - - return errors; - } - public cleanup() { if (existsSync(this.testDir)) { rmSync(this.testDir, { recursive: true, force: true }); @@ -100,7 +84,6 @@ export function expectValidationSuccess(result: ValidationResult) { export function expectValidationFailure(result: ValidationResult) { expect(result.success).toBe(false); - expect(result.errors.length).toBeGreaterThan(0); } export const baseSchema = ` diff --git a/tests/e2e/vitest.config.ts b/tests/e2e/vitest.config.ts index 04655403..75a9f709 100644 --- a/tests/e2e/vitest.config.ts +++ b/tests/e2e/vitest.config.ts @@ -1,4 +1,4 @@ +import base from '@zenstackhq/vitest-config/base'; import { defineConfig, mergeConfig } from 'vitest/config'; -import base from '../../vitest.base.config'; export default mergeConfig(base, defineConfig({}));