diff --git a/.coderabbit.yaml b/.coderabbit.yaml
new file mode 100644
index 00000000..c50c3b9e
--- /dev/null
+++ b/.coderabbit.yaml
@@ -0,0 +1,8 @@
+# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json
+language: 'en-US'
+early_access: false
+reviews:
+ auto_review:
+ enabled: true
+chat:
+ auto_reply: true
diff --git a/.github/workflows/bump-version.yml b/.github/workflows/bump-version.yml
index f28e6b4e..84da4866 100644
--- a/.github/workflows/bump-version.yml
+++ b/.github/workflows/bump-version.yml
@@ -34,7 +34,7 @@ jobs:
- name: Bump version
id: bump
- run: pnpm run bump-version
+ run: npx tsx scripts/bump-version.ts
- name: Create PR
uses: peter-evans/create-pull-request@v7
diff --git a/README.md b/README.md
index af01e34a..e9925aa5 100644
--- a/README.md
+++ b/README.md
@@ -6,6 +6,9 @@
ZenStack V3
+
+
+
diff --git a/package.json b/package.json
index 385f2f5d..0ebc1fc6 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "zenstack-v3",
- "version": "3.0.0-alpha.7",
+ "version": "3.0.0-alpha.8",
"description": "ZenStack",
"packageManager": "pnpm@10.12.1",
"scripts": {
@@ -10,7 +10,8 @@
"test": "turbo run test",
"format": "prettier --write \"**/*.{ts,tsx,md}\"",
"pr": "gh pr create --fill-first --base dev",
- "bump-version": "npx tsx scripts/bump-version.ts",
+ "merge-main": "gh pr create --title \"merge dev to main\" --body \"\" --base main --head dev",
+ "bump-version": "gh workflow run .github/workflows/bump-version.yml --ref dev",
"publish-all": "pnpm --filter \"./packages/**\" -r publish --access public --tag next",
"publish-preview": "pnpm --filter \"./packages/**\" -r publish --tag next --force --registry https://preview.registry.zenstack.dev/",
"unpublish-preview": "pnpm --filter \"./packages/**\" -r --shell-mode exec -- npm unpublish -f --registry https://preview.registry.zenstack.dev/ \"\\$PNPM_PACKAGE_NAME\""
diff --git a/packages/cli/package.json b/packages/cli/package.json
index dbfe7223..30c30071 100644
--- a/packages/cli/package.json
+++ b/packages/cli/package.json
@@ -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.7",
+ "version": "3.0.0-alpha.8",
"type": "module",
"author": {
"name": "ZenStack Team"
diff --git a/packages/common-helpers/package.json b/packages/common-helpers/package.json
index a1c267cb..c4888538 100644
--- a/packages/common-helpers/package.json
+++ b/packages/common-helpers/package.json
@@ -1,6 +1,6 @@
{
"name": "@zenstackhq/common-helpers",
- "version": "3.0.0-alpha.7",
+ "version": "3.0.0-alpha.8",
"description": "ZenStack Common Helpers",
"type": "module",
"scripts": {
diff --git a/packages/create-zenstack/package.json b/packages/create-zenstack/package.json
index 77e7f85f..7ad47cb4 100644
--- a/packages/create-zenstack/package.json
+++ b/packages/create-zenstack/package.json
@@ -1,6 +1,6 @@
{
"name": "create-zenstack",
- "version": "3.0.0-alpha.7",
+ "version": "3.0.0-alpha.8",
"description": "Create a new ZenStack project",
"type": "module",
"scripts": {
diff --git a/packages/eslint-config/package.json b/packages/eslint-config/package.json
index 52fa2cec..e61e7356 100644
--- a/packages/eslint-config/package.json
+++ b/packages/eslint-config/package.json
@@ -1,6 +1,6 @@
{
"name": "@zenstackhq/eslint-config",
- "version": "3.0.0-alpha.7",
+ "version": "3.0.0-alpha.8",
"type": "module",
"private": true,
"license": "MIT"
diff --git a/packages/ide/vscode/package.json b/packages/ide/vscode/package.json
index d104baec..c26fbf98 100644
--- a/packages/ide/vscode/package.json
+++ b/packages/ide/vscode/package.json
@@ -1,7 +1,7 @@
{
"name": "zenstack",
"publisher": "zenstack",
- "version": "3.0.0-alpha.7",
+ "version": "3.0.0-alpha.8",
"displayName": "ZenStack Language Tools",
"description": "VSCode extension for ZenStack ZModel language",
"private": true,
diff --git a/packages/language/package.json b/packages/language/package.json
index 631726e8..b10bb8fd 100644
--- a/packages/language/package.json
+++ b/packages/language/package.json
@@ -1,7 +1,7 @@
{
"name": "@zenstackhq/language",
"description": "ZenStack ZModel language specification",
- "version": "3.0.0-alpha.7",
+ "version": "3.0.0-alpha.8",
"license": "MIT",
"author": "ZenStack Team",
"files": [
diff --git a/packages/runtime/package.json b/packages/runtime/package.json
index 8cf17f83..8ef02401 100644
--- a/packages/runtime/package.json
+++ b/packages/runtime/package.json
@@ -1,6 +1,6 @@
{
"name": "@zenstackhq/runtime",
- "version": "3.0.0-alpha.7",
+ "version": "3.0.0-alpha.8",
"description": "ZenStack Runtime",
"type": "module",
"scripts": {
diff --git a/packages/runtime/src/client/client-impl.ts b/packages/runtime/src/client/client-impl.ts
index ce836d6e..d17cd23f 100644
--- a/packages/runtime/src/client/client-impl.ts
+++ b/packages/runtime/src/client/client-impl.ts
@@ -1,11 +1,13 @@
import { lowerCaseFirst } from '@zenstackhq/common-helpers';
import type { SqliteDialectConfig } from 'kysely';
import {
+ CompiledQuery,
DefaultConnectionProvider,
DefaultQueryExecutor,
Kysely,
Log,
PostgresDialect,
+ sql,
SqliteDialect,
type KyselyProps,
type PostgresDialectConfig,
@@ -209,6 +211,41 @@ export class ClientImpl {
get $auth() {
return this.auth;
}
+
+ $executeRaw(query: TemplateStringsArray, ...values: any[]) {
+ return createDeferredPromise(async () => {
+ const result = await sql(query, ...values).execute(this.kysely);
+ return Number(result.numAffectedRows ?? 0);
+ });
+ }
+
+ $executeRawUnsafe(query: string, ...values: any[]) {
+ return createDeferredPromise(async () => {
+ const compiledQuery = this.createRawCompiledQuery(query, values);
+ const result = await this.kysely.executeQuery(compiledQuery);
+ return Number(result.numAffectedRows ?? 0);
+ });
+ }
+
+ $queryRaw(query: TemplateStringsArray, ...values: any[]) {
+ return createDeferredPromise(async () => {
+ const result = await sql(query, ...values).execute(this.kysely);
+ return result.rows as T;
+ });
+ }
+
+ $queryRawUnsafe(query: string, ...values: any[]) {
+ return createDeferredPromise(async () => {
+ const compiledQuery = this.createRawCompiledQuery(query, values);
+ const result = await this.kysely.executeQuery(compiledQuery);
+ return result.rows as T;
+ });
+ }
+
+ private createRawCompiledQuery(query: string, values: any[]) {
+ const q = CompiledQuery.raw(query, values);
+ return { ...q, $raw: true } as CompiledQuery;
+ }
}
function createClientProxy(client: ClientImpl): ClientImpl {
diff --git a/packages/runtime/src/client/contract.ts b/packages/runtime/src/client/contract.ts
index 6a99af69..1a10a421 100644
--- a/packages/runtime/src/client/contract.ts
+++ b/packages/runtime/src/client/contract.ts
@@ -40,6 +40,44 @@ export type ClientContract = {
*/
readonly $options: ClientOptions;
+ /**
+ * Executes a prepared raw query and returns the number of affected rows.
+ * @example
+ * ```
+ * const result = await client.$executeRaw`UPDATE User SET cool = ${true} WHERE email = ${'user@email.com'};`
+ * ```
+ */
+ $executeRaw(query: TemplateStringsArray, ...values: any[]): Promise;
+
+ /**
+ * Executes a raw query and returns the number of affected rows.
+ * This method is susceptible to SQL injections.
+ * @example
+ * ```
+ * const result = await client.$executeRawUnsafe('UPDATE User SET cool = $1 WHERE email = $2 ;', true, 'user@email.com')
+ * ```
+ */
+ $executeRawUnsafe(query: string, ...values: any[]): Promise;
+
+ /**
+ * Performs a prepared raw query and returns the `SELECT` data.
+ * @example
+ * ```
+ * const result = await client.$queryRaw`SELECT * FROM User WHERE id = ${1} OR email = ${'user@email.com'};`
+ * ```
+ */
+ $queryRaw(query: TemplateStringsArray, ...values: any[]): Promise;
+
+ /**
+ * Performs a raw query and returns the `SELECT` data.
+ * This method is susceptible to SQL injections.
+ * @example
+ * ```
+ * const result = await client.$queryRawUnsafe('SELECT * FROM User WHERE id = $1 OR email = $2;', 1, 'user@email.com')
+ * ```
+ */
+ $queryRawUnsafe(query: string, ...values: any[]): Promise;
+
/**
* The current user identity.
*/
diff --git a/packages/runtime/src/client/executor/zenstack-query-executor.ts b/packages/runtime/src/client/executor/zenstack-query-executor.ts
index 381ec9af..bb9a5472 100644
--- a/packages/runtime/src/client/executor/zenstack-query-executor.ts
+++ b/packages/runtime/src/client/executor/zenstack-query-executor.ts
@@ -81,7 +81,9 @@ export class ZenStackQueryExecutor extends DefaultQuer
}
// proceed with the query with kysely interceptors
- const result = await this.proceedQueryWithKyselyInterceptors(queryNode, queryId);
+ // if the query is a raw query, we need to carry over the parameters
+ const queryParams = (compiledQuery as any).$raw ? compiledQuery.parameters : undefined;
+ const result = await this.proceedQueryWithKyselyInterceptors(queryNode, queryParams, queryId);
// call after mutation hooks
await this.callAfterQueryInterceptionFilters(result, queryNode, mutationInterceptionInfo);
@@ -96,8 +98,12 @@ export class ZenStackQueryExecutor extends DefaultQuer
return this.executeWithTransaction(task, !!mutationInterceptionInfo?.useTransactionForMutation);
}
- private proceedQueryWithKyselyInterceptors(queryNode: RootOperationNode, queryId: QueryId) {
- let proceed = (q: RootOperationNode) => this.proceedQuery(q, queryId);
+ private proceedQueryWithKyselyInterceptors(
+ queryNode: RootOperationNode,
+ parameters: readonly unknown[] | undefined,
+ queryId: QueryId,
+ ) {
+ let proceed = (q: RootOperationNode) => this.proceedQuery(q, parameters, queryId);
const makeTx = (p: typeof proceed) => (callback: OnKyselyQueryTransactionCallback) => {
return this.executeWithTransaction(() => callback(p));
@@ -125,10 +131,13 @@ export class ZenStackQueryExecutor extends DefaultQuer
return proceed(queryNode);
}
- private async proceedQuery(query: RootOperationNode, queryId: QueryId) {
+ private async proceedQuery(query: RootOperationNode, parameters: readonly unknown[] | undefined, queryId: QueryId) {
// run built-in transformers
const finalQuery = this.nameMapper.transformNode(query);
- const compiled = this.compileQuery(finalQuery);
+ let compiled = this.compileQuery(finalQuery);
+ if (parameters) {
+ compiled = { ...compiled, parameters };
+ }
try {
return this.driver.txConnection
? await super
diff --git a/packages/runtime/test/client-api/client-specs.ts b/packages/runtime/test/client-api/client-specs.ts
index f05c5fcd..6a14ab43 100644
--- a/packages/runtime/test/client-api/client-specs.ts
+++ b/packages/runtime/test/client-api/client-specs.ts
@@ -3,7 +3,7 @@ import { getSchema, schema } from '../test-schema';
import { makePostgresClient, makeSqliteClient } from '../utils';
import type { ClientContract } from '../../src';
-export function createClientSpecs(dbName: string, logQueries = false, providers = ['sqlite', 'postgresql'] as const) {
+export function createClientSpecs(dbName: string, logQueries = false, providers: string[] = ['sqlite', 'postgresql']) {
const logger = (provider: string) => (event: LogEvent) => {
if (event.level === 'query') {
console.log(`query(${provider}):`, event.query.sql, event.query.parameters);
diff --git a/packages/runtime/test/client-api/raw-query.test.ts b/packages/runtime/test/client-api/raw-query.test.ts
new file mode 100644
index 00000000..f8ad6d41
--- /dev/null
+++ b/packages/runtime/test/client-api/raw-query.test.ts
@@ -0,0 +1,79 @@
+import { afterEach, beforeEach, describe, expect, it } from 'vitest';
+import type { ClientContract } from '../../src/client';
+import { schema } from '../test-schema';
+import { createClientSpecs } from './client-specs';
+
+const PG_DB_NAME = 'client-api-raw-query-tests';
+
+describe.each(createClientSpecs(PG_DB_NAME, true))('Client raw query tests', ({ createClient, provider }) => {
+ let client: ClientContract;
+
+ beforeEach(async () => {
+ client = await createClient();
+ });
+
+ afterEach(async () => {
+ await client?.$disconnect();
+ });
+
+ it('works with executeRaw', async () => {
+ await client.user.create({
+ data: {
+ id: '1',
+ email: 'u1@test.com',
+ },
+ });
+
+ await expect(
+ client.$executeRaw`UPDATE "User" SET "email" = ${'u2@test.com'} WHERE "id" = ${'1'}`,
+ ).resolves.toBe(1);
+ await expect(client.user.findFirst()).resolves.toMatchObject({ email: 'u2@test.com' });
+ });
+
+ it('works with executeRawUnsafe', async () => {
+ await client.user.create({
+ data: {
+ id: '1',
+ email: 'u1@test.com',
+ },
+ });
+
+ const sql =
+ provider === 'postgresql'
+ ? `UPDATE "User" SET "email" = $1 WHERE "id" = $2`
+ : `UPDATE "User" SET "email" = ? WHERE "id" = ?`;
+ await expect(client.$executeRawUnsafe(sql, 'u2@test.com', '1')).resolves.toBe(1);
+ await expect(client.user.findFirst()).resolves.toMatchObject({ email: 'u2@test.com' });
+ });
+
+ it('works with queryRaw', async () => {
+ await client.user.create({
+ data: {
+ id: '1',
+ email: 'u1@test.com',
+ },
+ });
+
+ const uid = '1';
+ const users = await client.$queryRaw<
+ { id: string; email: string }[]
+ >`SELECT "User"."id", "User"."email" FROM "User" WHERE "User"."id" = ${uid}`;
+ expect(users).toEqual([{ id: '1', email: 'u1@test.com' }]);
+ });
+
+ it('works with queryRawUnsafe', async () => {
+ await client.user.create({
+ data: {
+ id: '1',
+ email: 'u1@test.com',
+ },
+ });
+
+ const sql =
+ provider === 'postgresql'
+ ? `SELECT "User"."id", "User"."email" FROM "User" WHERE "User"."id" = $1`
+ : `SELECT "User"."id", "User"."email" FROM "User" WHERE "User"."id" = ?`;
+ const users = await client.$queryRawUnsafe<{ id: string; email: string }[]>(sql, '1');
+ expect(users).toEqual([{ id: '1', email: 'u1@test.com' }]);
+ });
+});
diff --git a/packages/sdk/package.json b/packages/sdk/package.json
index cb9ffea7..0acfaebe 100644
--- a/packages/sdk/package.json
+++ b/packages/sdk/package.json
@@ -1,6 +1,6 @@
{
"name": "@zenstackhq/sdk",
- "version": "3.0.0-alpha.7",
+ "version": "3.0.0-alpha.8",
"description": "ZenStack SDK",
"type": "module",
"scripts": {
diff --git a/packages/tanstack-query/package.json b/packages/tanstack-query/package.json
index 1a2b23a0..d06bcbb2 100644
--- a/packages/tanstack-query/package.json
+++ b/packages/tanstack-query/package.json
@@ -1,6 +1,6 @@
{
"name": "@zenstackhq/tanstack-query",
- "version": "3.0.0-alpha.7",
+ "version": "3.0.0-alpha.8",
"description": "",
"main": "index.js",
"type": "module",
diff --git a/packages/testtools/package.json b/packages/testtools/package.json
index ee6cb091..c9d0a878 100644
--- a/packages/testtools/package.json
+++ b/packages/testtools/package.json
@@ -1,6 +1,6 @@
{
"name": "@zenstackhq/testtools",
- "version": "3.0.0-alpha.7",
+ "version": "3.0.0-alpha.8",
"description": "ZenStack Test Tools",
"type": "module",
"scripts": {
diff --git a/packages/testtools/src/schema.ts b/packages/testtools/src/schema.ts
index 46b733ea..b5a6f7d3 100644
--- a/packages/testtools/src/schema.ts
+++ b/packages/testtools/src/schema.ts
@@ -35,7 +35,7 @@ export async function generateTsSchema(
extraSourceFiles?: Record,
) {
const workDir = createTestProject();
- console.log(`Working directory: ${workDir}`);
+ console.log(`Work directory: ${workDir}`);
const zmodelPath = path.join(workDir, 'schema.zmodel');
const noPrelude = schemaText.includes('datasource ');
diff --git a/packages/typescript-config/package.json b/packages/typescript-config/package.json
index 24db35c1..7f21a7db 100644
--- a/packages/typescript-config/package.json
+++ b/packages/typescript-config/package.json
@@ -1,6 +1,6 @@
{
"name": "@zenstackhq/typescript-config",
- "version": "3.0.0-alpha.7",
+ "version": "3.0.0-alpha.8",
"private": true,
"license": "MIT"
}
diff --git a/packages/zod/package.json b/packages/zod/package.json
index ba0486e3..32069191 100644
--- a/packages/zod/package.json
+++ b/packages/zod/package.json
@@ -1,6 +1,6 @@
{
"name": "@zenstackhq/zod",
- "version": "3.0.0-alpha.7",
+ "version": "3.0.0-alpha.8",
"description": "",
"type": "module",
"main": "index.js",
diff --git a/samples/blog/package.json b/samples/blog/package.json
index 7a740480..516cd048 100644
--- a/samples/blog/package.json
+++ b/samples/blog/package.json
@@ -1,6 +1,6 @@
{
"name": "sample-blog",
- "version": "3.0.0-alpha.7",
+ "version": "3.0.0-alpha.8",
"description": "",
"main": "index.js",
"scripts": {
diff --git a/tests/e2e/package.json b/tests/e2e/package.json
index cb75d025..8c0ca98e 100644
--- a/tests/e2e/package.json
+++ b/tests/e2e/package.json
@@ -1,6 +1,6 @@
{
"name": "e2e",
- "version": "3.0.0-alpha.7",
+ "version": "3.0.0-alpha.8",
"private": true,
"scripts": {
"test": "vitest run"
diff --git a/turbo.json b/turbo.json
index 72d14c56..31aad504 100644
--- a/turbo.json
+++ b/turbo.json
@@ -3,6 +3,7 @@
"tasks": {
"build": {
"dependsOn": ["^build"],
+ "inputs": ["src/**"],
"outputs": ["dist/**"]
},
"lint": {