Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
bf6c9a4
refactor(orm): move validateInput logic into InputValidator (#2480)
ymc9 Mar 12, 2026
ae407ac
feat(orm): add $diagnostics() method for cache stats and slow query t…
ymc9 Mar 13, 2026
24089de
update
ymc9 Mar 13, 2026
5a55977
fix(orm): allow Infinity for slowQueryMaxRecords validation
ymc9 Mar 13, 2026
eef5fd6
improve test
ymc9 Mar 13, 2026
d75f7f3
address PR comments
ymc9 Mar 13, 2026
de4499f
Merge remote-tracking branch 'origin/dev' into feat/diagnostics-property
ymc9 Mar 13, 2026
1ba4e54
refactor(schema): widen types for attributes, default, and foreignKey…
ymc9 Mar 13, 2026
d25c934
fix test
ymc9 Mar 13, 2026
14b55e3
add `startedAt` field to query info
ymc9 Mar 13, 2026
c96bdba
address PR comments
ymc9 Mar 13, 2026
62dfcd1
feat(orm): add $diagnostics() for cache stats and slow query tracking…
ymc9 Mar 13, 2026
f85711a
refactor(schema): widen types for attributes, default, and foreignKey…
ymc9 Mar 13, 2026
e50ced4
fix(orm): diagnostics should return slow queries sorted by duration
ymc9 Mar 13, 2026
031701b
fix: change $diagnostics to a property
ymc9 Mar 13, 2026
23c86ad
update test
ymc9 Mar 13, 2026
7363096
fix(orm): diagnostics should return slow queries sorted by duration (…
ymc9 Mar 13, 2026
49395f7
perf(orm): avoid unnecessary pre-mutation read and transactions (#2484)
ymc9 Mar 14, 2026
0778e49
perf(orm): improve post query data processing performance (#2485)
ymc9 Mar 15, 2026
a6ce140
perf(orm): batch many-to-many relation manipulation (#2486)
ymc9 Mar 16, 2026
12aeb7b
feat(orm): add result plugin extension point (#2442)
genu Mar 16, 2026
c768af7
fix(orm): exclude Unsupported fields from ORM client (#2468)
ymc9 Mar 17, 2026
abae35a
feat(clients): add ExtResult support to TanStack Query hooks (#2490)
genu Mar 18, 2026
d982cc5
remove `resultField` helper
ymc9 Mar 18, 2026
397d2ab
fix(orm): improve result field extension plugin's typing (#2492)
ymc9 Mar 18, 2026
00768de
fix(orm): use uncapitalized model names in OmitConfig and ComputedFie…
ymc9 Mar 19, 2026
f361d32
feat(server): add OpenAPI spec generation for RESTful API (#2498)
ymc9 Mar 20, 2026
c17745d
fix(server): exclude stack trace from REST error responses (#2501)
ymc9 Mar 20, 2026
ece062f
fix(policy): allow dangerous raw SQL opt-in (#2502)
pkudinov Mar 20, 2026
41ea0c9
fix(zod): properly infer scalar array types (#2500)
haltcase Mar 20, 2026
1adf26c
fix: preserve transaction state in $use, $unuse, and $unuseAll (#2497)
pkudinov Mar 21, 2026
cbebe0c
feat(server): add specific 4xx error responses to REST OpenAPI spec (…
ymc9 Mar 21, 2026
cb49667
feat(ide): add telemetry tracking to VSCode extension (#2505)
ymc9 Mar 21, 2026
50b0be5
fix(ide): only error on missing telemetry token in CI (#2507)
ymc9 Mar 23, 2026
e6317dc
fix(orm): disallow `include` on models without relation fields (#2508)
ymc9 Mar 23, 2026
5bb9e67
docs: add README.md to all public packages
ymc9 Mar 23, 2026
bebb181
docs: add README.md to all public packages (#2509)
ymc9 Mar 23, 2026
80a449a
fix: avoid build failures during CI (#2510)
ymc9 Mar 23, 2026
8b1046b
[CI] Bump version 3.5.0 (#2511)
github-actions[bot] Mar 23, 2026
a017550
fix: a few small fixes (#2487)
elliots Mar 23, 2026
3e3b694
docs: add contributors section to README (#2512)
ymc9 Mar 24, 2026
169560a
Add nested routes support in the RestApiHandler (#2454)
lsmith77 Mar 24, 2026
109171b
feat(vscode): zmodel markdown preview (#2506)
ymc9 Mar 24, 2026
c36cf8e
feat(zod): add ORM-style select/include/omit options to makeModelSche…
marcsigmund Mar 24, 2026
29f045b
fix(orm): stricter validation in $setAuth and clean up imports (#2514)
ymc9 Mar 24, 2026
dbca9ed
docs: add Zod schema generation to README features
ymc9 Mar 24, 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 .github/workflows/build-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ on:

env:
TELEMETRY_TRACKING_TOKEN: ${{ secrets.TELEMETRY_TRACKING_TOKEN }}
VSCODE_TELEMETRY_TRACKING_TOKEN: ${{ secrets.VSCODE_TELEMETRY_TRACKING_TOKEN }}
DO_NOT_TRACK: '1'

permissions:
Expand Down
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ ZenStack is a TypeScript database toolkit for developing full-stack or backend N
- 🧩 Designed for extensibility and flexibility
- ⚙️ Automatic CRUD web APIs with adapters for popular frameworks
- 🏖️ Automatic [TanStack Query](https://github.com/TanStack/query) hooks for easy CRUD from the frontend
- 💎 [Zod](https://zod.dev) schema generation

# What's New in V3

Expand Down Expand Up @@ -111,3 +112,23 @@ Thank you for your generous support!
# Community

Join our [discord server](https://discord.gg/Ykhr738dUe) for chat and updates!

# Contributors

Thanks to all the contributors who have helped make ZenStack better!

### Source

<a href="https://github.com/zenstackhq/zenstack/graphs/contributors">
<img src="https://contrib.rocks/image?repo=zenstackhq/zenstack" />
</a>

### Docs

<a href="https://github.com/zenstackhq/zenstack-docs/graphs/contributors">
<img src="https://contrib.rocks/image?repo=zenstackhq/zenstack-docs" />
</a>

## License

[MIT](LICENSE)
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "zenstack-v3",
"version": "3.4.6",
"version": "3.5.0",
"description": "ZenStack",
"packageManager": "pnpm@10.23.0",
"type": "module",
Expand Down
2 changes: 1 addition & 1 deletion 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.4.6",
"version": "3.5.0",
"description": "ZenStack Better Auth Adapter. This adapter is modified from better-auth's Prisma adapter.",
"type": "module",
"scripts": {
Expand Down
24 changes: 24 additions & 0 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# @zenstackhq/cli

The command-line interface for ZenStack. Provides commands for initializing projects, generating TypeScript code from ZModel schemas, managing database migrations, and etc.

## Key Commands

- `zenstack init` — Initialize ZenStack in an existing project
- `zenstack generate` — Compile ZModel schema to TypeScript
- `zenstack db push` — Sync schema to the database
- `zenstack db pull` — Pull database schema changes into ZModel
- `zenstack migrate dev` — Create and apply database migrations
- `zenstack migrate deploy` — Deploy migrations to production
- `zenstack format` — Format ZModel schema files
- `zenstack proxy|studio` — Start a database proxy server for using studio

## Installation

```bash
npm install -D @zenstackhq/cli
```

## Learn More

- [ZenStack Documentation](https://zenstack.dev/docs)
2 changes: 1 addition & 1 deletion 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.4.6",
"version": "3.5.0",
"type": "module",
"author": {
"name": "ZenStack Team"
Expand Down
8 changes: 8 additions & 0 deletions packages/cli/src/actions/action-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,14 @@ export async function loadPluginModule(provider: string, basePath: string) {
}
}

// try jiti import for bare package specifiers (handles workspace packages)
try {
const result = (await jiti.import(moduleSpec, { default: true })) as CliPlugin;
return result;
} catch {
// fall through to last resort
}

// last resort, try to import as esm directly
try {
const mod = await import(moduleSpec);
Expand Down
4 changes: 3 additions & 1 deletion packages/cli/src/actions/check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ async function checkPluginResolution(schemaFile: string, model: Model) {
for (const plugin of plugins) {
const provider = getPluginProvider(plugin);
if (!provider.startsWith('@core/')) {
await loadPluginModule(provider, path.dirname(schemaFile));
const pluginSourcePath =
plugin.$cstNode?.parent?.element.$document?.uri?.fsPath ?? schemaFile;
await loadPluginModule(provider, path.dirname(pluginSourcePath));
}
}
}
8 changes: 6 additions & 2 deletions packages/cli/src/actions/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,11 @@ async function runPlugins(schemaFile: string, model: Model, outputPath: string,
throw new CliError(`Unknown core plugin: ${provider}`);
}
} else {
cliPlugin = await loadPluginModule(provider, path.dirname(schemaFile));
// resolve relative plugin paths against the schema file where the plugin is declared,
// not the entry schema file
const pluginSourcePath =
plugin.$cstNode?.parent?.element.$document?.uri?.fsPath ?? schemaFile;
cliPlugin = await loadPluginModule(provider, path.dirname(pluginSourcePath));
}

if (cliPlugin) {
Expand Down Expand Up @@ -252,7 +256,7 @@ async function runPlugins(schemaFile: string, model: Model, outputPath: string,
spinner?.succeed();
} catch (err) {
spinner?.fail();
console.error(err);
throw err;
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import 'dotenv/config';
import { ZModelLanguageMetaData } from '@zenstackhq/language';
import colors from 'colors';
import { Command, CommanderError, Option } from 'commander';
import 'dotenv/config';
import * as actions from './actions';
import { CliError } from './cli-error';
import { telemetry } from './telemetry';
Expand Down
81 changes: 80 additions & 1 deletion packages/cli/test/generate.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { formatDocument } from '@zenstackhq/language';
import fs from 'node:fs';
import path from 'node:path';
import { describe, expect, it } from 'vitest';
import { createProject, runCli } from './utils';
import { createProject, getDefaultPrelude, runCli } from './utils';

const model = `
model User {
Expand Down Expand Up @@ -272,6 +273,84 @@ model User {
expect(fs.existsSync(path.join(workDir, 'zenstack/schema.ts'))).toBe(true);
});

it('should load plugin from a bare package specifier via jiti', async () => {
const modelWithBarePlugin = `
plugin foo {
provider = 'my-test-plugin'
}

model User {
id String @id @default(cuid())
}
`;
const { workDir } = await createProject(modelWithBarePlugin);
// Create a fake node_modules package with a TS entry point
// This can only be resolved by jiti, not by native import() or fs.existsSync checks
const pkgDir = path.join(workDir, 'node_modules/my-test-plugin');
fs.mkdirSync(pkgDir, { recursive: true });
fs.writeFileSync(
path.join(pkgDir, 'package.json'),
JSON.stringify({ name: 'my-test-plugin', main: './index.ts' }),
);
fs.writeFileSync(
path.join(pkgDir, 'index.ts'),
`
const plugin = {
name: 'test-bare-plugin',
statusText: 'Testing bare plugin',
async generate() {},
};
export default plugin;
`,
);
runCli('generate', workDir);
expect(fs.existsSync(path.join(workDir, 'zenstack/schema.ts'))).toBe(true);
});

it('should resolve plugin paths relative to the schema file where the plugin is declared', async () => {
// Entry schema imports a sub-schema that declares a plugin with a relative path.
// The plugin path should resolve relative to the sub-schema, not the entry schema.
const { workDir } = await createProject(
`import './core/core'

${getDefaultPrelude()}

model User {
id String @id @default(cuid())
}
`,
{ customPrelude: true },
);

// Create core/ subdirectory with its own schema and plugin
const coreDir = path.join(workDir, 'zenstack/core');
fs.mkdirSync(coreDir, { recursive: true });

const coreSchema = await formatDocument(`
plugin foo {
provider = './my-core-plugin.ts'
}
`);
fs.writeFileSync(path.join(coreDir, 'core.zmodel'), coreSchema);

// Plugin lives next to the core schema, NOT next to the entry schema
fs.writeFileSync(
path.join(coreDir, 'my-core-plugin.ts'),
`
const plugin = {
name: 'core-plugin',
statusText: 'Testing core plugin',
async generate() {},
};
export default plugin;
`,
);

// This would fail if the plugin path was resolved relative to the entry schema
runCli('generate', workDir);
expect(fs.existsSync(path.join(workDir, 'zenstack/schema.ts'))).toBe(true);
});

it('should prefer CLI options over @core/typescript plugin settings for generateModels and generateInput', async () => {
const modelWithPlugin = `
plugin typescript {
Expand Down
2 changes: 1 addition & 1 deletion packages/clients/client-helpers/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@zenstackhq/client-helpers",
"version": "3.4.6",
"version": "3.5.0",
"description": "Helpers for implementing clients that consume ZenStack's CRUD service",
"type": "module",
"scripts": {
Expand Down
20 changes: 20 additions & 0 deletions packages/clients/client-helpers/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,28 @@
import type { ClientContract, QueryOptions } from '@zenstackhq/orm';
import type { SchemaDef } from '@zenstackhq/schema';

/**
* A type that represents either a value of type T or a Promise that resolves to type T.
*/
export type MaybePromise<T> = T | Promise<T> | PromiseLike<T>;

/**
* Infers the schema definition from a client contract type, or passes through a raw SchemaDef.
*/
export type InferSchema<T> = T extends { $schema: infer S extends SchemaDef } ? S : T extends SchemaDef ? T : never;

/**
* Extracts the ExtResult type from a client contract, or defaults to `{}`.
*/
export type InferExtResult<T> = T extends ClientContract<any, any, any, any, infer E> ? E : {};

/**
* Infers query options from a client contract type, or defaults to `QueryOptions<Schema>`.
*/
export type InferOptions<T, Schema extends SchemaDef> = T extends { $options: infer O extends QueryOptions<Schema> }
? O
: QueryOptions<Schema>;

/**
* List of ORM write actions.
*/
Expand Down
2 changes: 1 addition & 1 deletion packages/clients/tanstack-query/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@zenstackhq/tanstack-query",
"version": "3.4.6",
"version": "3.5.0",
"description": "TanStack Query Client for consuming ZenStack v3's CRUD service",
"type": "module",
"scripts": {
Expand Down
21 changes: 11 additions & 10 deletions packages/clients/tanstack-query/src/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ import type { FetchFn } from '@zenstackhq/client-helpers/fetch';
import type {
GetProcedureNames,
GetSlicedOperations,
OperationsIneligibleForDelegateModels,
ModelAllowsCreate,
OperationsRequiringCreate,
ProcedureFunc,
QueryOptions,
} from '@zenstackhq/orm';
import type { GetModels, IsDelegateModel, SchemaDef } from '@zenstackhq/schema';
import type { GetModels, SchemaDef } from '@zenstackhq/schema';

/**
* Context type for configuring the hooks.
Expand Down Expand Up @@ -59,8 +60,8 @@ export type ExtraMutationOptions = {
optimisticDataProvider?: OptimisticDataProvider;
} & QueryContext;

type HooksOperationsIneligibleForDelegateModels = OperationsIneligibleForDelegateModels extends any
? `use${Capitalize<OperationsIneligibleForDelegateModels>}`
type HooksOperationsRequiringCreate = OperationsRequiringCreate extends any
? `use${Capitalize<OperationsRequiringCreate>}`
: never;

type Modifiers = '' | 'Suspense' | 'Infinite' | 'SuspenseInfinite';
Expand All @@ -76,12 +77,12 @@ export type TrimSlicedOperations<
> = {
// trim operations based on slicing options
[Key in keyof T as Key extends `use${Modifiers}${Capitalize<GetSlicedOperations<Schema, Model, Options>>}`
? IsDelegateModel<Schema, Model> extends true
? // trim operations ineligible for delegate models
Key extends HooksOperationsIneligibleForDelegateModels
? never
: Key
: Key
? ModelAllowsCreate<Schema, Model> extends true
? Key
: // trim create operations for models that don't allow create
Key extends HooksOperationsRequiringCreate
? never
: Key
: never]: T[Key];
};

Expand Down
Loading
Loading