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
11 changes: 8 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ Even without using advanced features, ZenStack offers the following benefits as

# Get started

> You can also check the [blog sample](./samples/blog) for a complete example.

## Installation

### 1. Creating a new project
Expand Down Expand Up @@ -136,7 +138,9 @@ Now you can use the compiled TypeScript schema to instantiate a database client:
import { ZenStackClient } from '@zenstackhq/runtime';
import { schema } from './zenstack/schema';

const client = new ZenStackClient(schema);
const client = new ZenStackClient(schema, {
dialectConfig: { ... }
});
```

## Using `ZenStackClient`
Expand Down Expand Up @@ -207,14 +211,15 @@ ZenStack v3 allows you to define database-evaluated computed fields with the fol
model User {
...
/// number of posts owned by the user
postCount Int @computed
postCount Int @computed
}
```

2. Provide its implementation using query builder when constructing `ZenStackClient`

```ts
const client = new ZenStackClient(schema, {
...
computedFields: {
User: {
postCount: (eb) =>
Expand Down Expand Up @@ -367,7 +372,7 @@ See [Prisma Migrate](https://www.prisma.io/docs/orm/prisma-migrate) documentatio
1. Install "better-sqlite3" or "pg" based on database type
1. Move "schema.prisma" to "zenstack" folder and rename it to "schema.zmodel"
1. Run `npx zenstack generate`
1. Replace `new PrismaClient()` with `new ZenStackClient(schema)`
1. Replace `new PrismaClient()` with `new ZenStackClient(schema, { ... })`

# Limitations

Expand Down
1 change: 1 addition & 0 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
- [x] Custom table name
- [x] Custom field name
- [ ] Strict undefined check
- [ ] Implement changesets
- [ ] Polymorphism
- [ ] Validation
- [ ] Access Policy
Expand Down
4 changes: 3 additions & 1 deletion packages/cli/src/actions/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ export async function run(options: Options) {
import { ZenStackClient } from '@zenstackhq/runtime';
import { schema } from '${outputPath}/schema';

const client = new ZenStackClient(schema);
const client = new ZenStackClient(schema, {
dialectConfig: { ... }
});
\`\`\`
`);
}
Expand Down
7 changes: 6 additions & 1 deletion packages/cli/src/actions/templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,14 @@ model Post {

export const STARTER_MAIN_TS = `import { ZenStackClient } from '@zenstackhq/runtime';
import { schema } from './zenstack/schema';
import SQLite from 'better-sqlite3';

async function main() {
const client = new ZenStackClient(schema);
const client = new ZenStackClient(schema, {
dialectConfig: {
database: new SQLite('./zenstack/dev.db'),
},
});
const user = await client.user.create({
data: {
email: '[email protected]',
Expand Down
1 change: 0 additions & 1 deletion packages/cli/test/ts-schema-gen.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ model Post {

expect(schema.provider).toMatchObject({
type: 'sqlite',
dialectConfigProvider: expect.any(Function),
});

expect(schema.models).toMatchObject({
Expand Down
48 changes: 42 additions & 6 deletions packages/create-zenstack/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,18 @@ import { STARTER_MAIN_TS, STARTER_ZMODEL } from './templates';
const npmAgent = process.env['npm_config_user_agent'];
let agent = 'npm';
let agentExec = 'npx';
let initCommand = 'npm init -y';
let saveDev = '--save-dev';

if (npmAgent?.includes('pnpm')) {
agent = 'pnpm';
initCommand = 'pnpm init';
agentExec = 'pnpm';
} else if (npmAgent?.includes('yarn')) {
agent = 'yarn';
initCommand = 'yarn init';
agentExec = 'npx';
saveDev = '--dev';
} else if (npmAgent?.includes('bun')) {
agent = 'bun';
agentExec = 'bun';
initCommand = 'bun init -y -m';
}

const program = new Command('create-zenstack');
Expand All @@ -46,20 +42,60 @@ function initProject(name: string) {

console.log(colors.gray(`Using package manager: ${agent}`));

// initialize project
execSync(initCommand);
// create package.json
fs.writeFileSync(
'package.json',
JSON.stringify(
{
name: 'zenstack-app',
version: '1.0.0',
description: 'Scaffolded with create-zenstack',
type: 'module',
scripts: {
dev: 'tsx main.ts',
},
license: 'ISC',
},
null,
2
)
);

// install packages
const packages = [
{ name: '@zenstackhq/cli@next', dev: true },
{ name: '@zenstackhq/runtime@next', dev: false },
{ name: 'better-sqlite3', dev: false },
{ name: '@types/better-sqlite3', dev: true },
{ name: 'typescript', dev: true },
{ name: 'tsx', dev: true },
{ name: '@types/node', dev: true },
];
for (const pkg of packages) {
installPackage(pkg);
}

// create tsconfig.json
fs.writeFileSync(
'tsconfig.json',
JSON.stringify(
{
compilerOptions: {
module: 'esnext',
target: 'esnext',
moduleResolution: 'bundler',
sourceMap: true,
outDir: 'dist',
strict: true,
skipLibCheck: true,
esModuleInterop: true,
},
},
null,
2
)
);

// create schema.zmodel
fs.mkdirSync('zenstack');
fs.writeFileSync('zenstack/schema.zmodel', STARTER_ZMODEL);
Expand Down
21 changes: 0 additions & 21 deletions packages/runtime/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,26 +59,6 @@
"default": "./dist/plugins/policy.cjs"
}
},
"./utils/pg-utils": {
"import": {
"types": "./dist/utils/pg-utils.d.ts",
"default": "./dist/utils/pg-utils.js"
},
"require": {
"types": "./dist/utils/pg-utils.d.cts",
"default": "./dist/utils/pg-utils.cjs"
}
},
"./utils/sqlite-utils": {
"import": {
"types": "./dist/utils/sqlite-utils.d.ts",
"default": "./dist/utils/sqlite-utils.js"
},
"require": {
"types": "./dist/utils/sqlite-utils.d.cts",
"default": "./dist/utils/sqlite-utils.cjs"
}
},
"./package.json": {
"import": "./package.json",
"require": "./package.json"
Expand All @@ -91,7 +71,6 @@
"json-stable-stringify": "^1.3.0",
"kysely": "^0.27.5",
"nanoid": "^5.0.9",
"pg-connection-string": "^2.9.0",
"tiny-invariant": "^1.3.3",
"ts-pattern": "^5.6.0",
"ulid": "^3.0.0",
Expand Down
24 changes: 9 additions & 15 deletions packages/runtime/src/client/client-impl.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { SqliteDialectConfig } from 'kysely';
import {
DefaultConnectionProvider,
DefaultQueryExecutor,
Expand All @@ -7,7 +8,6 @@ import {
SqliteDialect,
type KyselyProps,
type PostgresDialectConfig,
type SqliteDialectConfig,
} from 'kysely';
import { match } from 'ts-pattern';
import type { GetModels, ProcedureDef, SchemaDef } from '../schema';
Expand Down Expand Up @@ -41,7 +41,7 @@ import { ResultProcessor } from './result-processor';
export const ZenStackClient = function <Schema extends SchemaDef>(
this: any,
schema: any,
options?: ClientOptions<Schema>
options: ClientOptions<Schema>
) {
return new ClientImpl<Schema>(schema, options);
} as unknown as ClientConstructor;
Expand All @@ -56,7 +56,7 @@ export class ClientImpl<Schema extends SchemaDef> {

constructor(
private readonly schema: Schema,
private options?: ClientOptions<Schema>,
private options: ClientOptions<Schema>,
baseClient?: ClientImpl<Schema>
) {
this.$schema = schema;
Expand Down Expand Up @@ -140,21 +140,15 @@ export class ClientImpl<Schema extends SchemaDef> {
}

private makePostgresKyselyDialect(): PostgresDialect {
const { dialectConfigProvider } = this.schema.provider;
const mergedConfig = {
...dialectConfigProvider(),
...this.options?.dialectConfig,
} as PostgresDialectConfig;
return new PostgresDialect(mergedConfig);
return new PostgresDialect(
this.options.dialectConfig as PostgresDialectConfig
);
}

private makeSqliteKyselyDialect(): SqliteDialect {
const { dialectConfigProvider } = this.schema.provider;
const mergedConfig = {
...dialectConfigProvider(),
...this.options?.dialectConfig,
} as SqliteDialectConfig;
return new SqliteDialect(mergedConfig);
return new SqliteDialect(
this.options.dialectConfig as SqliteDialectConfig
);
Comment on lines 142 to +151
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

No runtime guard that dialectConfig matches provider

If a caller passes a PostgreSQL dialectConfig while schema.provider.type is 'sqlite' (or vice-versa) the dialect will be built with an incompatible config and fail later.

Add an assertion to fail fast:

 private makePostgresKyselyDialect(): PostgresDialect {
-        return new PostgresDialect(
-            this.options.dialectConfig as PostgresDialectConfig
-        );
+        if (this.schema.provider.type !== 'postgresql') {
+            throw new Error('PostgreSQL dialect requested for non-postgres provider');
+        }
+        return new PostgresDialect(
+            this.options.dialectConfig as PostgresDialectConfig
+        );
 }

(Same check for SQLite.)

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private makePostgresKyselyDialect(): PostgresDialect {
const { dialectConfigProvider } = this.schema.provider;
const mergedConfig = {
...dialectConfigProvider(),
...this.options?.dialectConfig,
} as PostgresDialectConfig;
return new PostgresDialect(mergedConfig);
return new PostgresDialect(
this.options.dialectConfig as PostgresDialectConfig
);
}
private makeSqliteKyselyDialect(): SqliteDialect {
const { dialectConfigProvider } = this.schema.provider;
const mergedConfig = {
...dialectConfigProvider(),
...this.options?.dialectConfig,
} as SqliteDialectConfig;
return new SqliteDialect(mergedConfig);
return new SqliteDialect(
this.options.dialectConfig as SqliteDialectConfig
);
private makePostgresKyselyDialect(): PostgresDialect {
if (this.schema.provider.type !== 'postgresql') {
throw new Error('PostgreSQL dialect requested for non-postgres provider');
}
return new PostgresDialect(
this.options.dialectConfig as PostgresDialectConfig
);
}
private makeSqliteKyselyDialect(): SqliteDialect {
return new SqliteDialect(
this.options.dialectConfig as SqliteDialectConfig
);
}
🤖 Prompt for AI Agents
In packages/runtime/src/client/client-impl.ts around lines 142 to 151, add
runtime assertions to verify that the dialectConfig matches the expected
provider type before constructing the dialect. For the PostgreSQL dialect,
assert that the provider type is 'postgres', and for the SQLite dialect, assert
that it is 'sqlite'. This will ensure that incompatible configs are caught early
and prevent later failures.

}

async $transaction<T>(
Expand Down
2 changes: 1 addition & 1 deletion packages/runtime/src/client/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export type ClientOptions<Schema extends SchemaDef> = {
/**
* Database dialect configuration.
*/
dialectConfig?: DialectConfig<Schema['provider']>;
dialectConfig: DialectConfig<Schema['provider']>;

/**
* Custom function definitions.
Expand Down
12 changes: 0 additions & 12 deletions packages/runtime/src/utils/pg-utils.ts

This file was deleted.

21 changes: 0 additions & 21 deletions packages/runtime/src/utils/sqlite-utils.ts

This file was deleted.

8 changes: 3 additions & 5 deletions packages/runtime/test/client-api/default-values.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,6 @@ import { ExpressionUtils, type SchemaDef } from '../../src/schema';
const schema = {
provider: {
type: 'sqlite',
dialectConfigProvider: () =>
({
database: new SQLite(':memory:'),
} as any),
},
models: {
Model: {
Expand Down Expand Up @@ -68,7 +64,9 @@ const schema = {

describe('default values tests', () => {
it('supports generators', async () => {
const client = new ZenStackClient(schema);
const client = new ZenStackClient(schema, {
dialectConfig: { database: new SQLite(':memory:') },
});
await client.$pushSchema();

const entity = await client.model.create({ data: {} });
Expand Down
11 changes: 6 additions & 5 deletions packages/runtime/test/client-api/name-mapping.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,6 @@ describe('Name mapping tests', () => {
const schema = {
provider: {
type: 'sqlite',
dialectConfigProvider: () => ({
database: new SQLite(':memory:'),
}),
},
models: {
Foo: {
Expand Down Expand Up @@ -58,7 +55,9 @@ describe('Name mapping tests', () => {
} as const satisfies SchemaDef;

it('works with model and implicit field mapping', async () => {
const client = new ZenStackClient(schema);
const client = new ZenStackClient(schema, {
dialectConfig: { database: new SQLite(':memory:') },
});
await client.$pushSchema();
const r1 = await client.foo.create({
data: { id: '1', x: 1 },
Expand Down Expand Up @@ -88,7 +87,9 @@ describe('Name mapping tests', () => {
});

it('works with explicit field mapping', async () => {
const client = new ZenStackClient(schema);
const client = new ZenStackClient(schema, {
dialectConfig: { database: new SQLite(':memory:') },
});
await client.$pushSchema();
const r1 = await client.foo.create({
data: { id: '1', x: 1 },
Expand Down
11 changes: 7 additions & 4 deletions packages/runtime/test/plugin/kysely-on-query.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import SQLite from 'better-sqlite3';
import {
InsertQueryNode,
Kysely,
Expand All @@ -13,19 +14,21 @@ describe('Kysely onQuery tests', () => {
let _client: ClientContract<typeof schema>;

beforeEach(async () => {
_client = new ZenStackClient(schema);
_client = new ZenStackClient(schema, {
dialectConfig: { database: new SQLite(':memory:') },
});
await _client.$pushSchema();
});

it('intercepts queries', async () => {
let called = false;
const client = _client.$use({
id: 'test-plugin',
onKyselyQuery(args) {
if (args.query.kind === 'InsertQueryNode') {
onKyselyQuery({ query, proceed }) {
if (query.kind === 'InsertQueryNode') {
called = true;
}
return args.proceed(args.query);
return proceed(query);
},
});
await expect(
Expand Down
Loading