From 2aa0fd2b64c6a0f46f8776c05bbc3731a215b1c8 Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Tue, 25 Oct 2022 22:25:13 +0800 Subject: [PATCH 01/24] progress --- README.md | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 179e3af91..2fd137905 100644 --- a/README.md +++ b/README.md @@ -1 +1,19 @@ -

ZenStack

+
+ + + + +

ZenStack

+ + +
+ +## What is ZenStack? + +## Getting started + +## How does it work? + +## Client-side usage + +## Server-side usage From 17a0d929ab457440aa911f9efee469f45d1f201e Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Wed, 26 Oct 2022 10:31:48 +0800 Subject: [PATCH 02/24] Progress on README --- README.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/README.md b/README.md index 2fd137905..98de10a61 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,22 @@ ## What is ZenStack? +ZenStack is a toolkit for simplifying full-stack development with Node.js web frameworks like [Next.js](https://nextjs.org/), [Nuxt.js](https://nuxtjs.org/) and [SvelteKit](https://kit.svelte.dev/) , using Typescript language. + +With the increasing power of such frameworks, it's becoming more and more practical to build a complex web app all within one unified framework. However, you'll still need to spend significantly amout of engergy to design and build up the backend part of your app. + +Things to consider inlcude: + +- What kind of API to use? RESTful or GraphQL? +- How to model your data and map the model to both source code and database (ORM)? +- How to implement common CRUD operations? Manually construct it or use a generator? +- How to evolve your data model? +- How to authenticate users and authorize their requests? + +ZenStack aims to simplify these tasks by providing an intuitive data modeling language to define data types and access policies, integrated with user authentication. It generates RESTful services as well as a client-side library, allowing flexible and painless CRUD operations. Typescript types are generated for data models, CRUD input, filters, etc., so that you get great coding experiences in IDE and have much fewer chances to make mistakes. + +ZenStack is both heavily inspired and built above [Prisma ORM](https://www.prisma.io/), which is IMO the best ORM toolkit in the market. Familarity with Prisma should make it very easy to pick up ZenStack, but it's not prerequisite since the modeling language is intuitive and the development workflow is straightforward. + ## Getting started ## How does it work? @@ -17,3 +33,5 @@ ## Client-side usage ## Server-side usage + +## What's next? From d4318623e77bb2f6397ac8b6062b0c2e2a586545 Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Wed, 26 Oct 2022 13:23:04 +0800 Subject: [PATCH 03/24] update --- README.md | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 98de10a61..ba7d30eb1 100644 --- a/README.md +++ b/README.md @@ -24,10 +24,25 @@ Things to consider inlcude: ZenStack aims to simplify these tasks by providing an intuitive data modeling language to define data types and access policies, integrated with user authentication. It generates RESTful services as well as a client-side library, allowing flexible and painless CRUD operations. Typescript types are generated for data models, CRUD input, filters, etc., so that you get great coding experiences in IDE and have much fewer chances to make mistakes. -ZenStack is both heavily inspired and built above [Prisma ORM](https://www.prisma.io/), which is IMO the best ORM toolkit in the market. Familarity with Prisma should make it very easy to pick up ZenStack, but it's not prerequisite since the modeling language is intuitive and the development workflow is straightforward. +ZenStack is both heavily inspired and built above [Prisma ORM](https://www.prisma.io/), which is in our oppinion the best ORM toolkit in the market. Familarity with Prisma should make it very easy to pick up ZenStack, but it's not prerequisite since the modeling language is intuitive and the development workflow is straightforward. ## Getting started +### With Next.js + +The easiest way to start using ZenStack is by creating a new Next.js project from a preconfigured template. + +1. Make sure you have Node.js 16 or above and NPM 6 or above installed +1. + +### With Nuxt.js + +![](https://img.shields.io/badge/-Coming%20Soon-gray) + +### With SvelteKit + +![](https://img.shields.io/badge/-Coming%20Soon-gray) + ## How does it work? ## Client-side usage From 1a53b408bb0e1763ca322caf0722f0a9166be172 Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Wed, 26 Oct 2022 21:51:42 +0800 Subject: [PATCH 04/24] update readme --- README.md | 52 ++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 46 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index ba7d30eb1..bf5ea1be3 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ ZenStack is a toolkit for simplifying full-stack development with Node.js web frameworks like [Next.js](https://nextjs.org/), [Nuxt.js](https://nuxtjs.org/) and [SvelteKit](https://kit.svelte.dev/) , using Typescript language. -With the increasing power of such frameworks, it's becoming more and more practical to build a complex web app all within one unified framework. However, you'll still need to spend significantly amout of engergy to design and build up the backend part of your app. +Thanks to the increasing power of frameworks, it's becoming more and more practical to build a complex web app all within one unified framework. However, you'll still need to spend significantly amout of engergy to design and build up the backend part of your app. Things to consider inlcude: @@ -24,24 +24,60 @@ Things to consider inlcude: ZenStack aims to simplify these tasks by providing an intuitive data modeling language to define data types and access policies, integrated with user authentication. It generates RESTful services as well as a client-side library, allowing flexible and painless CRUD operations. Typescript types are generated for data models, CRUD input, filters, etc., so that you get great coding experiences in IDE and have much fewer chances to make mistakes. +Since CRUD APIs are automatically generated with access policies injected, you can implement most of your business logic in your front-end code safely. Read operations never return data that's not supposed to be visible for the current user, and writes will be rejected if unauthorized. The data-access client library also supports nested writes, which allows you to make a batch of creates/updates atomically, eliminating the needs for explicitly using a transaction. + ZenStack is both heavily inspired and built above [Prisma ORM](https://www.prisma.io/), which is in our oppinion the best ORM toolkit in the market. Familarity with Prisma should make it very easy to pick up ZenStack, but it's not prerequisite since the modeling language is intuitive and the development workflow is straightforward. ## Getting started ### With Next.js -The easiest way to start using ZenStack is by creating a new Next.js project from a preconfigured template. +The easiest way to start using ZenStack is by creating a new Next.js project from a preconfigured starter template. + +Here we demonstrate the process with a simple Blog starter using [Next-Auth](https://next-auth.js.org/) for user authentication. + +1. Make sure you have Node.js 16 or above and NPM 8 or above installed + +2. Create a new Next.js project from ZenStack starter + +```bash +npx create-next-app [project name] --use-npm -e https://github.com/zenstackhq/nextjs-auth-starter + +cd [project name] +``` + +3. Run ZenStack generator to generate data services, auth adapter, and the client library + +```bash +npm run generate +``` -1. Make sure you have Node.js 16 or above and NPM 6 or above installed -1. +4. Initialize your local db and creates the first migration + The starter is preconfigured with a local sqlite database. Run the following command to populate its schema and generates a migration history: + +```bash +npm run db:migrate -- -n init +``` + +5. Start the app + +```bash +npm run dev +``` + +If everything worked correctly, you should have a blog site where you can signup, author drafts and publish them. + +You can also try signing up multiple accounts and verify that drafts created by different users are isolated. + +Checkout [the starter's documentation](https://github.com/zenstackhq/nextjs-auth-starter#readme) for more details. ### With Nuxt.js -![](https://img.shields.io/badge/-Coming%20Soon-gray) +![](https://img.shields.io/badge/-Coming%20Soon-lightgray) ### With SvelteKit -![](https://img.shields.io/badge/-Coming%20Soon-gray) +![](https://img.shields.io/badge/-Coming%20Soon-lightgray) ## How does it work? @@ -49,4 +85,8 @@ The easiest way to start using ZenStack is by creating a new Next.js project fro ## Server-side usage +## Development workflow + ## What's next? + +## Reach out to us for any issue, feedback or ideas! From 6040cc191e854f04e262ec99a6fbe11b0c29e27f Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Wed, 26 Oct 2022 22:29:54 +0800 Subject: [PATCH 05/24] Readme progress --- README.md | 37 ++++++++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index bf5ea1be3..d1d8a7f03 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ ZenStack is a toolkit for simplifying full-stack development with Node.js web fr Thanks to the increasing power of frameworks, it's becoming more and more practical to build a complex web app all within one unified framework. However, you'll still need to spend significantly amout of engergy to design and build up the backend part of your app. -Things to consider inlcude: +Things that make you stressful include: - What kind of API to use? RESTful or GraphQL? - How to model your data and map the model to both source code and database (ORM)? @@ -22,7 +22,11 @@ Things to consider inlcude: - How to evolve your data model? - How to authenticate users and authorize their requests? -ZenStack aims to simplify these tasks by providing an intuitive data modeling language to define data types and access policies, integrated with user authentication. It generates RESTful services as well as a client-side library, allowing flexible and painless CRUD operations. Typescript types are generated for data models, CRUD input, filters, etc., so that you get great coding experiences in IDE and have much fewer chances to make mistakes. +ZenStack aims to simplify these tasks by providing an intuitive data modeling language to define data types and access policies, integrated with user authentication. It maps your data model to a relational database schema, generates RESTful services as well as a client-side library, allowing flexible and painless CRUD operations. + +Typescript types are generated for data models, CRUD input, filters, etc., so that you get great coding experiences in IDE and have much fewer chances to make mistakes. + +![Diagram here] Since CRUD APIs are automatically generated with access policies injected, you can implement most of your business logic in your front-end code safely. Read operations never return data that's not supposed to be visible for the current user, and writes will be rejected if unauthorized. The data-access client library also supports nested writes, which allows you to make a batch of creates/updates atomically, eliminating the needs for explicitly using a transaction. @@ -81,12 +85,35 @@ Checkout [the starter's documentation](https://github.com/zenstackhq/nextjs-auth ## How does it work? -## Client-side usage +ZenStack has four essential responsibilities: + +1. Mapping data model to db schema and program types (ORM) +1. Integrating with authentication +1. Generating CRUD APIs and enforcing data access policy checks +1. Providing type-safe client CRUD library + +We'll briefly go through each of them in this section. + +### ORM + +### Authentication -## Server-side usage +### Data access policy checking + +### Type-safe client library + +## Developing with the generated code + +### Client-side + +### Server-side usage ## Development workflow +## Database considerations + ## What's next? -## Reach out to us for any issue, feedback or ideas! +## Reach out to us for issues, feedback and ideas! + +[Discussions](../discussions) [Issues](../issues) [Discord]() [Twitter]() From 93c5777b07d09213b2cb23c839a03cd93cf7960e Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Thu, 27 Oct 2022 14:36:43 +0800 Subject: [PATCH 06/24] doc progress --- README.md | 53 ++----------------- docs/get-started/NextJS.md | 105 +++++++++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+), 48 deletions(-) create mode 100644 docs/get-started/NextJS.md diff --git a/README.md b/README.md index d1d8a7f03..10534cf35 100644 --- a/README.md +++ b/README.md @@ -34,54 +34,11 @@ ZenStack is both heavily inspired and built above [Prisma ORM](https://www.prism ## Getting started -### With Next.js +### [For Next.js](docs/get-started/NextJS.md) -The easiest way to start using ZenStack is by creating a new Next.js project from a preconfigured starter template. +### For Nuxt.js -Here we demonstrate the process with a simple Blog starter using [Next-Auth](https://next-auth.js.org/) for user authentication. - -1. Make sure you have Node.js 16 or above and NPM 8 or above installed - -2. Create a new Next.js project from ZenStack starter - -```bash -npx create-next-app [project name] --use-npm -e https://github.com/zenstackhq/nextjs-auth-starter - -cd [project name] -``` - -3. Run ZenStack generator to generate data services, auth adapter, and the client library - -```bash -npm run generate -``` - -4. Initialize your local db and creates the first migration - The starter is preconfigured with a local sqlite database. Run the following command to populate its schema and generates a migration history: - -```bash -npm run db:migrate -- -n init -``` - -5. Start the app - -```bash -npm run dev -``` - -If everything worked correctly, you should have a blog site where you can signup, author drafts and publish them. - -You can also try signing up multiple accounts and verify that drafts created by different users are isolated. - -Checkout [the starter's documentation](https://github.com/zenstackhq/nextjs-auth-starter#readme) for more details. - -### With Nuxt.js - -![](https://img.shields.io/badge/-Coming%20Soon-lightgray) - -### With SvelteKit - -![](https://img.shields.io/badge/-Coming%20Soon-lightgray) +### For SvelteKit ## How does it work? @@ -102,11 +59,11 @@ We'll briefly go through each of them in this section. ### Type-safe client library -## Developing with the generated code +## Programming with the generated code ### Client-side -### Server-side usage +### Server-side ## Development workflow diff --git a/docs/get-started/NextJS.md b/docs/get-started/NextJS.md new file mode 100644 index 000000000..99846868f --- /dev/null +++ b/docs/get-started/NextJS.md @@ -0,0 +1,105 @@ +# Getting started for Next.js + +## Create a new project from a starter + +The easiest way to start using ZenStack is by creating a new Next.js project from a preconfigured starter template. + +Here we demonstrate the process with a simple Blog starter using [Next-Auth](https://next-auth.js.org/) for user authentication. + +1. Make sure you have Node.js 16 or above and NPM 8 or above installed + +2. Create a new Next.js project from ZenStack starter + +```bash +npx create-next-app [project name] --use-npm -e https://github.com/zenstackhq/nextjs-auth-starter + +cd [project name] +``` + +3. Run ZenStack generator to generate data services, auth adapter, and the client library + +```bash +npm run generate +``` + +4. Initialize your local db and creates the first migration + The starter is preconfigured with a local sqlite database. Run the following command to populate its schema and generates a migration history: + +```bash +npm run db:migrate -- -n init +``` + +5. Start the app + +```bash +npm run dev +``` + +If everything worked correctly, you should have a blog site where you can signup, author drafts and publish them. Congratulations! In case anything broke, [reach out to us](#reach-out-to-us-for-issues-feedback-and-ideas) and we'll help. + +You can also try signing up multiple accounts and verify that drafts created by different users are isolated. + +Checkout [the starter's documentation](https://github.com/zenstackhq/nextjs-auth-starter#readme) for more details. + +## Add to an existing project + +1. Install the CLI and runtime + +```bash +npm i -D zenstack + +npm i @zenstackhq/runtime @zenstackhq/internal +``` + +2. Install [VSCode extension](https://marketplace.visualstudio.com/items?itemName=zenstack.zenstack) for authoring the model file + +3. Create a "zenstack" folder at your project root, and add a "schema.zmodel" file in it + +4. Configure database connection in "schema.model" + +5. Add models and define access policies as needed by your requirements, and then generate code + +```bash +npx zenstack generate +``` + +5. Mount data-access API to endpoint + Create file `pages/api/zenstack/[...path].ts`, with content: + +```ts +import { NextApiRequest, NextApiResponse } from 'next'; +import { + type RequestHandlerOptions, + requestHandler, +} from '@zenstackhq/runtime/server'; +import { authOptions } from '@api/auth/[...nextauth]'; +import { unstable_getServerSession } from 'next-auth'; +import service from '@zenstackhq/runtime'; + +const options: RequestHandlerOptions = { + async getServerUser(req: NextApiRequest, res: NextApiResponse) { + // return User object for current session, the concrete logic depends on how you authenticate users and maintain sessions + }, +}; +export default requestHandler(service, options); +``` + +6. Initialize your local db and creates the first migration + +```bash +npm run db:migrate -- -n init +``` + +7. Start building your app by using the generated React hooks + E.g.: + +```ts +import { usePost } from '@zenstackhq/runtime/hooks'; + +... + +const { get } = usePost(); +const posts = get({ where: { public: true } }); + +return <>{posts.map(post => ())} +``` From 76aa7e74a572e61ab9859ba4bb0de02e02114d52 Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Thu, 27 Oct 2022 15:23:22 +0800 Subject: [PATCH 07/24] update --- docs/get-started/NextJS.md | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/docs/get-started/NextJS.md b/docs/get-started/NextJS.md index 99846868f..7e4df80a5 100644 --- a/docs/get-started/NextJS.md +++ b/docs/get-started/NextJS.md @@ -53,18 +53,32 @@ npm i @zenstackhq/runtime @zenstackhq/internal 2. Install [VSCode extension](https://marketplace.visualstudio.com/items?itemName=zenstack.zenstack) for authoring the model file -3. Create a "zenstack" folder at your project root, and add a "schema.zmodel" file in it +3. Create a "zenstack" folder under your project root, and add a "schema.zmodel" file in it 4. Configure database connection in "schema.model" -5. Add models and define access policies as needed by your requirements, and then generate code + Here's an example for using a Postgres database with connection string specified in `DATABASE_URL` environment variable: + +```prisma +datasource db { + provider = 'postgresql' + url = env('DATABASE_URL') +} +``` + +5. Add models and define access policies as needed + +You can use the [sample model](https://github.com/zenstackhq/nextjs-auth-starter/blob/main/zenstack/schema.zmodel) in the start project as a reference. + +6. Generate server and client code from your model ```bash npx zenstack generate ``` -5. Mount data-access API to endpoint - Create file `pages/api/zenstack/[...path].ts`, with content: +7. Mount data-access API to endpoint + + Create file `pages/api/zenstack/[...path].ts`, with content: ```ts import { NextApiRequest, NextApiResponse } from 'next'; @@ -72,25 +86,26 @@ import { type RequestHandlerOptions, requestHandler, } from '@zenstackhq/runtime/server'; -import { authOptions } from '@api/auth/[...nextauth]'; -import { unstable_getServerSession } from 'next-auth'; import service from '@zenstackhq/runtime'; const options: RequestHandlerOptions = { async getServerUser(req: NextApiRequest, res: NextApiResponse) { - // return User object for current session, the concrete logic depends on how you authenticate users and maintain sessions + // return User object for current session, the concrete logic + // depends on how you authenticate users and maintain sessions + // + // database can be accessed with "service.db" }, }; export default requestHandler(service, options); ``` -6. Initialize your local db and creates the first migration +7. Initialize your local db and creates the first migration ```bash npm run db:migrate -- -n init ``` -7. Start building your app by using the generated React hooks +8. Start building your app by using the generated React hooks E.g.: ```ts From a522cc0ee8b7fced967ca17856295a69691a8cd4 Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Thu, 27 Oct 2022 17:40:02 +0800 Subject: [PATCH 08/24] update --- README.md | 31 ++++++++++++++++++++++++++++--- docs/get-started/NextJS.md | 9 ++++----- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 10534cf35..1fdd6d28a 100644 --- a/README.md +++ b/README.md @@ -44,18 +44,43 @@ ZenStack is both heavily inspired and built above [Prisma ORM](https://www.prism ZenStack has four essential responsibilities: -1. Mapping data model to db schema and program types (ORM) +1. Modeling data and maps the model to db schema and program types 1. Integrating with authentication 1. Generating CRUD APIs and enforcing data access policy checks 1. Providing type-safe client CRUD library We'll briefly go through each of them in this section. -### ORM +### Data Modeling + +ZenStack uses a schema language called `ZModel` to define data types and their relationship. The `zenstack` CLI takes a schema file as input and generates database client client code automatically. Such client code allows you program against database in server-side code in a fully typed way, without writing any SQL. It also provides commands for synchronizing data model with database schema, as well generating "migration reords" when your data model evolves. + +Please checkout [Data Modeling Cheatsheet](/docs/get-started/data-modeling-cheatsheet) on how to carry out common tasks. + +Internally, ZenStack completely relies on Prisma for ORM tasks. The ZModel language is a superset of Prisma's schema language. When `zenstack generate` is run, a Prisma schema named 'schema.prisma' is generated beside your ZModel schema file. You don't need to commit schema.prisma to source control. The recommended practice is to run `zenstack generate` during deployment, so Prisma schema is regenerated on the fly. ### Authentication -### Data access policy checking +ZenStack is not an authentication library, but it gets involved in two ways. + +Firstly, if you use any authentication method that involves persisting user's identity, you'll model user's shape in ZModel. Some auth library, like NextAuth, requires user entity to include certain fields, and your model should reflect this. Credential-based authentication requires validating user-provided credentials, and you should implement this using database client generated by ZenStack. + +To simplify the task, ZenStack automatically generates an adapter for NextAuth when it detects that `next-auth` npm package is installed. Please refer to [the starter code](https://github.com/zenstackhq/nextjs-auth-starter/blob/main/pages/api/auth/%5B...nextauth%5D.ts) for how to use it. We'll keep adding integrations/samples for other auth libraries in the future. + +Secondly, authentication is almost always connected to authorization. ZModel allows you to reference the current login user via `auth()` function in access policy expressions. Like, + +```prisma +model Post { + owner User @relation(fields: [ownerId], references: [id]) + ... + + @@deny('all', auth() == null) + @@allow('update', auth() == owner) +``` + +The value returned by `auth()` is provided by your auth solution, via the `getServerUser` hook function you provide when mounting ZenStack APIs. Check [this code](https://github.com/zenstackhq/nextjs-auth-starter/blob/main/pages/api/zenstack/%5B...path%5D.ts) for an example. + +### Data access policy ### Type-safe client library diff --git a/docs/get-started/NextJS.md b/docs/get-started/NextJS.md index 7e4df80a5..dc591bf6a 100644 --- a/docs/get-started/NextJS.md +++ b/docs/get-started/NextJS.md @@ -90,10 +90,9 @@ import service from '@zenstackhq/runtime'; const options: RequestHandlerOptions = { async getServerUser(req: NextApiRequest, res: NextApiResponse) { - // return User object for current session, the concrete logic - // depends on how you authenticate users and maintain sessions - // - // database can be accessed with "service.db" + // TODO: return User object for current session, the concrete logic + // depends on how you authenticate users and maintain sessions. + // Database can be accessed with "service.db" }, }; export default requestHandler(service, options); @@ -116,5 +115,5 @@ import { usePost } from '@zenstackhq/runtime/hooks'; const { get } = usePost(); const posts = get({ where: { public: true } }); -return <>{posts.map(post => ())} +return <>{posts?.map(post => ())} ``` From 54889abfa1c882a51b5a486620db308f00707776 Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Thu, 27 Oct 2022 18:11:50 +0800 Subject: [PATCH 09/24] update --- README.md | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1fdd6d28a..31fe96e7b 100644 --- a/README.md +++ b/README.md @@ -49,9 +49,9 @@ ZenStack has four essential responsibilities: 1. Generating CRUD APIs and enforcing data access policy checks 1. Providing type-safe client CRUD library -We'll briefly go through each of them in this section. +Let's briefly go through each of them in this section. -### Data Modeling +### Data modeling ZenStack uses a schema language called `ZModel` to define data types and their relationship. The `zenstack` CLI takes a schema file as input and generates database client client code automatically. Such client code allows you program against database in server-side code in a fully typed way, without writing any SQL. It also provides commands for synchronizing data model with database schema, as well generating "migration reords" when your data model evolves. @@ -82,6 +82,36 @@ The value returned by `auth()` is provided by your auth solution, via the `getSe ### Data access policy +The main value that ZenStack adds over a traditional ORM is the built-in data access policy engine. This allows most business logic to be safely implemented in front-end code. Since ZenStack delegates database access to Prisma, it enforces access policies through analyzing queries sent to Prisma and injecting guarding conditions. For example, a policy saying "a post can only be seen by its owner if it's not published", expressed in ZModel as: + +```` +@@deny('all', auth() != owner && !published) +```. + +When client code sends a query to list all `Post`s, ZenStack's generated code intercepts it and injects a `where` clause before passing it through to Prisma (conceptually): +```js +{ + where: { + AND: [ + { ...userProvidedFilter }, + { + // injected by ZenStack, "user" object is fetched from context + NOT: { + AND: [ + { owner: { not: { id: user.id } } }, + { NOT: { published: true } } + ] + } + } + ] + } +} +```` + +Similar procedures are applied to write operations, as well as more complex queries that involve nested reads and writes. + +To ensure good performance, ZenStack generates conditions statically so it doesn't need to introspect ZModel at runtime. + ### Type-safe client library ## Programming with the generated code From 7060e53a634b5f5336ca86d00fa3172a6d52fa28 Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Thu, 27 Oct 2022 18:13:13 +0800 Subject: [PATCH 10/24] update --- README.md | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 31fe96e7b..6981a7714 100644 --- a/README.md +++ b/README.md @@ -82,13 +82,14 @@ The value returned by `auth()` is provided by your auth solution, via the `getSe ### Data access policy -The main value that ZenStack adds over a traditional ORM is the built-in data access policy engine. This allows most business logic to be safely implemented in front-end code. Since ZenStack delegates database access to Prisma, it enforces access policies through analyzing queries sent to Prisma and injecting guarding conditions. For example, a policy saying "a post can only be seen by its owner if it's not published", expressed in ZModel as: +The main value that ZenStack adds over a traditional ORM is the built-in data access policy engine. This allows most business logic to be safely implemented in front-end code. Since ZenStack delegates database access to Prisma, it enforces access policies through analyzing queries sent to Prisma and injecting guarding conditions. Suppose we have a policy saying "a post can only be seen by its owner if it's not published", expressed in ZModel as: -```` +``` @@deny('all', auth() != owner && !published) -```. +``` When client code sends a query to list all `Post`s, ZenStack's generated code intercepts it and injects a `where` clause before passing it through to Prisma (conceptually): + ```js { where: { @@ -99,14 +100,14 @@ When client code sends a query to list all `Post`s, ZenStack's generated code in NOT: { AND: [ { owner: { not: { id: user.id } } }, - { NOT: { published: true } } - ] - } - } - ] + { NOT: { published: true } }, + ], + }, + }, + ]; } } -```` +``` Similar procedures are applied to write operations, as well as more complex queries that involve nested reads and writes. From a620ed40214707ab5abaa380270b3fc98921ffca Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Thu, 27 Oct 2022 18:47:51 +0800 Subject: [PATCH 11/24] update --- README.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 6981a7714..17499b73c 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,7 @@ The main value that ZenStack adds over a traditional ORM is the built-in data ac @@deny('all', auth() != owner && !published) ``` -When client code sends a query to list all `Post`s, ZenStack's generated code intercepts it and injects a `where` clause before passing it through to Prisma (conceptually): +When client code sends a query to list all `Post`s, ZenStack's generated code intercepts it and injects int the `where` clause before passing it through to Prisma (conceptually): ```js { @@ -109,12 +109,20 @@ When client code sends a query to list all `Post`s, ZenStack's generated code in } ``` -Similar procedures are applied to write operations, as well as more complex queries that involve nested reads and writes. +Similar procedures are applied to write operations, as well as more complex queries that involve nested reads and writes. To ensure good performance, ZenStack generates conditions statically so it doesn't need to introspect ZModel at runtime. The engine also makes best effort to push down policy constaints to the database to avoid fetching data unnecessarily. -To ensure good performance, ZenStack generates conditions statically so it doesn't need to introspect ZModel at runtime. +Please **beware** that policy checking is only applied when data access is done using the generated React hooks or equivalently the RESTful API. If you use `service.db` to access database directly from server-side code, policies are bypassed and you have to do all necessary checking by yourself. We've planned to add helper functions for "injecting" the policy checking on the server side in the future. ### Type-safe client library +Thanks to Prisma's power, ZenStack generates accurate Typescript types for your data models: + +- The model itself +- Argument types for listing models, including filtering, sorting, pagination, and nested reads for related models +- Argument for creating and updating models, including nested writes for related models + +The cool thing is that, the generated types are shared between client-side and server-side code, so no matter which side of code you're writing, you can always enjoy the pleasant IDE intellisense and typescript compiler's error checking. + ## Programming with the generated code ### Client-side From d51dae217690ba1badef8a7660edafe921393852 Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Thu, 27 Oct 2022 18:56:07 +0800 Subject: [PATCH 12/24] update --- README.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 17499b73c..3265d2116 100644 --- a/README.md +++ b/README.md @@ -129,11 +129,15 @@ The cool thing is that, the generated types are shared between client-side and s ### Server-side -## Development workflow +## What's next? -## Database considerations +### [Learning the ZModel language](/docs/ref/learning-the-zmodel-language) -## What's next? +### [Learning the zenstack cli](/docs/ref/learning-the-zmodel-language) + +### [Evolving data model with migration](/docs/ref/evolving-data-model-with-migration) + +### [Database hosting considerations](/docs/ref/database-hosting-considerations) ## Reach out to us for issues, feedback and ideas! From 6abadc04df5680c47e4d92cf86c952fdd4ab5664 Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Fri, 28 Oct 2022 09:27:52 +0800 Subject: [PATCH 13/24] update --- README.md | 54 ++++++++++++++++++++++++++++++++++---- docs/get-started/NextJS.md | 11 +++++--- 2 files changed, 57 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 3265d2116..dc5b667c5 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,53 @@ Things that make you stressful include: - How to evolve your data model? - How to authenticate users and authorize their requests? -ZenStack aims to simplify these tasks by providing an intuitive data modeling language to define data types and access policies, integrated with user authentication. It maps your data model to a relational database schema, generates RESTful services as well as a client-side library, allowing flexible and painless CRUD operations. +ZenStack aims to simplify these tasks by providing: + +- An intuitive data modeling language for defining data types, relationship and access policies + +```prisma +model User { + id String @id @default(cuid()) + email String @unique + + // one-to-many relation to Post + posts Post[] +} + +model Post { + id String @id @default(cuid()) + title String + content String + published Boolean @default(false) + + // one-to-many relation from User + author User? @relation(fields: [authorId], references: [id]) + authorId String? + + // must signin to CRUD any post + @@deny('all', auth() == null) + + // allow CRUD by author + @@allow('all', author == auth()) +} +``` + +- Data access services and strongly typed front-end library + +```jsx +// React example + +const { find } = usePost(); +const posts = get({ where: { public: true } }); +// only posts owned by current login user are returned +return ( + <> + {posts?.map((post) => ( + + ))} + +); +``` Typescript types are generated for data models, CRUD input, filters, etc., so that you get great coding experiences in IDE and have much fewer chances to make mistakes. @@ -55,8 +101,6 @@ Let's briefly go through each of them in this section. ZenStack uses a schema language called `ZModel` to define data types and their relationship. The `zenstack` CLI takes a schema file as input and generates database client client code automatically. Such client code allows you program against database in server-side code in a fully typed way, without writing any SQL. It also provides commands for synchronizing data model with database schema, as well generating "migration reords" when your data model evolves. -Please checkout [Data Modeling Cheatsheet](/docs/get-started/data-modeling-cheatsheet) on how to carry out common tasks. - Internally, ZenStack completely relies on Prisma for ORM tasks. The ZModel language is a superset of Prisma's schema language. When `zenstack generate` is run, a Prisma schema named 'schema.prisma' is generated beside your ZModel schema file. You don't need to commit schema.prisma to source control. The recommended practice is to run `zenstack generate` during deployment, so Prisma schema is regenerated on the fly. ### Authentication @@ -131,9 +175,9 @@ The cool thing is that, the generated types are shared between client-side and s ## What's next? -### [Learning the ZModel language](/docs/ref/learning-the-zmodel-language) +### [Learning the `ZModel` language](/docs/get-started/learning-the-zmodel-language) -### [Learning the zenstack cli](/docs/ref/learning-the-zmodel-language) +### [Learning the `zenstack` cli](/docs/get-started/learning-the-zenstack-cli) ### [Evolving data model with migration](/docs/ref/evolving-data-model-with-migration) diff --git a/docs/get-started/NextJS.md b/docs/get-started/NextJS.md index dc591bf6a..5c2b42a74 100644 --- a/docs/get-started/NextJS.md +++ b/docs/get-started/NextJS.md @@ -107,13 +107,18 @@ npm run db:migrate -- -n init 8. Start building your app by using the generated React hooks E.g.: -```ts +```jsx import { usePost } from '@zenstackhq/runtime/hooks'; ... const { get } = usePost(); const posts = get({ where: { public: true } }); - -return <>{posts?.map(post => ())} +return ( + <> + {posts?.map((post) => ( + + ))} + +); ``` From f35bfd7a3df2fc39b196c01473f886baffd4eaac Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Fri, 28 Oct 2022 09:47:43 +0800 Subject: [PATCH 14/24] update --- README.md | 2 +- docs/get-started/{NextJS.md => next-js.md} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename docs/get-started/{NextJS.md => next-js.md} (100%) diff --git a/README.md b/README.md index dc5b667c5..b2be08667 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,7 @@ ZenStack is both heavily inspired and built above [Prisma ORM](https://www.prism ## Getting started -### [For Next.js](docs/get-started/NextJS.md) +### [For Next.js](docs/get-started/next-js) ### For Nuxt.js diff --git a/docs/get-started/NextJS.md b/docs/get-started/next-js.md similarity index 100% rename from docs/get-started/NextJS.md rename to docs/get-started/next-js.md From 291843779850d98bc2da1e5844c76c3c3fca1081 Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Fri, 28 Oct 2022 11:17:16 +0800 Subject: [PATCH 15/24] update readme --- README.md | 122 +++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 103 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index b2be08667..200bd7464 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ ZenStack is a toolkit for simplifying full-stack development with Node.js web frameworks like [Next.js](https://nextjs.org/), [Nuxt.js](https://nuxtjs.org/) and [SvelteKit](https://kit.svelte.dev/) , using Typescript language. -Thanks to the increasing power of frameworks, it's becoming more and more practical to build a complex web app all within one unified framework. However, you'll still need to spend significantly amout of engergy to design and build up the backend part of your app. +Thanks to the increasing power of frameworks, it's becoming more and more practical to build a complex web app all within one unified framework. However, you'll still need to spend significantly amout of engergy to design and build up the server-side part of your app. Things that make you stressful include: @@ -24,7 +24,7 @@ Things that make you stressful include: ZenStack aims to simplify these tasks by providing: -- An intuitive data modeling language for defining data types, relationship and access policies +- An intuitive data modeling language for defining data types, relationships and access policies ```prisma model User { @@ -53,7 +53,7 @@ model Post { } ``` -- Data access services and strongly typed front-end library +- Auto-generated CRUD services and strongly typed front-end library ```jsx // React example @@ -70,17 +70,13 @@ return ( ); ``` -Typescript types are generated for data models, CRUD input, filters, etc., so that you get great coding experiences in IDE and have much fewer chances to make mistakes. - -![Diagram here] - -Since CRUD APIs are automatically generated with access policies injected, you can implement most of your business logic in your front-end code safely. Read operations never return data that's not supposed to be visible for the current user, and writes will be rejected if unauthorized. The data-access client library also supports nested writes, which allows you to make a batch of creates/updates atomically, eliminating the needs for explicitly using a transaction. +Since CRUD APIs are automatically generated with access policies injected, you can implement most of your business logic in your front-end code safely. Read operations never return data that's not supposed to be visible to the current user, and writes will be rejected if unauthorized. The generated front-end library also supports nested writes, which allows you to make a batch of creates/updates atomically, eliminating the needs for explicitly using a transaction. ZenStack is both heavily inspired and built above [Prisma ORM](https://www.prisma.io/), which is in our oppinion the best ORM toolkit in the market. Familarity with Prisma should make it very easy to pick up ZenStack, but it's not prerequisite since the modeling language is intuitive and the development workflow is straightforward. ## Getting started -### [For Next.js](docs/get-started/next-js) +### [For Next.js](docs/get-started/next-js.md) ### For Nuxt.js @@ -92,7 +88,7 @@ ZenStack has four essential responsibilities: 1. Modeling data and maps the model to db schema and program types 1. Integrating with authentication -1. Generating CRUD APIs and enforcing data access policy checks +1. Generating CRUD APIs and enforcing data access policies 1. Providing type-safe client CRUD library Let's briefly go through each of them in this section. @@ -107,7 +103,7 @@ Internally, ZenStack completely relies on Prisma for ORM tasks. The ZModel langu ZenStack is not an authentication library, but it gets involved in two ways. -Firstly, if you use any authentication method that involves persisting user's identity, you'll model user's shape in ZModel. Some auth library, like NextAuth, requires user entity to include certain fields, and your model should reflect this. Credential-based authentication requires validating user-provided credentials, and you should implement this using database client generated by ZenStack. +Firstly, if you use any authentication method that involves persisting user's identity, you'll model user's shape in ZModel. Some auth library, like [NextAuth](https://next-auth.js.org/), requires user entity to include certain fields, and your model should fulfill such requirements. Credential-based authentication requires validating user-provided credentials, and you should implement this using the database client generated by ZenStack. To simplify the task, ZenStack automatically generates an adapter for NextAuth when it detects that `next-auth` npm package is installed. Please refer to [the starter code](https://github.com/zenstackhq/nextjs-auth-starter/blob/main/pages/api/auth/%5B...nextauth%5D.ts) for how to use it. We'll keep adding integrations/samples for other auth libraries in the future. @@ -115,21 +111,21 @@ Secondly, authentication is almost always connected to authorization. ZModel all ```prisma model Post { - owner User @relation(fields: [ownerId], references: [id]) + author User @relation(fields: [authorId], references: [id]) ... @@deny('all', auth() == null) - @@allow('update', auth() == owner) + @@allow('all', auth() == author) ``` The value returned by `auth()` is provided by your auth solution, via the `getServerUser` hook function you provide when mounting ZenStack APIs. Check [this code](https://github.com/zenstackhq/nextjs-auth-starter/blob/main/pages/api/zenstack/%5B...path%5D.ts) for an example. ### Data access policy -The main value that ZenStack adds over a traditional ORM is the built-in data access policy engine. This allows most business logic to be safely implemented in front-end code. Since ZenStack delegates database access to Prisma, it enforces access policies through analyzing queries sent to Prisma and injecting guarding conditions. Suppose we have a policy saying "a post can only be seen by its owner if it's not published", expressed in ZModel as: +The main value that ZenStack adds over a traditional ORM is the built-in data access policy engine. This allows most business logic to be safely implemented in front-end code. Since ZenStack delegates database access to Prisma, it enforces access policies through analyzing queries sent to Prisma and injecting guarding conditions. Suppose we have a policy saying "a post can only be accessed by its author if it's not published", expressed in ZModel as: ``` -@@deny('all', auth() != owner && !published) +@@deny('all', auth() != author && !published) ``` When client code sends a query to list all `Post`s, ZenStack's generated code intercepts it and injects int the `where` clause before passing it through to Prisma (conceptually): @@ -143,8 +139,8 @@ When client code sends a query to list all `Post`s, ZenStack's generated code in // injected by ZenStack, "user" object is fetched from context NOT: { AND: [ - { owner: { not: { id: user.id } } }, - { NOT: { published: true } }, + { author: { not: { id: user.id } } }, + { published: { not: true } }, ], }, }, @@ -171,13 +167,101 @@ The cool thing is that, the generated types are shared between client-side and s ### Client-side +#### For Next.js + +The generated CRUD services should be mounted at `/api/zenstack`. React hooks are generated for calling these services without explicitly writing Http requests. + +The following hooks methods are generated: + +- find: listing entities with filtering, ordering, pagination and nested relations + +```ts +const { find } = usePost(); +// lists unpublished posts with their author's data +const posts = find({ + where: { published: false }, + include: { author: true }, + orderBy: { updatedAt: 'desc' }, +}); +``` + +- get: fetching a single entity by Id, with nested relations + +```ts +const { get } = usePost(); +// fetches a post with its author's data +const post = get(id, { + include: { author: true }, +}); +``` + +- create: creating a new entity, with support for nested creation of related models + +```ts +const { create } = usePost(); +// creating a new post for current user with a nested comment +const post = await create({ + data: { + title: 'My New Post', + author: { + connect: { id: session.user.id }, + }, + comments: { + create: [{ content: 'First comment' }], + }, + }, +}); +``` + +- update: updating an entity, with support for nested creation/update of related models + +```ts +const { update } = usePost(); +// updating a post's content and create a new comment +const post = await update(id, { + data: { + const: 'My post content', + comments: { + create: [{ content: 'A new comment' }], + }, + }, +}); +``` + +- del: deleting an entity + +```js +const { del } = usePost(); +const post = await del(id); +``` + +Internally ZenStack generated code uses [SWR](https://swr.vercel.app/) to do data fetching, so that you can enjoy its caching, polling and automatic revalidation features. + ### Server-side +If you need to do server side coding, either through implementing an API endpoint or by using `getServerSideProps` for SSR, you can directly access the database client generated by Prisma: + +```ts +import service from '@zenstackhq/runtime'; + +export const getServerSideProps: GetServerSideProps = async () => { + const posts = await service.db.post.findMany({ + where: { published: true }, + include: { author: true }, + }); + return { + props: { posts }, + }; +}; +``` + +**Please note** that server-side database access is not protected by the access policies. This is by-design so as to provide a way of bypassing the policies. Please make sure you implement authorization properly. + ## What's next? -### [Learning the `ZModel` language](/docs/get-started/learning-the-zmodel-language) +### [Learning the ZModel language](/docs/get-started/learning-the-zmodel-language) -### [Learning the `zenstack` cli](/docs/get-started/learning-the-zenstack-cli) +### [Learning the zenstack cli](/docs/get-started/learning-the-zenstack-cli) ### [Evolving data model with migration](/docs/ref/evolving-data-model-with-migration) From 985ae9fdd4bb9b23da3501654a6967a434480326 Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Fri, 28 Oct 2022 12:48:33 +0800 Subject: [PATCH 16/24] update --- README.md | 14 +- .../learning-the-zmodel-language.md | 184 ++++++++++++++++++ docs/get-started/next-js.md | 10 +- 3 files changed, 198 insertions(+), 10 deletions(-) create mode 100644 docs/get-started/learning-the-zmodel-language.md diff --git a/README.md b/README.md index 200bd7464..faca1242f 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ Things that make you stressful include: ZenStack aims to simplify these tasks by providing: -- An intuitive data modeling language for defining data types, relationships and access policies +- An intuitive data modeling language for defining data types, relations and access policies ```prisma model User { @@ -95,7 +95,7 @@ Let's briefly go through each of them in this section. ### Data modeling -ZenStack uses a schema language called `ZModel` to define data types and their relationship. The `zenstack` CLI takes a schema file as input and generates database client client code automatically. Such client code allows you program against database in server-side code in a fully typed way, without writing any SQL. It also provides commands for synchronizing data model with database schema, as well generating "migration reords" when your data model evolves. +ZenStack uses a schema language called `ZModel` to define data types and their relations. The `zenstack` CLI takes a schema file as input and generates database client client code automatically. Such client code allows you program against database in server-side code in a fully typed way, without writing any SQL. It also provides commands for synchronizing data model with database schema, as well generating "migration reords" when your data model evolves. Internally, ZenStack completely relies on Prisma for ORM tasks. The ZModel language is a superset of Prisma's schema language. When `zenstack generate` is run, a Prisma schema named 'schema.prisma' is generated beside your ZModel schema file. You don't need to commit schema.prisma to source control. The recommended practice is to run `zenstack generate` during deployment, so Prisma schema is regenerated on the fly. @@ -124,7 +124,7 @@ The value returned by `auth()` is provided by your auth solution, via the `getSe The main value that ZenStack adds over a traditional ORM is the built-in data access policy engine. This allows most business logic to be safely implemented in front-end code. Since ZenStack delegates database access to Prisma, it enforces access policies through analyzing queries sent to Prisma and injecting guarding conditions. Suppose we have a policy saying "a post can only be accessed by its author if it's not published", expressed in ZModel as: -``` +```prisma @@deny('all', auth() != author && !published) ``` @@ -259,13 +259,13 @@ export const getServerSideProps: GetServerSideProps = async () => { ## What's next? -### [Learning the ZModel language](/docs/get-started/learning-the-zmodel-language) +### [Learning the ZModel language](/docs/get-started/learning-the-zmodel-language.md) -### [Learning the zenstack cli](/docs/get-started/learning-the-zenstack-cli) +### [Learning the zenstack cli](/docs/get-started/learning-the-zenstack-cli.md) -### [Evolving data model with migration](/docs/ref/evolving-data-model-with-migration) +### [Evolving data model with migration](/docs/ref/evolving-data-model-with-migration.md) -### [Database hosting considerations](/docs/ref/database-hosting-considerations) +### [Database hosting considerations](/docs/ref/database-hosting-considerations.md) ## Reach out to us for issues, feedback and ideas! diff --git a/docs/get-started/learning-the-zmodel-language.md b/docs/get-started/learning-the-zmodel-language.md new file mode 100644 index 000000000..c8b9c09a3 --- /dev/null +++ b/docs/get-started/learning-the-zmodel-language.md @@ -0,0 +1,184 @@ +# Learning the ZModel Language + +ZModel is a declarative language for defining data models, relations and access policies. +ZModel is a superset of [Prisma's Schema Language](https://www.prisma.io/docs/concepts/components/prisma-schema), so if you're already familar with Prisma, feel free to jump directly to the [Access Policies](#access-policies) section. Otherwise, don't worry, the syntax is intuitive and easy to learn. + +## Data source + +Every model needs to include exactly one `datasource` declaration, providing information on how to connect to the underlying databases. + +The recommended way is to load connection string from an environment variable, like: + +```prisma +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} +``` + +Do not commit the `DATABASE_URL` value in source code, instead configure it in your deployment as an environment variable. + +## Data models + +Data models define shapes of entities in your application domain. They include fields and attributes for attaching additional metadata. Data models are mapped to your database schema, also used for generating CRUD services and front-end library code. + +Here's an example of a `Post` model: + +```prisma +model Post { + // the mandatory primary key of this model with a default UUID value + id String @id @default(uuid()) + + // fields can be DateTime + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // or string + title String + + // or integer + viewCount Int @default(0) + + // and optional + content String? + + // and a list too + tags String[] +} +``` + +As you can see, fields are typed, and can be a list or optional. Default values can be attached with the `@default` attribute. You can find all built-in types [here](https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference#model-field-scalar-types). + +Model's fields can also be typed as other Models. We'll cover this in the [Relations](#relations) section. + +## Attributes + +Attributes attach additional metadata to models and fields. Some attributes take input parameters, and you can provide values by giving either literal expressions or calling attribute functions. + +Attributes attached to fields are prefixed with '@', and those to models are prefixed with '@@'. + +Here're some examples for commonly used attributes: + +```prisma +model Post { + // @id is field attribute, marking the field as a primary key + // @default is another field attribute for specifying a default value for the field if it's not given at creation time + // uuid() is a function for generating a UUID + id String @id @default(uuid()) + + // now() is a function that returns current time + createdAt DateTime @default(now()) + + // @updatedAt is a field attibute indicating its value should be updated to current time whenever the model entity is updated + updatedAt DateTime @updatedAt + + // @unique adds uniqueness constraint to field + slug String @unique + + // @map can be used to give a different column name in database + title String @map("my_title") + + // @@map can be used to give a different table name in database + @@map("posts") + + // uniqueness constraint can also be expressed via @@unique model attribute, and you can combine multiple fields here + @@unique([slug]) + + // use @@index to specify fields to create database index for + @@index([slug]) +} +``` + +For an exaustive list of attributes and functions, please refer to [Prisma's documentation](https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference#attributes). + +## Relations + +Relations are expressed by the special @relation attribute. Here're some examples. + +- One-to-one + +```prisma +model User { + id String @id + profile Profile? +} + +model Profile { + id String @id + user @relation(fields: [userId], references: [id]) + userId String @unique +} +``` + +- One-to-many + +```prisma +model User { + id String @id + posts Post[] +} + +model Post { + id String @id + author User? @relation(fields: [authorId], references: [id]) + authorId String? +} +``` + +- Many-to-many + +```prisma +enum UserRole { + USER + ADMIN +} + +model Space { + id String @id + members Membership[] +} + +model Membership { + id String @id() + space Space @relation(fields: [spaceId], references: [id]) + spaceId String + user User @relation(fields: [userId], references: [id]) + userId String + role UserRole + @@unique([userId, spaceId]) +} + +model User { + id String @id + membership Membership[] +} + +``` + +## Access policies + +Access policies use `@@allow` and `@@deny` rules to specify eligibility of an operation over a model entity. The signature of the attbutes are: + +``` +@@allow(operation, condition) +@@deny(operation, condition) +``` + +, where `operation` can be of: "all", "read", "create", "update" and "delete" (or comma-separated string of multiple values, like "create,update"), and `condition` must be a boolean expression. + +The logic of permitting/rejecting an operation is: + +- By default, all operations are rejected if there isn't any @@allow rule in a model +- The operation is rejected if any of the conditions in @@deny rules evaluates to `true` +- Otherwise, the operation is permitted if any of the conditions in @@allow rules evaluates to `true` +- Otherwise, the operation is rejected + +Here're some examples: + +```prisma + +``` + +## Summary + +This document serves as a quick overview for starting with the ZModel language. For more thorough explainations about data modeling, please checkout [Prisma's schema references](https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference). diff --git a/docs/get-started/next-js.md b/docs/get-started/next-js.md index 5c2b42a74..e63531874 100644 --- a/docs/get-started/next-js.md +++ b/docs/get-started/next-js.md @@ -90,9 +90,13 @@ import service from '@zenstackhq/runtime'; const options: RequestHandlerOptions = { async getServerUser(req: NextApiRequest, res: NextApiResponse) { - // TODO: return User object for current session, the concrete logic - // depends on how you authenticate users and maintain sessions. - // Database can be accessed with "service.db" + // If you're using next-auth, current user can be fetched like: + // import { authOptions } from '@api/auth/[...nextauth]'; + // const session = await unstable_getServerSession(req, res, authOptions); + // return session?.user; + // + // Otherwise implement logic to return the user object for the current + // session. Database can be accessed with: "service.db". }, }; export default requestHandler(service, options); From 5af5e240aa81c0e09b700974340e5977d6c67c5a Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Fri, 28 Oct 2022 14:19:40 +0800 Subject: [PATCH 17/24] update --- README.md | 2 +- .../learning-the-zmodel-language.md | 131 +++++++++++++++++- docs/get-started/using-the-zenstack-cli.md | 1 + docs/ref/database-hosting-considerations.md | 1 + .../ref/evolving-data-model-with-migration.md | 1 + 5 files changed, 128 insertions(+), 8 deletions(-) create mode 100644 docs/get-started/using-the-zenstack-cli.md create mode 100644 docs/ref/database-hosting-considerations.md create mode 100644 docs/ref/evolving-data-model-with-migration.md diff --git a/README.md b/README.md index faca1242f..2ab80b855 100644 --- a/README.md +++ b/README.md @@ -261,7 +261,7 @@ export const getServerSideProps: GetServerSideProps = async () => { ### [Learning the ZModel language](/docs/get-started/learning-the-zmodel-language.md) -### [Learning the zenstack cli](/docs/get-started/learning-the-zenstack-cli.md) +### [Using the zenstack cli](/docs/get-started/using-the-zenstack-cli.md) ### [Evolving data model with migration](/docs/ref/evolving-data-model-with-migration.md) diff --git a/docs/get-started/learning-the-zmodel-language.md b/docs/get-started/learning-the-zmodel-language.md index c8b9c09a3..e6542bc94 100644 --- a/docs/get-started/learning-the-zmodel-language.md +++ b/docs/get-started/learning-the-zmodel-language.md @@ -128,23 +128,24 @@ model Post { - Many-to-many ```prisma -enum UserRole { - USER - ADMIN -} - model Space { id String @id members Membership[] } +// Membership is the "join-model" between User and Space model Membership { id String @id() + + // one-to-many from Space space Space @relation(fields: [spaceId], references: [id]) spaceId String + + // one-to-many from User user User @relation(fields: [userId], references: [id]) userId String - role UserRole + + // a user can be member of a space for only once @@unique([userId, spaceId]) } @@ -173,12 +174,128 @@ The logic of permitting/rejecting an operation is: - Otherwise, the operation is permitted if any of the conditions in @@allow rules evaluates to `true` - Otherwise, the operation is rejected -Here're some examples: +### A simple example with Post model ```prisma +model Post { + // reject all operations if user's not logged in + @@deny('all', auth() == null) + // allow all operations if the entity's owner matches current user + @@allow('all', auth() == owner) + + // posts are readable to anyone + @allow('read', true) +} ``` +### A more complex example with multi-user spaces + +```prisma +model Space { + id String @id + members Membership[] + owner User @relation(fields: [ownerId], references: [id]) + ownerId String + + // require login + @@deny('all', auth() == null) + + // everyone can create a space + @@allow('create', true) + + // owner can do everything + @@allow('all', auth() == owner) + + // any user in the space can read the space + // + // Here the ?[condition] syntax is called + // "Collection Predicate", used to check if any element + // in the "collection" matches the "condition" + @@allow('read', members?[user == auth()]) +} + +// Membership is the "join-model" between User and Space +model Membership { + id String @id() + + // one-to-many from Space + space Space @relation(fields: [spaceId], references: [id]) + spaceId String + + // one-to-many from User + user User @relation(fields: [userId], references: [id]) + userId String + + // a user can be member of a space for only once + @@unique([userId, spaceId]) + + // require login + @@deny('all', auth() == null) + + // space owner can create/update/delete + @@allow('create,update,delete', space.owner == auth()) + + // user can read entries for spaces which he's a member of + @@allow('read', space.members?[user == auth()]) +} + +model User { + id String @id + email String @unique + membership Membership[] + ownedSpaces Space[] + + // allow signup + @@allow('create', true) + + // user can do everything to herself, note that "this" represents + // current entity + @@allow('all', auth() == this) + + // can be read by users sharing a space + @@allow('read', membership?[space.members?[user == auth()]]) +} + +``` + +### Accessing relation fields in policy + +As you've seen in the examples above, in policy expressions you can access fields from relations. For example, to express "a user can be read by any user sharing a space" in the `User` model, you can directly read into its `membership` field. + +```prisma + @@allow('read', membership?[space.members?[user == auth()]]) +``` + +In most cases when you use a "to-many" relation in policy rule, you'll use "Collection Predicate" to expression a condition. See [next section](#collection-predicate-expressions) for details. + +### Collection predicate expressions + +Collection predicate are boolean expressions used to express condition over a list. It's mainly designed for building policy rules for "to-many" relations. It has three forms of syntaxes: + +1. ?[condition] + Any element in `collection` matches `condition` +2. ![condition] + All elements in `collection` match `condition` +3. ^[condition] + None element in `collection` matches `condition` + +The `condition` expression has direct access to fields defined in the model of `collection`. E.g.: + +```prisma + @@allow('read', members?[user == auth()]) +``` + +, in condition `user == auth()`, `user` refers to the `user` field in model `Membership`, because the collection `members` is resolved to `Membership` model. + +Also, collection predicates can be nested to express complex condition involving multi-level relation lookup. E.g.: + +```prisma + @@allow('read', membership?[space.members?[user == auth()]]) +``` + +In this example, `user` refers to `user` field of `Membership` model because `space.members` is resolved to `Membership` model. + ## Summary This document serves as a quick overview for starting with the ZModel language. For more thorough explainations about data modeling, please checkout [Prisma's schema references](https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference). diff --git a/docs/get-started/using-the-zenstack-cli.md b/docs/get-started/using-the-zenstack-cli.md new file mode 100644 index 000000000..706449103 --- /dev/null +++ b/docs/get-started/using-the-zenstack-cli.md @@ -0,0 +1 @@ +# TBD diff --git a/docs/ref/database-hosting-considerations.md b/docs/ref/database-hosting-considerations.md new file mode 100644 index 000000000..706449103 --- /dev/null +++ b/docs/ref/database-hosting-considerations.md @@ -0,0 +1 @@ +# TBD diff --git a/docs/ref/evolving-data-model-with-migration.md b/docs/ref/evolving-data-model-with-migration.md new file mode 100644 index 000000000..706449103 --- /dev/null +++ b/docs/ref/evolving-data-model-with-migration.md @@ -0,0 +1 @@ +# TBD From 21d3d8722649d70c9f248a1296e7500b503ed209 Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Fri, 28 Oct 2022 14:24:20 +0800 Subject: [PATCH 18/24] update --- .../learning-the-zmodel-language.md | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/docs/get-started/learning-the-zmodel-language.md b/docs/get-started/learning-the-zmodel-language.md index e6542bc94..225decd2d 100644 --- a/docs/get-started/learning-the-zmodel-language.md +++ b/docs/get-started/learning-the-zmodel-language.md @@ -160,7 +160,7 @@ model User { Access policies use `@@allow` and `@@deny` rules to specify eligibility of an operation over a model entity. The signature of the attbutes are: -``` +```prisma @@allow(operation, condition) @@deny(operation, condition) ``` @@ -273,12 +273,21 @@ In most cases when you use a "to-many" relation in policy rule, you'll use "Coll Collection predicate are boolean expressions used to express condition over a list. It's mainly designed for building policy rules for "to-many" relations. It has three forms of syntaxes: -1. ?[condition] - Any element in `collection` matches `condition` -2. ![condition] - All elements in `collection` match `condition` -3. ^[condition] - None element in `collection` matches `condition` +1. Any + ``` + ?[condition] + ``` + Any element in `collection` matches `condition` +2. All + ``` + ![condition] + ``` + All elements in `collection` match `condition` +3. None + ``` + ^[condition] + ``` + None element in `collection` matches `condition` The `condition` expression has direct access to fields defined in the model of `collection`. E.g.: @@ -296,6 +305,10 @@ Also, collection predicates can be nested to express complex condition involving In this example, `user` refers to `user` field of `Membership` model because `space.members` is resolved to `Membership` model. +### A complete example + +Please checkout the [Collaborative Todo](../../samples/todo) for a complete example on using access policy. + ## Summary This document serves as a quick overview for starting with the ZModel language. For more thorough explainations about data modeling, please checkout [Prisma's schema references](https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference). From 6a04627ad3e13b95f70379a6715efeaf9e30e2b6 Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Fri, 28 Oct 2022 14:25:56 +0800 Subject: [PATCH 19/24] update --- docs/get-started/learning-the-zmodel-language.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/get-started/learning-the-zmodel-language.md b/docs/get-started/learning-the-zmodel-language.md index 225decd2d..9df8ed228 100644 --- a/docs/get-started/learning-the-zmodel-language.md +++ b/docs/get-started/learning-the-zmodel-language.md @@ -274,19 +274,27 @@ In most cases when you use a "to-many" relation in policy rule, you'll use "Coll Collection predicate are boolean expressions used to express condition over a list. It's mainly designed for building policy rules for "to-many" relations. It has three forms of syntaxes: 1. Any + ``` ?[condition] ``` + Any element in `collection` matches `condition` + 2. All + ``` ![condition] ``` + All elements in `collection` match `condition` + 3. None + ``` ^[condition] ``` + None element in `collection` matches `condition` The `condition` expression has direct access to fields defined in the model of `collection`. E.g.: From b695942a5a2da6bc3954dd2fb9786830007f7dd5 Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Fri, 28 Oct 2022 15:10:29 +0800 Subject: [PATCH 20/24] update --- README.md | 2 -- docs/get-started/using-the-zenstack-cli.md | 1 - docs/ref/database-hosting-considerations.md | 12 +++++++++++- 3 files changed, 11 insertions(+), 4 deletions(-) delete mode 100644 docs/get-started/using-the-zenstack-cli.md diff --git a/README.md b/README.md index 2ab80b855..4fb14ffde 100644 --- a/README.md +++ b/README.md @@ -261,8 +261,6 @@ export const getServerSideProps: GetServerSideProps = async () => { ### [Learning the ZModel language](/docs/get-started/learning-the-zmodel-language.md) -### [Using the zenstack cli](/docs/get-started/using-the-zenstack-cli.md) - ### [Evolving data model with migration](/docs/ref/evolving-data-model-with-migration.md) ### [Database hosting considerations](/docs/ref/database-hosting-considerations.md) diff --git a/docs/get-started/using-the-zenstack-cli.md b/docs/get-started/using-the-zenstack-cli.md deleted file mode 100644 index 706449103..000000000 --- a/docs/get-started/using-the-zenstack-cli.md +++ /dev/null @@ -1 +0,0 @@ -# TBD diff --git a/docs/ref/database-hosting-considerations.md b/docs/ref/database-hosting-considerations.md index 706449103..438f8ed69 100644 --- a/docs/ref/database-hosting-considerations.md +++ b/docs/ref/database-hosting-considerations.md @@ -1 +1,11 @@ -# TBD +# Database Hosting Considerations + +ZenStack is agnostic about where and how you deploy your web app, but hosting on serverless platforms like [Vercel](https://vercel.com/) is definitely a popular choice. + +Serverless architecture has some implications on how you should care about your database hosting. Different from traditional architecture where you have a fixed number of long-running Node.js servers, in a serverless environment, a new Node.js context can potentially be created for each user request, and if traffic volume is high, this can quickly exhaust your database's connection limit, if you connect to the database directly without a proxy. + +You'll likely be OK if your app has a low number of concurrent users, otherwise you should consider using a proxy in front of your database server. Here's a number of (incomplete) solutions you can consider: + +- [Prisma Data Proxy](https://www.prisma.io/data-platform/proxy) +- [Supabase](https://supabase.com/)'s [connection pool](https://supabase.com/docs/guides/database/connecting-to-postgres#connection-pool) +- [Deploy pgbouncer with Postgres on Heroku](https://devcenter.heroku.com/articles/postgres-connection-pooling) From 47461eeaba1c563e83e2c19c0ea6f0916ff62796 Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Fri, 28 Oct 2022 16:20:31 +0800 Subject: [PATCH 21/24] update --- .../ref/evolving-data-model-with-migration.md | 64 ++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/docs/ref/evolving-data-model-with-migration.md b/docs/ref/evolving-data-model-with-migration.md index 706449103..ee4a7c9ae 100644 --- a/docs/ref/evolving-data-model-with-migration.md +++ b/docs/ref/evolving-data-model-with-migration.md @@ -1 +1,63 @@ -# TBD +# Evolving Data Model with Migration + +When using ZenStack, your schema.zmodel file represents the current status of your app's data model and your database's schema. When you make changes to schema.zmodel, however, your data model drifts away from database schema. At your app's deployment time, such drift needs to be "fixed", and so that your database schema stays synchronized with your data model. This processing of "fixing" is called migration. + +Here we summarize a few common scenarios and show how you should work on migration. + +## For a newly created schema.zmodel + +When you're just starting out a ZenStack project, you have an empty migration history. After creating schema.zmodel, adding a development datasource and adding some models, run the following command to bootstrap your migration history and synchronize your development database schema: + +```bash +npx zenstack migrate dev -n init +``` + +After it's run, you should find a folder named `migrations` created under `zenstack` folder, inside of which you can find a .sql file containing script that initializes your database. Please note that when you run "migration dev", the generated migration script is automatically run agains your datasource specified in schema.zmodel. + +Make sure you commit the `migrations` folder into source control. + +## After updating an existing schema.zmodel + +After making update to schema.zmodel, run the "migrate dev" command to generate an incremental migration record: + +```bash +npx zenstack migrate dev -n [short-name-for-the-change] +``` + +If any database schema change is needed based on the previous version of data model, a new .sql file will be generated under `zenstack/migrations` folder. Your development database's schema is automatically synchronized after running the command. + +Make sure you review that the generated .sql script reflects your intention before committing it to source control. + +## Pushing model changes to database without creating migration + +This is helpful when you're prototyping locally and don't want to create migration records. Simply run: + +```bash +npx zenstack db push +``` + +, and your database schema will be synced with schema.zmodel. After prototyping, reset your local database and generate migration records: + +```bash +npx zenstack migrate reset + +npx zenstack migrate dev -n [name] +``` + +### During deployment + +When deploying your app to an official environment (a shared dev environment, staging, or production), **DO NOT** run `migrate dev` command in CI scripts. Instead, run `migrate deploy`. + +```bash +npx zenstack migrate deploy +``` + +The `migrate deploy` command does not generate new migration records. It simply detects records that are created after the previous deployment and execute them in order. As a result, your database schema is synchronized with data model. + +If you've always been taking the "migrate dev" and "migrate deploy" loop during development, your migration should run smoothly. However manually changing db schema, manually changing/deleting migration records can result in failure during migration. Please refer to this documentation for [troubleshooting migration issues in production](https://www.prisma.io/docs/guides/database/production-troubleshooting). + +## Summary + +ZenStack is built over Prisma and it internally delegates all ORM tasks to Prisma. The migration workflow is exactly the same as Prisma's workflow, with the only exception that the source of input is schema.zmodel, and a Prisma schema is generated on the fly. The set of migration commands that ZModel CLI offers, like "migrate dev" and "migrate deploy", are simple wrappers around Prisma commands. + +Prisma has [excellent documentation](https://www.prisma.io/docs/concepts/components/prisma-migrate) about migration. Make sure you look into those for a more thorough understanding. From 2b280e1a504ad7c97fb5596d69e019ceaecc10e3 Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Fri, 28 Oct 2022 16:24:42 +0800 Subject: [PATCH 22/24] update --- docs/ref/evolving-data-model-with-migration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ref/evolving-data-model-with-migration.md b/docs/ref/evolving-data-model-with-migration.md index ee4a7c9ae..140648c5e 100644 --- a/docs/ref/evolving-data-model-with-migration.md +++ b/docs/ref/evolving-data-model-with-migration.md @@ -58,6 +58,6 @@ If you've always been taking the "migrate dev" and "migrate deploy" loop during ## Summary -ZenStack is built over Prisma and it internally delegates all ORM tasks to Prisma. The migration workflow is exactly the same as Prisma's workflow, with the only exception that the source of input is schema.zmodel, and a Prisma schema is generated on the fly. The set of migration commands that ZModel CLI offers, like "migrate dev" and "migrate deploy", are simple wrappers around Prisma commands. +ZenStack is built over [Prisma](https://www.prisma.io) and it internally delegates all ORM tasks to Prisma. The migration workflow is exactly the same as Prisma's workflow, with the only exception that the source of input is schema.zmodel, and a Prisma schema is generated on the fly. The set of migration commands that ZModel CLI offers, like "migrate dev" and "migrate deploy", are simple wrappers around Prisma commands. Prisma has [excellent documentation](https://www.prisma.io/docs/concepts/components/prisma-migrate) about migration. Make sure you look into those for a more thorough understanding. From ddcc915692a51d324851b57cf016245393fffc4c Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Fri, 28 Oct 2022 18:39:20 +0800 Subject: [PATCH 23/24] copy writing cleanup --- README.md | 54 +++++++++++++++++++++++++++--------------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 4fb14ffde..f4ae3a8fa 100644 --- a/README.md +++ b/README.md @@ -10,21 +10,21 @@ ## What is ZenStack? -ZenStack is a toolkit for simplifying full-stack development with Node.js web frameworks like [Next.js](https://nextjs.org/), [Nuxt.js](https://nuxtjs.org/) and [SvelteKit](https://kit.svelte.dev/) , using Typescript language. +ZenStack is a toolkit for simplifying full-stack development with Node.js web frameworks like [Next.js](https://nextjs.org/), [Nuxt.js](https://nuxtjs.org/) , and [SvelteKit](https://kit.svelte.dev/) , using Typescript language. -Thanks to the increasing power of frameworks, it's becoming more and more practical to build a complex web app all within one unified framework. However, you'll still need to spend significantly amout of engergy to design and build up the server-side part of your app. +Thanks to the increasing power of frameworks, building a complex web app within one unified framework is becoming more practical than ever. However, you'll still need to spend a significant amount of energy designing and building up your app's server-side part. -Things that make you stressful include: +Things that make you stressed include: - What kind of API to use? RESTful or GraphQL? - How to model your data and map the model to both source code and database (ORM)? -- How to implement common CRUD operations? Manually construct it or use a generator? +- How to implement CRUD operations? Manually construct it or use a generator? - How to evolve your data model? - How to authenticate users and authorize their requests? ZenStack aims to simplify these tasks by providing: -- An intuitive data modeling language for defining data types, relations and access policies +- An intuitive data modeling language for defining data types, relations, and access policies ```prisma model User { @@ -70,9 +70,9 @@ return ( ); ``` -Since CRUD APIs are automatically generated with access policies injected, you can implement most of your business logic in your front-end code safely. Read operations never return data that's not supposed to be visible to the current user, and writes will be rejected if unauthorized. The generated front-end library also supports nested writes, which allows you to make a batch of creates/updates atomically, eliminating the needs for explicitly using a transaction. +Since CRUD APIs are automatically generated with access policies injected, you can safely implement most of your business logic in your front-end code. Read operations never return data that's not supposed to be visible to the current user, and writes will be rejected if unauthorized. The generated front-end library also supports nested writes, allowing you to make a batch of creates/updates atomically, eliminating the need for explicitly using a transaction. -ZenStack is both heavily inspired and built above [Prisma ORM](https://www.prisma.io/), which is in our oppinion the best ORM toolkit in the market. Familarity with Prisma should make it very easy to pick up ZenStack, but it's not prerequisite since the modeling language is intuitive and the development workflow is straightforward. +ZenStack is heavily inspired and built over [Prisma](https://www.prisma.io) ORM, which is, in our opinion, the best ORM toolkit in the market. Familiarity with Prisma should make it easy to pick up ZenStack, but it's not a prerequisite since the modeling language is intuitive and the development workflow is straightforward. ## Getting started @@ -86,7 +86,7 @@ ZenStack is both heavily inspired and built above [Prisma ORM](https://www.prism ZenStack has four essential responsibilities: -1. Modeling data and maps the model to db schema and program types +1. Modeling data and mapping the model to DB schema and program types 1. Integrating with authentication 1. Generating CRUD APIs and enforcing data access policies 1. Providing type-safe client CRUD library @@ -95,17 +95,17 @@ Let's briefly go through each of them in this section. ### Data modeling -ZenStack uses a schema language called `ZModel` to define data types and their relations. The `zenstack` CLI takes a schema file as input and generates database client client code automatically. Such client code allows you program against database in server-side code in a fully typed way, without writing any SQL. It also provides commands for synchronizing data model with database schema, as well generating "migration reords" when your data model evolves. +ZenStack uses a schema language called `ZModel` to define data types and their relations. The `zenstack` CLI takes a schema file as input and generates database client code. Such client code allows you to program against database in server-side code in a fully typed way without writing any SQL. It also provides commands for synchronizing data models with DB schema, and generating "migration records" when your data model evolves. -Internally, ZenStack completely relies on Prisma for ORM tasks. The ZModel language is a superset of Prisma's schema language. When `zenstack generate` is run, a Prisma schema named 'schema.prisma' is generated beside your ZModel schema file. You don't need to commit schema.prisma to source control. The recommended practice is to run `zenstack generate` during deployment, so Prisma schema is regenerated on the fly. +Internally, ZenStack entirely relies on Prisma for ORM tasks. The ZModel language is a superset of Prisma's schema language. When `zenstack generate` is run, a Prisma schema named 'schema.prisma' is generated beside your ZModel file. You don't need to commit schema.prisma to source control. The recommended practice is to run `zenstack generate` during deployment, so Prisma schema is regenerated on the fly. ### Authentication ZenStack is not an authentication library, but it gets involved in two ways. -Firstly, if you use any authentication method that involves persisting user's identity, you'll model user's shape in ZModel. Some auth library, like [NextAuth](https://next-auth.js.org/), requires user entity to include certain fields, and your model should fulfill such requirements. Credential-based authentication requires validating user-provided credentials, and you should implement this using the database client generated by ZenStack. +Firstly, if you use any authentication method that involves persisting users' identity, you'll model the user's shape in ZModel. Some auth libraries, like [NextAuth](https://next-auth.js.org/), require user entity to include specific fields, and your model should fulfill such requirements. In addition, credential-based authentication requires validating user-provided credentials, and you should implement this using the database client generated by ZenStack. -To simplify the task, ZenStack automatically generates an adapter for NextAuth when it detects that `next-auth` npm package is installed. Please refer to [the starter code](https://github.com/zenstackhq/nextjs-auth-starter/blob/main/pages/api/auth/%5B...nextauth%5D.ts) for how to use it. We'll keep adding integrations/samples for other auth libraries in the future. +To simplify the task, ZenStack automatically generates an adapter for NextAuth when it detects that the `next-auth` npm package is installed. Please refer to [the starter code](https://github.com/zenstackhq/nextjs-auth-starter/blob/main/pages/api/auth/%5B...nextauth%5D.ts) for how to use it. We'll keep adding integrations/samples for other auth libraries in the future. Secondly, authentication is almost always connected to authorization. ZModel allows you to reference the current login user via `auth()` function in access policy expressions. Like, @@ -118,17 +118,17 @@ model Post { @@allow('all', auth() == author) ``` -The value returned by `auth()` is provided by your auth solution, via the `getServerUser` hook function you provide when mounting ZenStack APIs. Check [this code](https://github.com/zenstackhq/nextjs-auth-starter/blob/main/pages/api/zenstack/%5B...path%5D.ts) for an example. +The value returned by `auth()` is provided by your auth solution via the `getServerUser` hook function you provide when mounting ZenStack APIs. Check [this code](https://github.com/zenstackhq/nextjs-auth-starter/blob/main/pages/api/zenstack/%5B...path%5D.ts) for an example. ### Data access policy -The main value that ZenStack adds over a traditional ORM is the built-in data access policy engine. This allows most business logic to be safely implemented in front-end code. Since ZenStack delegates database access to Prisma, it enforces access policies through analyzing queries sent to Prisma and injecting guarding conditions. Suppose we have a policy saying "a post can only be accessed by its author if it's not published", expressed in ZModel as: +The primary value that ZenStack adds over a traditional ORM is the built-in data access policy engine. This allows most business logic to be safely implemented in front-end code. Since ZenStack delegates database access to Prisma, it enforces access policies by analyzing queries sent to Prisma and injecting guarding conditions. For example, suppose we have a policy saying "a post can only be accessed by its author if it's not published", expressed in ZModel as: ```prisma @@deny('all', auth() != author && !published) ``` -When client code sends a query to list all `Post`s, ZenStack's generated code intercepts it and injects int the `where` clause before passing it through to Prisma (conceptually): +When client code sends a query to list all `Post`s, ZenStack's generated code intercepts it and injects the `where` clause before passing it through to Prisma (conceptually): ```js { @@ -149,9 +149,9 @@ When client code sends a query to list all `Post`s, ZenStack's generated code in } ``` -Similar procedures are applied to write operations, as well as more complex queries that involve nested reads and writes. To ensure good performance, ZenStack generates conditions statically so it doesn't need to introspect ZModel at runtime. The engine also makes best effort to push down policy constaints to the database to avoid fetching data unnecessarily. +Similar procedures are applied to write operations and more complex queries involving nested reads and writes. To ensure good performance, ZenStack generates conditions statically, so it doesn't need to introspect ZModel at runtime. The engine also makes the best effort to push down policy constraints to the database to avoid fetching data unnecessarily and discarding afterward. -Please **beware** that policy checking is only applied when data access is done using the generated React hooks or equivalently the RESTful API. If you use `service.db` to access database directly from server-side code, policies are bypassed and you have to do all necessary checking by yourself. We've planned to add helper functions for "injecting" the policy checking on the server side in the future. +Please **beware** that policy checking is only applied when data access is done using the generated client-side hooks or, equivalently, the RESTful API. If you use `service.db` to access the database directly from server-side code, policies are bypassed, and you have to do all necessary checking by yourself. We've planned to add helper functions for "injecting" the policy checking on the server side in the future. ### Type-safe client library @@ -159,9 +159,9 @@ Thanks to Prisma's power, ZenStack generates accurate Typescript types for your - The model itself - Argument types for listing models, including filtering, sorting, pagination, and nested reads for related models -- Argument for creating and updating models, including nested writes for related models +- Argument types for creating and updating models, including nested writes for related models -The cool thing is that, the generated types are shared between client-side and server-side code, so no matter which side of code you're writing, you can always enjoy the pleasant IDE intellisense and typescript compiler's error checking. +The cool thing is that the generated types are shared between client-side and server-side code, so no matter which side of code you're writing, you can always enjoy the pleasant IDE intellisense and typescript compiler's error checking. ## Programming with the generated code @@ -169,11 +169,11 @@ The cool thing is that, the generated types are shared between client-side and s #### For Next.js -The generated CRUD services should be mounted at `/api/zenstack`. React hooks are generated for calling these services without explicitly writing Http requests. +The generated CRUD services should be mounted at `/api/zenstack` route. React hooks are generated for calling these services without explicitly writing Http requests. The following hooks methods are generated: -- find: listing entities with filtering, ordering, pagination and nested relations +- find: listing entities with filtering, ordering, pagination, and nested relations ```ts const { find } = usePost(); @@ -185,7 +185,7 @@ const posts = find({ }); ``` -- get: fetching a single entity by Id, with nested relations +- get: fetching a single entity by id, with nested relations ```ts const { get } = usePost(); @@ -195,7 +195,7 @@ const post = get(id, { }); ``` -- create: creating a new entity, with support for nested creation of related models +- create: creating a new entity, with the support for nested creation of related models ```ts const { create } = usePost(); @@ -213,7 +213,7 @@ const post = await create({ }); ``` -- update: updating an entity, with support for nested creation/update of related models +- update: updating an entity, with the support for nested creation/update of related models ```ts const { update } = usePost(); @@ -235,11 +235,11 @@ const { del } = usePost(); const post = await del(id); ``` -Internally ZenStack generated code uses [SWR](https://swr.vercel.app/) to do data fetching, so that you can enjoy its caching, polling and automatic revalidation features. +Internally ZenStack generated code uses [SWR](https://swr.vercel.app/) to do data fetching so that you can enjoy its caching, polling, and automatic revalidation features. ### Server-side -If you need to do server side coding, either through implementing an API endpoint or by using `getServerSideProps` for SSR, you can directly access the database client generated by Prisma: +If you need to do server-side coding, either through implementing an API endpoint or by using `getServerSideProps` for SSR, you can directly access the database client generated by Prisma: ```ts import service from '@zenstackhq/runtime'; @@ -255,7 +255,7 @@ export const getServerSideProps: GetServerSideProps = async () => { }; ``` -**Please note** that server-side database access is not protected by the access policies. This is by-design so as to provide a way of bypassing the policies. Please make sure you implement authorization properly. +**Please note** that server-side database access is not protected by access policies. This is by-design so as to provide a way of bypassing the policies. Please make sure you implement authorization properly. ## What's next? From 10c5651bb612c9940e712d24ac1acf9b8070e359 Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Fri, 28 Oct 2022 20:03:08 +0800 Subject: [PATCH 24/24] copy writing cleanup --- .../learning-the-zmodel-language.md | 79 ++++++++++++------- docs/get-started/next-js.md | 8 +- 2 files changed, 56 insertions(+), 31 deletions(-) diff --git a/docs/get-started/learning-the-zmodel-language.md b/docs/get-started/learning-the-zmodel-language.md index 9df8ed228..25b92c80f 100644 --- a/docs/get-started/learning-the-zmodel-language.md +++ b/docs/get-started/learning-the-zmodel-language.md @@ -1,13 +1,13 @@ # Learning the ZModel Language ZModel is a declarative language for defining data models, relations and access policies. -ZModel is a superset of [Prisma's Schema Language](https://www.prisma.io/docs/concepts/components/prisma-schema), so if you're already familar with Prisma, feel free to jump directly to the [Access Policies](#access-policies) section. Otherwise, don't worry, the syntax is intuitive and easy to learn. +ZModel is a superset of [Prisma's Schema Language](https://www.prisma.io/docs/concepts/components/prisma-schema), so if you're already familiar with Prisma, feel free to jump directly to the [Access Policies](#access-policies) section. Otherwise, don't worry. The syntax is intuitive and easy to learn. ## Data source Every model needs to include exactly one `datasource` declaration, providing information on how to connect to the underlying databases. -The recommended way is to load connection string from an environment variable, like: +The recommended way is to load the connection string from an environment variable, like: ```prisma datasource db { @@ -16,13 +16,13 @@ datasource db { } ``` -Do not commit the `DATABASE_URL` value in source code, instead configure it in your deployment as an environment variable. +Do not commit the `DATABASE_URL` value in source code; instead configure it in your deployment as an environment variable. ## Data models -Data models define shapes of entities in your application domain. They include fields and attributes for attaching additional metadata. Data models are mapped to your database schema, also used for generating CRUD services and front-end library code. +Data models define the shapes of entities in your application domain. They include fields and attributes for attaching additional metadata. Data models are mapped to your database schema and used to generate CRUD services and front-end library code. -Here's an example of a `Post` model: +Here's an example of a blog post model: ```prisma model Post { @@ -47,21 +47,21 @@ model Post { } ``` -As you can see, fields are typed, and can be a list or optional. Default values can be attached with the `@default` attribute. You can find all built-in types [here](https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference#model-field-scalar-types). +As you can see, fields are typed and can be a list or optional. Default values can be attached with the `@default` attribute. You can find all built-in types [here](https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference#model-field-scalar-types). -Model's fields can also be typed as other Models. We'll cover this in the [Relations](#relations) section. +Model fields can also be typed as other Models. We'll cover this in the [Relations](#relations) section. ## Attributes -Attributes attach additional metadata to models and fields. Some attributes take input parameters, and you can provide values by giving either literal expressions or calling attribute functions. +Attributes attach additional metadata to models and fields. Some attributes take input parameters, and you can provide values by using literal expressions or calling attribute functions. Attributes attached to fields are prefixed with '@', and those to models are prefixed with '@@'. -Here're some examples for commonly used attributes: +Here're some examples of commonly used attributes: ```prisma model Post { - // @id is field attribute, marking the field as a primary key + // @id is a field attribute, marking the field as a primary key // @default is another field attribute for specifying a default value for the field if it's not given at creation time // uuid() is a function for generating a UUID id String @id @default(uuid()) @@ -69,7 +69,7 @@ model Post { // now() is a function that returns current time createdAt DateTime @default(now()) - // @updatedAt is a field attibute indicating its value should be updated to current time whenever the model entity is updated + // @updatedAt is a field attribute indicating its value should be updated to the current time whenever the model entity is updated updatedAt DateTime @updatedAt // @unique adds uniqueness constraint to field @@ -89,11 +89,11 @@ model Post { } ``` -For an exaustive list of attributes and functions, please refer to [Prisma's documentation](https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference#attributes). +Please refer to [Prisma's documentation](https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference#attributes) for an exhaustive list of attributes and functions. ## Relations -Relations are expressed by the special @relation attribute. Here're some examples. +The special `@relation` attribute expresses relations between data models. Here're some examples. - One-to-one @@ -158,22 +158,47 @@ model User { ## Access policies -Access policies use `@@allow` and `@@deny` rules to specify eligibility of an operation over a model entity. The signature of the attbutes are: +Access policies use `@@allow` and `@@deny` rules to specify the eligibility of an operation over a model entity. The signatures of the attributes are: ```prisma @@allow(operation, condition) @@deny(operation, condition) ``` -, where `operation` can be of: "all", "read", "create", "update" and "delete" (or comma-separated string of multiple values, like "create,update"), and `condition` must be a boolean expression. +, where `operation` can be of: "all", "read", "create", "update" and "delete" (or a comma-separated string of multiple values, like "create,update"), and `condition` must be a boolean expression. -The logic of permitting/rejecting an operation is: +The logic of permitting/rejecting an operation is as follows: - By default, all operations are rejected if there isn't any @@allow rule in a model -- The operation is rejected if any of the conditions in @@deny rules evaluates to `true` -- Otherwise, the operation is permitted if any of the conditions in @@allow rules evaluates to `true` +- The operation is rejected if any of the conditions in @@deny rules evaluate to `true` +- Otherwise, the operation is permitted if any of the conditions in @@allow rules evaluate to `true` - Otherwise, the operation is rejected +### Using authentication in policy rules + +It's very common to use the current login user to verdict if an operation should be permitted. Therefore, ZenStack provides a built-in `auth()` attribute function that evaluates to the `User` entity corresponding to the current user. To use the function, your ZModel file must define a `User` data model. + +You can use `auth()` to: + +- Check if a user is logged in + +```prisma +deny('all', auth() == null) +``` + +- Access user's fields + +```prisma +allow('update', auth().role == 'ADMIN') +``` + +- Compare user identity + +```prisma +// owner is a relation field to User model +allow('update', auth() == owner) +``` + ### A simple example with Post model ```prisma @@ -181,7 +206,7 @@ model Post { // reject all operations if user's not logged in @@deny('all', auth() == null) - // allow all operations if the entity's owner matches current user + // allow all operations if the entity's owner matches the current user @@allow('all', auth() == owner) // posts are readable to anyone @@ -249,8 +274,8 @@ model User { // allow signup @@allow('create', true) - // user can do everything to herself, note that "this" represents - // current entity + // user can do everything to herself; note that "this" represents + // the current entity @@allow('all', auth() == this) // can be read by users sharing a space @@ -261,17 +286,17 @@ model User { ### Accessing relation fields in policy -As you've seen in the examples above, in policy expressions you can access fields from relations. For example, to express "a user can be read by any user sharing a space" in the `User` model, you can directly read into its `membership` field. +As you've seen in the examples above, you can access fields from relations in policy expressions. For example, to express "a user can be read by any user sharing a space" in the `User` model, you can directly read into its `membership` field. ```prisma @@allow('read', membership?[space.members?[user == auth()]]) ``` -In most cases when you use a "to-many" relation in policy rule, you'll use "Collection Predicate" to expression a condition. See [next section](#collection-predicate-expressions) for details. +In most cases, when you use a "to-many" relation in a policy rule, you'll use "Collection Predicate" to express a condition. See [next section](#collection-predicate-expressions) for details. ### Collection predicate expressions -Collection predicate are boolean expressions used to express condition over a list. It's mainly designed for building policy rules for "to-many" relations. It has three forms of syntaxes: +Collection predicate expressions are boolean expressions used to express conditions over a list. It's mainly designed for building policy rules for "to-many" relations. It has three forms of syntaxes: 1. Any @@ -305,7 +330,7 @@ The `condition` expression has direct access to fields defined in the model of ` , in condition `user == auth()`, `user` refers to the `user` field in model `Membership`, because the collection `members` is resolved to `Membership` model. -Also, collection predicates can be nested to express complex condition involving multi-level relation lookup. E.g.: +Also, collection predicates can be nested to express complex conditions involving multi-level relation lookup. E.g.: ```prisma @@allow('read', membership?[space.members?[user == auth()]]) @@ -315,8 +340,8 @@ In this example, `user` refers to `user` field of `Membership` model because `sp ### A complete example -Please checkout the [Collaborative Todo](../../samples/todo) for a complete example on using access policy. +Please check out the [Collaborative Todo](../../samples/todo) for a complete example on using access policies. ## Summary -This document serves as a quick overview for starting with the ZModel language. For more thorough explainations about data modeling, please checkout [Prisma's schema references](https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference). +This document serves as a quick overview for starting with the ZModel language. For more thorough explanations about data modeling, please check out [Prisma's schema references](https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference). diff --git a/docs/get-started/next-js.md b/docs/get-started/next-js.md index e63531874..0471c6a88 100644 --- a/docs/get-started/next-js.md +++ b/docs/get-started/next-js.md @@ -8,7 +8,7 @@ Here we demonstrate the process with a simple Blog starter using [Next-Auth](htt 1. Make sure you have Node.js 16 or above and NPM 8 or above installed -2. Create a new Next.js project from ZenStack starter +2. Create a new Next.js project from the ZenStack starter ```bash npx create-next-app [project name] --use-npm -e https://github.com/zenstackhq/nextjs-auth-starter @@ -22,7 +22,7 @@ cd [project name] npm run generate ``` -4. Initialize your local db and creates the first migration +4. Initialize your local DB and create the first migration The starter is preconfigured with a local sqlite database. Run the following command to populate its schema and generates a migration history: ```bash @@ -35,7 +35,7 @@ npm run db:migrate -- -n init npm run dev ``` -If everything worked correctly, you should have a blog site where you can signup, author drafts and publish them. Congratulations! In case anything broke, [reach out to us](#reach-out-to-us-for-issues-feedback-and-ideas) and we'll help. +If everything worked correctly, you should have a blog site where you can signup, author drafts, and publish them. Congratulations! In case anything broke, [reach out to us](#reach-out-to-us-for-issues-feedback-and-ideas), and we'll help. You can also try signing up multiple accounts and verify that drafts created by different users are isolated. @@ -57,7 +57,7 @@ npm i @zenstackhq/runtime @zenstackhq/internal 4. Configure database connection in "schema.model" - Here's an example for using a Postgres database with connection string specified in `DATABASE_URL` environment variable: + Here's an example of using a Postgres database with connection string specified in `DATABASE_URL` environment variable: ```prisma datasource db {