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
1 change: 1 addition & 0 deletions .coderabbit.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ early_access: false
reviews:
auto_review:
enabled: true
sequence_diagrams: false
chat:
auto_reply: true
7 changes: 6 additions & 1 deletion TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,10 @@
- [x] Count
- [x] Aggregate
- [x] Group by
- [ ] Raw queries
- [x] Raw queries
- [ ] Transactions
- [x] Interactive transaction
- [x] Sequential transaction
- [ ] Extensions
- [x] Query builder API
- [x] Computed fields
Expand All @@ -69,6 +72,8 @@
- [x] Custom field name
- [ ] Strict undefined checks
- [ ] Benchmark
- [ ] Plugin
- [ ] Post-mutation hooks should be called after transaction is committed
- [ ] Polymorphism
- [ ] Validation
- [ ] Access Policy
Expand Down
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.0.0-alpha.8",
"version": "3.0.0-alpha.9",
"description": "ZenStack",
"packageManager": "pnpm@10.12.1",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/bin/cli
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
#!/usr/bin/env node --no-warnings
#!/usr/bin/env node

import '../dist/index.js';
3 changes: 2 additions & 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.0.0-alpha.8",
"version": "3.0.0-alpha.9",
"type": "module",
"author": {
"name": "ZenStack Team"
Expand All @@ -18,6 +18,7 @@
"data modeling"
],
"bin": {
"zen": "bin/cli",
"zenstack": "bin/cli"
},
"scripts": {
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-alpha.8",
"version": "3.0.0-alpha.9",
"description": "ZenStack Common Helpers",
"type": "module",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion packages/create-zenstack/bin/cli
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
#!/usr/bin/env node --no-warnings
#!/usr/bin/env node

import '../dist/index.js';
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-alpha.8",
"version": "3.0.0-alpha.9",
"description": "Create a new ZenStack project",
"type": "module",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion packages/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-alpha.8",
"version": "3.0.0-alpha.9",
"type": "module",
"private": true,
"license": "MIT"
Expand Down
2 changes: 1 addition & 1 deletion packages/ide/vscode/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "zenstack",
"publisher": "zenstack",
"version": "3.0.0-alpha.8",
"version": "3.0.0-alpha.9",
"displayName": "ZenStack Language Tools",
"description": "VSCode extension for ZenStack ZModel language",
"private": true,
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-alpha.8",
"version": "3.0.0-alpha.9",
"license": "MIT",
"author": "ZenStack Team",
"files": [
Expand Down
12 changes: 1 addition & 11 deletions packages/runtime/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@zenstackhq/runtime",
"version": "3.0.0-alpha.8",
"version": "3.0.0-alpha.9",
"description": "ZenStack Runtime",
"type": "module",
"scripts": {
Expand Down Expand Up @@ -29,16 +29,6 @@
"default": "./dist/index.cjs"
}
},
"./client": {
"import": {
"types": "./dist/client.d.ts",
"default": "./dist/client.js"
},
"require": {
"types": "./dist/client.d.cts",
"default": "./dist/client.cjs"
}
},
"./schema": {
"import": {
"types": "./dist/schema.d.ts",
Expand Down
135 changes: 107 additions & 28 deletions packages/runtime/src/client/client-impl.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { lowerCaseFirst } from '@zenstackhq/common-helpers';
import type { SqliteDialectConfig } from 'kysely';
import { invariant, lowerCaseFirst } from '@zenstackhq/common-helpers';
import type { QueryExecutor, SqliteDialectConfig } from 'kysely';
import {
CompiledQuery,
DefaultConnectionProvider,
Expand All @@ -15,7 +15,8 @@ import {
import { match } from 'ts-pattern';
import type { GetModels, ProcedureDef, SchemaDef } from '../schema';
import type { AuthType } from '../schema/auth';
import type { ClientConstructor, ClientContract, ModelOperations } from './contract';
import type { UnwrapTuplePromises } from '../utils/type-utils';
import type { ClientConstructor, ClientContract, ModelOperations, TransactionIsolationLevel } from './contract';
import { AggregateOperationHandler } from './crud/operations/aggregate';
import type { CrudOperation } from './crud/operations/base';
import { BaseOperationHandler } from './crud/operations/base';
Expand All @@ -33,7 +34,7 @@ import * as BuiltinFunctions from './functions';
import { SchemaDbPusher } from './helpers/schema-db-pusher';
import type { ClientOptions, ProceduresOptions } from './options';
import type { RuntimePlugin } from './plugin';
import { createDeferredPromise } from './promise';
import { createZenStackPromise, type ZenStackPromise } from './promise';
import type { ToKysely } from './query-builder';
import { ResultProcessor } from './result-processor';

Expand All @@ -60,6 +61,7 @@ export class ClientImpl<Schema extends SchemaDef> {
private readonly schema: Schema,
private options: ClientOptions<Schema>,
baseClient?: ClientImpl<Schema>,
executor?: QueryExecutor,
) {
this.$schema = schema;
this.$options = options ?? ({} as ClientOptions<Schema>);
Expand All @@ -73,22 +75,24 @@ export class ClientImpl<Schema extends SchemaDef> {
if (baseClient) {
this.kyselyProps = {
...baseClient.kyselyProps,
executor: new ZenStackQueryExecutor(
this,
baseClient.kyselyProps.driver as ZenStackDriver,
baseClient.kyselyProps.dialect.createQueryCompiler(),
baseClient.kyselyProps.dialect.createAdapter(),
new DefaultConnectionProvider(baseClient.kyselyProps.driver),
),
executor:
executor ??
new ZenStackQueryExecutor(
this,
baseClient.kyselyProps.driver as ZenStackDriver,
baseClient.kyselyProps.dialect.createQueryCompiler(),
baseClient.kyselyProps.dialect.createAdapter(),
new DefaultConnectionProvider(baseClient.kyselyProps.driver),
),
};
this.kyselyRaw = baseClient.kyselyRaw;
this.auth = baseClient.auth;
} else {
const dialect = this.getKyselyDialect();
const driver = new ZenStackDriver(dialect.createDriver(), new Log(this.$options.log ?? []));
const compiler = dialect.createQueryCompiler();
const adapter = dialect.createAdapter();
const connectionProvider = new DefaultConnectionProvider(driver);
const executor = new ZenStackQueryExecutor(this, driver, compiler, adapter, connectionProvider);

this.kyselyProps = {
config: {
Expand All @@ -97,7 +101,7 @@ export class ClientImpl<Schema extends SchemaDef> {
},
dialect,
driver,
executor,
executor: executor ?? new ZenStackQueryExecutor(this, driver, compiler, adapter, connectionProvider),
};

// raw kysely instance with default executor
Expand All @@ -112,14 +116,25 @@ export class ClientImpl<Schema extends SchemaDef> {
return createClientProxy(this);
}

public get $qb() {
get $qb() {
return this.kysely;
}

public get $qbRaw() {
get $qbRaw() {
return this.kyselyRaw;
}

get isTransaction() {
return this.kysely.isTransaction;
}

/**
* Create a new client with a new query executor.
*/
withExecutor(executor: QueryExecutor) {
return new ClientImpl(this.schema, this.$options, this, executor);
}

private getKyselyDialect() {
return match(this.schema.provider.type)
.with('sqlite', () => this.makeSqliteKyselyDialect())
Expand All @@ -135,12 +150,76 @@ export class ClientImpl<Schema extends SchemaDef> {
return new SqliteDialect(this.options.dialectConfig as SqliteDialectConfig);
}

async $transaction<T>(callback: (tx: ClientContract<Schema>) => Promise<T>): Promise<T> {
return this.kysely.transaction().execute((tx) => {
const txClient = new ClientImpl<Schema>(this.schema, this.$options);
// overload for interactive transaction
$transaction<T>(
callback: (tx: ClientContract<Schema>) => Promise<T>,
options?: { isolationLevel?: TransactionIsolationLevel },
): Promise<T>;

// overload for sequential transaction
$transaction<P extends ZenStackPromise<Schema, any>[]>(
arg: [...P],
options?: { isolationLevel?: TransactionIsolationLevel },
): Promise<UnwrapTuplePromises<P>>;

// implementation
async $transaction(input: any, options?: { isolationLevel?: TransactionIsolationLevel }) {
invariant(
typeof input === 'function' || (Array.isArray(input) && input.every((p) => p.then && p.cb)),
'Invalid transaction input, expected a function or an array of ZenStackPromise',
);
if (typeof input === 'function') {
return this.interactiveTransaction(input, options);
} else {
return this.sequentialTransaction(input, options);
}
}

private async interactiveTransaction(
callback: (tx: ClientContract<Schema>) => Promise<any>,
options?: { isolationLevel?: TransactionIsolationLevel },
): Promise<any> {
if (this.kysely.isTransaction) {
// proceed directly if already in a transaction
return callback(this as unknown as ClientContract<Schema>);
} else {
// otherwise, create a new transaction, clone the client, and execute the callback
let txBuilder = this.kysely.transaction();
if (options?.isolationLevel) {
txBuilder = txBuilder.setIsolationLevel(options.isolationLevel);
}
return txBuilder.execute((tx) => {
const txClient = new ClientImpl<Schema>(this.schema, this.$options, this);
txClient.kysely = tx;
return callback(txClient as unknown as ClientContract<Schema>);
});
}
}

private async sequentialTransaction(
arg: ZenStackPromise<Schema, any>[],
options?: { isolationLevel?: TransactionIsolationLevel },
) {
const execute = async (tx: Kysely<any>) => {
const txClient = new ClientImpl<Schema>(this.schema, this.$options, this);
txClient.kysely = tx;
return callback(txClient as unknown as ClientContract<Schema>);
});
const result: any[] = [];
for (const promise of arg) {
result.push(await promise.cb(txClient as unknown as ClientContract<Schema>));
}
return result;
};
if (this.kysely.isTransaction) {
// proceed directly if already in a transaction
return execute(this.kysely);
} else {
// otherwise, create a new transaction, clone the client, and execute the callback
let txBuilder = this.kysely.transaction();
if (options?.isolationLevel) {
txBuilder = txBuilder.setIsolationLevel(options.isolationLevel);
}
return txBuilder.execute((tx) => execute(tx as Kysely<any>));
}
}

get $procedures() {
Expand Down Expand Up @@ -213,29 +292,29 @@ export class ClientImpl<Schema extends SchemaDef> {
}

$executeRaw(query: TemplateStringsArray, ...values: any[]) {
return createDeferredPromise(async () => {
return createZenStackPromise(async () => {
const result = await sql(query, ...values).execute(this.kysely);
return Number(result.numAffectedRows ?? 0);
});
}

$executeRawUnsafe(query: string, ...values: any[]) {
return createDeferredPromise(async () => {
return createZenStackPromise(async () => {
const compiledQuery = this.createRawCompiledQuery(query, values);
const result = await this.kysely.executeQuery(compiledQuery);
return Number(result.numAffectedRows ?? 0);
});
}

$queryRaw<T = unknown>(query: TemplateStringsArray, ...values: any[]) {
return createDeferredPromise(async () => {
return createZenStackPromise(async () => {
const result = await sql(query, ...values).execute(this.kysely);
return result.rows as T;
});
}

$queryRawUnsafe<T = unknown>(query: string, ...values: any[]) {
return createDeferredPromise(async () => {
return createZenStackPromise(async () => {
const compiledQuery = this.createRawCompiledQuery(query, values);
const result = await this.kysely.executeQuery(compiledQuery);
return result.rows as T;
Expand All @@ -262,7 +341,7 @@ function createClientProxy<Schema extends SchemaDef>(client: ClientImpl<Schema>)
const model = Object.keys(client.$schema.models).find((m) => m.toLowerCase() === prop.toLowerCase());
if (model) {
return createModelCrudHandler(
client as ClientContract<Schema>,
client as unknown as ClientContract<Schema>,
model as GetModels<Schema>,
inputValidator,
resultProcessor,
Expand All @@ -288,9 +367,9 @@ function createModelCrudHandler<Schema extends SchemaDef, Model extends GetModel
postProcess = false,
throwIfNoResult = false,
) => {
return createDeferredPromise(async () => {
let proceed = async (_args?: unknown, tx?: ClientContract<Schema>) => {
const _handler = tx ? handler.withClient(tx) : handler;
return createZenStackPromise(async (txClient?: ClientContract<Schema>) => {
let proceed = async (_args?: unknown) => {
const _handler = txClient ? handler.withClient(txClient) : handler;
const r = await _handler.handle(operation, _args ?? args);
if (!r && throwIfNoResult) {
throw new NotFoundError(model);
Expand Down
5 changes: 5 additions & 0 deletions packages/runtime/src/client/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,8 @@ export const CONTEXT_COMMENT_PREFIX = '-- $$context:';
* The types of fields that are numeric.
*/
export const NUMERIC_FIELD_TYPES = ['Int', 'Float', 'BigInt', 'Decimal'];

/**
* Client API methods that are not supported in transactions.
*/
export const TRANSACTION_UNSUPPORTED_METHODS = ['$transaction', '$disconnect', '$use'] as const;
Loading