From b3cff74e31e14fe8d9947a9747250532023cfb30 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Mon, 4 Nov 2024 11:59:38 -0800 Subject: [PATCH 1/5] docs: release 2.8.0 --- docs/guides/authentication/lucia.md | 6 +- docs/guides/authentication/next-auth.md | 10 +- docs/guides/edge.md | 2 +- docs/guides/typing-json.md | 198 ++++++++++++++++++++++++ docs/reference/zmodel-language.md | 70 ++++++++- 5 files changed, 274 insertions(+), 12 deletions(-) create mode 100644 docs/guides/typing-json.md diff --git a/docs/guides/authentication/lucia.md b/docs/guides/authentication/lucia.md index 5fb86ead..84563958 100644 --- a/docs/guides/authentication/lucia.md +++ b/docs/guides/authentication/lucia.md @@ -4,7 +4,11 @@ sidebar_position: 5 sidebar_label: Lucia --- -# Integrating With Lucia +# Integrating With Lucia (Deprecated) + +:::info +Lucia's is [not in active development anymore](https://github.com/lucia-auth/lucia/discussions/1714). +::: [Lucia](https://lucia-auth.com/) is an auth library for your server that abstracts away the complexity of handling sessions. It is a good choice if you need to add custom logic to the auth flow or use email and password authentication. diff --git a/docs/guides/authentication/next-auth.md b/docs/guides/authentication/next-auth.md index 35ae5aff..be1663aa 100644 --- a/docs/guides/authentication/next-auth.md +++ b/docs/guides/authentication/next-auth.md @@ -6,13 +6,13 @@ sidebar_label: Auth.js (NextAuth) # Integrating With Auth.js (NextAuth) -[NextAuth](https://authjs.dev/) is a comprehensive framework for implementing authentication in Next.js projects. It offers a pluggable mechanism for configuring how user data is persisted. +[Auth.js](https://authjs.dev/) is a comprehensive framework for implementing authentication in Next.js projects. It offers a pluggable mechanism for configuring how user data is persisted. -To get access policies to work, ZenStack needs to be connected to the authentication system to get the user's identity. This guide introduces tasks required for integrating ZenStack with NextAuth. You can find a complete example [here](https://github.com/zenstackhq/sample-todo-nextjs ':target=blank'). +To get access policies to work, ZenStack needs to be connected to the authentication system to get the user's identity. This guide introduces tasks required for integrating ZenStack with Auth.js. You can find a complete example [here](https://github.com/zenstackhq/sample-todo-nextjs ':target=blank'). ### Data model requirement -NextAuth is agnostic about the underlying database type, but it requires specific table structures, depending on how you configure it. Therefore, your ZModel definitions should reflect these requirements. A sample `User` model is shown here (to be used with `CredentialsProvider`): +Auth.js is agnostic about the underlying database type, but it requires specific table structures, depending on how you configure it. Therefore, your ZModel definitions should reflect these requirements. A sample `User` model is shown here (to be used with `CredentialsProvider`): ```zmodel title='/schema.zmodel' model User { @@ -35,7 +35,7 @@ You can find the detailed database model requirements [here](https://authjs.dev/ ### Adapter -Adapter is a NextAuth mechanism for hooking in custom persistence of auth-related entities, like User, Account, etc. Since ZenStack is based on Prisma, you can use PrismaAdapter for the job: +Adapter is a Auth.js mechanism for hooking in custom persistence of auth-related entities, like User, Account, etc. Since ZenStack is based on Prisma, you can use PrismaAdapter for the job: ```ts title='/src/pages/api/auth/[...nextauth].ts' import { PrismaAdapter } from "@next-auth/prisma-adapter"; @@ -108,7 +108,7 @@ function authorize(prisma: PrismaClient) { You can create an enhanced Prisma client which automatically validates access policies, field validation rules etc., during CRUD operations. For more details, please refer to [ZModel Language](../../reference/zmodel-language) reference. -To create such a client with a standard setup, call the `enhance` API with a regular Prisma client and the current user (fetched with NextAuth API). Here's an example: +To create such a client with a standard setup, call the `enhance` API with a regular Prisma client and the current user (fetched with Auth.js API). Here's an example: ```ts import type { NextApiRequest, NextApiResponse } from 'next'; diff --git a/docs/guides/edge.md b/docs/guides/edge.md index 19589a51..72f87c8e 100644 --- a/docs/guides/edge.md +++ b/docs/guides/edge.md @@ -3,7 +3,7 @@ description: Guide for deploying ZenStack to edge runtime. sidebar_position: 12 --- -# Deploying to Edge Runtime (Preview) +# Deploying to Edge Runtime ## Introduction diff --git a/docs/guides/typing-json.md b/docs/guides/typing-json.md new file mode 100644 index 00000000..1ac19b16 --- /dev/null +++ b/docs/guides/typing-json.md @@ -0,0 +1,198 @@ +--- +description: Typing JSON fields +sidebar_position: 15 +--- + +# Typing JSON Fields (Preview) + +> The design and implementation are inspired by [prisma-json-types-generator](https://github.com/arthurfiorette/prisma-json-types-generator). + +Relational databases provide the benefit of strong schema enforcement, but sometimes, you want to step out of that guardrail and have more flexibility around data modeling. Prisma's JSON type allows you to store arbitrary JSON data in a column. It can be a good fit for use cases like: + +- You want to store objects along with the main entity, but it's not worth creating a separate table for them. +- Your objects have many possible fields, but you don't want to create tables with wide columns. +- Your object's structure is not fixed and can change over time in a non-backward-compatible way. + +JSON fields provide great flexibility but at the cost of losing strong typing and validation. ZenStack's strongly typed JSON field feature helps you regain the lost benefits. + +## Defining types + +To type JSON fields, you'll first need to create type declarations in your ZModel schema. For example, you can define a user profile type like: + +```zmodel +type Profile { + name String + age Int +} +``` + +Types can include fields of other types too: + +```zmodel +type Address { + state String + city String + zip String +} + +type Profile { + name String + age Int + address Address? +} +``` + +Type declarations are similar to model declarations with the following differences: + +- Types are not mapped to database tables. +- Types cannot inherit from other types yet. This will be supported in future releases. +- Types cannot have relations with models (since they aren't tables anyway). + +## Defining strongly-typed JSON fields + +You can then use these types to define strongly typed JSON fields in data models: + +```zmodel +model User { + id String @id @default(cuid()) + profile Profile @json + + @@allow('all', true) +} +``` + +:::info + +Strongly typed JSON fields must be annotated with the `@json` attribute. The purpose is to make them easily distinguishable from relational fields. + +::: + +## `PrismaClient` typing + +ZenStack's enhanced `PrismaClient` provides type-safe mutation input and query results for strongly typed JSON fields. + +```ts +import { enhance } from '@zenstackhq/runtime'; +import { prisma } from '~/db'; + +const db = enhance(prisma); + +// The following create call results in a type error because of the +// incorrect type of the `age` field +await db.user.create({ + data: { + profile: { name: 'Alice', age: '30' /* incorrect type */ } + } +}); + +// The query result is typed as: +// { +// id: string; +// profile: { +// name: string; +// age: number; +// address: { +// state: string; +// city: string; +// zip: string; +// } | null; +// }; +// } +await user = await db.user.findFirstOrThrow(); +``` + +The query also deals with transforming JSON fields into proper JavaScript types. For example, `DateTime` field values will be parsed from strings into `Date` objects. + +Please note that ZenStack **DOES NOT** validate if the query result data conforms to the JSON field types. If you have non-conforming existing data, you'll have to do additional checking or transformation, instead of trusting the TypeScript typing. We thought this was a reasonable trade-off to preserve the flexibility of JSON fields. + +:::info +Prisma filter clauses are not enhanced, which means when you filter by the JSON fields, you'll still need to use [Prisma's JSON filter syntax](https://www.prisma.io/docs/orm/prisma-client/special-fields-and-types/working-with-json-fields#filter-on-a-json-field-simple). + +In the future, we may provide a more natural filtering experience, like: + +```ts +await db.user.findMany({ + where: { + profile: { name: 'Alice' } + } +}); +``` + +However, it can potentially confuse code readers on whether the filter happens on a JSON field or a relational field (which involves joins). [Let us know your thoughts](https://discord.gg/Ykhr738dUe) on this! + +::: + +## Mutation payload validation + +During mutation, the enhanced `PrismaClient` validates if the input data conforms to the JSON field type. Even if you bypass TypeScript's type checking, ZenStack will reject an incorrect payload with a runtime error. + +```ts +await db.user.create({ + data: { + email: 'abc', + profile: { name: 'Alice', age: '30' /* incorrect type */ }, + }, +} as any // bypass TypeScript type checking +); +``` + +```plain +Error calling enhanced Prisma method `user.create`: denied by policy: user +entities failed 'create' check, input failed validation: Validation error: +Expected number, received string at "profile.age" +``` + +You can also annotate the type declaration with additional [validation attributes](../reference/zmodel-language#data-validation): + +```zmodel +type Profile { + name String + age Int @gte(18) @lt(150) // must be between 18 and 150 + address Address? +} +``` + +Such validations will also be enforced during mutation. For example, the following call will be rejected: + +```ts +await db.user.create({ + data: { + email: 'abc', + profile: { name: 'Alice', age: 16 }, + }, +}); +``` + +```plain +Error calling enhanced Prisma method `user.create`: denied by policy: user +entities failed 'create' check, input failed validation: Validation error: +Number must be greater than or equal to 18 at "profile.age" +``` + +## TypeScript types + +ZenStack generates TypeScript types for the "type" declarations. You can import them from `@zenstackhq/runtime/models`: + +```ts +import type { Profile } from '@zenstackhq/runtime/models'; + +const profile: Profile = { + name: 'Alice', + age: 30, + address: { state: 'WA', city: 'Seattle', zip: '98019' } +}; +``` + +## Zod schemas + +ZenStack also generates Zod schemas for the "type" declarations. You can import them from `@zenstackhq/runtime/zod/models`: + +```ts +import { ProfileSchema } from '@zenstackhq/runtime/zod/models'; + +const profile = ProfileSchema.parse({ + name: 'Alice', + age: 30, + address: { state: 'WA', city: 'Seattle', zip: '98019' } +}); +``` diff --git a/docs/reference/zmodel-language.md b/docs/reference/zmodel-language.md index 863a6f96..b1c2fe0d 100644 --- a/docs/reference/zmodel-language.md +++ b/docs/reference/zmodel-language.md @@ -23,14 +23,14 @@ We made that choice to extend the Prisma schema for several reasons: ::: -However, the standard capability of Prisma schema doesn't allow us to build the functionalities we want in a natural way, so we made a few extensions to the language by adding the following: +However, the standard capability of Prisma schema doesn't allow us to build the functionalities we want in a natural way, so we made several extensions to the language by adding the following: 1. Custom attributes 1. Custom attribute functions 1. Built-in attributes and functions for defining access policies 1. Built-in attributes for defining field validation rules 1. Utility attributes like `@password` and `@omit` -1. Multi-schema files support +1. Multi-schema files support Some of these extensions have been asked for by the Prisma community for some time, so we hope that ZenStack can be helpful even just as an extensible version of Prisma. @@ -244,13 +244,14 @@ Models represent the business entities of your application. A model inherits all - **[abstract]**: Optional. If present, the model is marked as abstract would not be mapped to a database table. Abstract models are only used as base classes for other models. + - **[NAME]**: Name of the model. Needs to be unique in the entire model. Needs to be a valid identifier matching regular expression `[A-Za-z][a-za-z0-9_]\*`. - **[FIELD]**: - Arbitrary number of fields. See [next section](#field) for details. + Arbitrary number of fields. See [Field](#field) for details. - **[ABSTRACT_MODEL_NAME]**: @@ -286,6 +287,49 @@ model User { } ``` +## Type + +Types provide a way to modeling object shapes without mapping them to a database table. The use cases of "types" include: + +- Typing JSON fields in models. +- Typing [plugin](#plugin) options. +- Interfacing with data models from external sources (auth providers like [Clerk](https://clerk.com), payment providers like [Stripe](https://stripe.com), etc.). + +### Syntax +```zmodel +type [NAME] { + [FIELD]* +} +``` + +- **[NAME]**: + + Name of the model. Needs to be unique in the entire model. Needs to be a valid identifier matching regular expression `[A-Za-z][a-za-z0-9_]\*`. + +- **[FIELD]**: + + Arbitrary number of fields. See [Field](#field) for details. + +### Example + +```zmodel +type Profile { + email String @email + name String +} + +model User { + id String @id + profile Profile? @json +} +``` + +### Limitations + +- Inheritance is not supported for types yet. A type cannot inherit from another type. A model cannot extend a type. +- Type cannot have type-level attributes like `@@validate`. However, fields in a type can have field-level attributes. + +We may address these limitations in future versions. ## Attribute @@ -652,6 +696,14 @@ attribute @omit() Indicates that the field should be omitted when read from the generated services. Commonly used together with `@password` attribute. +##### @json + +```zmodel +attribute @json() +``` + +Marks a field to be strong-typed JSON. The field's type must be a [Type](#type) declaration. + ##### @prisma.passthrough ```zmodel @@ -1105,7 +1157,7 @@ You can find examples of custom attributes and functions in [ZModel Standard Lib ## Field -Fields are typed members of models. +Fields are typed members of models and types. ### Syntax @@ -1115,13 +1167,21 @@ model Model { } ``` +Or + +```zmodel +type Type { + [FIELD_NAME] [FIELD_TYPE] (FIELD_ATTRIBUTES)? +} +``` + - **[FIELD_NAME]** Name of the field. Needs to be unique in the containing model. Needs to be a valid identifier matching regular expression `[A-Za-z][a-za-z0-9_]\*`. - **[FIELD_TYPE]** - Type of the field. Can be a scalar type or a reference to another model. + Type of the field. Can be a scalar type, a reference to another model if the field belongs to a [model](#model), or a reference to another type if it belongs to a [type](#type). The following scalar types are supported: From 31b8329ad60e6828a96668b21f75499b194897d4 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Mon, 4 Nov 2024 14:39:38 -0800 Subject: [PATCH 2/5] add limitations --- docs/guides/typing-json.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/guides/typing-json.md b/docs/guides/typing-json.md index 1ac19b16..2220b4ba 100644 --- a/docs/guides/typing-json.md +++ b/docs/guides/typing-json.md @@ -196,3 +196,13 @@ const profile = ProfileSchema.parse({ address: { state: 'WA', city: 'Seattle', zip: '98019' } }); ``` + +## Limitations + +This feature is in preview and has the following limitations: + +1. Types cannot inherit from other types. +2. Models cannot inherit from types. +3. JSON field members cannot be used in access policies. + +We would like to continue gathering feedback and address these limitations in future releases. From cfa0d1e8d0a6a254fe21f812035268597f5b49ba Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Mon, 4 Nov 2024 15:11:17 -0800 Subject: [PATCH 3/5] updates --- docs/reference/cli.md | 3 ++- docs/reference/zmodel-language.md | 33 +++++++++++++++++++++++ docs/the-complete-guide/part1/1-zmodel.md | 16 +++++++++++ 3 files changed, 51 insertions(+), 1 deletion(-) diff --git a/docs/reference/cli.md b/docs/reference/cli.md index faf6832b..e6188f8b 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -157,7 +157,8 @@ You don't need to `await` the Prisma method call result. The REPL session will a | ----- | ------------------- | ------- | | --debug | Enable debug output. Can be toggled on the fly in the repl session with the ".debug" command. | false | | --table | Enable table format. Can be toggled on the fly in the repl session with the ".table" command. | false | -| --prisma-client | Path to load PrismaClient module. | "./node_modules/.prisma/client" | +| --prisma-client <path> | Path to load PrismaClient module. | "node_modules/.prisma/client" | +| --load-path <path> | Path to load modules generated by ZenStack. | "node_modules/.zenstack" | #### Repl Commands diff --git a/docs/reference/zmodel-language.md b/docs/reference/zmodel-language.md index b1c2fe0d..c8a60bec 100644 --- a/docs/reference/zmodel-language.md +++ b/docs/reference/zmodel-language.md @@ -1850,3 +1850,36 @@ model Profile { userId String @unique } ``` + +## Comments + +ZModel supports both line comments (starting with `//`) and block comments (starting with `/*` and ending with `*/`). Comments on declarations (models, enums, fields, etc.) starting with triple slashes (`///`) are treated as documentation: + +- They show up as hover tooltips in IDEs. +- They are passed along to the generated Prisma schema. + +```zmodel +/// A user model +model User { + id String @id + + /// The user's email + email String @unique +} +``` + +You can also use JSDoc-style comments as documentation, however they are not passed along to the generated Prisma schema. + +```zmodel +/** + * A user model + */ +model User { + id String @id + + /** + * The user's email + */ + email String @unique +} +``` diff --git a/docs/the-complete-guide/part1/1-zmodel.md b/docs/the-complete-guide/part1/1-zmodel.md index 9d68a153..98365704 100644 --- a/docs/the-complete-guide/part1/1-zmodel.md +++ b/docs/the-complete-guide/part1/1-zmodel.md @@ -175,6 +175,22 @@ model User { ZenStack comes with a [VSCode extension](https://marketplace.visualstudio.com/items?itemName=zenstack.zenstack) and a [JetBrains IDE plugin](https://plugins.jetbrains.com/plugin/23397-zenstack-language-tools). You can find more information about IDE support [here](../../guides/ide). +:::tip Documentation + +ZModel supports both line comments (`//`) and block comments (`/* ... */`). Comments starting with triple slashes (`///`) are treated as documentation: they show up as hover tooltip in IDEs, and they are passed along to the generated Prisma schema. + +```zmodel +/// A user model +model User { + id String @id + + /// The user's email + email String @unique +} +``` + +::: + ### Full Documentation Check out the [ZModel Language](../../reference/zmodel-language) reference documentation for a complete language description. From a7edfdb8e7ad877da4b857f62464d3fd07770ee0 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Mon, 4 Nov 2024 15:22:48 -0800 Subject: [PATCH 4/5] update --- docs/guides/typing-json.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/guides/typing-json.md b/docs/guides/typing-json.md index 2220b4ba..c3c3506c 100644 --- a/docs/guides/typing-json.md +++ b/docs/guides/typing-json.md @@ -202,7 +202,8 @@ const profile = ProfileSchema.parse({ This feature is in preview and has the following limitations: 1. Types cannot inherit from other types. -2. Models cannot inherit from types. -3. JSON field members cannot be used in access policies. +1. Models cannot inherit from types. +1. Types and abstract models are conceptually similar and they should probably be consolidated. +1. JSON field members cannot be used in access policies. We would like to continue gathering feedback and address these limitations in future releases. From 79ea8d9fb64543ff35ccb3c68a28b19104b130df Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Tue, 5 Nov 2024 18:42:30 -0800 Subject: [PATCH 5/5] update --- docs/guides/typing-json.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/guides/typing-json.md b/docs/guides/typing-json.md index c3c3506c..9b33dce4 100644 --- a/docs/guides/typing-json.md +++ b/docs/guides/typing-json.md @@ -65,6 +65,8 @@ model User { Strongly typed JSON fields must be annotated with the `@json` attribute. The purpose is to make them easily distinguishable from relational fields. +The feature is only supported for PostgreSQL database for now. + ::: ## `PrismaClient` typing