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
2 changes: 2 additions & 0 deletions BREAKINGCHANGES.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
1. `update` and `delete` policy rejection throws `NotFoundError`
1. `check()` ORM api has been removed
1. non-optional to-one relation doesn't automatically filter parent read when evaluating access policies
1. `@omit` and `@password` attributes have been removed
1. SWR plugin is removed
8 changes: 3 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co

### Testing

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

### ZenStack CLI Commands
Expand All @@ -35,7 +33,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co

### Core Components

- **@zenstackhq/runtime** - Main database client and ORM engine built on Kysely
- **@zenstackhq/orm** - ORM engine built above 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
Expand All @@ -56,14 +54,14 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co

### Package Dependencies

- **Runtime**: Depends on Kysely, Zod, and various utility libraries
- **ORM**: 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
- ORM 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
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ Even without using advanced features, ZenStack offers the following benefits as
2. More TypeScript type inference, less code generation.
3. Fully-typed query-builder API as a better escape hatch compared to Prisma's [raw queries](https://www.prisma.io/docs/orm/prisma-client/using-raw-sql/raw-queries) or [typed SQL](https://www.prisma.io/docs/orm/prisma-client/using-raw-sql/typedsql).

> Although ZenStack v3's runtime doesn't depend on Prisma anymore (specifically, `@prisma/client`), it still relies on Prisma to handle database migration. See [database migration](https://zenstack.dev/docs/3.x/orm/migration) for more details.
> Although ZenStack v3's ORM runtime doesn't depend on Prisma anymore (specifically, `@prisma/client`), it still relies on Prisma to handle database migration. See [database migration](https://zenstack.dev/docs/3.x/orm/migration) for more details.

# Quick start

Expand Down Expand Up @@ -77,7 +77,7 @@ Alternatively, you can set it up manually:

```bash
npm install -D @zenstackhq/cli@next
npm install @zenstackhq/runtime@next
npm install @zenstackhq/orm@next
```

Then create a `zenstack` folder and a `schema.zmodel` file in it.
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"name": "zenstack-v3",
"version": "3.0.0-beta.13",
"version": "3.0.0-beta.14",
"description": "ZenStack",
"packageManager": "pnpm@10.12.1",
"packageManager": "pnpm@10.20.0",
"scripts": {
"build": "turbo run build",
"watch": "turbo run watch build",
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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-beta.13",
"version": "3.0.0-beta.14",
"type": "module",
"author": {
"name": "ZenStack Team"
Expand Down Expand Up @@ -49,7 +49,7 @@
"@types/semver": "^7.7.0",
"@types/tmp": "catalog:",
"@zenstackhq/eslint-config": "workspace:*",
"@zenstackhq/runtime": "workspace:*",
"@zenstackhq/orm": "workspace:*",
"@zenstackhq/testtools": "workspace:*",
"@zenstackhq/typescript-config": "workspace:*",
"@zenstackhq/vitest-config": "workspace:*",
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/actions/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export async function run(options: Options) {
console.log(`You can now create a ZenStack client with it.

\`\`\`ts
import { ZenStackClient } from '@zenstackhq/runtime';
import { ZenStackClient } from '@zenstackhq/orm';
import { schema } from '${outputPath}/schema';

const client = new ZenStackClient(schema, {
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/actions/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { STARTER_ZMODEL } from './templates';
export async function run(projectPath: string) {
const packages = [
{ name: '@zenstackhq/cli@next', dev: true },
{ name: '@zenstackhq/runtime@next', dev: false },
{ name: '@zenstackhq/orm@next', dev: false },
];
let pm = await detect();
if (!pm) {
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/actions/migrate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ async function runReset(prismaSchemaFile: string, options: ResetOptions) {
'prisma migrate reset',
` --schema "${prismaSchemaFile}"`,
' --skip-generate',
options.force ? ' --force' : ''
options.force ? ' --force' : '',
].join('');

await execPackage(cmd);
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/actions/templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ model Post {
}
`;

export const STARTER_MAIN_TS = `import { ZenStackClient } from '@zenstackhq/runtime';
export const STARTER_MAIN_TS = `import { ZenStackClient } from '@zenstackhq/orm';
import SQLite from 'better-sqlite3';
import { SqliteDialect } from 'kysely';
import { schema } from './zenstack/schema';
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/test/ts-schema-gen.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ExpressionUtils } from '@zenstackhq/runtime/schema';
import { ExpressionUtils } from '@zenstackhq/orm/schema';
import { createTestProject, generateTsSchema, generateTsSchemaInPlace } from '@zenstackhq/testtools';
import fs from 'node:fs';
import path from 'node:path';
Expand Down
2 changes: 1 addition & 1 deletion packages/common-helpers/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@zenstackhq/common-helpers",
"version": "3.0.0-beta.13",
"version": "3.0.0-beta.14",
"description": "ZenStack Common Helpers",
"type": "module",
"scripts": {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { isPlainObject } from '@zenstackhq/common-helpers';
import { isPlainObject } from './is-plain-object';

/**
* Clones the given object. Only arrays and plain objects are cloned. Other values are returned as is.
Expand Down
2 changes: 2 additions & 0 deletions packages/common-helpers/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export * from './clone';
export * from './enumerable';
export * from './is-plain-object';
export * from './lower-case-first';
export * from './param-case';
Expand Down
2 changes: 1 addition & 1 deletion packages/config/eslint-config/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@zenstackhq/eslint-config",
"version": "3.0.0-beta.13",
"version": "3.0.0-beta.14",
"type": "module",
"private": true,
"license": "MIT"
Expand Down
2 changes: 1 addition & 1 deletion packages/config/typescript-config/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@zenstackhq/typescript-config",
"version": "3.0.0-beta.13",
"version": "3.0.0-beta.14",
"private": true,
"license": "MIT"
}
2 changes: 1 addition & 1 deletion packages/config/vitest-config/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@zenstackhq/vitest-config",
"type": "module",
"version": "3.0.0-beta.13",
"version": "3.0.0-beta.14",
"private": true,
"license": "MIT",
"exports": {
Expand Down
2 changes: 1 addition & 1 deletion packages/create-zenstack/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "create-zenstack",
"version": "3.0.0-beta.13",
"version": "3.0.0-beta.14",
"description": "Create a new ZenStack project",
"type": "module",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion packages/create-zenstack/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ function initProject(name: string) {
// install packages
const packages = [
{ name: '@zenstackhq/cli@next', dev: true },
{ name: '@zenstackhq/runtime@next', dev: false },
{ name: '@zenstackhq/orm@next', dev: false },
{ name: 'better-sqlite3', dev: false },
{ name: '@types/better-sqlite3', dev: true },
{ name: 'typescript', dev: true },
Expand Down
2 changes: 1 addition & 1 deletion packages/dialects/sql.js/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@zenstackhq/kysely-sql-js",
"version": "3.0.0-beta.13",
"version": "3.0.0-beta.14",
"description": "Kysely dialect for sql.js",
"type": "module",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion packages/language/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@zenstackhq/language",
"description": "ZenStack ZModel language specification",
"version": "3.0.0-beta.13",
"version": "3.0.0-beta.14",
"license": "MIT",
"author": "ZenStack Team",
"files": [
Expand Down
File renamed without changes.
8 changes: 4 additions & 4 deletions packages/runtime/package.json → packages/orm/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@zenstackhq/runtime",
"version": "3.0.0-beta.13",
"description": "ZenStack Runtime",
"name": "@zenstackhq/orm",
"version": "3.0.0-beta.14",
"description": "ZenStack ORM",
"type": "module",
"scripts": {
"build": "tsc --noEmit && tsup-node",
Expand Down Expand Up @@ -97,6 +97,6 @@
"@zenstackhq/typescript-config": "workspace:*",
"@zenstackhq/vitest-config": "workspace:*",
"tsx": "^4.19.2",
"zod": "~3.25.0"
"zod": "^4.1.0"
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { invariant, isPlainObject } from '@zenstackhq/common-helpers';
import { enumerate, invariant, isPlainObject } from '@zenstackhq/common-helpers';
import type { Expression, ExpressionBuilder, ExpressionWrapper, SqlBool, ValueNode } from 'kysely';
import { expressionBuilder, sql, type SelectQueryBuilder } from 'kysely';
import { match, P } from 'ts-pattern';
import type { BuiltinType, DataSourceProviderType, FieldDef, GetModels, ModelDef, SchemaDef } from '../../../schema';
import { enumerate } from '../../../utils/enumerate';
import type { OrArray } from '../../../utils/type-utils';
import { AGGREGATE_OPERATORS, DELEGATE_JOINED_FIELD_PREFIX, LOGICAL_COMBINATORS } from '../../constants';
import type {
Expand Down Expand Up @@ -755,7 +754,7 @@ export abstract class BaseCrudDialect<Schema extends SchemaDef> {
: this.fieldRef(model, field, modelAlias);
};

enumerate(orderBy).forEach((orderBy) => {
enumerate(orderBy).forEach((orderBy, index) => {
for (const [field, value] of Object.entries<any>(orderBy)) {
if (!value) {
continue;
Expand Down Expand Up @@ -841,15 +840,16 @@ export abstract class BaseCrudDialect<Schema extends SchemaDef> {
}
} else {
// order by to-one relation
result = result.leftJoin(relationModel, (join) => {
const joinPairs = buildJoinPairs(this.schema, model, modelAlias, field, relationModel);
const joinAlias = `${modelAlias}$orderBy$${index}`;
result = result.leftJoin(`${relationModel} as ${joinAlias}`, (join) => {
const joinPairs = buildJoinPairs(this.schema, model, modelAlias, field, joinAlias);
return join.on((eb) =>
this.and(
...joinPairs.map(([left, right]) => eb(this.eb.ref(left), '=', this.eb.ref(right))),
),
);
});
result = this.buildOrderBy(result, fieldDef.type, relationModel, value, negated);
result = this.buildOrderBy(result, relationModel, joinAlias, value, negated);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,14 @@ export class PostgresCrudDialect<Schema extends SchemaDef> extends BaseCrudDiale
: value,
)
.with('Decimal', () => (value !== null ? value.toString() : value))
.with('Json', () => {
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
// postgres requires simple JSON values to be stringified
return JSON.stringify(value);
} else {
return value;
}
})
.otherwise(() => value);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createId } from '@paralleldrive/cuid2';
import { invariant, isPlainObject } from '@zenstackhq/common-helpers';
import { clone, enumerate, invariant, isPlainObject } from '@zenstackhq/common-helpers';
import {
DeleteResult,
expressionBuilder,
Expand All @@ -17,8 +17,6 @@ import * as uuid from 'uuid';
import type { ClientContract } from '../..';
import type { BuiltinType, Expression, FieldDef } from '../../../schema';
import { ExpressionUtils, type GetModels, type ModelDef, type SchemaDef } from '../../../schema';
import { clone } from '../../../utils/clone';
import { enumerate } from '../../../utils/enumerate';
import { extractFields, fieldsToSelectObject } from '../../../utils/object-utils';
import { NUMERIC_FIELD_TYPES } from '../../constants';
import { TransactionIsolationLevel, type CRUD } from '../../contract';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { invariant } from '@zenstackhq/common-helpers';
import { enumerate, invariant } from '@zenstackhq/common-helpers';
import Decimal from 'decimal.js';
import stableStringify from 'json-stable-stringify';
import { match, P } from 'ts-pattern';
import { z, ZodSchema, ZodType } from 'zod';
import { z, ZodType } from 'zod';
import {
type AttributeApplication,
type BuiltinType,
Expand All @@ -12,7 +12,6 @@ import {
type ModelDef,
type SchemaDef,
} from '../../../schema';
import { enumerate } from '../../../utils/enumerate';
import { extractFields } from '../../../utils/object-utils';
import { formatError } from '../../../utils/zod-utils';
import { AGGREGATE_OPERATORS, LOGICAL_COMBINATORS, NUMERIC_FIELD_TYPES } from '../../constants';
Expand Down Expand Up @@ -831,7 +830,7 @@ export class InputValidator<Schema extends SchemaDef> {

private makeCreateSchema(model: string) {
const dataSchema = this.makeCreateDataSchema(model, false);
let schema: ZodSchema = z.strictObject({
let schema: ZodType = z.strictObject({
data: dataSchema,
select: this.makeSelectSchema(model).optional(),
include: this.makeIncludeSchema(model).optional(),
Expand Down Expand Up @@ -1108,7 +1107,7 @@ export class InputValidator<Schema extends SchemaDef> {
// #region Update

private makeUpdateSchema(model: string) {
let schema: ZodSchema = z.strictObject({
let schema: ZodType = z.strictObject({
where: this.makeWhereSchema(model, true),
data: this.makeUpdateDataSchema(model),
select: this.makeSelectSchema(model).optional(),
Expand All @@ -1130,7 +1129,7 @@ export class InputValidator<Schema extends SchemaDef> {

private makeUpdateManyAndReturnSchema(model: string) {
const base = this.makeUpdateManySchema(model);
let schema: ZodSchema = base.extend({
let schema: ZodType = base.extend({
select: this.makeSelectSchema(model).optional(),
omit: this.makeOmitSchema(model).optional(),
});
Expand All @@ -1139,7 +1138,7 @@ export class InputValidator<Schema extends SchemaDef> {
}

private makeUpsertSchema(model: string) {
let schema: ZodSchema = z.strictObject({
let schema: ZodType = z.strictObject({
where: this.makeWhereSchema(model, true),
create: this.makeCreateDataSchema(model, false),
update: this.makeUpdateDataSchema(model),
Expand Down Expand Up @@ -1258,7 +1257,7 @@ export class InputValidator<Schema extends SchemaDef> {
// #region Delete

private makeDeleteSchema(model: GetModels<Schema>) {
let schema: ZodSchema = z.strictObject({
let schema: ZodType = z.strictObject({
where: this.makeWhereSchema(model, true),
select: this.makeSelectSchema(model).optional(),
include: this.makeIncludeSchema(model).optional(),
Expand Down Expand Up @@ -1388,7 +1387,7 @@ export class InputValidator<Schema extends SchemaDef> {
});

// fields used in `having` must be either in the `by` list, or aggregations
schema = schema.refine((value) => {
schema = schema.refine((value: any) => {
const bys = typeof value.by === 'string' ? [value.by] : value.by;
if (value.having && typeof value.having === 'object') {
for (const [key, val] of Object.entries(value.having)) {
Expand All @@ -1415,7 +1414,7 @@ export class InputValidator<Schema extends SchemaDef> {
}, 'fields in "having" must be in "by"');

// fields used in `orderBy` must be either in the `by` list, or aggregations
schema = schema.refine((value) => {
schema = schema.refine((value: any) => {
const bys = typeof value.by === 'string' ? [value.by] : value.by;
if (
value.orderBy &&
Expand Down
Loading