Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
f0278ca
chore(deps-dev): bump elysia from 1.4.13 to 1.4.18 (#2384)
dependabot[bot] Feb 17, 2026
da3b0af
feat: ORM api slicing (#2383)
ymc9 Feb 18, 2026
fd7c5ec
chore(orm): add aliased exports for all write operations (#2386)
ymc9 Feb 18, 2026
3e03ab1
refactor: remove import from orm package in generated schema (#2387)
ymc9 Feb 20, 2026
7a98d41
feat: creating zod schemas for zmodel constructs and ORM query input …
ymc9 Feb 20, 2026
ba735ba
fix(zod): a couple of improvements
ymc9 Feb 21, 2026
960b861
address PR comments
ymc9 Feb 21, 2026
7e838a6
fix(zod): a couple of improvements (#2391)
ymc9 Feb 21, 2026
1e70052
feat(zod): introduce an option to control depth of the built zod sche…
ymc9 Feb 21, 2026
608133a
refactor(proxy): change database driver packages to optional peer dep…
jiashengguo Feb 21, 2026
d49c39e
fix: enhance delegate model interaction with afterEntityMutation plug…
genu Feb 22, 2026
f3a9850
fix: reject select with only false fields to prevent empty SELECT SQL…
ymc9 Feb 24, 2026
defb707
chore: add regression test for #2375 (#2400)
ymc9 Feb 24, 2026
89e3acb
fix: auto-add "views" preview feature to generated Prisma schema (#23…
ymc9 Feb 24, 2026
f2c567b
fix(orm): _count is not included in select clause's typing when query…
ymc9 Feb 24, 2026
17922f0
feat(transaction): implement transaction handling with sequential ope…
jiashengguo Feb 25, 2026
ca8f437
fix(orm): use compact alias names when transforming ORM queries to Ky…
ymc9 Feb 25, 2026
c7c3d86
chore: fix npm audit issues (#2407)
ymc9 Feb 26, 2026
2da0c21
[CI] Bump version 3.4.0 (#2408)
github-actions[bot] Feb 26, 2026
851e744
fix(cli): update version check tag to latest (#2412)
ymc9 Feb 27, 2026
75bc4a1
fix(policy): wrong table alias used when injecting for field policies…
ymc9 Feb 27, 2026
4fe27ef
chore(cli): show notifications for generate command (#2409)
jiashengguo Feb 27, 2026
e5e452c
Fix: disconnect correct client instance in zod test `finally` block (…
Copilot Feb 27, 2026
7c83f7c
Fix order-dependent assertions in slicing E2E tests (#2416)
Copilot Feb 27, 2026
e46ddc5
fix(zod): exclude computed and delegate fields from create/update sch…
ymc9 Feb 28, 2026
4b42ed9
fix(orm): disallow create/update on computed fields and delegate disc…
ymc9 Feb 28, 2026
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
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
### Testing

- E2E tests are in `tests/e2e/` directory
- Regression tests for GitHub issues go in `tests/regression/test/` as `issue-{number}.test.ts`

### ZenStack CLI Commands

Expand Down
9 changes: 7 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "zenstack-v3",
"version": "3.3.3",
"version": "3.4.0",
"description": "ZenStack",
"packageManager": "pnpm@10.23.0",
"type": "module",
Expand Down Expand Up @@ -50,6 +50,11 @@
"better-sqlite3",
"esbuild",
"vue-demi"
]
],
"overrides": {
"cookie@<0.7.0": ">=0.7.0",
"lodash-es@>=4.0.0 <=4.17.22": ">=4.17.23",
"lodash@>=4.0.0 <=4.17.22": ">=4.17.23"
}
}
}
8 changes: 4 additions & 4 deletions packages/auth-adapters/better-auth/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@zenstackhq/better-auth",
"version": "3.3.3",
"version": "3.4.0",
"description": "ZenStack Better Auth Adapter. This adapter is modified from better-auth's Prisma adapter.",
"type": "module",
"scripts": {
Expand Down Expand Up @@ -46,9 +46,9 @@
"better-auth": "^1.3.0"
},
"devDependencies": {
"@better-auth/core": "1.4.17",
"better-auth": "1.4.17",
"@better-auth/cli": "1.4.17",
"@better-auth/core": "1.4.19",
"better-auth": "1.4.19",
"@better-auth/cli": "1.4.19",
"@types/tmp": "catalog:",
"@zenstackhq/cli": "workspace:*",
"@zenstackhq/eslint-config": "workspace:*",
Expand Down
2 changes: 1 addition & 1 deletion packages/auth-adapters/better-auth/src/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ export const zenstackAdapter = <Schema extends SchemaDef>(db: ClientContract<Sch
const whereClause = convertWhereClause(model, where);
return await modelDb.update({
where: whereClause,
data: update as UpdateInput<SchemaDef, GetModels<SchemaDef>>,
data: update as UpdateInput<SchemaDef, GetModels<SchemaDef>, any>,
});
},

Expand Down
29 changes: 22 additions & 7 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.3.3",
"version": "3.4.0",
"type": "module",
"author": {
"name": "ZenStack Team"
Expand Down Expand Up @@ -37,12 +37,11 @@
},
"dependencies": {
"@zenstackhq/common-helpers": "workspace:*",
"@zenstackhq/schema": "workspace:*",
"@zenstackhq/language": "workspace:*",
"@zenstackhq/orm": "workspace:*",
"@zenstackhq/schema": "workspace:*",
"@zenstackhq/sdk": "workspace:*",
"@zenstackhq/server": "workspace:*",
"better-sqlite3": "catalog:",
"chokidar": "^5.0.0",
"colors": "1.4.0",
"commander": "^8.3.0",
Expand All @@ -53,13 +52,13 @@
"jiti": "^2.6.1",
"langium": "catalog:",
"mixpanel": "^0.18.1",
"mysql2": "catalog:",
"ora": "^5.4.1",
"package-manager-detector": "^1.3.0",
"pg": "catalog:",
"prisma": "catalog:",
"semver": "^7.7.2",
"ts-pattern": "catalog:"
"terminal-link": "^5.0.0",
"ts-pattern": "catalog:",
"zod": "catalog:"
},
"devDependencies": {
"@types/better-sqlite3": "catalog:",
Expand All @@ -74,7 +73,23 @@
"@zenstackhq/vitest-config": "workspace:*",
"tmp": "catalog:"
},
"peerDependencies": {
"better-sqlite3": "catalog:",
"mysql2": "catalog:",
"pg": "catalog:"
},
"peerDependenciesMeta": {
"pg": {
"optional": true
},
"better-sqlite3": {
"optional": true
},
"mysql2": {
"optional": true
}
},
"engines": {
"node": ">=20"
}
}
}
68 changes: 68 additions & 0 deletions packages/cli/src/actions/action-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import fs from 'node:fs';
import { createRequire } from 'node:module';
import path from 'node:path';
import { CliError } from '../cli-error';
import terminalLink from 'terminal-link';
import { z } from 'zod';

export function getSchemaFile(file?: string) {
if (file) {
Expand Down Expand Up @@ -216,3 +218,69 @@ export async function getZenStackPackages(

return result.filter((p) => !!p);
}

const FETCH_CLI_MAX_TIME = 1000;
const CLI_CONFIG_ENDPOINT = 'https://zenstack.dev/config/cli-v3.json';

const usageTipsSchema = z.object({
notifications: z.array(z.object({ title: z.string(), url: z.url().optional(), active: z.boolean() })),
});

/**
* Starts the usage tips fetch in the background. Returns a callback that, when invoked check if the fetch
* is complete. If not complete, it will wait until the max time is reached. After that, if fetch is still
* not complete, just return.
*/
export function startUsageTipsFetch() {
let fetchedData: z.infer<typeof usageTipsSchema> | undefined = undefined;
let fetchComplete = false;

const start = Date.now();
const controller = new AbortController();

fetch(CLI_CONFIG_ENDPOINT, {
headers: { accept: 'application/json' },
signal: controller.signal,
})
.then(async (res) => {
if (!res.ok) return;
const data = await res.json();
const parseResult = usageTipsSchema.safeParse(data);
if (parseResult.success) {
fetchedData = parseResult.data;
}
})
.catch(() => {
// noop
})
.finally(() => {
fetchComplete = true;
});

return async () => {
const elapsed = Date.now() - start;

if (!fetchComplete && elapsed < FETCH_CLI_MAX_TIME) {
// wait for the timeout
await new Promise((resolve) => setTimeout(resolve, FETCH_CLI_MAX_TIME - elapsed));
}

if (!fetchComplete) {
controller.abort();
return;
}

if (!fetchedData) return;

const activeItems = fetchedData.notifications.filter((item) => item.active);
// show a random active item
if (activeItems.length > 0) {
const item = activeItems[Math.floor(Math.random() * activeItems.length)]!;
if (item.url) {
console.log(terminalLink(item.title, item.url));
} else {
console.log(item.title);
}
}
};
}
43 changes: 34 additions & 9 deletions packages/cli/src/actions/generate.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,36 @@
import { invariant, singleDebounce } from '@zenstackhq/common-helpers';
import { ZModelLanguageMetaData } from '@zenstackhq/language';
import { type AbstractDeclaration, isPlugin, LiteralExpr, Plugin, type Model } from '@zenstackhq/language/ast';
import { isPlugin, LiteralExpr, Plugin, type AbstractDeclaration, type Model } from '@zenstackhq/language/ast';
import { getLiteral, getLiteralArray } from '@zenstackhq/language/utils';
import { type CliPlugin } from '@zenstackhq/sdk';
import { watch } from 'chokidar';
import colors from 'colors';
import { createJiti } from 'jiti';
import fs from 'node:fs';
import path from 'node:path';
import { pathToFileURL } from 'node:url';
import { watch } from 'chokidar';
import ora, { type Ora } from 'ora';
import semver from 'semver';
import { CliError } from '../cli-error';
import * as corePlugins from '../plugins';
import { getOutputPath, getSchemaFile, getZenStackPackages, loadSchemaDocument } from './action-utils';
import semver from 'semver';
import {
getOutputPath,
getSchemaFile,
getZenStackPackages,
loadSchemaDocument,
startUsageTipsFetch,
} from './action-utils';

type Options = {
schema?: string;
output?: string;
silent: boolean;
watch: boolean;
lite: boolean;
liteOnly: boolean;
lite?: boolean;
liteOnly?: boolean;
generateModels?: boolean;
generateInput?: boolean;
tips?: boolean;
};

/**
Expand All @@ -33,8 +42,13 @@ export async function run(options: Options) {
} catch (err) {
console.warn(colors.yellow(`Failed to check for mismatched ZenStack packages: ${err}`));
}

const maybeShowUsageTips = options.tips && !options.silent && !options.watch ? startUsageTipsFetch() : undefined;

const model = await pureGenerate(options, false);

await maybeShowUsageTips?.();

if (options.watch) {
const logsEnabled = !options.silent;

Expand Down Expand Up @@ -181,12 +195,18 @@ async function runPlugins(schemaFile: string, model: Model, outputPath: string,

// merge CLI options
if (provider === '@core/typescript') {
if (pluginOptions['lite'] === undefined) {
if (options.lite !== undefined) {
pluginOptions['lite'] = options.lite;
}
if (pluginOptions['liteOnly'] === undefined) {
if (options.liteOnly !== undefined) {
pluginOptions['liteOnly'] = options.liteOnly;
}
if (options.generateModels !== undefined) {
pluginOptions['generateModels'] = options.generateModels;
}
if (options.generateInput !== undefined) {
pluginOptions['generateInput'] = options.generateInput;
}
}

processedPlugins.push({ cliPlugin, pluginOptions });
Expand All @@ -196,7 +216,12 @@ async function runPlugins(schemaFile: string, model: Model, outputPath: string,
const defaultPlugins = [
{
plugin: corePlugins['typescript'],
options: { lite: options.lite, liteOnly: options.liteOnly },
options: {
lite: options.lite,
liteOnly: options.liteOnly,
generateModels: options.generateModels,
generateInput: options.generateInput,
},
},
];
defaultPlugins.forEach(({ plugin, options }) => {
Expand Down
42 changes: 33 additions & 9 deletions packages/cli/src/actions/proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@ import { PostgresDialect } from '@zenstackhq/orm/dialects/postgres';
import { SqliteDialect } from '@zenstackhq/orm/dialects/sqlite';
import { RPCApiHandler } from '@zenstackhq/server/api';
import { ZenStackMiddleware } from '@zenstackhq/server/express';
import SQLite from 'better-sqlite3';
import type BetterSqlite3 from 'better-sqlite3';
import colors from 'colors';
import cors from 'cors';
import express from 'express';
import { createJiti } from 'jiti';
import { createPool as createMysqlPool } from 'mysql2';
import type { createPool as MysqlCreatePool } from 'mysql2';
import path from 'node:path';
import { Pool as PgPool } from 'pg';
import type { Pool as PgPoolType } from 'pg';
import { CliError } from '../cli-error';
import { getVersion } from '../utils/version-utils';
import { getOutputPath, getSchemaFile, loadSchemaDocument } from './action-utils';
Expand Down Expand Up @@ -67,7 +67,7 @@ export async function run(options: Options) {

const provider = getStringLiteral(dataSource?.fields.find((f) => f.name === 'provider')?.value)!;

const dialect = createDialect(provider, databaseUrl!, outputPath);
const dialect = await createDialect(provider, databaseUrl!, outputPath);

const jiti = createJiti(import.meta.url);

Expand Down Expand Up @@ -137,9 +137,17 @@ function redactDatabaseUrl(url: string): string {
}
}

function createDialect(provider: string, databaseUrl: string, outputPath: string) {
async function createDialect(provider: string, databaseUrl: string, outputPath: string) {
switch (provider) {
case 'sqlite': {
let SQLite: typeof BetterSqlite3;
try {
SQLite = (await import('better-sqlite3')).default;
} catch {
throw new CliError(
`Package "better-sqlite3" is required for SQLite support. Please install it with: npm install better-sqlite3`,
);
}
let resolvedUrl = databaseUrl.trim();
if (resolvedUrl.startsWith('file:')) {
const filePath = resolvedUrl.substring('file:'.length);
Expand All @@ -152,20 +160,36 @@ function createDialect(provider: string, databaseUrl: string, outputPath: string
database: new SQLite(resolvedUrl),
});
}
case 'postgresql':
case 'postgresql': {
let PgPool: typeof PgPoolType;
try {
PgPool = (await import('pg')).Pool;
} catch {
throw new CliError(
`Package "pg" is required for PostgreSQL support. Please install it with: npm install pg`,
);
}
console.log(colors.gray(`Connecting to PostgreSQL database at: ${redactDatabaseUrl(databaseUrl)}`));
return new PostgresDialect({
pool: new PgPool({
connectionString: databaseUrl,
}),
});

case 'mysql':
}
case 'mysql': {
let createMysqlPool: typeof MysqlCreatePool;
try {
createMysqlPool = (await import('mysql2')).createPool;
} catch {
throw new CliError(
`Package "mysql2" is required for MySQL support. Please install it with: npm install mysql2`,
);
}
console.log(colors.gray(`Connecting to MySQL database at: ${redactDatabaseUrl(databaseUrl)}`));
return new MysqlDialect({
pool: createMysqlPool(databaseUrl),
});

}
default:
throw new CliError(`Unsupported database provider: ${provider}`);
}
Expand Down
Loading
Loading