Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
2a10bca
fix(orm): export UncheckedCreateInput/CheckedCreateInput and add XOR …
ymc9 Apr 29, 2026
9cef027
chore: upgrade to TypeScript 6 (#2629)
ymc9 Apr 30, 2026
67da884
fix(tanstack-query): support DbNull/JsonNull/AnyNull serialization ov…
ymc9 Apr 30, 2026
516a2a2
update test
ymc9 Apr 30, 2026
a31a32e
fix(tanstack-query): support DbNull/JsonNull/AnyNull serialization ov…
ymc9 Apr 30, 2026
22e0fd4
feat(tanstack-query): add useTransaction hook for sequential transact…
ymc9 May 4, 2026
679f91f
feat(orm): add fuzzy search and relevance ordering (PostgreSQL) (#2573)
docloulou May 4, 2026
eff4263
refactor(tanstack-query, orm): thread plugin generics through transac…
ymc9 May 5, 2026
8ddbfde
feat(orm): add field-level @fuzzy attribute to gate fuzzy search (#2642)
ymc9 May 5, 2026
090be2c
fix(zod): json type compatibility between inferred zod types and @zen…
Azzerty23 May 6, 2026
d5e7900
fix(orm, zod): allow null in inferred type of required Json fields (#…
ymc9 May 6, 2026
2ef5a99
refactor(orm): make ZenStackPromise compatible with standard Promise …
ymc9 May 6, 2026
f0fa5ea
chore: run test:generate during build for orm/schema/zod
ymc9 May 6, 2026
9d147b9
feat(fetch-client): implement fetch-based CRUD API client (#2651)
ymc9 May 6, 2026
d0dd954
feat(orm): add @fullText attribute and Postgres full-text search
ymc9 May 6, 2026
1997cf3
fix(orm): coalesce NULL → '' in single-field _ftsRelevance ORDER BY
ymc9 May 7, 2026
9bfc3fe
feat(orm): implement postgres full-text search (#2653)
ymc9 May 7, 2026
d1db37c
fix(orm): format Date as HH:MM:SS for @db.Time / @db.Timetz columns (…
erwan-joly May 8, 2026
7283d0e
fix(orm): handle cyclic JSON typedef references in zod factory (#2654…
ymc9 May 8, 2026
899e74d
test(regression): add regression test for issue #2639 (#2657)
ymc9 May 8, 2026
b53e908
[CI] Bump version 3.7.0 (#2656)
github-actions[bot] May 8, 2026
08c11e7
fix(better-auth): keep schema-generator import lazy in CJS output (#2…
ymc9 May 8, 2026
ce50d3b
fix(orm): coerce ISO strings on DateTime input, with strictDateInput …
erwan-joly May 8, 2026
1a4de21
fix: detect policy plugin by stable id (#2663)
Albatrosso May 12, 2026
79498da
test(fetch-client): restore globalThis.fetch in afterEach (#2668)
ymc9 May 12, 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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "zenstack-v3",
"displayName": "ZenStack",
"description": "ZenStack",
"version": "3.6.4",
"version": "3.7.0",
"type": "module",
"author": {
"name": "ZenStack Team",
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
Expand Up @@ -2,7 +2,7 @@
"name": "@zenstackhq/better-auth",
"displayName": "ZenStack Better Auth Adapter",
"description": "ZenStack Better Auth Adapter. This adapter is modified from better-auth's Prisma adapter.",
"version": "3.6.4",
"version": "3.7.0",
"type": "module",
"author": {
"name": "ZenStack Team",
Expand Down
4 changes: 3 additions & 1 deletion packages/auth-adapters/better-auth/src/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,9 @@ export const zenstackAdapter = <Schema extends SchemaDef>(db: ClientContract<Sch
options: config,

createSchema: async ({ file, tables }) => {
const generateSchema = (await import('./schema-generator')).generateSchema;
// Self-import via package subpath (not a relative './schema-generator') so the
// bundler treats it as external and keeps it lazy in the CJS output — see tsdown.config.ts.
const generateSchema = (await import('@zenstackhq/better-auth/schema-generator')).generateSchema;
return generateSchema(file, tables, config, options);
},
};
Expand Down
6 changes: 5 additions & 1 deletion packages/auth-adapters/better-auth/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@
"extends": "@zenstackhq/typescript-config/base.json",
"compilerOptions": {
"rootDir": ".",
"noPropertyAccessFromIndexSignature": false
"noPropertyAccessFromIndexSignature": false,
"types": ["node"],
"paths": {
"@zenstackhq/better-auth/schema-generator": ["./src/schema-generator.ts"]
}
},
"include": ["src/**/*"]
}
17 changes: 16 additions & 1 deletion packages/auth-adapters/better-auth/tsdown.config.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,18 @@
import { createConfig } from '@zenstackhq/tsdown-config';

export default createConfig({ entry: { index: 'src/index.ts', 'schema-generator': 'src/schema-generator.ts' } });
// `index` and `schema-generator` are built as two separate tsdown invocations so that
// the lazy `await import('@zenstackhq/better-auth/schema-generator')` in the adapter
// stays lazy in the CJS output. When both entries live in a single build, Rolldown
// treats them as siblings and injects a top-level `require('./schema-generator.cjs')`
// into `index.cjs`, which eagerly pulls in `@zenstackhq/language` (Langium) at adapter
// load time. Splitting the builds hides that relationship; `neverBundle` then keeps
// the dynamic import as a package-name reference that Node resolves at first call.
export default [
createConfig({
entry: { index: 'src/index.ts' },
deps: { neverBundle: ['@zenstackhq/better-auth/schema-generator'] },
}),
createConfig({
entry: { 'schema-generator': 'src/schema-generator.ts' },
}),
];
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@zenstackhq/cli",
"displayName": "ZenStack CLI",
"description": "FullStack database toolkit with built-in access control and automatic API generation.",
"version": "3.6.4",
"version": "3.7.0",
"type": "module",
"author": {
"name": "ZenStack Team",
Expand Down
6 changes: 3 additions & 3 deletions packages/cli/src/actions/proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { ZenStackClient, type ClientContract } from '@zenstackhq/orm';
import { MysqlDialect } from '@zenstackhq/orm/dialects/mysql';
import { PostgresDialect } from '@zenstackhq/orm/dialects/postgres';
import { SqliteDialect } from '@zenstackhq/orm/dialects/sqlite';
import type { SchemaDef } from '@zenstackhq/orm/schema';
import { RPCApiHandler } from '@zenstackhq/server/api';
import { ZenStackMiddleware } from '@zenstackhq/server/express';
import type BetterSqlite3 from 'better-sqlite3';
Expand All @@ -24,7 +25,6 @@ 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';
import type { SchemaDef } from '@zenstackhq/orm/schema';

type Options = {
output?: string;
Expand Down Expand Up @@ -198,7 +198,7 @@ async function createDialect(provider: string, databaseUrl: string, outputPath:
}
}

export function createProxyApp(client: ClientContract<any, any>, schema: any): express.Application {
export function createProxyApp(client: ClientContract<SchemaDef>, schema: SchemaDef): express.Application {
const app = express();
app.use(cors());
app.use(express.json({ limit: '5mb' }));
Expand All @@ -219,7 +219,7 @@ export function createProxyApp(client: ClientContract<any, any>, schema: any): e
return app;
}

function startServer(client: ClientContract<any, any>, schema: any, options: Options) {
function startServer(client: ClientContract<SchemaDef>, schema: any, options: Options) {
const app = createProxyApp(client, schema);

const server = app.listen(options.port, () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/test/plugins/custom-plugin.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { execSync } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
import { describe, expect, it } from 'vitest';
import { createProject, runCli } from '../utils';
import { execSync } from 'node:child_process';

describe('Custom plugins tests', () => {
it('runs custom plugin generator', async () => {
Expand Down
4 changes: 3 additions & 1 deletion packages/cli/tsdown.config.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { createConfig } from '@zenstackhq/tsdown-config';

export default createConfig({ entry: { index: 'src/index.ts' } });
export default createConfig({
entry: { index: 'src/index.ts' },
});
2 changes: 1 addition & 1 deletion packages/clients/client-helpers/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@zenstackhq/client-helpers",
"displayName": "ZenStack Client Helpers",
"description": "Helpers for implementing clients that consume ZenStack's CRUD service",
"version": "3.6.4",
"version": "3.7.0",
"type": "module",
"author": {
"name": "ZenStack Team",
Expand Down
9 changes: 7 additions & 2 deletions packages/clients/client-helpers/src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
/**
* The default query endpoint.
* Route segment used for custom procedures.
*/
export const DEFAULT_QUERY_ENDPOINT = '/api/model';
export const CUSTOM_PROC_ROUTE_NAME = '$procs';

/**
* Route prefix used for transactions.
*/
export const TRANSACTION_ROUTE_PREFIX = '$transaction';
28 changes: 28 additions & 0 deletions packages/clients/client-helpers/src/fetch.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { lowerCaseFirst } from '@zenstackhq/common-helpers';
import { AnyNull, AnyNullClass, DbNull, DbNullClass, JsonNull, JsonNullClass } from '@zenstackhq/orm/common-types';
import Decimal from 'decimal.js';
import SuperJSON from 'superjson';
import type { QueryError } from './types';
Expand Down Expand Up @@ -65,6 +66,33 @@ SuperJSON.registerCustom<Decimal, string>(
'Decimal',
);

SuperJSON.registerCustom<DbNullClass, string>(
{
isApplicable: (v): v is DbNullClass => v instanceof DbNullClass,
serialize: () => 'DbNull',
deserialize: () => DbNull,
},
'DbNull',
);

SuperJSON.registerCustom<JsonNullClass, string>(
{
isApplicable: (v): v is JsonNullClass => v instanceof JsonNullClass,
serialize: () => 'JsonNull',
deserialize: () => JsonNull,
},
'JsonNull',
);

SuperJSON.registerCustom<AnyNullClass, string>(
{
isApplicable: (v): v is AnyNullClass => v instanceof AnyNullClass,
serialize: () => 'AnyNull',
deserialize: () => AnyNull,
},
'AnyNull',
);

/**
* Serialize the given value with superjson
*/
Expand Down
2 changes: 2 additions & 0 deletions packages/clients/client-helpers/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { AnyNull, DbNull, JsonNull } from '@zenstackhq/orm/common-types';
export * from './constants';
export * from './invalidation';
export * from './logging';
Expand All @@ -6,4 +7,5 @@ export * from './nested-read-visitor';
export * from './nested-write-visitor';
export * from './optimistic';
export * from './query-analysis';
export * from './transaction';
export * from './types';
9 changes: 8 additions & 1 deletion packages/clients/client-helpers/src/invalidation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,11 @@ export function createInvalidator(
invalidator: InvalidateFunc,
logging: Logger | undefined,
) {
const normalizedModel = normalizeModelName(model, schema);
return async (...args: unknown[]) => {
const [_, variables] = args;
const predicate = await getInvalidationPredicate(
model,
normalizedModel,
operation as ORMWriteActionType,
variables,
schema,
Expand Down Expand Up @@ -87,3 +88,9 @@ function findNestedRead(visitingModel: string, targetModels: string[], schema: S
const modelsRead = getReadModels(visitingModel, schema, args);
return targetModels.some((m) => modelsRead.includes(m));
}

// resolves a model name to its canonical form as defined in the schema (case-insensitive match)
function normalizeModelName(model: string, schema: SchemaDef) {
const target = model.toLowerCase();
return Object.keys(schema.models).find((k) => k.toLowerCase() === target) ?? model;
}
4 changes: 0 additions & 4 deletions packages/clients/client-helpers/src/nested-write-visitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -297,10 +297,6 @@ export class NestedWriteVisitor {
}
}
break;

default: {
throw new Error(`unhandled action type ${action}`);
}
}
}

Expand Down
70 changes: 70 additions & 0 deletions packages/clients/client-helpers/src/transaction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import type {
CoreCrudOperations,
CrudArgsMap,
CrudReturnMap,
ExtQueryArgsBase,
ExtResultBase,
GetSlicedOperations,
ModelAllowsCreate,
OperationsRequiringCreate,
QueryOptions,
} from '@zenstackhq/orm';
import type { GetModels, SchemaDef } from '@zenstackhq/schema';

/**
* Operations available in a sequential transaction.
*/
type AllowedTransactionOps<
Schema extends SchemaDef,
Model extends GetModels<Schema>,
Options extends QueryOptions<Schema> = QueryOptions<Schema>,
> =
ModelAllowsCreate<Schema, Model> extends true
? GetSlicedOperations<Schema, Model, Options> & CoreCrudOperations
: Exclude<GetSlicedOperations<Schema, Model, Options> & CoreCrudOperations, OperationsRequiringCreate>;

/**
* Represents a single operation to execute within a sequential transaction.
*
* The `model`, `op`, and `args` fields are correlated: `op` is constrained to
* the CRUD operations available on `model` (respecting `Options['slicing']`), and
* `args` is typed accordingly.
*/
export type TransactionOperation<
Schema extends SchemaDef,
Options extends QueryOptions<Schema> = QueryOptions<Schema>,
ExtQueryArgs extends ExtQueryArgsBase = {},
ExtResult extends ExtResultBase<Schema> = {},
> = {
[Model in GetModels<Schema>]: {
[Op in AllowedTransactionOps<Schema, Model, Options>]: {} extends CrudArgsMap<
Schema,
Model,
Options,
ExtQueryArgs,
ExtResult
>[Op]
? { model: Model; op: Op; args?: CrudArgsMap<Schema, Model, Options, ExtQueryArgs, ExtResult>[Op] }
: { model: Model; op: Op; args: CrudArgsMap<Schema, Model, Options, ExtQueryArgs, ExtResult>[Op] };
}[AllowedTransactionOps<Schema, Model, Options>];
}[GetModels<Schema>];

/**
* Maps each operation in a transaction tuple to its precise result type, preserving
* per-position typing.
*/
export type TransactionResults<
Schema extends SchemaDef,
Ops extends readonly TransactionOperation<Schema, any, any, any>[],
Options extends QueryOptions<Schema> = QueryOptions<Schema>,
ExtResult extends ExtResultBase<Schema> = {},
> = {
[K in keyof Ops]: Ops[K] extends { model: infer M; op: infer O; args?: infer A }
? M extends GetModels<Schema>
? O extends keyof CrudReturnMap<Schema, M, A, Options, ExtResult>
? CrudReturnMap<Schema, M, A, Options, ExtResult>[O]
: never
: never
: never;
};

9 changes: 7 additions & 2 deletions packages/clients/client-helpers/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ClientContract, QueryOptions } from '@zenstackhq/orm';
import { ExtQueryArgsMarker, ExtResultMarker, type QueryOptions } from '@zenstackhq/orm';
import type { SchemaDef } from '@zenstackhq/schema';

/**
Expand All @@ -11,10 +11,15 @@ export type MaybePromise<T> = T | Promise<T> | PromiseLike<T>;
*/
export type InferSchema<T> = T extends { $schema: infer S extends SchemaDef } ? S : T extends SchemaDef ? T : never;

/**
* Extracts the ExtQueryArgs type from a client contract, or defaults to `{}`.
*/
export type InferExtQueryArgs<T> = T extends { [ExtQueryArgsMarker]?: infer E } ? (unknown extends E ? {} : E) : {};

/**
* 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 : {};
export type InferExtResult<T> = T extends { [ExtResultMarker]?: infer E } ? (unknown extends E ? {} : E) : {};

/**
* Infers query options from a client contract type, or defaults to `QueryOptions<Schema>`.
Expand Down
19 changes: 0 additions & 19 deletions packages/clients/client-helpers/test/nested-write-visitor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1097,25 +1097,6 @@ describe('NestedWriteVisitor tests', () => {
}),
).resolves.not.toThrow();
});

it('throws error for unhandled action type', async () => {
const schema = createSchema({
User: {
name: 'User',
fields: {
id: createField('id', 'String'),
},
uniqueFields: {},
idFields: ['id'],
},
});

const visitor = new NestedWriteVisitor(schema, {});

await expect(visitor.visit('User', 'invalidAction' as any, { data: {} })).rejects.toThrow(
'unhandled action type',
);
});
});

describe('complex real-world scenarios', () => {
Expand Down
4 changes: 4 additions & 0 deletions packages/clients/fetch-client/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import config from '@zenstackhq/eslint-config/base.js';

/** @type {import("eslint").Linter.Config} */
export default config;
Loading
Loading