Skip to content

Commit a43be54

Browse files
authored
docs: add README (#27)
1 parent acdc66e commit a43be54

File tree

17 files changed

+399
-32
lines changed

17 files changed

+399
-32
lines changed

README.md

Lines changed: 375 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,376 @@
1-
# ZenStack V3
1+
<div align="center">
2+
<a href="https://zenstack.dev">
3+
<picture>
4+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/zenstackhq/zenstack-docs/main/static/img/logo-dark.png">
5+
<img src="https://raw.githubusercontent.com/zenstackhq/zenstack-docs/main/static/img/logo.png" height="128">
6+
</picture>
7+
</a>
8+
<h1>ZenStack V3</h1>
9+
<img src="https://github.com/zenstackhq/zenstack-v3/actions/workflows/build-test.yml/badge.svg">
10+
<a href="https://twitter.com/zenstackhq">
11+
<img src="https://img.shields.io/twitter/url?style=social&url=https%3A%2F%2Fgithub.com%2Fzenstackhq%2Fzenstack">
12+
</a>
13+
<a href="https://discord.gg/Ykhr738dUe">
14+
<img src="https://img.shields.io/discord/1035538056146595961">
15+
</a>
16+
<a href="https://github.com/zenstackhq/zenstack/blob/main/LICENSE">
17+
<img src="https://img.shields.io/badge/license-MIT-green">
18+
</a>
19+
</div>
220

3-
See [samples/blog](./samples/blog) for a demo.
21+
> V3 is currently in alpha phase and not ready for production use. Feedback and bug reports are greatly appreciated. Please visit this dedicated [discord channel](https://discord.com/channels/1035538056146595961/1352359627525718056) for chat and support.
22+
23+
# What's ZenStack
24+
25+
ZenStack is a TypeScript database toolkit for developing full-stack or backend Node.js/Bun applications. It provides a unified data modeling and access solution with the following features:
26+
27+
- A modern schema-first ORM that's compatible with [Prisma](https://github.com/prisma/prisma)'s schema and API
28+
- Versatile data access APIs: high-level (Prisma-style) ORM queries + low-level ([Kysely](https://github.com/kysely-org/kysely)) query builder
29+
- Built-in access control and data validation
30+
- Advanced data modeling patterns like [polymorphism](https://zenstack.dev/blog/polymorphism)
31+
- Designed for extensibility and flexibility: plugins, life-cycle hooks, etc.
32+
- Automatic CRUD web APIs with adapters for popular frameworks
33+
- Automatic [TanStack Query](https://github.com/TanStack/query) hooks for easy CRUD from the frontend
34+
35+
# What's new with V3
36+
37+
ZenStack V3 is a major rewrite of [V2](https://github.com/zenstackhq/zenstack). The biggest change is V3 doesn't have a runtime dependency to Prisma anymore. Instead of working as a big wrapper of Prisma as in V2, V3 made a bold move and implemented the entire ORM engine using [Kysely](https://github.com/kysely-org/kysely), while keeping the query API fully compatible with Prisma.
38+
39+
Please check [this blog post](https://zenstack.dev/blog/next-chapter-1) for why we made this big architectural change decision.
40+
41+
Even without using advanced features, ZenStack offers the following benefits as a drop-in replacement to Prisma:
42+
43+
1. Pure TypeScript implementation without any Rust/WASM components.
44+
2. More TypeScript type inference, less code generation.
45+
3. Fully-typed query-builder API as a better escape hatch compared to Prisma's [raw queries](https://www.prisma.io/docs/orm/prisma-client/using-raw-sql/raw-queries) or [typed SQL](https://www.prisma.io/docs/orm/prisma-client/using-raw-sql/typedsql).
46+
47+
> Although ZenStack v3's runtime doesn't depend on Prisma anymore (specifically, `@prisma/client`), it still relies on Prisma to handle database migration. See [database migration](#database-migration) for more details.
48+
49+
# Get started
50+
51+
## Installation
52+
53+
### 1. Creating a new project
54+
55+
Use the following command to scaffold a simple TypeScript command line application with ZenStack configured:
56+
57+
```bash
58+
npm create zenstack@next my-project
59+
```
60+
61+
### 2. Setting up an existing project
62+
63+
Or, if you have an existing project, use the CLI to initialize it:
64+
65+
```bash
66+
npx @zenstackhq/cli@next init
67+
```
68+
69+
### 3. Manual setup
70+
71+
Alternatively, you can set it up manually:
72+
73+
```bash
74+
npm install -D @zenstackhq/cli@next
75+
npm install @zenstackhq/runtime@next
76+
```
77+
78+
Then create a `zenstack` folder and a `schema.zmodel` file in it.
79+
80+
## Writing ZModel schema
81+
82+
ZenStack uses a DSL named ZModel to model different aspects of database:
83+
84+
- Tables and fields
85+
- Validation rules (coming soon)
86+
- Access control policies (coming soon)
87+
- ...
88+
89+
ZModel is a super set of [Prisma Schema Language](https://www.prisma.io/docs/orm/prisma-schema/overview), i.e., every valid Prisma schema is a valid ZModel.
90+
91+
## Installing a database driver
92+
93+
ZenStack doesn't bundle any database drivers. You need to install by yourself based on the database provider you use.
94+
95+
> The project scaffolded by `npm create zenstack` is pre-configured to use SQLite. You only need to follow instructions here if you want to change it.
96+
97+
For SQLite:
98+
99+
```bash
100+
npm install better-sqlite3
101+
```
102+
103+
For Postgres:
104+
105+
```bash
106+
npm install pg
107+
```
108+
109+
## Pushing schema to the database
110+
111+
Run the following command to sync schema to the database for local development:
112+
113+
```bash
114+
npx zenstack db push
115+
```
116+
117+
> Under the hood, the command uses `prisma db push` to do the job.
118+
119+
See [database migration](#database-migration) for how to use migration to manage schema changes for production.
120+
121+
## Compiling ZModel schema
122+
123+
ZModel needs to be compiled to TypeScript before being used to create a database client. Simply run the following command:
124+
125+
```bash
126+
npx zenstack generate
127+
```
128+
129+
A `schema.ts` file will be created inside the `zenstack` folder. The file should be included as part of your source tree for compilation/bundling. You may choose to include or ignore it in source control (and generate on the fly during build). Just remember to rerun the "generate" command whenever you make changes to the ZModel schema.
130+
131+
## Creating ZenStack client
132+
133+
Now you can use the compiled TypeScript schema to instantiate a database client:
134+
135+
```ts
136+
import { ZenStackClient } from '@zenstackhq/runtime';
137+
import { schema } from './zenstack/schema';
138+
139+
const client = new ZenStackClient(schema);
140+
```
141+
142+
## Using `ZenStackClient`
143+
144+
### ORM API
145+
146+
`ZenStackClient` offers the full set of CRUD APIs that `PrismaClient` has - `findMany`, `create`, `aggregate`, etc. See [prisma documentation](https://www.prisma.io/docs/orm/prisma-client/queries) for detailed guide.
147+
148+
A few quick examples:
149+
150+
```ts
151+
const user = await client.user.create({
152+
data: {
153+
name: 'Alex',
154+
155+
posts: { create: { title: 'Hello world' } },
156+
},
157+
});
158+
159+
const userWithPosts = await client.user.findUnique({
160+
where: { id: user.id },
161+
include: { posts: true },
162+
});
163+
164+
const groupedPosts = await client.post.groupBy({
165+
by: 'published',
166+
_count: true,
167+
});
168+
```
169+
170+
Under the hood, all ORM queries are transformed into Kysely queries for execution.
171+
172+
### Query builder API
173+
174+
ZenStack uses Kysely to handle database operations, and it also directly exposes Kysely's query builder. You can use it when your use case outgrows the ORM API's capabilities. The query builder API is fully typed, and its types are directly inferred from `schema.ts` so no extra set up is needed.
175+
176+
Please check [Kysely documentation](https://kysely.dev/docs/intro) for more details. Here're a few quick examples:
177+
178+
```ts
179+
await client.$qb
180+
.selectFrom('User')
181+
.leftJoin('Post', 'Post.authorId', 'User.id')
182+
.select(['User.id', 'User.email', 'Post.title'])
183+
.execute();
184+
```
185+
186+
Query builder can also be "blended" into ORM API calls as a local escape hatch for building complex filter conditions. It allows for greater flexibility without forcing you to entirely resort to the query builder API.
187+
188+
```ts
189+
await client.user.findMany({
190+
where: {
191+
age: { gt: 18 },
192+
// "eb" is a Kysely expression builder
193+
$expr: (eb) => eb('email', 'like', '%@zenstack.dev'),
194+
},
195+
});
196+
```
197+
198+
It provides a good solution to the long standing `whereRaw` [Prisma feature request](https://github.com/prisma/prisma/issues/5560). We may make similar extensions to the `select` and `orderBy` clauses in the future.
199+
200+
### Computed fields
201+
202+
ZenStack v3 allows you to define database-evaluated computed fields with the following two steps:
203+
204+
1. Declare it in ZModel
205+
206+
```prisma
207+
model User {
208+
...
209+
/// number of posts owned by the user
210+
postCount Int @computed
211+
}
212+
```
213+
214+
2. Provide its implementation using query builder when constructing `ZenStackClient`
215+
216+
```ts
217+
const client = new ZenStackClient(schema, {
218+
computedFields: {
219+
User: {
220+
postCount: (eb) =>
221+
eb
222+
.selectFrom('Post')
223+
.whereRef('Post.authorId', '=', 'User.id')
224+
.select(({ fn }) =>
225+
fn.countAll<number>().as('postCount')
226+
),
227+
},
228+
},
229+
});
230+
```
231+
232+
You can then use the computed field anywhere a regular field can be used, for field selection, filtering, sorting, etc. The field is fully evaluated at the database side so performance will be optimal.
233+
234+
### Polymorphic models
235+
236+
_Coming soon..._
237+
238+
### Access policies
239+
240+
_Coming soon..._
241+
242+
### Validation rules
243+
244+
_Coming soon..._
245+
246+
### Custom procedures
247+
248+
_Coming soon..._
249+
250+
### Runtime plugins
251+
252+
V3 introduces a new runtime plugin mechanism that allows you to tap into the ORM's query execution in various ways. A plugin implements the [RuntimePlugin](./packages/runtime/src/client/plugin.ts#L121) interface, and can be installed with the `ZenStackClient.$use` API.
253+
254+
You can use a plugin to achieve the following goals:
255+
256+
#### 1. ORM query interception
257+
258+
ORM query interception allows you to intercept the high-level ORM API calls.
259+
260+
```ts
261+
client.$use({
262+
id: 'cost-logger',
263+
async onQuery({ model, operation, proceed, queryArgs }) {
264+
const start = Date.now();
265+
const result = await proceed(queryArgs);
266+
console.log(
267+
`[cost] ${model} ${operation} took ${Date.now() - start}ms`
268+
);
269+
return result;
270+
},
271+
});
272+
```
273+
274+
Usually a plugin would call the `proceed` callback to trigger the execution of the original query, but you can choose to completely override the query behavior with custom logic.
275+
276+
#### 2. Kysely query interception
277+
278+
Kysely query interception allows you to intercept the low-level query builder API calls. Since ORM queries are transformed into Kysely queries before execution, they are automatically captured as well.
279+
280+
Kysely query interception works against the low-level Kysely `OperationNode` structures. It's harder to implement but can guarantee intercepting all CRUD operations.
281+
282+
```ts
283+
client.$use({
284+
id: 'insert-interception-plugin',
285+
onKyselyQuery({query, proceed}) {
286+
if (query.kind === 'InsertQueryNode') {
287+
query = sanitizeInsertData(query);
288+
}
289+
return proceed(query);
290+
},
291+
});
292+
293+
function sanitizeInsertData(query: InsertQueryNode) {
294+
...
295+
}
296+
```
297+
298+
#### 3. Entity mutation interception
299+
300+
Another popular interception use case is, instead of intercepting calls, "listening on" entity changes.
301+
302+
```ts
303+
client.$use({
304+
id: 'mutation-hook-plugin',
305+
beforeEntityMutation({ model, action }) {
306+
console.log(`Before ${model} ${action}`);
307+
},
308+
afterEntityMutation({ model, action }) {
309+
console.log(`After ${model} ${action}`);
310+
},
311+
});
312+
```
313+
314+
You can provide an extra `mutationInterceptionFilter` to control what to intercept, and opt in for loading the affected entities before and/or after the mutation.
315+
316+
```ts
317+
client.$use({
318+
id: 'mutation-hook-plugin',
319+
mutationInterceptionFilter: ({ model }) => {
320+
return {
321+
intercept: model === 'User',
322+
// load entities affected before the mutation (defaults to false)
323+
loadBeforeMutationEntity: true,
324+
// load entities affected after the mutation (defaults to false)
325+
loadAfterMutationEntity: true,
326+
};
327+
},
328+
beforeEntityMutation({ model, action, entities }) {
329+
console.log(`Before ${model} ${action}: ${entities}`);
330+
},
331+
afterEntityMutation({ model, action, afterMutationEntities }) {
332+
console.log(`After ${model} ${action}: ${afterMutationEntities}`);
333+
},
334+
});
335+
```
336+
337+
# Other guides
338+
339+
## Database migration
340+
341+
ZenStack v3 delegates database schema migration to Prisma. The CLI provides Prisma CLI wrappers for managing migrations.
342+
343+
- Sync schema to dev database and create a migration record:
344+
345+
```bash
346+
npx zenstack migrate dev
347+
```
348+
349+
- Deploy new migrations:
350+
351+
```bash
352+
npx zenstack migrate deploy
353+
```
354+
355+
- Reset dev database
356+
357+
```bash
358+
npx zenstack migrate reset
359+
```
360+
361+
See [Prisma Migrate](https://www.prisma.io/docs/orm/prisma-migrate) documentation for more details.
362+
363+
## Migrating Prisma projects
364+
365+
1. Install "@zenstackhq/cli@next" and "@zenstackhq/runtime@next" packages
366+
1. Remove "@prisma/client" dependency
367+
1. Install "better-sqlite3" or "pg" based on database type
368+
1. Move "schema.prisma" to "zenstack" folder and rename it to "schema.zmodel"
369+
1. Run `npx zenstack generate`
370+
1. Replace `new PrismaClient()` with `new ZenStackClient(schema)`
371+
372+
# Limitations
373+
374+
1. Only SQLite (better-sqlite3) and Postgres (pg) database providers are supported.
375+
1. Prisma client extensions are not supported.
376+
1. Prisma custom generators are not supported (may add support in the future).

TODO.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,13 +66,14 @@
6666
- [x] Custom table name
6767
- [x] Custom field name
6868
- [ ] Strict undefined check
69+
- [ ] Polymorphism
70+
- [ ] Validation
6971
- [ ] Access Policy
7072
- [ ] Short-circuit pre-create check for scalar-field only policies
7173
- [ ] Inject "replace into"
7274
- [ ] Inject "on conflict do update"
73-
- [ ] Polymorphism
7475
- [x] Migration
7576
- [ ] Databases
7677
- [x] SQLite
7778
- [x] PostgreSQL
78-
- [x] Multi-schema
79+
- [ ] Multi-schema

0 commit comments

Comments
 (0)