Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,7 @@
packages/language/src/generated/**
**/schema.ts
**/test/**/schema.ts
**/test/**/models.ts
**/test/**/input.ts
samples/**/schema.ts
samples/**/models.ts
samples/**/input.ts
15 changes: 13 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,26 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Development Commands

### Build System

- `pnpm build` - Build all packages using Turbo
- `pnpm watch` - Watch mode for all packages
- `pnpm lint` - Run ESLint across all packages
- `pnpm test` - Run tests for all packages

### Package Management

- Uses `pnpm` with workspaces
- Package manager is pinned to `pnpm@10.12.1`
- Packages are located in `packages/`, `samples/`, and `tests/`

### Testing

- Runtime package tests: `pnpm test` (includes vitest, typing generation, and typecheck)
- CLI tests: `pnpm test`
- CLI tests: `pnpm test`
- E2E tests are in `tests/e2e/` directory

### ZenStack CLI Commands

- `npx zenstack init` - Initialize ZenStack in a project
- `npx zenstack generate` - Compile ZModel schema to TypeScript
- `npx zenstack db push` - Sync schema to database (uses Prisma)
Expand All @@ -30,44 +34,51 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Architecture Overview

### Core Components

- **@zenstackhq/runtime** - Main database client and ORM engine built on Kysely
- **@zenstackhq/cli** - Command line interface and project management
- **@zenstackhq/language** - ZModel language specification and parser (uses Langium)
- **@zenstackhq/sdk** - Code generation utilities and schema processing

### Key Architecture Patterns

- **Monorepo Structure**: Uses pnpm workspaces with Turbo for build orchestration
- **Language-First Design**: ZModel DSL compiles to TypeScript, not runtime code generation
- **Kysely-Based ORM**: V3 uses Kysely as query builder instead of Prisma runtime dependency
- **Plugin Architecture**: Runtime plugins for query interception and entity mutation hooks

### ZModel to TypeScript Flow

1. ZModel schema (`schema.zmodel`) defines database structure and policies
2. `zenstack generate` compiles ZModel to TypeScript schema (`schema.ts`)
3. Schema is used to instantiate `ZenStackClient` with type-safe CRUD operations
4. Client provides both high-level ORM API and low-level Kysely query builder

### Package Dependencies

- **Runtime**: Depends on Kysely, Zod, and various utility libraries
- **CLI**: Depends on language package, Commander.js, and Prisma (for migrations)
- **Language**: Uses Langium for grammar parsing and AST generation
- **Database Support**: SQLite (better-sqlite3) and PostgreSQL (pg) only

### Testing Strategy

- Runtime package has comprehensive client API tests and policy tests
- CLI has action-specific tests for commands
- E2E tests validate real-world schema compatibility (cal.com, formbricks, trigger.dev)
- Type coverage tests ensure TypeScript inference works correctly

## Key Differences from Prisma

- No runtime dependency on @prisma/client
- Pure TypeScript implementation without Rust/WASM
- Built-in access control and validation (coming soon)
- Kysely query builder as escape hatch instead of raw SQL
- Schema-first approach with ZModel DSL extension of Prisma schema language

## Development Notes

- Always run `zenstack generate` after modifying ZModel schemas
- Database migrations still use Prisma CLI under the hood
- Plugin system allows interception at ORM, Kysely, and entity mutation levels
- Computed fields are evaluated at database level for performance
- Computed fields are evaluated at database level for performance
9 changes: 7 additions & 2 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
- [x] validate
- [ ] format
- [ ] db seed
- [ ] ZModel
- [ ] View support
- [ ] ORM
- [x] Create
- [x] Input validation
Expand Down Expand Up @@ -56,13 +58,14 @@
- [x] Aggregate
- [x] Group by
- [x] Raw queries
- [ ] Transactions
- [x] Transactions
- [x] Interactive transaction
- [x] Sequential transaction
- [ ] Extensions
- [x] Query builder API
- [x] Computed fields
- [x] Prisma client extension
- [ ] Custom procedures
- [ ] Misc
- [x] JSDoc for CRUD methods
- [x] Cache validation schemas
Expand All @@ -71,14 +74,16 @@
- [x] Many-to-many relation
- [ ] Empty AND/OR/NOT behavior
- [?] Logging
- [ ] Error system
- [x] Error system
- [x] Custom table name
- [x] Custom field name
- [ ] Strict undefined checks
- [ ] DbNull vs JsonNull
- [ ] Benchmark
- [ ] Plugin
- [ ] Post-mutation hooks should be called after transaction is committed
- [x] TypeDef and mixin
- [ ] Strongly typed JSON
- [ ] Polymorphism
- [ ] Validation
- [ ] Access Policy
Expand Down
4 changes: 3 additions & 1 deletion packages/cli/src/actions/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ type Options = {
* CLI action for generating code from schema
*/
export async function run(options: Options) {
const start = Date.now();

const schemaFile = getSchemaFile(options.schema);

const model = await loadSchemaDocument(schemaFile);
Expand All @@ -40,7 +42,7 @@ export async function run(options: Options) {
}

if (!options.silent) {
console.log(colors.green('Generation completed successfully.'));
console.log(colors.green(`Generation completed successfully in ${Date.now() - start}ms.`));
console.log(`You can now create a ZenStack client with it.

\`\`\`ts
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/actions/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@ export async function run(options: Options) {
// Re-throw to maintain CLI exit code behavior
throw error;
}
}
}
85 changes: 85 additions & 0 deletions packages/cli/test/ts-schema-gen.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,4 +181,89 @@ model Post {
},
});
});

it('merges fields and attributes from mixins', async () => {
const { schema } = await generateTsSchema(`
type Timestamped {
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}

type Named {
name String
@@unique([name])
}

model User with Timestamped Named {
id String @id @default(uuid())
email String @unique
}
`);
expect(schema).toMatchObject({
models: {
User: {
fields: {
id: { type: 'String' },
email: { type: 'String' },
createdAt: {
type: 'DateTime',
default: expect.objectContaining({ function: 'now', kind: 'call' }),
},
updatedAt: { type: 'DateTime', updatedAt: true },
name: { type: 'String' },
},
uniqueFields: expect.objectContaining({
name: { type: 'String' },
}),
},
},
});
});

it('generates type definitions', async () => {
const { schema } = await generateTsSchema(`
type Base {
name String
@@meta('foo', 'bar')
}

type Address with Base {
street String
city String
}
`);
expect(schema).toMatchObject({
typeDefs: {
Base: {
fields: {
name: { type: 'String' },
},
attributes: [
{
name: '@@meta',
args: [
{ name: 'name', value: { kind: 'literal', value: 'foo' } },
{ name: 'value', value: { kind: 'literal', value: 'bar' } },
],
},
],
},
Address: {
fields: {
street: { type: 'String' },
city: { type: 'String' },
},
attributes: [
{
name: '@@meta',
args: [
{ name: 'name', value: { kind: 'literal', value: 'foo' } },
{ name: 'value', value: { kind: 'literal', value: 'bar' } },
],
},
],
},
},
});
});
});
11 changes: 11 additions & 0 deletions packages/language/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,16 @@
"default": "./dist/ast.cjs"
}
},
"./utils": {
"import": {
"types": "./dist/utils.d.ts",
"default": "./dist/utils.js"
},
"require": {
"types": "./dist/utils.d.cts",
"default": "./dist/utils.cjs"
}
},
"./package.json": {
"import": "./package.json",
"require": "./package.json"
Expand All @@ -51,6 +61,7 @@
"@types/pluralize": "^0.0.33",
"@zenstackhq/eslint-config": "workspace:*",
"@zenstackhq/typescript-config": "workspace:*",
"@zenstackhq/common-helpers": "workspace:*",
"langium-cli": "catalog:"
},
"volta": {
Expand Down
Loading