From 29771e632dc0aa4f5c3aca2b380de78e5bab50c5 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Fri, 1 Aug 2025 21:24:34 +0800 Subject: [PATCH 01/13] docs: v3 docs structure --- docusaurus.config.js | 4 + package.json | 1 + pnpm-lock.yaml | 8 + src/components/StackBlitzEmbed.tsx | 26 + src/components/StackBlitzGithubEmbed.tsx | 26 + .../version-3.x/_components/PackageExec.tsx | 28 + .../_components/PackageInstall.tsx | 33 + .../_components/_zmodel-starter.md | 23 + versioned_docs/version-3.x/faq.md | 9 + .../version-3.x/migration/_category_.yml | 7 + .../version-3.x/migration/introduction.md | 6 + versioned_docs/version-3.x/orm/_category_.yml | 7 + .../orm/access-control/_category_.yml | 4 + .../orm/access-control/introduction.md | 3 + .../version-3.x/orm/api/_category_.yml | 7 + .../version-3.x/orm/api/aggregate.md | 6 + versioned_docs/version-3.x/orm/api/count.md | 6 + versioned_docs/version-3.x/orm/api/create.md | 6 + versioned_docs/version-3.x/orm/api/delete.md | 6 + versioned_docs/version-3.x/orm/api/find.md | 6 + .../version-3.x/orm/api/transaction.md | 6 + versioned_docs/version-3.x/orm/api/update.md | 6 + versioned_docs/version-3.x/orm/cli.md | 6 + .../version-3.x/orm/computed-fields.md | 7 + .../version-3.x/orm/database-client.mdx | 65 + .../version-3.x/orm/introduction.md | 120 + .../version-3.x/orm/plugins/_category_.yml | 7 + .../version-3.x/orm/plugins/introduction.md | 6 + .../version-3.x/orm/query-builder.md | 6 + .../version-3.x/orm/quick-start.mdx | 67 + versioned_docs/version-3.x/orm/ts-types.md | 6 + versioned_docs/version-3.x/orm/validation.md | 8 + .../version-3.x/orm/zmodel/_category_.yml | 7 + .../version-3.x/orm/zmodel/attributes.md | 6 + .../version-3.x/orm/zmodel/builtin-types.md | 6 + .../version-3.x/orm/zmodel/components.md | 6 + .../version-3.x/orm/zmodel/custom-types.md | 6 + .../version-3.x/orm/zmodel/datasource.md | 7 + .../version-3.x/orm/zmodel/models.md | 7 + .../version-3.x/orm/zmodel/multi-file.md | 9 + .../version-3.x/orm/zmodel/polymorphism.md | 7 + .../version-3.x/orm/zmodel/relations.md | 6 + .../version-3.x/orm/zmodel/reusing-fields.md | 7 + .../version-3.x/recipes/_category_.yml | 7 + .../version-3.x/reference/_category_.yml | 7 + versioned_docs/version-3.x/reference/cli.md | 251 +++ .../version-3.x/reference/error-handling.md | 43 + .../version-3.x/reference/limitations.md | 40 + .../version-3.x/reference/runtime-api.md | 130 ++ .../reference/server-adapters/_category_.yml | 7 + .../server-adapters/_error-handling.md | 3 + .../reference/server-adapters/_options.mdx | 66 + .../reference/server-adapters/_using-api.mdx | 137 ++ .../api-handlers/_data_type_serialization.md | 16 + .../server-adapters/api-handlers/index.mdx | 31 + .../server-adapters/api-handlers/rest.mdx | 982 ++++++++ .../server-adapters/api-handlers/rpc.mdx | 275 +++ .../reference/server-adapters/elysia.mdx | 69 + .../reference/server-adapters/express.mdx | 66 + .../reference/server-adapters/fastify.mdx | 56 + .../reference/server-adapters/hono.mdx | 57 + .../reference/server-adapters/nestjs.mdx | 268 +++ .../reference/server-adapters/next.mdx | 124 + .../reference/server-adapters/nuxt.mdx | 44 + .../reference/server-adapters/sveltekit.mdx | 63 + .../version-3.x/reference/zmodel-language.md | 1987 +++++++++++++++++ versioned_docs/version-3.x/samples.md | 8 + .../version-3.x/service/_category_.yml | 7 + .../version-3.x/service/introduction.md | 3 + versioned_docs/version-3.x/upgrade.md | 10 + .../version-3.x/utilities/_category_.yml | 7 + versioned_docs/version-3.x/utilities/zod.md | 6 + versioned_docs/version-3.x/welcome.md | 10 + versioned_sidebars/version-3.x-sidebars.json | 8 + versions.json | 2 +- 75 files changed, 5395 insertions(+), 1 deletion(-) create mode 100644 src/components/StackBlitzEmbed.tsx create mode 100644 src/components/StackBlitzGithubEmbed.tsx create mode 100644 versioned_docs/version-3.x/_components/PackageExec.tsx create mode 100644 versioned_docs/version-3.x/_components/PackageInstall.tsx create mode 100644 versioned_docs/version-3.x/_components/_zmodel-starter.md create mode 100644 versioned_docs/version-3.x/faq.md create mode 100644 versioned_docs/version-3.x/migration/_category_.yml create mode 100644 versioned_docs/version-3.x/migration/introduction.md create mode 100644 versioned_docs/version-3.x/orm/_category_.yml create mode 100644 versioned_docs/version-3.x/orm/access-control/_category_.yml create mode 100644 versioned_docs/version-3.x/orm/access-control/introduction.md create mode 100644 versioned_docs/version-3.x/orm/api/_category_.yml create mode 100644 versioned_docs/version-3.x/orm/api/aggregate.md create mode 100644 versioned_docs/version-3.x/orm/api/count.md create mode 100644 versioned_docs/version-3.x/orm/api/create.md create mode 100644 versioned_docs/version-3.x/orm/api/delete.md create mode 100644 versioned_docs/version-3.x/orm/api/find.md create mode 100644 versioned_docs/version-3.x/orm/api/transaction.md create mode 100644 versioned_docs/version-3.x/orm/api/update.md create mode 100644 versioned_docs/version-3.x/orm/cli.md create mode 100644 versioned_docs/version-3.x/orm/computed-fields.md create mode 100644 versioned_docs/version-3.x/orm/database-client.mdx create mode 100644 versioned_docs/version-3.x/orm/introduction.md create mode 100644 versioned_docs/version-3.x/orm/plugins/_category_.yml create mode 100644 versioned_docs/version-3.x/orm/plugins/introduction.md create mode 100644 versioned_docs/version-3.x/orm/query-builder.md create mode 100644 versioned_docs/version-3.x/orm/quick-start.mdx create mode 100644 versioned_docs/version-3.x/orm/ts-types.md create mode 100644 versioned_docs/version-3.x/orm/validation.md create mode 100644 versioned_docs/version-3.x/orm/zmodel/_category_.yml create mode 100644 versioned_docs/version-3.x/orm/zmodel/attributes.md create mode 100644 versioned_docs/version-3.x/orm/zmodel/builtin-types.md create mode 100644 versioned_docs/version-3.x/orm/zmodel/components.md create mode 100644 versioned_docs/version-3.x/orm/zmodel/custom-types.md create mode 100644 versioned_docs/version-3.x/orm/zmodel/datasource.md create mode 100644 versioned_docs/version-3.x/orm/zmodel/models.md create mode 100644 versioned_docs/version-3.x/orm/zmodel/multi-file.md create mode 100644 versioned_docs/version-3.x/orm/zmodel/polymorphism.md create mode 100644 versioned_docs/version-3.x/orm/zmodel/relations.md create mode 100644 versioned_docs/version-3.x/orm/zmodel/reusing-fields.md create mode 100644 versioned_docs/version-3.x/recipes/_category_.yml create mode 100644 versioned_docs/version-3.x/reference/_category_.yml create mode 100644 versioned_docs/version-3.x/reference/cli.md create mode 100644 versioned_docs/version-3.x/reference/error-handling.md create mode 100644 versioned_docs/version-3.x/reference/limitations.md create mode 100644 versioned_docs/version-3.x/reference/runtime-api.md create mode 100644 versioned_docs/version-3.x/reference/server-adapters/_category_.yml create mode 100644 versioned_docs/version-3.x/reference/server-adapters/_error-handling.md create mode 100644 versioned_docs/version-3.x/reference/server-adapters/_options.mdx create mode 100644 versioned_docs/version-3.x/reference/server-adapters/_using-api.mdx create mode 100644 versioned_docs/version-3.x/reference/server-adapters/api-handlers/_data_type_serialization.md create mode 100644 versioned_docs/version-3.x/reference/server-adapters/api-handlers/index.mdx create mode 100644 versioned_docs/version-3.x/reference/server-adapters/api-handlers/rest.mdx create mode 100644 versioned_docs/version-3.x/reference/server-adapters/api-handlers/rpc.mdx create mode 100644 versioned_docs/version-3.x/reference/server-adapters/elysia.mdx create mode 100644 versioned_docs/version-3.x/reference/server-adapters/express.mdx create mode 100644 versioned_docs/version-3.x/reference/server-adapters/fastify.mdx create mode 100644 versioned_docs/version-3.x/reference/server-adapters/hono.mdx create mode 100644 versioned_docs/version-3.x/reference/server-adapters/nestjs.mdx create mode 100644 versioned_docs/version-3.x/reference/server-adapters/next.mdx create mode 100644 versioned_docs/version-3.x/reference/server-adapters/nuxt.mdx create mode 100644 versioned_docs/version-3.x/reference/server-adapters/sveltekit.mdx create mode 100644 versioned_docs/version-3.x/reference/zmodel-language.md create mode 100644 versioned_docs/version-3.x/samples.md create mode 100644 versioned_docs/version-3.x/service/_category_.yml create mode 100644 versioned_docs/version-3.x/service/introduction.md create mode 100644 versioned_docs/version-3.x/upgrade.md create mode 100644 versioned_docs/version-3.x/utilities/_category_.yml create mode 100644 versioned_docs/version-3.x/utilities/zod.md create mode 100644 versioned_docs/version-3.x/welcome.md create mode 100644 versioned_sidebars/version-3.x-sidebars.json diff --git a/docusaurus.config.js b/docusaurus.config.js index 0dd2e501..5eef4d59 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -44,6 +44,10 @@ const config = { label: '1.x', banner: 'none', }, + '3.x': { + label: '3.0 Alpha', + banner: 'none', + }, }, }, blog: false, diff --git a/package.json b/package.json index fa70489e..85733b49 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "@docusaurus/theme-mermaid": "3.4.0", "@giscus/react": "^2.4.0", "@mdx-js/react": "^3.0.1", + "@stackblitz/sdk": "^1.11.0", "autoprefixer": "^10.4.13", "clsx": "^1.2.1", "postcss": "^8.4.21", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 78043273..7b0326f0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,6 +29,9 @@ importers: '@mdx-js/react': specifier: ^3.0.1 version: 3.0.1(@types/react@18.0.26)(react@18.2.0) + '@stackblitz/sdk': + specifier: ^1.11.0 + version: 1.11.0 autoprefixer: specifier: ^10.4.13 version: 10.4.13(postcss@8.4.21) @@ -1113,6 +1116,9 @@ packages: '@slorber/remark-comment@1.0.0': resolution: {integrity: sha512-RCE24n7jsOj1M0UPvIQCHTe7fI0sFL4S2nwKVWwHyVr/wI/H8GosgsJGyhnsZoGFnD/P2hLf1mSbrrgSLN93NA==} + '@stackblitz/sdk@1.11.0': + resolution: {integrity: sha512-DFQGANNkEZRzFk1/rDP6TcFdM82ycHE+zfl9C/M/jXlH68jiqHWHFMQURLELoD8koxvu/eW5uhg94NSAZlYrUQ==} + '@svgr/babel-plugin-add-jsx-attribute@8.0.0': resolution: {integrity: sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==} engines: {node: '>=14'} @@ -6914,6 +6920,8 @@ snapshots: micromark-util-character: 1.2.0 micromark-util-symbol: 1.1.0 + '@stackblitz/sdk@1.11.0': {} + '@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.23.9)': dependencies: '@babel/core': 7.23.9 diff --git a/src/components/StackBlitzEmbed.tsx b/src/components/StackBlitzEmbed.tsx new file mode 100644 index 00000000..db7b6418 --- /dev/null +++ b/src/components/StackBlitzEmbed.tsx @@ -0,0 +1,26 @@ +import React, { useEffect, useRef } from 'react'; +import sdk from '@stackblitz/sdk'; + +interface StackBlitzEmbedProps { + projectId: string; + height?: string; +} + +const StackBlitzEmbed: React.FC = ({ projectId, height = '600px' }) => { + const containerRef = useRef(null); + + useEffect(() => { + if (containerRef.current) { + sdk.embedProjectId(containerRef.current, projectId, { + openFile: 'main.ts', + height, + view: 'editor', + forceEmbedLayout: true, + }); + } + }, [projectId, height]); + + return
; +}; + +export default StackBlitzEmbed; diff --git a/src/components/StackBlitzGithubEmbed.tsx b/src/components/StackBlitzGithubEmbed.tsx new file mode 100644 index 00000000..5bddace1 --- /dev/null +++ b/src/components/StackBlitzGithubEmbed.tsx @@ -0,0 +1,26 @@ +import React, { useEffect, useRef } from 'react'; +import sdk from '@stackblitz/sdk'; + +interface StackBlitzGithubEmbedProps { + repoPath: string; + height?: string; +} + +const StackBlitzGithubEmbed: React.FC = ({ repoPath, height = '600px' }) => { + const containerRef = useRef(null); + + useEffect(() => { + if (containerRef.current) { + sdk.embedGithubProject(containerRef.current, repoPath, { + openFile: 'main.ts', + height, + view: 'editor', + forceEmbedLayout: true, + }); + } + }, [repoPath, height]); + + return
; +}; + +export default StackBlitzGithubEmbed; diff --git a/versioned_docs/version-3.x/_components/PackageExec.tsx b/versioned_docs/version-3.x/_components/PackageExec.tsx new file mode 100644 index 00000000..e3df2110 --- /dev/null +++ b/versioned_docs/version-3.x/_components/PackageExec.tsx @@ -0,0 +1,28 @@ +import CodeBlock from '@theme/CodeBlock'; +import TabItem from '@theme/TabItem'; +import Tabs from '@theme/Tabs'; + +interface Props { + command: string; +} + +const pkgManagers = [ + { name: 'npm', command: 'npx' }, + { name: 'pnpm', command: 'pnpm' }, + { name: 'bun', command: 'bunx' }, + { name: 'yarn', command: 'npx' }, +]; + +const PackageInstall = ({ command }: Props) => { + return ( + + {pkgManagers.map((pkg) => ( + + {`${pkg.command} ${command}`} + + ))} + + ); +}; + +export default PackageInstall; diff --git a/versioned_docs/version-3.x/_components/PackageInstall.tsx b/versioned_docs/version-3.x/_components/PackageInstall.tsx new file mode 100644 index 00000000..53bf8805 --- /dev/null +++ b/versioned_docs/version-3.x/_components/PackageInstall.tsx @@ -0,0 +1,33 @@ +import CodeBlock from '@theme/CodeBlock'; +import TabItem from '@theme/TabItem'; +import Tabs from '@theme/Tabs'; + +interface Props { + devDependencies: string[]; + dependencies: string[]; +} + +const pkgManagers = [ + { name: 'npm', command: 'npm install', dev: '--save-dev' }, + { name: 'pnpm', command: 'pnpm add', dev: '--save-dev' }, + { name: 'bun', command: 'bun add', dev: '--dev' }, + { name: 'yarn', command: 'yarn add', dev: '--dev' }, +]; + +const PackageInstall = ({ devDependencies, dependencies }: Props) => { + return ( + + {pkgManagers.map((pkg) => ( + + + {`${devDependencies?.length ? `${pkg.command} ${pkg.dev} ${devDependencies.join(' ')}\n` : ''}${ + dependencies?.length ? `${pkg.command} ${dependencies.join(' ')}` : '' + }`} + + + ))} + + ); +}; + +export default PackageInstall; diff --git a/versioned_docs/version-3.x/_components/_zmodel-starter.md b/versioned_docs/version-3.x/_components/_zmodel-starter.md new file mode 100644 index 00000000..029ef799 --- /dev/null +++ b/versioned_docs/version-3.x/_components/_zmodel-starter.md @@ -0,0 +1,23 @@ + ```zmodel + datasource db { + provider = 'sqlite' + url = "file:./dev.db" + } + + model User { + id String @id @default(cuid()) + email String @unique @email @length(6, 32) + posts Post[] + } + + model Post { + id String @id @default(cuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + title String @length(1, 256) + content String + published Boolean @default(false) + author User @relation(fields: [authorId], references: [id]) + authorId String + } + ``` \ No newline at end of file diff --git a/versioned_docs/version-3.x/faq.md b/versioned_docs/version-3.x/faq.md new file mode 100644 index 00000000..b0e81a65 --- /dev/null +++ b/versioned_docs/version-3.x/faq.md @@ -0,0 +1,9 @@ +--- +description: ZenStack FAQ. + +slug: /faq +sidebar_label: FAQ +sidebar_position: 100 +--- + +# πŸ™‹πŸ» FAQ diff --git a/versioned_docs/version-3.x/migration/_category_.yml b/versioned_docs/version-3.x/migration/_category_.yml new file mode 100644 index 00000000..be8bc9d1 --- /dev/null +++ b/versioned_docs/version-3.x/migration/_category_.yml @@ -0,0 +1,7 @@ +position: 3 +label: Migration +collapsible: true +collapsed: true +link: + type: generated-index + title: Migration diff --git a/versioned_docs/version-3.x/migration/introduction.md b/versioned_docs/version-3.x/migration/introduction.md new file mode 100644 index 00000000..2e8a25db --- /dev/null +++ b/versioned_docs/version-3.x/migration/introduction.md @@ -0,0 +1,6 @@ +--- +sidebar_position: 1 +description: Introduction to ZenStack migration +--- + +# Introduction diff --git a/versioned_docs/version-3.x/orm/_category_.yml b/versioned_docs/version-3.x/orm/_category_.yml new file mode 100644 index 00000000..7e156336 --- /dev/null +++ b/versioned_docs/version-3.x/orm/_category_.yml @@ -0,0 +1,7 @@ +position: 2 +label: ORM +collapsible: true +collapsed: true +link: + type: generated-index + title: ORM diff --git a/versioned_docs/version-3.x/orm/access-control/_category_.yml b/versioned_docs/version-3.x/orm/access-control/_category_.yml new file mode 100644 index 00000000..dcb120ba --- /dev/null +++ b/versioned_docs/version-3.x/orm/access-control/_category_.yml @@ -0,0 +1,4 @@ +position: 8 +label: Access Control +collapsible: true +collapsed: true diff --git a/versioned_docs/version-3.x/orm/access-control/introduction.md b/versioned_docs/version-3.x/orm/access-control/introduction.md new file mode 100644 index 00000000..261e992b --- /dev/null +++ b/versioned_docs/version-3.x/orm/access-control/introduction.md @@ -0,0 +1,3 @@ +# Introduction + +Coming soon 🚧 \ No newline at end of file diff --git a/versioned_docs/version-3.x/orm/api/_category_.yml b/versioned_docs/version-3.x/orm/api/_category_.yml new file mode 100644 index 00000000..2a384497 --- /dev/null +++ b/versioned_docs/version-3.x/orm/api/_category_.yml @@ -0,0 +1,7 @@ +position: 6 +label: ORM API +collapsible: true +collapsed: true +link: + type: generated-index + title: ORM API diff --git a/versioned_docs/version-3.x/orm/api/aggregate.md b/versioned_docs/version-3.x/orm/api/aggregate.md new file mode 100644 index 00000000..0b545613 --- /dev/null +++ b/versioned_docs/version-3.x/orm/api/aggregate.md @@ -0,0 +1,6 @@ +--- +sidebar_position: 6 +description: Aggregate API +--- + +# Aggregate \ No newline at end of file diff --git a/versioned_docs/version-3.x/orm/api/count.md b/versioned_docs/version-3.x/orm/api/count.md new file mode 100644 index 00000000..d22fd667 --- /dev/null +++ b/versioned_docs/version-3.x/orm/api/count.md @@ -0,0 +1,6 @@ +--- +sidebar_position: 5 +description: Count API +--- + +# Count \ No newline at end of file diff --git a/versioned_docs/version-3.x/orm/api/create.md b/versioned_docs/version-3.x/orm/api/create.md new file mode 100644 index 00000000..7bd11da3 --- /dev/null +++ b/versioned_docs/version-3.x/orm/api/create.md @@ -0,0 +1,6 @@ +--- +sidebar_position: 2 +description: Create API +--- + +# Create \ No newline at end of file diff --git a/versioned_docs/version-3.x/orm/api/delete.md b/versioned_docs/version-3.x/orm/api/delete.md new file mode 100644 index 00000000..11e0142c --- /dev/null +++ b/versioned_docs/version-3.x/orm/api/delete.md @@ -0,0 +1,6 @@ +--- +sidebar_position: 4 +description: Delete API +--- + +# Delete \ No newline at end of file diff --git a/versioned_docs/version-3.x/orm/api/find.md b/versioned_docs/version-3.x/orm/api/find.md new file mode 100644 index 00000000..633a25c8 --- /dev/null +++ b/versioned_docs/version-3.x/orm/api/find.md @@ -0,0 +1,6 @@ +--- +sidebar_position: 1 +description: Find API +--- + +# Find \ No newline at end of file diff --git a/versioned_docs/version-3.x/orm/api/transaction.md b/versioned_docs/version-3.x/orm/api/transaction.md new file mode 100644 index 00000000..d24f053d --- /dev/null +++ b/versioned_docs/version-3.x/orm/api/transaction.md @@ -0,0 +1,6 @@ +--- +sidebar_position: 7 +description: Transaction API +--- + +# Transaction \ No newline at end of file diff --git a/versioned_docs/version-3.x/orm/api/update.md b/versioned_docs/version-3.x/orm/api/update.md new file mode 100644 index 00000000..468e8cac --- /dev/null +++ b/versioned_docs/version-3.x/orm/api/update.md @@ -0,0 +1,6 @@ +--- +sidebar_position: 3 +description: Update API +--- + +# Update \ No newline at end of file diff --git a/versioned_docs/version-3.x/orm/cli.md b/versioned_docs/version-3.x/orm/cli.md new file mode 100644 index 00000000..98a18870 --- /dev/null +++ b/versioned_docs/version-3.x/orm/cli.md @@ -0,0 +1,6 @@ +--- +sidebar_position: 4 +description: Using the CLI +--- + +# Using the CLI diff --git a/versioned_docs/version-3.x/orm/computed-fields.md b/versioned_docs/version-3.x/orm/computed-fields.md new file mode 100644 index 00000000..d6e2cbd7 --- /dev/null +++ b/versioned_docs/version-3.x/orm/computed-fields.md @@ -0,0 +1,7 @@ +--- +sidebar_position: 10 +description: Computed fields in ZModel +--- + +# Computed Fields + diff --git a/versioned_docs/version-3.x/orm/database-client.mdx b/versioned_docs/version-3.x/orm/database-client.mdx new file mode 100644 index 00000000..ed04be38 --- /dev/null +++ b/versioned_docs/version-3.x/orm/database-client.mdx @@ -0,0 +1,65 @@ +--- +sidebar_position: 5 +description: Creating a database client +--- + +import TabItem from '@theme/TabItem'; +import Tabs from '@theme/Tabs'; +import PackageInstall from '../_components/PackageInstall'; + +# Database Client + +The `zen generate` command compiles the ZModel schema into TypeScript code, which we can in turn use to initialize a type-safe database client. ZenStack uses Kysely to handle the low-level database operations, so the client is initialize with a Kysely dialect - an object that encapsulates database details. + +The samples below only shows creating a client using SQLite (via [better-sqlite3](https://github.com/WiseLibs/better-sqlite3)) and PostgreSQL (via [node-postgres](https://github.com/brianc/node-postgres)), however you can also use any other Kysely dialects. + + + + + + + +```ts title='db.ts' +import { ZenStackClient } from '@zenstackhq/runtime'; +import { SqliteDialect } from 'kysely'; +import SQLite from 'better-sqlite3'; +import { schema } from '@/zenstack/schema'; + +export const db = new ZenStackClient(schema, { + dialect: new SqliteDialect({ + database: new SQLite(':memory:'), + }), +}); +``` + + + + + + +```ts title='db.ts' +import { ZenStackClient } from '@zenstackhq/runtime'; +import { schema } from '@/zenstack/schema'; +import { PostgresDialect } from 'kysely'; +import { Pool } from 'pg'; + +export const db = new ZenStackClient(schema, { + dialect: new PostgresDialect({ + pool: new Pool({ + connectionString: process.env.DATABASE_URL, + }), + }), +}); +``` + + + + +The created `db` object has the full ORM API inferred from the type of the `schema` parameter. When necessary, you can also explicitly get the inferred client type like: + +```ts +import type { ClientContract } from '@zenstackhq/runtime'; +import type { SchemaType } from '@/zenstack/schema'; + +export type DbClient = ClientContract; +``` diff --git a/versioned_docs/version-3.x/orm/introduction.md b/versioned_docs/version-3.x/orm/introduction.md new file mode 100644 index 00000000..8bcaf843 --- /dev/null +++ b/versioned_docs/version-3.x/orm/introduction.md @@ -0,0 +1,120 @@ +--- +sidebar_position: 1 +description: Introduction to ZenStack ORM +--- + +# Introduction + +ZenStack ORM is a schema-first ORM for modern TypeScript applications. It learnt from the prior arts and aims to provide an awesome developer experience by combining the best ingredients into a cohesive package. + +## Key Facts + +### [Prisma](https://prisma.io/orm)-Compatible Schema Language + +The schema language used by ZenStack (called ZModel) is a superset of Prisma schema language. Every valid Prisma schema can be used with ZenStack unchanged. In addition, ZModel added many important extensions to the language to improve the developer experience and enable advanced features. + +```zmodel +model User { + id String @id @default(cuid()) + email String @unique + posts Post[] +} + +model Post { + id String @id @default(cuid()) + title String @length(1, 256) + author User @relation(fields: [authorId], references: [id]) + authorId String +} +``` + +### [Prisma](https://prisma.io/orm)-Compatible Query API + +Although ZenStack has a completely different implementation (based on [Kysely](https://kysely.dev/)), it replicated Prisma ORM's query API so that you can use it pretty much as a drop-in replacement. Even if you're not a Prisma user, the query API is very intuitive and easy to learn. + +```ts +await db.user.findMany({ + where: { + email: { + endsWith: 'zenstack.dev', + }, + }, + orderBy: { + createdAt: 'desc', + }, + include: { posts: true } +}); +``` + +### Low-Level Query-Builder Powered by [Kysely](https://kysely.dev/) + +ORM APIs are concise and pleasant, but they have their limitations. When you need extra power, you can fall back to the low-level query builder API powered by [Kysely](https://kysely.dev/) - limitless expressiveness, full type safety, and zero extra setup. + +```ts +await db.$qb + .selectFrom('User') + .leftJoin('Post', 'Post.authorId', 'User.id') + .select(['User.id', 'User.email', 'Post.title']) + .execute(); +``` + +### Access Control + +ZenStack ORM comes with a powerful built-in access control system. You can define access rules right inside the schema. The rules are enforced at runtime via query injection, so it doesn't rely on any database specific row-level security features. + +```zmodel +model Post { + id String @id @default(cuid()) + title. String @length(1, 256) + published Boolean @default(false) + author User @relation(fields: [authorId], references: [id]) + authorId String + + // no anonymous access + @@deny('all', auth() == null) + + // author has full access + @@allow('all', authorId == auth().id) + + // published posts are readable by anyone + @@allow('read', published) +} +``` + +### Polymorphic Models + +Real-world applications often involves storing polymorphic data which is notoriously complex to model and query. ZenStack does the heavy-lifting for you so you can model an inheritance hierarchy with simple annotations, and query them with perfect type safety. + +```zmodel +model Asset { + id String @id @default(cuid()) + title String @length(1, 256) + type String + + // the ORM uses the `type` field to determine to which concrete model + // a query should be delegated + @@delegate(type) +} + +model Post extends Asset { + content String +} +``` + +```ts +const asset = await db.asset.findFirst(); +if (asset.type === 'Post') { + // asset's type is narrowed down to `Post` + console.log(asset.content); +} else { + // other asset type +} +``` + +### Straightforward and Light-Weighted + +Compared to Prisma and previous versions of ZenStack, v3 is more straightforward and light-weighted. + +- No runtime dependency to Prisma, thus no overhead of Rust/WASM query engines. +- No magic generating into `node_modules`. You fully control how the generated code is compiled and bundled. +- Less code generation, more type inference. diff --git a/versioned_docs/version-3.x/orm/plugins/_category_.yml b/versioned_docs/version-3.x/orm/plugins/_category_.yml new file mode 100644 index 00000000..60e83429 --- /dev/null +++ b/versioned_docs/version-3.x/orm/plugins/_category_.yml @@ -0,0 +1,7 @@ +position: 12 +label: Plugins +collapsible: true +collapsed: true +link: + type: generated-index + title: Plugins diff --git a/versioned_docs/version-3.x/orm/plugins/introduction.md b/versioned_docs/version-3.x/orm/plugins/introduction.md new file mode 100644 index 00000000..8211aef1 --- /dev/null +++ b/versioned_docs/version-3.x/orm/plugins/introduction.md @@ -0,0 +1,6 @@ +--- +sidebar_position: 1 +description: ORM plugin introduction +--- + +# Introduction to Plugins diff --git a/versioned_docs/version-3.x/orm/query-builder.md b/versioned_docs/version-3.x/orm/query-builder.md new file mode 100644 index 00000000..106b4ae4 --- /dev/null +++ b/versioned_docs/version-3.x/orm/query-builder.md @@ -0,0 +1,6 @@ +--- +sidebar_position: 7 +description: Query builder API +--- + +# Query Builder API diff --git a/versioned_docs/version-3.x/orm/quick-start.mdx b/versioned_docs/version-3.x/orm/quick-start.mdx new file mode 100644 index 00000000..3ffcc406 --- /dev/null +++ b/versioned_docs/version-3.x/orm/quick-start.mdx @@ -0,0 +1,67 @@ +--- +sidebar_position: 2 +description: Quick start guide +--- + +import StackBlitzGithubEmbed from '@site/src/components/StackBlitzGithubEmbed'; +import ZModelStarter from '../_components/_zmodel-starter.md'; +import PackageInstall from '../_components/PackageInstall.tsx'; +import PackageExec from '../_components/PackageExec.tsx'; + +# Quick Start + +:::info +All v3 packages are published under the "@next" tag. +::: + +There are multiple ways to start using ZenStack. + +## 1. Creating a project from scratch + +Run the following command to scaffold a new project with a pre-configured minimal starter: + +```bash +npm create zenstack@next my-project +``` + +Or simply use the following playground to experience it inside the browser. + + + +## 2. Adding to an existing project + +To add ZenStack to an existing project, run the CLI `init` command to install dependencies and create a sample schema: + + + +Then create a `zenstack/schema.zmodel` file in the root of your project. You can use the following sample schema to get started: + + + +Finally, run `zen generate` to compile the schema into TypeScript. + + + +## 3. Manual setup + +You can also always configure a project manually with the following steps: + +1. Install dependencies + + + +2. Create a `zenstack/schema.zmodel` file + + You can use the following sample schema to get started: + + + +3. Run the CLI `generate` command to compile the schema into TypeScript + + + +## 4. Custom schema and output paths + +By default, ZenStack CLI loads the schema from `zenstack/schema.zmodel`. You can change this by passing the `--schema` option. TypeScript files are by default generated to the same directory as the schema file. You can change this by passing the `--output` option. + +You can choose to either commit the generated TypeScript files to your source control, or add them to `.gitignore` and generate them on the fly in your CI/CD pipeline. diff --git a/versioned_docs/version-3.x/orm/ts-types.md b/versioned_docs/version-3.x/orm/ts-types.md new file mode 100644 index 00000000..9fd5432d --- /dev/null +++ b/versioned_docs/version-3.x/orm/ts-types.md @@ -0,0 +1,6 @@ +--- +sidebar_position: 11 +description: TypeScript types derived from the ZModel schema +--- + +# TypeScript Types diff --git a/versioned_docs/version-3.x/orm/validation.md b/versioned_docs/version-3.x/orm/validation.md new file mode 100644 index 00000000..7d0fb918 --- /dev/null +++ b/versioned_docs/version-3.x/orm/validation.md @@ -0,0 +1,8 @@ +--- +sidebar_position: 9 +description: Data validation in ZModel +--- + +# Data Validation + + diff --git a/versioned_docs/version-3.x/orm/zmodel/_category_.yml b/versioned_docs/version-3.x/orm/zmodel/_category_.yml new file mode 100644 index 00000000..396fcc06 --- /dev/null +++ b/versioned_docs/version-3.x/orm/zmodel/_category_.yml @@ -0,0 +1,7 @@ +position: 3 +label: Writing Schema +collapsible: true +collapsed: true +link: + type: generated-index + title: Writing Schema diff --git a/versioned_docs/version-3.x/orm/zmodel/attributes.md b/versioned_docs/version-3.x/orm/zmodel/attributes.md new file mode 100644 index 00000000..be676265 --- /dev/null +++ b/versioned_docs/version-3.x/orm/zmodel/attributes.md @@ -0,0 +1,6 @@ +--- +sidebar_position: 4 +description: Attributes in ZModel +--- + +# Attributes diff --git a/versioned_docs/version-3.x/orm/zmodel/builtin-types.md b/versioned_docs/version-3.x/orm/zmodel/builtin-types.md new file mode 100644 index 00000000..d3142702 --- /dev/null +++ b/versioned_docs/version-3.x/orm/zmodel/builtin-types.md @@ -0,0 +1,6 @@ +--- +sidebar_position: 6 +description: Built-in types in ZModel +--- + +# Built-in Types diff --git a/versioned_docs/version-3.x/orm/zmodel/components.md b/versioned_docs/version-3.x/orm/zmodel/components.md new file mode 100644 index 00000000..b0635163 --- /dev/null +++ b/versioned_docs/version-3.x/orm/zmodel/components.md @@ -0,0 +1,6 @@ +--- +sidebar_position: 1 +description: ZModel schema components +--- + +# Schema Components diff --git a/versioned_docs/version-3.x/orm/zmodel/custom-types.md b/versioned_docs/version-3.x/orm/zmodel/custom-types.md new file mode 100644 index 00000000..7208914f --- /dev/null +++ b/versioned_docs/version-3.x/orm/zmodel/custom-types.md @@ -0,0 +1,6 @@ +--- +sidebar_position: 7 +description: Custom types in ZModel +--- + +# Custom Types diff --git a/versioned_docs/version-3.x/orm/zmodel/datasource.md b/versioned_docs/version-3.x/orm/zmodel/datasource.md new file mode 100644 index 00000000..3078fe6e --- /dev/null +++ b/versioned_docs/version-3.x/orm/zmodel/datasource.md @@ -0,0 +1,7 @@ +--- +sidebar_position: 2 +description: Datasource in ZModel +--- + +# Data Source + diff --git a/versioned_docs/version-3.x/orm/zmodel/models.md b/versioned_docs/version-3.x/orm/zmodel/models.md new file mode 100644 index 00000000..f54ac510 --- /dev/null +++ b/versioned_docs/version-3.x/orm/zmodel/models.md @@ -0,0 +1,7 @@ +--- +sidebar_position: 3 +description: Models in ZModel +--- + +# Models + diff --git a/versioned_docs/version-3.x/orm/zmodel/multi-file.md b/versioned_docs/version-3.x/orm/zmodel/multi-file.md new file mode 100644 index 00000000..81500291 --- /dev/null +++ b/versioned_docs/version-3.x/orm/zmodel/multi-file.md @@ -0,0 +1,9 @@ +--- +sidebar_position: 10 +description: Breaking down complex schemas into multiple files +--- + +# Multi-file Schemas + + + diff --git a/versioned_docs/version-3.x/orm/zmodel/polymorphism.md b/versioned_docs/version-3.x/orm/zmodel/polymorphism.md new file mode 100644 index 00000000..62c1797d --- /dev/null +++ b/versioned_docs/version-3.x/orm/zmodel/polymorphism.md @@ -0,0 +1,7 @@ +--- +sidebar_position: 9 +description: Polymorphic models in ZModel +--- + +# Polymorphic Models + diff --git a/versioned_docs/version-3.x/orm/zmodel/relations.md b/versioned_docs/version-3.x/orm/zmodel/relations.md new file mode 100644 index 00000000..96794f67 --- /dev/null +++ b/versioned_docs/version-3.x/orm/zmodel/relations.md @@ -0,0 +1,6 @@ +--- +sidebar_position: 5 +description: Relations in ZModel +--- + +# Relations diff --git a/versioned_docs/version-3.x/orm/zmodel/reusing-fields.md b/versioned_docs/version-3.x/orm/zmodel/reusing-fields.md new file mode 100644 index 00000000..eed27f91 --- /dev/null +++ b/versioned_docs/version-3.x/orm/zmodel/reusing-fields.md @@ -0,0 +1,7 @@ +--- +sidebar_position: 8 +description: Reusing common fields across models in ZModel +--- + +# Reusing Common Fields + diff --git a/versioned_docs/version-3.x/recipes/_category_.yml b/versioned_docs/version-3.x/recipes/_category_.yml new file mode 100644 index 00000000..acb7bc3f --- /dev/null +++ b/versioned_docs/version-3.x/recipes/_category_.yml @@ -0,0 +1,7 @@ +position: 5 +label: Recipes +collapsible: true +collapsed: true +link: + type: generated-index + title: Recipes diff --git a/versioned_docs/version-3.x/reference/_category_.yml b/versioned_docs/version-3.x/reference/_category_.yml new file mode 100644 index 00000000..3e14b45e --- /dev/null +++ b/versioned_docs/version-3.x/reference/_category_.yml @@ -0,0 +1,7 @@ +position: 6 +label: Reference +collapsible: true +collapsed: true +link: + type: generated-index + title: Reference diff --git a/versioned_docs/version-3.x/reference/cli.md b/versioned_docs/version-3.x/reference/cli.md new file mode 100644 index 00000000..e6188f8b --- /dev/null +++ b/versioned_docs/version-3.x/reference/cli.md @@ -0,0 +1,251 @@ +--- +description: CLI references +sidebar_position: 2 +sidebar_label: CLI +--- + +# ZenStack CLI Reference + +## Usage + +``` +zenstack [options] [command] + +ΞΆ ZenStack is a Prisma power pack for building full-stack apps. + +Documentation: https://zenstack.dev. + +Options: + -v --version display CLI version + -h, --help display help for command + +Commands: + info [path] Get information of installed ZenStack and related packages. + init [options] [path] Initialize an existing project for ZenStack. + generate [options] Generates RESTful API and Typescript client for your data model. + repl [options] Start a REPL session. + format [options] Format a ZenStack schema file. + check [options] Check a ZenStack schema file for syntax or semantic errors. + help [command] Display help for a command. +``` + +## Sub Commands + +### info + +Get information of installed ZenStack and related packages. + +```bash +zenstack info [options] [path] +``` + +#### Arguments + +| Name | Description | Default | +| ---- | ------------ | -------------- | +| path | Project path | current folder | + +### init + +Initializes an existing project to use ZenStack. + +```bash +zenstack init [options] [path] +``` + +#### Arguments + +| Name | Description | Default | +| ---- | ------------ | -------------- | +| path | Project path | current folder | + +#### Options + +| Name | Description | Default | +| --------------------- | ------------------------------------------------ | ----------------------------------------- | +| --prisma | location of Prisma schema file to bootstrap from | <project path>/prisma/schema.prisma | +| -p, --package-manager | package manager to use: "npm", "yarn", or "pnpm" | auto detect | +| --no-version-check | do not check for new versions of ZenStack | false | + +#### Examples + +Initialize current folder with default settings. + +```bash +npx zenstack init +``` + +Initialize "myapp" folder with custom package manager and schema location. + +```bash +npx zenstack init -p pnpm --prisma prisma/my.schema myapp +``` + +### generate + +Generates Prisma schema and other artifacts as specified by "plugin"s in ZModel. + +```bash +zenstack generate [options] +``` + +#### Arguments + +| Name | Description | Default | +| ---- | ------------ | -------------- | +| path | Project path | current folder | + +#### Options + +| Name | Description | Default | +| --------------------- | ------------------------------------------------ | ---------------------- | +| --schema | schema file (with extension .zmodel) | ./schema.zmodel | +| -o, --output <path> | default output directory for TS/JS files generated by built-in plugins | node_modules/.zenstack | +| --with-plugins | only run specific plugins | | +| --without-plugins | exclude specific plugins | | +| --no-default-plugins | do not automatically run built-in plugins | false | +| --no-compile | do not compile the output of built-in plugins | false | +| --no-version-check | do not check for new versions of ZenStack | false | + +You can also specify the ZModel schema location in the "package.json" file of your project like the following: + +```json title="package.json" +{ + "zenstack": { + "schema": "./db/schema.zmodel" + } +} +``` + +#### Examples + +Generate with default settings. + +```bash +npx zenstack generate +``` + +Generate with custom schema location. + +```bash +npx zenstack generate --schema src/my.zmodel +``` + +### repl + +Starts a REPL session. You should run the command inside the package where you ran `zenstack generate`. + +```bash +npx zenstack repl +``` + +You can call PrismaClient methods interactively in the REPL session. The following variables are available in the REPL session. + +- `prisma` + + The original PrismaClient instance (without ZenStack enhancement). + +- `db` + + The ZenStack enhanced PrismaClient instance. + +You don't need to `await` the Prisma method call result. The REPL session will automatically await and print the result. + +#### Options + +| Name | Description | Default | +| ----- | ------------------- | ------- | +| --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> | Path to load PrismaClient module. | "node_modules/.prisma/client" | +| --load-path <path> | Path to load modules generated by ZenStack. | "node_modules/.zenstack" | + +#### Repl Commands + +You can use the following commands in the REPL session. + +- `.debug [on|off]` + + Toggle debug output. + +- `.table [on|off]` + + Toggle table format. + +- `.auth [user object]` + + Set current user. E.g.: `.auth { id: 1 }`. Run the command without argument to reset to anonymous user. + +#### Examples + +Start the session: +```bash +npx zenstack repl +``` + +Inside the session: +```ts +> prisma.user.findMany() +[ + { + id: '7aa301d2-7a29-4e1e-a041-822913a3ea78', + createdAt: 2023-09-05T04:04:43.793Z, + updatedAt: 2023-09-05T04:04:43.793Z, + email: 'yiming@whimslab.io', + ... + } +] + +> .auth { id: '7aa301d2-7a29-4e1e-a041-822913a3ea78' } +Auth user: { id: '7aa301d2-7a29-4e1e-a041-822913a3ea78' }. Use ".auth" to switch to anonymous. + +> .table +Table output: true + +> db.list.findMany({select: { title: true, private: true}}) +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ (index) β”‚ title β”‚ private β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ 0 β”‚ 'L1' β”‚ false β”‚ +β”‚ 1 β”‚ 'Wonderful new world' β”‚ false β”‚ +β”‚ 2 β”‚ 'Model for a space in which users can collaborate' β”‚ false β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +### format + +Format a ZenStack schema file. + +```bash +zenstack format [options] +``` + +#### Options + +| Name | Description | Default | +| --------------------- | ------------------------------------------------ | ---------------------- | +| --schema | schema file (with extension .zmodel) | ./schema.zmodel | + +You can also specify the ZModel schema location in the "package.json" file of your project like the following: + +```json title="package.json" +{ + "zenstack": { + "schema": "./db/schema.zmodel" + } +} +``` + +### check + +Check a ZenStack schema file for syntax or semantic errors. You can use the CLI's exit code to determine if the schema is valid. + +```bash +zenstack check [options] +``` + +#### Options + +| Name | Description | Default | +| --------------------- | ------------------------------------------------ | ---------------------- | +| --schema | schema file (with extension .zmodel) | ./schema.zmodel | diff --git a/versioned_docs/version-3.x/reference/error-handling.md b/versioned_docs/version-3.x/reference/error-handling.md new file mode 100644 index 00000000..07d31586 --- /dev/null +++ b/versioned_docs/version-3.x/reference/error-handling.md @@ -0,0 +1,43 @@ +--- +description: Error handling +sidebar_position: 8 +--- + +# Error Handling + +ZenStack's enhancements to Prisma clients are transparent proxies, so normal errors thrown from a Prisma client simply pass through, and you can handle them the same way as you do in a regular Prisma project. It's good to read these references from Prisma: + +- [Handling exceptions and errors](https://www.prisma.io/docs/concepts/components/prisma-client/handling-exceptions-and-errors) +- [Error message reference](https://www.prisma.io/docs/reference/api-reference/error-reference) + +The enhanced Prisma client can throw extra errors when an operation is rejected by [access policies](./zmodel-language#access-policy) or its data fails [validation rules](./zmodel-language#data-validation). To keep a consistent programming experience, a `PrismaClientKnownRequestError` is thrown with code [`P2004`](https://www.prisma.io/docs/reference/api-reference/error-reference#p2004) is used in such cases: + +```ts +throw new PrismaClientKnownRequestError(message, { + clientVersion: getVersion(), + code: 'P2004', + meta: ... +}); +``` + +The error contains a `meta` field providing more information about the error. It contains the following fields: + +- `reason` + + A string field indicating the detailed reason of the rejection. Its value can be one of the following: + + - *ACCESS_POLICY_VIOLATION* + + CRUD failed because of access policy violation. + + - *RESULT_NOT_READABLE* + + CRUD succeeded but the result was not readable (violating "read" access policies). + + - *DATA_VALIDATION_VIOLATION* + + CRUD failed because of a [data validation](./zmodel-language#data-validation) error. + +- `zodErrors` + + An object field containing the raw Zod validation errors. Only present if the `reason` is `DATA_VALIDATION_VIOLATION`. See [Zod documentation](https://zod.dev/?id=error-handling) for more details. diff --git a/versioned_docs/version-3.x/reference/limitations.md b/versioned_docs/version-3.x/reference/limitations.md new file mode 100644 index 00000000..96d02bc0 --- /dev/null +++ b/versioned_docs/version-3.x/reference/limitations.md @@ -0,0 +1,40 @@ +--- +description: Current limitations +sidebar_position: 100 +--- + +# Limitations + +This section lists the current limitations of ZenStack. + +### Sequential operations transaction + +[Sequential operations transaction](https://www.prisma.io/docs/concepts/components/prisma-client/transactions#sequential-prisma-client-operations) is not supported by enhanced Prisma clients yet. + +As a workaround, use [interactive transactions](https://www.prisma.io/docs/concepts/components/prisma-client/transactions#interactive-transactions) instead. + +### Minimum transaction isolation level + +To ensure access policies are properly enforced, the database's transaction isolation level should be set to at least [Repeatable Read](https://en.wikipedia.org/wiki/Isolation_(database_systems)#Repeatable_reads). This can be done by changing the settings of the database, or providing the `isolationLevel` option when creating `PrismaClient`: + +```ts +const prisma = new PrismaClient({ + transactionOptions: { + isolationLevel: Prisma.TransactionIsolationLevel.RepeatableRead, + }, +}); +``` + +If you don't want to change the global settings, alternatively you can set the `transactionIsolationLevel` option when calling ZenStack's `enhance` API. All the transactions initiated internally by ZenStack will use the specified isolation level. + +```ts +const db = enhance(prisma, { user }, { transactionIsolationLevel: 'RepeatableRead' }); +``` + +### MongoDB is not supported + +Right now, the focus of this project is SQL databases, and there's no plan to support MongoDB in the near future. + +### Cloudflare D1 database is not supported + +Prisma doesn't support interactive transactions with D1 databases. ZenStack relies on the feature to enforce access policies in certain cases. diff --git a/versioned_docs/version-3.x/reference/runtime-api.md b/versioned_docs/version-3.x/reference/runtime-api.md new file mode 100644 index 00000000..10d7cf39 --- /dev/null +++ b/versioned_docs/version-3.x/reference/runtime-api.md @@ -0,0 +1,130 @@ +--- +description: Runtime API references +sidebar_position: 3 +sidebar_label: Runtime API +--- + +# Runtime API Reference + +This document provides references for runtime APIs exported from the `@zenstackhq/runtime` package. + +### enhance + +#### Description + +Creates an enhanced wrapper for a `PrismaClient`. The return value has the same APIs as the original `PrismaClient`. + +#### Signature + +```ts +function enhance( + prisma: DbClient, + context?: EnhancementContext, + options?: EnhancementOptions +): DbClient; +``` + +##### Parameter `prisma` + +The PrismaClient instance to enhance. + +##### Parameter `context` + +The context to for evaluating access policies with the following typing. + +```ts +type EnhancementContext = { + user?: Record +}; +``` + +| Field | Description | +| ----- | ----------- | +| user | The user object that provides value for the `auth()` function call in access policies. If provided. Its shape should be consistent with the `User` model in your ZModel, with all fields optional except for id field(s). Pass `undefined` to represent an anonymous user, and the `auth()` function call will evaluate to `null` in that case. | + +##### Parameter `options` + +Options with the following typing. + +```ts +type TransactionIsolationLevel = + | 'ReadUncommitted' + | 'ReadCommitted' + | 'RepeatableRead' + | 'Snapshot' + | 'Serializable'; + +type SimpleEncryption = { + encryptionKey: Uint8Array; + decryptionKeys?: Uint8Array[]; +} + +type CustomEncryption = { + encrypt: (model: string, field: FieldInfo, plain: string) => Promise; + decrypt: (model: string, field: FieldInfo, cipher: string) => Promise; +}; + +type ValidationOptions = { + inputOnlyValidationForUpdate?: boolean; +}; + +type EnhancementOptions = { + kinds?: EnhancementKind[]; + logPrismaQuery?: boolean; + errorTransformer?: ErrorTransformer; + transactionMaxWait?: number; + transactionTimeout?: number; + transactionIsolationLevel?: TransactionIsolationLevel; + encryption?: SimpleEncryption | CustomEncryption; + validation?: ValidationOptions; +}; +``` + +| Field | Description | Default | +| ------------------------- | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------- | +| kinds | The kinds of enhancements to apply. By default all enhancements are applied. See [the next section](#enhancement-kinds) for more details. | All enhancement kinds | +| logPrismaQuery | Whether to log queries sent to Prisma client. Log will be emitted with "info" level, so please make sure you [turn that level on](https://www.prisma.io/docs/concepts/components/prisma-client/working-with-prismaclient/logging#log-to-stdout) when creating Prisma client | false | +| errorTransformer | A function for transforming error thrown by the enhanced `PrismaClient` into a custom one. | | +| transactionMaxWait | The `maxWait` option (in ms) passed to `prisma.$transaction()` call for transactions initiated by ZenStack. | Database default | +| transactionTimeout | The `timeout` option (in ms) passed to `prisma.$transaction()` call for transactions initiated by ZenStack. | Database default | +| transactionIsolationLevel | The `isolationLevel` option passed to `prisma.$transaction()` call for transactions initiated by ZenStack. | Database default | +| encryption | Field encryption settings. Only required when using the [field encryption](../guides/field-encryption.md) feature. | | +| validation.inputOnlyValidationForUpdate | By default, ZenStack validates an entity after "update" operation to ensure the final result satisfies validation rules as a whole. This implies if the record under update doesn't satisfy the rules prior to update, the update operation will fail even if the fields causing validation errors are not affected by the operation. You can set this option to `true` to let ZenStack only validate data contained in the input args. | false | + +#### Enhancement Kinds + +Here are the kinds of enhancements available: + +- `policy` + + Enforces model-level and field-level access policies defined with `@@allow`, `@@deny`, `@allow`, and `@deny`. + +- `validation` + + Validates create and update input data against rules defined with [data validation attributes](../reference/zmodel-language#data-validation). + +- `delegate` + + Support for modeling [polymorphic relations](../guides/polymorphism) with delegated types pattern. + +- `password` + + Automatically hashes fields marked with the `@password` attribute using `bcryptjs` before saving to the database. + +- `omit` + + Automatically omits fields marked with the `@omit` attribute from read results. + +- `encryption` + + Transparently encrypt and decrypt fields marked with the `@encrypted` attribute. See [this guide](../guides/field-encryption.md) for more details. + +#### Example + +```ts +const session = getSession(); +const enhancedClient = enhance(prisma, + { user: session.user }, + { kinds: ['policy', 'password']} +); +``` diff --git a/versioned_docs/version-3.x/reference/server-adapters/_category_.yml b/versioned_docs/version-3.x/reference/server-adapters/_category_.yml new file mode 100644 index 00000000..f6e05f3e --- /dev/null +++ b/versioned_docs/version-3.x/reference/server-adapters/_category_.yml @@ -0,0 +1,7 @@ +position: 7 +label: Server Adapters +collapsible: true +collapsed: true +link: + type: generated-index + title: Server Adapters diff --git a/versioned_docs/version-3.x/reference/server-adapters/_error-handling.md b/versioned_docs/version-3.x/reference/server-adapters/_error-handling.md new file mode 100644 index 00000000..db2a9b05 --- /dev/null +++ b/versioned_docs/version-3.x/reference/server-adapters/_error-handling.md @@ -0,0 +1,3 @@ +### Error Handling + +Refer to the specific sections for [RPC Handler](./api-handlers/rpc#http-status-code-and-error-responses) and [RESTful Handler](./api-handlers/rest#error-handling). diff --git a/versioned_docs/version-3.x/reference/server-adapters/_options.mdx b/versioned_docs/version-3.x/reference/server-adapters/_options.mdx new file mode 100644 index 00000000..dee02d69 --- /dev/null +++ b/versioned_docs/version-3.x/reference/server-adapters/_options.mdx @@ -0,0 +1,66 @@ +import CodeBlock from '@theme/CodeBlock'; + +- getPrisma (required) + +
{props.getPrisma}
+ + A callback for getting a PrismaClient instance for talking to the database. Usually you'll use an enhanced instance created with ZenStack's [`enhance`](../runtime-api#enhance) API to ensure access policies are enforced. + +- logger (optional) + +
{'LoggerConfig'}
+ + Configuration for customizing logging behavior. + +- modelMeta (optional) + +
{'ModelMeta'}
+ + Model metadata. By default loaded from the `node_module/.zenstack/model-meta` module. You can pass it in explicitly if you configured ZenStack to output to a different location. E.g.: `require('output/model-meta').default`. + +- zodSchemas (optional) + +
{'ModelZodSchema | boolean | undefined'}
+ + Provides the Zod schemas used for validating CRUD request input. The Zod schemas can be generated with the `@core/zod` plugin. Pass `true` for this option to load the schemas from the default location. If you configured `@core/zod` plugin to output to a custom location, you can load the schemas explicitly and pass the loaded module to this option. E.g.: + + ```ts + factory({ + ... + zodSchemas: require('./zod'), + }); + ``` + + Not passing this option or passing in `undefined` disables input validation. + +- handler (optional) + +
{'(req: RequestContext) => Promise'}
+ + The request handler function. This option determines the API endpoints and its input and output formats. Currently ZenStack supports two styles of APIs: RPC (the default) and RESTful. + + - RPC + + The goal of the RPC-style API handler is to fully mirror [PrismaClient's API](https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#model-queries) across the network, so that developers can continue enjoying the convenience and flexibility of Prisma's query syntax. This is the default choice for the `handler` option. + + The RPC-style handler can be created like: + + ```ts + import { RPCApiHandler } from '@zenstackhq/server/api'; + const handler = RPCApiHandler(); + ``` + + For more details, please check out [RPC API Handler](./api-handlers/rpc). + + - RESTful + + The goal of RESTful-style API handler is to provide a resource-centric RESTful API using [JSON:API](https://jsonapi.org/) as transportation format. + + The RESTful-style handler can be created like: + + ```ts + import { RestApiHandler } from '@zenstackhq/server/api'; + const handler = RestApiHandler({ endpoint: 'http://myhost/api' }); + ``` + + For more details, please check out [RESTful API Handler](./api-handlers/rest). diff --git a/versioned_docs/version-3.x/reference/server-adapters/_using-api.mdx b/versioned_docs/version-3.x/reference/server-adapters/_using-api.mdx new file mode 100644 index 00000000..1d16241c --- /dev/null +++ b/versioned_docs/version-3.x/reference/server-adapters/_using-api.mdx @@ -0,0 +1,137 @@ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +The APIs can be used in the following three ways: + +1. With generated client hooks + + ZenStack provides plugins to generate client hooks from the ZModel targeting the most popular frontend data fetching libraries: [TanStack Query](https://tanstack.com/query/latest) and [SWR](https://swr.vercel.app/). The generated hooks can be used to make API calls to the server adapters. Refer to the follow docs for detailed usage: + + - [`@zenstackhq/tanstack-query`](../plugins/tanstack-query) + - [`@zenstackhq/swr`](../plugins/swr) +

+ + :::info + The generated client hooks assumes the server adapter uses [RPC-style API handler](./api-handlers/rpc) (which is the default setting). + ::: + +1. With direct HTTP calls + + You can make direct HTTP calls to the server adapter using your favorite client libraries like `fetch` or `axios`. Refer to the documentation of the [API Handlers](./api-handlers/) for the API endpoints and data formats. + + Here's an example using `fetch`: + + + + + ```ts + // create a user with two posts + const r = await fetch(`/api/user/create`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + include: { posts: true }, + data: { + email: 'user1@abc.com', + posts: { + create: [{ title: 'Post 1' }, { title: 'Post 2' }], + }, + }, + }), + }); + + console.log(await r.json()); + ``` + + Output: + + ```json + { + "id": 1, + "email": "user1@abc.com", + "posts": [ + { + "id": 1, + "createdAt": "2023-03-14T07:45:04.036Z", + "updatedAt": "2023-03-14T07:45:04.036Z", + "title": "Post 1", + "authorId": 1 + }, + { + "id": 2, + "createdAt": "2023-03-14T07:45:04.036Z", + "updatedAt": "2023-03-14T07:45:04.036Z", + "title": "Post 2", + "authorId": 1 + } + ] + } + ``` + + + + + + ```ts + // create a user and attach two posts + const r = await fetch(`/api/user`, { + method: 'POST', + headers: { 'Content-Type': 'application/vnd.api+json' }, + body: JSON.stringify({ + data: { + type: 'user', + attributes: { + email: 'user1@abc.com' + }, + relationships: { + posts: { + data: [ + { type: 'post', id: 1 }, + { type: 'post', id: 2 } + ] + } + } + } + }) + }); + + console.log(await r.json()); + ``` + + Output: + + ```json + { + "jsonapi": { "version": "1.1" }, + "data": { + "type": "user", + "id": 1, + "attributes": { + "email": "user1@abc.com", + }, + "links": { + "self": "http://localhost/api/user/1", + }, + "relationships": { + "posts": { + "links": { + "self": "http://localhost/api/user/1/relationships/posts", + "related": "http://localhost/api/user/1/posts", + }, + "data": [ + { "type": "post", "id": 1 }, + { "type": "post", "id": 2 }, + ], + }, + }, + }, + } + ``` + + + + + +1. With third-party client generators + + ZenStack provides an [OpenAPI](../plugins/openapi) plugin for generating Open API 3.x specification from the ZModel. The generated OpenAPI spec can be used to generate client libraries for various languages and frameworks. For example, you can use [openapi-typescript](https://github.com/drwpow/openapi-typescript) to generate a typescript client. diff --git a/versioned_docs/version-3.x/reference/server-adapters/api-handlers/_data_type_serialization.md b/versioned_docs/version-3.x/reference/server-adapters/api-handlers/_data_type_serialization.md new file mode 100644 index 00000000..d22a602f --- /dev/null +++ b/versioned_docs/version-3.x/reference/server-adapters/api-handlers/_data_type_serialization.md @@ -0,0 +1,16 @@ +- `DateTime` + + ISO 8601 string + +- `Bytes` + + Base64-encoded string + +- `BigInt` + + String representation + +- `Decimal` + + String representation + \ No newline at end of file diff --git a/versioned_docs/version-3.x/reference/server-adapters/api-handlers/index.mdx b/versioned_docs/version-3.x/reference/server-adapters/api-handlers/index.mdx new file mode 100644 index 00000000..df4074ec --- /dev/null +++ b/versioned_docs/version-3.x/reference/server-adapters/api-handlers/index.mdx @@ -0,0 +1,31 @@ +--- +sidebar_position: 100 +--- + +import useBaseUrl from '@docusaurus/useBaseUrl'; +import ThemedImage from '@theme/ThemedImage'; + +# API Handlers + +ZenStack supports two API styles: ***rpc*** and ***rest***: + +- ***rpc*** style API directly mirrors Prisma client's API, thus provides endpoints like `[model]/findMany`, `[model]/create`, etc. Its input and output data format also aligns with that of Prisma client. + +- ***rest*** style API is designed to be more RESTful, thus provides endpoints like `[model]`, `[model]/[id]`, `[model]/[id]/relationships/[relationship]`, etc. It uses [JSON:API](https://jsonapi.org/) as its data format. + +API handlers are framework-agnostic and deals with canonicalized request and response objects. It's the responsibility of the server adapters to translate the framework-specific request and response types. You can use either of these two API handlers with any server adapter. + +The following diagram illustrates their relationships with server adapters. + + + +Checkout the reference pages for detailed information about these API handlers: + +- [RPC API Handler](./rpc) +- [RESTful API Handler](./rest) diff --git a/versioned_docs/version-3.x/reference/server-adapters/api-handlers/rest.mdx b/versioned_docs/version-3.x/reference/server-adapters/api-handlers/rest.mdx new file mode 100644 index 00000000..67365c6f --- /dev/null +++ b/versioned_docs/version-3.x/reference/server-adapters/api-handlers/rest.mdx @@ -0,0 +1,982 @@ +--- +description: RESTful-style API handler that provides resource-centric endpoints +sidebar_position: 2 +title: RESTful API Handler +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; +import DataTypeSerialization from './_data_type_serialization.md'; + +# RESTful API Handler + +## Introduction + +The RESTful-style API handler exposes CRUD APIs as RESTful endpoints using [JSON:API](https://jsonapi.org/) as transportation format. The API handler is not meant to be used directly; instead, you should use it together with a [server adapter](../../../category/server-adapters) which handles the request and response API for a specific framework. + +It can be created as the following: + + + + +```ts title='/src/app/api/model/[...path]/route.ts' +import { NextRequestHandler } from '@zenstackhq/server/next'; +import { RestApiHandler } from '@zenstackhq/server/api'; +import { getPrisma } from '~/lib/db'; + +const handler = NextRequestHandler({ + getPrisma, + useAppDir: true, + handler: RestApiHandler({ endpoint: 'http://myhost/api' }) +}); + +export { + handler as GET, + handler as POST, + handler as PUT, + handler as PATCH, + handler as DELETE, +}; +``` + + + + + +```ts title='/src/hooks.server.ts' +import { SvelteKitHandler } from '@zenstackhq/server/sveltekit'; +import { RestApiHandler } from '@zenstackhq/server/api'; +import { getPrisma } from './lib/db'; + +export const handle = SvelteKitHandler({ + prefix: '/api/model', + getPrisma, + handler: RestApiHandler({ endpoint: 'http://myhost/api/model' }) +}); +``` + + + + + +```ts title='/server/api/model/[...].ts' +import { createEventHandler } from '@zenstackhq/server/nuxt'; +import { RestApiHandler } from '@zenstackhq/server/api'; +import { getPrisma } from './lib/db'; + +export default createEventHandler({ + handler: RestApiHandler({ endpoint: 'http://myhost/api/model' }) + getPrisma +}); +``` + + + + + +The factory function accepts an options object with the following fields: + +- endpoint + + Required. A `string` field representing the base URL of the RESTful API, used for generating resource links. + +- pageSize + + Optional. A `number` field representing the default page size for listing resources and relationships. Defaults to 100. Set to Infinity to disable pagination. + +- modelNameMapping + + Optional. An `Record` value that provides a mapping from model names (as defined in ZModel) to URL path names. This is useful for example when you want to use plural names in URL endpoints: + + ```ts + // endpoint for accessing User model will then be ".../users" + RestApiHandler({ + modelNameMapping: { + User: 'users' + } + }) + ``` + + The mapping can be partial. You only need to specify the model names that you want to override. If a mapping is provided, only the mapped url path is valid, and accessing to unmapped path will be denied. + + +## Endpoints and Features + +The RESTful API handler conforms to the the [JSON:API](https://jsonapi.org/format/) v1.1 specification for its URL design and input/output format. The following sections list the endpoints and features are implemented. The examples refer to the following schema modeling a blogging app: + +```zmodel +model User { + id Int @id @default(autoincrement()) + email String + posts Post[] +} + +model Profile { + id Int @id @default(autoincrement()) + gender String + user User @relation(fields: [userId], references: [id]) + userId Int @unique +} + +model Post { + id Int @id @default(autoincrement()) + title String + published Boolean @default(false) + viewCount Int @default(0) + author User @relation(fields: [authorId], references: [id]) + authorId Int + comments Comment[] +} + +model Comment { + id Int @id @default(autoincrement()) + content String + post Post @relation(fields: [postId], references: [id]) + postId Int +} +``` + +### Listing resources + +A specific type of resource can be listed using the following endpoint: + +``` +GET /:type +``` + +#### Status codes + +- 200: The request was successful and the response body contains the requested resources. +- 400: The request was malformed. +- 403: The request was forbidden. +- 404: The requested resource type does not exist. +- 422: The request violated data validation rules. + +#### Examples + +```ts +GET /post +``` + +```json +{ + "meta": { + "total": 1 + }, + "data": [ + { + "attributes": { + "authorId": 1, + "published": true, + "title": "My Awesome Post", + "viewCount": 0 + }, + "id": 1, + "links": { + "self": "http://myhost/api/post/1" + }, + "relationships": { + "author": { + "data": { "id": 1, "type": "user" }, + "links": { + "related": "http://myhost/api/post/1/author/1", + "self": "http://myhost/api/post/1/relationships/author/1" + } + } + }, + "type": "post" + } + ], + "jsonapi": { + "version": "1.1" + }, + "links": { + "first": "http://myhost/api/post?page%5Blimit%5D=100", + "last": "http://myhost/api/post?page%5Boffset%5D=0", + "next": null, + "prev": null, + "self": "http://myhost/api/post" + } +} +``` + +### Fetching a resource + +A unique resource can be fetched using the following endpoint: + +```ts +GET /:type/:id +``` + +#### Status codes + +- 200: The request was successful and the response body contains the requested resource. +- 400: The request was malformed. +- 403: The request was forbidden. +- 404: The requested resource type or ID does not exist. + +#### Examples + +```ts +GET /post/1 +``` + +```json +{ + "data": { + "attributes": { + "authorId": 1, + "published": true, + "title": "My Awesome Post", + "viewCount": 0 + }, + "id": 1, + "links": { + "self": "http://myhost/api/post/1" + }, + "relationships": { + "author": { + "data": { "id": 1, "type": "user" }, + "links": { + "related": "http://myhost/api/post/1/author/1", + "self": "http://myhost/api/post/1/relationships/author/1" + } + } + }, + "type": "post" + }, + "jsonapi": { + "version": "1.1" + }, + "links": { + "self": "http://myhost/api/post/1" + } +} +``` + +### Fetching relationships + +A resource's relationships can be fetched using the following endpoint: + +```ts +GET /:type/:id/relationships/:relationship +``` + +#### Status codes + +- 200: The request was successful and the response body contains the requested relationships. +- 400: The request was malformed. +- 403: The request was forbidden. +- 404: The requested resource type, ID, or relationship does not exist. + +#### Examples + +1. Fetching a to-one relationship + + ```ts + GET /post/1/relationships/author + ``` + + ```json + { + "data" : { "id" : 1, "type" : "user" }, + "jsonapi" : { + "version" : "1.1" + }, + "links" : { + "self" : "http://myhost/api/post/1/relationships/author" + } + } + ``` + +1. Fetching a to-many relationship + + ```ts + GET /user/1/relationships/posts + ``` + + ```json + { + "data" : [ + { "id" : 1, "type" : "post" }, + { "id" : 2, "type" : "post" } + ], + "jsonapi" : { + "version" : "1.1" + }, + "links" : { + "first" : "http://myhost/api/user/1/relationships/posts?page%5Blimit%5D=100", + "last" : "http://myhost/api/user/1/relationships/posts?page%5Boffset%5D=0", + "next" : null, + "prev" : null, + "self" : "http://myhost/api/user/1/relationships/posts" + } + } + ``` + +### Fetching related resources + +```ts +GET /:type/:id/:relationship +``` + +#### Status codes + +- 200: The request was successful and the response body contains the requested relationship. +- 400: The request was malformed. +- 403: The request was forbidden. +- 404: The requested resource type, ID, or relationship does not exist. + +#### Examples + +```ts +GET /post/1/author +``` + +```json +{ + "data" : { + "attributes" : { + "email" : "emily@zenstack.dev", + "name" : "Emily" + }, + "id" : 1, + "links" : { + "self" : "http://myhost/api/user/1" + }, + "relationships" : { + "posts" : { + "links" : { + "related" : "http://myhost/api/user/1/posts", + "self" : "http://myhost/api/user/1/relationships/posts" + } + } + }, + "type" : "user" + }, + "jsonapi" : { + "version" : "1.1" + }, + "links" : { + "self" : "http://myhost/api/post/1/author" + } +} +``` + +### Fine-grained data fetching + +#### Filtering + +You can use the `filter[:selector1][:selector2][...]=value` [query parameter family](https://jsonapi.org/format/#query-parameters-families) to filter resource collections or relationship collections. + +##### Examples + +1. Equality filter against plain field + + ```ts + GET /api/post?filter[published]=false + ``` + +1. Equality filter against relationship + + Relationship field can be filtered directly by its id. + + ```ts + GET /api/post?filter[author]=1 + ``` + + If the relationship is to-many, the filter has "some" semantic and evaluates to `true` if any of the items in the relationship matches. + + ```ts + GET /api/user?filter[posts]=1 + ``` + +1. Filtering with multiple values + + Multiple filter values can be separated by comma. Items statisfying any of the values will be returned. + + ```ts + GET /api/post?filter[author]=1,2 + ``` + +1. Multiple filters + + A request can carry multiple filters. Only items statisfying all filters will be returned. + + ```ts + GET /api/post?filter[author]=1&filter[published]=true + ``` + +1. Deep filtering + + A filter can carry multiple field selectors to reach into relationships. + + ```ts + GET /api/post?filter[author][name]=Emily + ``` + + When reaching into a to-many relationship, the filter has "some" semantic and evaluates to `true` if any of the items in the relationship matches. + + ``` + GET /api/user?filter[posts][published]=true + ``` + +1. Filtering with comparison operators + + Filters can go beyond equality by appending an "operator suffix". + + ```ts + GET /api/post?filter[viewCount$gt]=100 + ``` + + The following operators are supported: + + - **$lt** + + Less than + + - **$lte** + + Less than or equal to + + - **$gt** + + Greater than + + - **$gte** + + Greater than or equal to + + - **$contains** + + String contains + + - **$icontains** + + Case-insensitive string contains + + - **$search** + + String full-text search + + - **$startsWith** + + String starts with + + - **$endsWith** + + String ends with + + - **$has** + + Collection has value + + - **$hasEvery** + + Collection has every element in value + + - **$hasSome** + + Collection has some elements in value + + - **$isEmpty** + + Collection is empty + +#### Sorting + +You can use the `sort` query parameter to sort resource collections or relationship collections. The value of the parameter is a comma-separated list of fields names. The order of the fields in the list determines the order of sorting. By default, sorting is done in ascending order. To sort in descending order, prefix the field name with a minus sign. + +##### Examples + +```ts +GET /api/post?sort=createdAt,-viewCount +``` + +#### Pagination + +When creating a RESTful API handler, you can pass in a `pageSize` option to control pagination behavior of fetching a collection of resources, related resources, and relationships. By default the page size is 100, and you can disable pagination by setting `pageSize` option to `Infinity`. + +When fetching a collection resource or relationship, you can use the `page[offset]=value` and `page[limit]=value` [query parameter family](https://jsonapi.org/format/#query-parameters-families) to fetch a specific page. They're mapped to `skip` and `take` parameters in the query arguments sent to PrismaClient. + +The response data of collection fetching contains pagination links that facilitate navigating through the collection. The "meta" section also contains the total count available. E.g.: + +```json +{ + "meta": { + "total": 10 + }, + "data" : [ + ... + ], + "links" : { + "first" : "http://myhost/api/post?page%5Blimit%5D=2", + "last" : "http://myhost/api/post?page%5Boffset%5D=4", + "next" : "http://myhost/api/post?page%5Boffset%5D=4&page%5Blimit%5D=2", + "prev" : "http://myhost/api/post?page%5Boffset%5D=0&page%5Blimit%5D=2", + "self" : "http://myhost/api/post" + } +} +``` + +##### Examples + +1. Fetching a specific page of resources + + ```ts + GET /api/post?page[offset]=10&page[limit]=5 + ``` + +1. Fetching a specific page of relationships + + ```ts + GET /api/user/1/relationships/posts?page[offset]=10&page[limit]=5 + ``` + +1. Fetching a specific page of related resources + + ```ts + GET /api/user/1/posts?page[offset]=10&page[limit]=5 + ``` + +#### Including related resources + +You can use the `include` query parameter to include related resources in the response. The value of the parameter is a comma-separated list of fields names. Field names can contain dots to reach into nested relationships. + +When including related resources, the response data takes the form of [Compound Documents](https://jsonapi.org/format/#document-compound-documents) and contains a `included` field carrying normalized related resources. E.g.: + +```json +{ + "data" : [ + { + "attributes" : { + ... + }, + "id" : 1, + "relationships" : { + "author" : { + "data" : { "id" : 1, "type" : "user" } + } + }, + "type" : "post" + } + ], + "included" : [ + { + "attributes" : { + "email" : "emily@zenstack.dev", + "name" : "Emily" + }, + "id" : 1, + "links" : { + "self" : "http://myhost/api/user/1" + }, + "relationships" : { + "posts" : { + "links" : { + "related" : "http://myhost/api/user/1/posts", + "self" : "http://myhost/api/user/1/relationships/posts" + } + } + }, + "type" : "user" + } + ] +} +``` + +##### Examples + +1. Including a direct relationship + + ```ts + GET /api/post?include=author + ``` + +1. Including a deep relationship + + ```ts + GET /api/post?include=author.profile + ``` + +1. Including multiple relationships + + ```ts + GET /api/post?include=author,comments + ``` + +### Creating a resource + +A new resource can be created using the following endpoint: + +``` +POST /:type +``` + +#### Status codes + +- 201: The request was successful and the resource was created. +- 400: The request was malformed. +- 403: The request was forbidden. +- 404: The requested resource type does not exist. + +#### Examples + +1. Creating a resource + ```json + POST /user + { + "data": { + "type": "user", + "attributes": { + "name": "Emily", + "email": "emily@zenstack.dev" + } + } + } + ``` + +1. Creating a resource with relationships attached + + ```json + POST /user + { + "data": { + "type": "user", + "attributes": { + "name": "Emily", + "email": "emily@zenstack.dev" + }, + "relationships": { + "posts": { + "data": [{ "type": "post", "id": 1 }] + } + } + } + } + ``` + +### Updating a resource + +A resource can be updated using the following endpoints: + +```ts +PUT /:type/:id +PATCH /:type/:id +``` + +Both `PUT` and `PATCH` do partial update and has exactly the same behavior. + +:::info +Besides plain fields, you can also include relationships in the request body. Please note that this won't update the related resource; instead if only replaces the relationships. If you update a to-many relationship, the new collection will entirely replace the old one. + +Relationships can also be manipulated directly. See [Manipulating Relationships](#manipulating-relationships) for more details. +::: + +#### Status codes + +- 200: The request was successful and the resource was updated. +- 400: The request was malformed. +- 403: The request was forbidden. +- 404: The requested resource type or ID does not exist. + +#### Examples + +1. Updating a resource + + ```json + PUT /post/1 + { + "data": { + "type": "post", + "attributes": { + "title": "My Awesome Post" + } + } + } + ``` + +1. Updating a resource's relationships + + ```json + PUT /user/1 + { + "data": { + "type": "user", + "relationships": { + "posts": { + "data": [{ "type": "post", "id": 2 }] + } + } + } + } + ``` + + +### Upserting a resource + +JSON:API didn't specify a convention for "upsert" operations. ZenStack uses a variation of the "create" operation to represent "upsert", and uses the request meta to indicate the intention. See details in [examples](#examples-10). + +``` +POST /:type +``` + +#### Status codes + +- 201: The request was successful and the resource was created. +- 200: The request was successful and the resource was updated. +- 400: The request was malformed. +- 403: The request was forbidden. +- 404: The requested resource type does not exist. + +#### Examples + +```json +POST /user +{ + "data": { + "type": "user", + "attributes": { + "id": 1, + "name": "Emily", + "email": "emily@zenstack.dev" + } + }, + "meta": { + "operation": "upsert", + "matchFields": ["id"], + } +} +``` + +The `meta.operation` field must be "upsert", and the `meta.matchFields` field must be an array of field names that are used to determine if the resource already exists. If an existing resource is found, "update" operation is conducted, otherwise "create". The `meta.matchFields` fields must be unique fields, and they must have corresponding entries in `data.attributes`. + +### Deleting a resource + +A resource can be deleted using the following endpoint: + +#### Status codes + +- 204: The request was successful and the resource was deleted. +- 403: The request was forbidden. +- 404: The requested resource type or ID does not exist. + +```ts +DELETE /:type/:id +``` + +### Manipulating relationships + +Relationships can be manipulated using the following endpoints: + +#### Adding to a to-many relationship + +```ts +POST /:type/:id/relationships/:relationship +``` + +##### Status codes + +- 200: The request was successful and the relationship was updated. +- 403: The request was forbidden. +- 404: The requested resource type, ID, or relationship does not exist. + +##### Examples + +```json +POST /user/1/relationships/posts +{ + "data": [ + { "type": "post", "id": "1" }, + { "type": "post", "id": "2" } + ] +} +``` + +#### Updating a relationship (to-one or to-many) + +```ts +PUT /:type/:id/relationships/:relationship +PATCH /:type/:id/relationships/:relationship +``` + +:::info +`PUT` and `PATCH` has exactly the same behavior and both relace the existing relationships with the new ones entirely. +::: + +##### Status codes + +- 200: The request was successful and the relationship was updated. +- 403: The request was forbidden. +- 404: The requested resource type, ID, or relationship does not exist. + +##### Examples + +1. Replacing a to-many relationship + + ```json + PUT /user/1/relationships/posts + { + "data": [ + { "type": "post", "id": "1" }, + { "type": "post", "id": "2" } + ] + } + ``` + +1. Replacing a to-one relationship + + ```json + PUT /post/1/relationships/author + { + "data": { "type": "user", "id": "2" } + } + ``` + +1. Clearing a to-many relationship + + ```json + PUT /user/1/relationships/posts + { + "data": [] + } + ``` + +1. Clearing a to-one relationship + + ```json + PUT /post/1/relationships/author + { + "data": null + } + ``` + +## Compound ID Fields + +Prisma allows a model to have compound ID fields, e.g.: + +```zmodel +model Post { + id1 Int + id2 Int + @@id([id1, id2]) +} +``` + +The JSON:API specification doesn't have a native way to represent compound IDs. To mitigate this limitation, when returning an entity with compound IDs, ZenStack synthesizes an "id" field to carry the ID values joined with underscore: + +```json +{ + "data": { + "id": "1_2", + "attributes": { + "id1": 1, + "id2": 2, + ... + }, + "links" : { + "self" : "http://localhost:3100/api/model/post/1_2" + }, + ... + } +} +``` + +You can use this ID value convension in places where an ID is needed, e.g., reading a single entity. + +```ts +GET /post/1_2 +``` + +Limitations: + +1. Joining ID values with underscore implies that the ID values themselves cannot contain underscores. We'll make the separator configurable in the future. +1. Prisma allows you to create a name for the compound ID field. Such usage is not yet supported by the RESTful API handler. + +> *Special thanks to [Thomas Sunde Nielsen](https://github.com/thomassnielsen) for implementing this feature!* + +## Serialization + +ZenStack uses [superjson](https://github.com/blitz-js/superjson) to serialize and deserialize data. Superjson generates two parts during serialization: + +- json: + + The JSON-compatible serialization result. + +- meta: + + The serialization metadata including information like field types that facilitates deserialization. + +If the data only involves simple data types, the serialization result is the same as regular `JSON.stringify`, and no `meta` part is generated. However, for complex data types (like `Bytes`, `Decimal`, etc.), a `meta` object will be generated, which needs to be carried along when sending the request, and will also be included in the response. + +When sending requests, if superjson-serializing the request body results in a `meta` object, it should be put into a ```{ "serialization": meta }``` object and included in the `meta` field of the request body. For example, if you have a `bytes` field of type `Bytes`, the request body should look like: + +```json +POST /post +{ + "data": { + "type": "post", + "attributes": { + ... + "bytes": "AQID" // base64-encoded bytes + } + }, + "meta": { + "serialization": {"values": { "data.attributes.bytes": [[ "custom", "Bytes"]] } } + } +} +``` + +Correspondingly, the response body of a query may look like: + +```json +GET /post/1 +{ + "data": { + "id": "1", + "type": "post", + "attributes": { + ... + "bytes": "AQID" // base64-encoded bytes + } + }, + "meta": { + "serialization": {"values": { "data.attributes.bytes": [[ "custom", "Bytes"]] } } + } +} +``` + +You should use the `meta.serialization` field value to superjson-deserialize the response body. + +### Data Type Serialization Format + + + +## Error Handling + +An error response is an object containing the following fields: + +- errors + + An array of error objects, each containing the following fields: + + - code: `string`, error code + - status: `number`, HTTP status code + - title: `string`, error title + - detail: `string`, error detail + - prismaCode: `string`, Prisma error code, if the error is thrown by Prisma + +### Example + +```json +{ + "errors" : [ + { + "code" : "unsupported-model", + "detail" : "Model foo doesn't exist", + "status" : 404, + "title" : "Unsupported model type" + } + ] +} +``` diff --git a/versioned_docs/version-3.x/reference/server-adapters/api-handlers/rpc.mdx b/versioned_docs/version-3.x/reference/server-adapters/api-handlers/rpc.mdx new file mode 100644 index 00000000..ecfd1c96 --- /dev/null +++ b/versioned_docs/version-3.x/reference/server-adapters/api-handlers/rpc.mdx @@ -0,0 +1,275 @@ +--- +description: RPC-style API handler that fully mirrors PrismaClient's query API +sidebar_position: 1 +title: RPC API Handler +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; +import DataTypeSerialization from './_data_type_serialization.md'; + +# RPC API Handler + +## Introduction + +The RPC-style API handler exposes CRUD endpoints that fully mirror [PrismaClient's query API](https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#model-queries). Consuming the APIs feels like making RPC calls to a PrismaClient then. The API handler is not meant to be used directly; instead, you should use it together with a [server adapter](../../../category/server-adapters) which handles the request and response API for a specific framework. + +It can be created and used as the following: + + + + +```ts title='/src/app/api/model/[...path]/route.ts' +import { NextRequestHandler } from '@zenstackhq/server/next'; +import { RestApiHandler } from '@zenstackhq/server/api'; +import { getPrisma } from '~/lib/db'; + +const handler = NextRequestHandler({ + getPrisma, + useAppDir: true, + handler: RPCApiHandler() // you can also omit it since `RPCApiHandler` is the default +}); + +export { + handler as GET, + handler as POST, + handler as PUT, + handler as PATCH, + handler as DELETE, +}; +``` + + + + + +```ts title='/src/hooks.server.ts' +import { SvelteKitHandler } from '@zenstackhq/server/sveltekit'; +import { RPCApiHandler } from '@zenstackhq/server/api'; +import { getPrisma } from './lib/db'; + +export const handle = SvelteKitHandler({ + prefix: '/api/model', + handler: RPCApiHandler(), // you can also omit it since `RPCApiHandler` is the default + getPrisma +}); +``` + + + + + +```ts title='/server/api/model/[...].ts' +import { createEventHandler } from '@zenstackhq/server/nuxt'; +import { RPCApiHandler } from '@zenstackhq/server/api'; +import { getPrisma } from './lib/db'; + +export default createEventHandler({ + handler: RPCApiHandler(), // you can also omit it since `RPCApiHandler` is the default + getPrisma +}); +``` + + + + + +## Wire Format + +### Input + +For endpoints using `GET` and `DELETE` Http verbs, the query body is serialized and passed as the `q` query parameter. E.g.: + +```ts +GET /api/post/findMany?q=%7B%22where%22%3A%7B%22public%22%3Atrue%7D%7D +``` + +- Endpoint: /api/post/findMany +- Query parameters: `q` -> `{ "where" : { "public": true } }` + +For endpoints using other HTTP verbs, the query body is passed as `application/json` in the request body. E.g.: + +```json +POST /api/post/create +{ "data": { "title": "Hello World" } } +``` + +### Output + +The output shape conforms to the data structure returned by the corresponding PrismaClient API, wrapped into a `data` field. E.g.: + +```json +GET /api/post/findMany + +{ + "data": [ { "id": 1, "title": "Hello World" } ] +} +``` + +### Serialization + +This section explains the details about data serialization. If you're using generated hooks to consume the API, the generated code already automatically deals with serialization for you, and you don't need to do any further processing. + +ZenStack uses [superjson](https://github.com/blitz-js/superjson) to serialize and deserialize data - including the `q` query parameter, the request body, and the response body. Superjson generates two parts during serialization: + +- json: + + The JSON-compatible serialization result. + +- meta: + + The serialization metadata including information like field types that facilitates deserialization. + +If the data only involves simple data types, the serialization result is the same as regular `JSON.stringify`, and no `meta` part is generated. However, for complex data types (like `Bytes`, `Decimal`, etc.), a `meta` object will be generated, which needs to be carried along when sending the request, and will also be included in the response. + +The following part explains how the `meta` information is included for different situations: + +- The `q` query parameter + + If during superjson-serialization of the `q` parameter, a `meta` object is generated, it should be put into an object `{ serialization: meta }`, JSON-stringified, and included as an additional query parameter `meta`. For example, if you have a field named `bytes` of `Bytes` type, and you may want to query with a filter like ```{ where: { bytes: Buffer.from([1,2,3]) } }```. Superjson-serializing the query object results in: + ```json + { + "json": { "where": { "bytes": "AQID" } }, // base-64 encoded bytes + "meta": { "values": { "where.bytes": [["custom","Bytes"]] } } + } + ``` + Your query URL should look like: + ```json + GET /api/post/findMany?q={"where":{"bytes":"AQID"}}&meta={"serialization":{"values":{"where.bytes":[["custom","Bytes"]]}}} + ``` + +- The request body + + If during superjson-serialization of the request body, a `meta` object is generated, it should be put into an object `{ serialization: meta }`, and included as an additional field `meta` field in the request body. For example, if you have a field named `bytes` of `Bytes` type, and you may want to create a record with a value like ```{ data: { bytes: Buffer.from([1,2,3]) } }```. Superjson-serializing the request body results in: + ```json + { + "json": { "bytes": "AQID" }, // base-64 encoded bytes + "meta": { "values": { "bytes": [[ "custom", "Bytes" ]] } } + } + ``` + Your request body should look like: + ```json + POST /api/post/create + + { + "data": { "bytes": "AQID" }, + "meta": { "serialization": {"values": { "bytes": [[ "custom","Bytes" ]] } } } + } + ``` + +- The response body + + If during superjson-serialization of the response body, a `meta` object is generated, it will be put into an object `{ serialization: meta }`, and included as an additional field `meta` field in the response body. For example, if you have a field named `bytes` of `Bytes` type, and a `findFirst` query returns ```{ id: 1, bytes: Buffer.from([1,2,3]) }```. Superjson-serializing the request body results in: + ```json + { + "json": { "id": 1, "bytes":"AQID" }, // base-64 encoded bytes + "meta": { "values": { "bytes": [[ "custom", "Bytes" ]] } } + } + ``` + Your response body will look like: + ```json + GET /api/post/findFirst + + { + "data": { "id": 1, "bytes": "AQID" }, + "meta": { "serialization": {"values": { "bytes": [[ "custom","Bytes"]] } } } + } + ``` + + You should use the meta.serialization field value to superjson-deserialize the response body. + +#### Data Type Serialization Format + + + +## Endpoints + +- **[model]/findMany** + + _Http method:_ `GET` + +- **[model]/findUnique** + + _Http method:_ `GET` + +- **[model]/findFirst** + + _Http method:_ `GET` + +- **[model]/count** + + _Http method:_ `GET` + +- **[model]/aggregate** + + _Http method:_ `GET` + +- **[model]/groupBy** + + _Http method:_ `GET` + +- **[model]/create** + + _Http method:_ `POST` + +- **[model]/createMany** + + _Http method:_ `POST` + +- **[model]/update** + + _Http method:_ `PATCH` or `PUT` + +- **[model]/updateMany** + + _Http method:_ `PATCH` or `PUT` + +- **[model]/upsert** + + _Http method:_ `POST` + +- **[model]/delete** + + _Http method:_ `DELETE` + +- **[model]/deleteMany** + + _Http method:_ `DELETE` + +- **[model]/check** + + _Http method:_ `GET` + +## HTTP Status Code and Error Responses + +### Status code + +The HTTP status code used by the endpoints follows the following rules: + +- `create` and `createMany` use `201` for success. Other endpoints use `200`. +- `403` is used for to indicate the request is denied due to lack of permissions, usually caused by access policy violation. +- `400` is used for invalid requests, e.g., malformed request body. +- `422` is used for data validation errors. +- `500` is used for other unexpected errors. + +### Error response format + +```ts +{ + // true to indicate the failure is due to a Prisma error + prisma?: boolean; + + // true to indicate the failure is due to access policy violation + rejectedByPolicy?: boolean; + + // original Prisma error code, available when `prisma` is true + code?: string; + + // error message + message: string; + + // extra reason about why a failure happened (e.g., 'RESULT_NOT_READABLE' indicates + // a mutation succeeded but the result cannot be read back due to access policy) + reason?: string; +} +``` diff --git a/versioned_docs/version-3.x/reference/server-adapters/elysia.mdx b/versioned_docs/version-3.x/reference/server-adapters/elysia.mdx new file mode 100644 index 00000000..41582343 --- /dev/null +++ b/versioned_docs/version-3.x/reference/server-adapters/elysia.mdx @@ -0,0 +1,69 @@ +--- +title: Elysia +description: Adapter for integrating with Elysia +sidebar_position: 8 +--- + +import ErrorHandling from './_error-handling.md'; +import AdapterOptions from './_options.mdx'; +import UsingAPI from './_using-api.mdx' + +# Elysia Adapter + +The `@zenstackhq/server/elysia` module provides a quick way to install a CRUD middleware onto an [Elysia](https://elysiajs.com/) app. Combined with ZenStack's power of enhancing Prisma with access policies, you can achieve a secure data backend without manually coding it. + +This feature is contributed by [@rodrigoburigool](https://github.com/rodrigoburigool). + +### Installation + +```bash +bun install @zenstackhq/server +``` + +### Mounting the API + +You can use the `createElysiaHandler` API to create an Elysia request handler that handles CRUD requests automatically: + +```ts +import { PrismaClient } from '@prisma/client'; +import { Elysia, Context } from 'elysia'; +import { enhance } from '@zenstackhq/runtime'; +import { createElysiaHandler } from '@zenstackhq/server/elysia'; + +const prisma = new PrismaClient(); + +const app = new Elysia({ prefix: '/api' }); + +// install the CRUD middleware under route "/api/crud" +app.group('/crud', (app) => + app.use( + createElysiaHandler({ + getPrisma: (context) => enhance(prisma, { user: getCurrentUser(context) }), + basePath: '/api/crud', + }) + ) +); + +function getCurrentUser(context: Context) { + // the implementation depends on your authentication mechanism + ... +} + +app.listen(3000); +``` + +The middleware factory takes the following options to initialize: + + + +- basePath (optional) + +
string
+ + Optional base path to strip from the request path before passing to the API handler. E.g., if your CRUD handler is mounted at `/api/crud`, set this field to `'/api/crud'`. + +### Using the API + + + + diff --git a/versioned_docs/version-3.x/reference/server-adapters/express.mdx b/versioned_docs/version-3.x/reference/server-adapters/express.mdx new file mode 100644 index 00000000..abc413ec --- /dev/null +++ b/versioned_docs/version-3.x/reference/server-adapters/express.mdx @@ -0,0 +1,66 @@ +--- +title: Express.js +description: Adapter for integrating with Express.js +sidebar_position: 4 +--- + +import ErrorHandling from './_error-handling.md'; +import AdapterOptions from './_options.mdx'; +import UsingAPI from './_using-api.mdx' + +# Express.js Adapter + +The `@zenstackhq/server/express` module provides a quick way to install API routes onto a [Express.js](https://expressjs.com/) project for database CRUD operations. Combined with ZenStack's power of enhancing Prisma with access policies, it's surprisingly simple to achieve a secure data backend without manually coding it. + +### Installation + +```bash +npm install @zenstackhq/server +``` + +### Mounting the API + +You can integrate ZenStack into your project with the `ZenStackMiddleware` [express middleware](https://expressjs.com/en/guide/using-middleware.html): + +```ts +import { PrismaClient } from '@prisma/client'; +import { enhance } from '@zenstackhq/runtime'; +import { ZenStackMiddleware } from '@zenstackhq/server/express'; +import express from 'express'; + +const prisma = new PrismaClient(); +const app = express(); + +app.use(express.json()); + +app.use( + '/api/model', + ZenStackMiddleware({ + // getSessionUser extracts the current session user from the request, its + // implementation depends on your auth solution + getPrisma: (request) => enhance(prisma, { user: getSessionUser(request) }), + }) +); +``` + +The Express.js adapter takes the following options to initialize: + + + +- sendResponse (optional) + +
boolean
+ + Controls if the middleware directly sends a response. If set to false, the response is stored in the `res.locals` object and then the middleware calls the `next()` function to pass the control to the next middleware. Subsequent middleware or request handlers need to make sure to send a response. + + Defaults to `true`. + +### Using the API + + + + + +### Fully working example + +You can find a fully working example [here](https://github.com/zenstackhq/docs-tutorial-express). diff --git a/versioned_docs/version-3.x/reference/server-adapters/fastify.mdx b/versioned_docs/version-3.x/reference/server-adapters/fastify.mdx new file mode 100644 index 00000000..be568e77 --- /dev/null +++ b/versioned_docs/version-3.x/reference/server-adapters/fastify.mdx @@ -0,0 +1,56 @@ +--- +title: Fastify +description: Adapter for integrating with Fastify +sidebar_position: 5 +--- + +import ErrorHandling from './_error-handling.md'; +import AdapterOptions from './_options.mdx'; +import UsingAPI from './_using-api.mdx'; + +# Fastify Adapter + +The `@zenstackhq/server/fastify` module provides a quick way to install API routes onto a [Fastify](https://www.fastify.io/) project for database CRUD operations. Combined with ZenStack's power of enhancing Prisma with access policies, it's surprisingly simple to achieve a secure data backend without manually coding it. + +### Installation + +```bash +npm install @zenstackhq/server +``` + +### Mounting the API + +You can integrate ZenStack into your project with the `ZenStackFastifyPlugin` [fastify plugin](https://www.fastify.io/docs/latest/Reference/Plugins/): + +```ts +import { enhance } from '@zenstackhq/runtime'; +import { ZenStackFastifyPlugin } from '@zenstackhq/server/fastify'; +import { prisma } from './db.ts'; +import { getSessionUser } from './auth.ts'; + +const server = fastify(); + +// serve OpenAPI at /api/model +server.register(ZenStackFastifyPlugin, { + prefix: '/api/model', + // getSessionUser extracts the current session user from the request, its + // implementation depends on your auth solution + getPrisma: (request) => enhance(prisma, { user: getSessionUser(request) }), +}); +``` + +The Fastify adapter takes the following options to initialize: + +- prefix + +
string
+ + Prefix for the mounted API endpoints. E.g.: /api/model. + + + +### Using the API + + + + diff --git a/versioned_docs/version-3.x/reference/server-adapters/hono.mdx b/versioned_docs/version-3.x/reference/server-adapters/hono.mdx new file mode 100644 index 00000000..67fdea3e --- /dev/null +++ b/versioned_docs/version-3.x/reference/server-adapters/hono.mdx @@ -0,0 +1,57 @@ +--- +title: Hono +description: Adapter for integrating with Hono +sidebar_position: 7 +--- + +import ErrorHandling from './_error-handling.md'; +import AdapterOptions from './_options.mdx'; +import UsingAPI from './_using-api.mdx' + +# Hono Adapter + +The `@zenstackhq/server/hono` module provides a quick way to install API routes onto a [Hono](https://hono.dev/) project for database CRUD operations. Combined with ZenStack's power of enhancing Prisma with access policies, it's surprisingly simple to achieve a secure data backend without manually coding it. + +### Installation + +```bash +npm install @zenstackhq/server +``` + +### Mounting the API + +You can use the `createHonoHandler` API to create a [Hono middleware](https://hono.dev/docs/getting-started/basic#using-middleware) that handles CRUD requests automatically: + +```ts +import { PrismaClient } from '@prisma/client'; +import { enhance } from '@zenstackhq/runtime'; +import { createHonoHandler } from '@zenstackhq/server/hono'; +import { Context, Hono } from 'hono'; + +const prisma = new PrismaClient(); +const app = new Hono(); + +app.use( + '/api/model/*', + createHonoHandler({ + getPrisma: (ctx) => { + return enhance(prisma, { user: getCurrentUser(ctx) }); + }, + }) +); + +function getCurrentUser(ctx: Context) { + // the implementation depends on your authentication mechanism + ... +} +``` + +The middleware factory takes the following options to initialize: + + + +### Using the API + + + + diff --git a/versioned_docs/version-3.x/reference/server-adapters/nestjs.mdx b/versioned_docs/version-3.x/reference/server-adapters/nestjs.mdx new file mode 100644 index 00000000..1d558d3d --- /dev/null +++ b/versioned_docs/version-3.x/reference/server-adapters/nestjs.mdx @@ -0,0 +1,268 @@ +--- +title: NestJS +description: Adapter for integrating with NestJS +sidebar_position: 6 +--- + +import ErrorHandling from './_error-handling.md'; +import AdapterOptions from './_options.mdx'; +import UsingAPI from './_using-api.mdx'; + +# NestJS Adapter + +The `@zenstackhq/server/nestjs` module provides a quick way to install a ZenStack-enhanced Prisma service as a dependency injection provider onto a [NestJS](https://nestjs.com/) application. + +### Installation + +```bash +npm install @zenstackhq/server +``` + +### Registering the provider + +You can register the enhanced Prisma service by importing the `ZenStackModule` NestJS module. + +```ts +import { ZenStackModule } from '@zenstackhq/server/nestjs'; +import { enhance } from '@zenstackhq/runtime'; +import { PrismaService } from './prisma.service'; + +@Module({ + imports: [ + ZenStackModule.registerAsync({ + useFactory: (prisma: PrismaService) => { + return { + getEnhancedPrisma: () => enhance(prisma, { user: ... }), + }; + }, + inject: [PrismaService], + extraProviders: [PrismaService], + }), + ], +}) +export class AppModule {} +``` + +The `registerAsync` API takes as input a factory function that returns a config used for creating an enhanced prisma service. The config contains a callback function where you should create and return an enhanced `PrismaClient`. It'll be called each time a Prisma method is invoked. + +You'll usually pass in a user context when calling `enhance` inside the callback. The way how the user context is fetched depends on your authentication mechanism. You can check the [NestJS quick start guide](../../quick-start/nestjs) for a reference solution. + +### Using the enhanced Prisma service + +Inside your NestJS controllers or services, you can inject the enhanced Prisma service and use it as you would with the regular Prisma service. Just use the special token name `ENHANCED_PRISMA` when injecting the service. + +```ts +import { ENHANCED_PRISMA } from '@zenstackhq/server/nestjs'; + +@Controller() +export class MyController { + constructor( + @Inject(ENHANCED_PRISMA) private readonly prismaService: PrismaService, + ) {} + + ... +} +``` + +You can still use the regular Prisma service by injecting as usual. + +### Using the API handler service to automate request handling + +This module also provides an `ApiHandlerService` that can be used to automate the request handling using API handlers. This is useful if you want to handle requests in a more structured way, such as using a controller or middleware. Before using it, you should ensure `ENHANCED_PRISMA` is registered in the module. For example: + +```ts +import { ZenStackModule, ApiHandlerService } from '@zenstackhq/server/nestjs'; +import { enhance } from '@zenstackhq/runtime'; +import { PrismaService } from './prisma.service'; + +@Module({ + imports: [ + // Register the ZenStack module + // The module exports the ENHANCED_PRISMA token and could be used in the ApiHandlerService + ZenStackModule.registerAsync({ + useFactory: (prisma: PrismaService) => { + return { + getEnhancedPrisma: () => enhance(prisma, { user: ... }), + }; + }, + inject: [PrismaService], + extraProviders: [PrismaService], + }), + ], + providers: [ApiHandlerService] +}) +export class AppModule {} +``` + +Then, you can inject the `ApiHandlerService` into your code and use it to handle requests. The service provides a method `handleRequest` to handle request using API handler automatically. + +RPC API Handler: + +```ts +import { Controller, Get, Post } from '@nestjs/common'; +import { ApiHandlerService } from '@zenstackhq/server/nestjs'; + +@Controller('post') +export class PostController { + constructor(private readonly apiHandlerService: ApiHandlerService) {} + + // should align with the route generated by API handler + @Get('findMany') + async findMany() { + return this.apiHandlerService.handleRequest() + } + + @Post('create') + async create() { + return this.apiHandlerService.handleRequest() + } +} +``` + +RESTful API Handler: + +```ts +import { Controller, Get, Post } from '@nestjs/common'; +import { ApiHandlerService } from '@zenstackhq/server/nestjs'; +import RESTApiHandler from '@zenstackhq/server/api/rest'; + +const ENDPOINT = 'http://localhost'; + +@Controller('post') +export class PostController { + constructor(private readonly apiHandlerService: ApiHandlerService) {} + + // should align with the route generated by API handler + @Get() + async list() { + return this.apiHandlerService.handleRequest( + { + handler: RESTApiHandler({ + endpoint: ENDPOINT, + }), + } + ) + } + + @Post() + async create() { + return this.apiHandlerService.handleRequest( + { + handler: RESTApiHandler({ + endpoint: ENDPOINT, + }), + } + ) + } +} +``` + +### API reference + +#### `ZenStackModule.registerAsync` + +##### Signature + +```ts +registerAsync(options: ZenStackModuleAsyncOptions): DynamicModule; +``` + +##### Parameter `options` + +```ts +interface ZenStackModuleAsyncOptions { + /** + * Optional list of imported modules that export the providers which are + * required in this module. + */ + imports?: Array | DynamicModule | Promise | ForwardReference>; + + /** + * Whether the module is global-scoped. + */ + global?: boolean; + + /** + * The token to export the enhanced Prisma service. Default is `'ENHANCED_PRISMA'`. + */ + exportToken?: string; + + /** + * The factory function to create the enhancement options. + */ + useFactory: (...args: unknown[]) => Promise | ZenStackModuleOptions; + + /** + * The dependencies to inject into the factory function. + */ + inject?: FactoryProvider['inject']; + + /** + * Extra providers to facilitate dependency injection. + */ + extraProviders?: Provider[]; +} +``` + +```ts +interface ZenStackModuleOptions { + /** + * A callback for getting an enhanced `PrismaClient`. + */ + getEnhancedPrisma: (model?: string | symbol) => unknown; +} +``` + +#### `ApiHandlerService.handleRequest` + +##### Signature + +```ts +handleRequest(options?: ApiHandlerOptions): Promise; +``` + +##### Parameter `options` + +```ts +interface ApiHandlerOptions { + /** + * Logger settings + */ + logger?: LoggerConfig; + + /** + * Model metadata. By default loaded from the `node_module/.zenstack/model-meta` + * module. You can pass it in explicitly if you configured ZenStack to output to + * a different location. + */ + modelMeta?: ModelMeta; + + /** + * Zod schemas for validating request input. Pass `true` to load from standard location + * (need to enable `@core/zod` plugin in schema.zmodel) or omit to disable input validation. + */ + zodSchemas?: ZodSchemas | boolean; + + /** + * Api request handler function. Can be created using `@zenstackhq/server/api/rest` or `@zenstackhq/server/api/rpc` factory functions. + * Defaults to RPC-style API handler. + */ + handler?: HandleRequestFn; + + /** + * The base URL for the API handler. This is used to determine the base path for the API requests. + * If you are using the ApiHandlerService in a route with a prefix, you should set this to the prefix. + * + * e.g. + * without baseUrl(API handler default route): + * - RPC API handler: [model]/findMany + * - RESTful API handler: /:type + * + * with baseUrl(/api/crud): + * - RPC API handler: /api/crud/[model]/findMany + * - RESTful API handler: /api/crud/:type + */ + baseUrl?: string; +} +``` + diff --git a/versioned_docs/version-3.x/reference/server-adapters/next.mdx b/versioned_docs/version-3.x/reference/server-adapters/next.mdx new file mode 100644 index 00000000..48647a64 --- /dev/null +++ b/versioned_docs/version-3.x/reference/server-adapters/next.mdx @@ -0,0 +1,124 @@ +--- +title: Next.js +description: Adapter for integrating with Next.js +sidebar_position: 1 +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; +import ErrorHandling from './_error-handling.md'; +import AdapterOptions from './_options.mdx'; +import UsingAPI from './_using-api.mdx'; + +# Next.js Server Adapter + +The `@zenstackhq/server/next` module provides a quick way to install API endpoints onto a [Next.js](https://nextjs.org/) project for database CRUD operations. Combined with ZenStack's power of enhancing Prisma with access policies, it's surprisingly simple to achieve a secure data backend without manually coding it. + +The server adapter supports both the "pages" routes for older versions of Next.js and the new "app" routes for Next.js 13. + +### Installation + +```bash +npm install @zenstackhq/server +``` + +### Mounting the API + +You can use it to create a request handler in an API endpoint like: + + + + + +```ts title='/src/app/api/model/[...path]/route.ts' +import { NextRequestHandler } from '@zenstackhq/server/next'; +import type { NextRequest } from "next/server"; +import { enhance } from '@zenstackhq/runtime'; +import { prisma } from '~/lib/db.ts'; +import { getSessionUser } from '~/lib/auth.ts'; + +// create an enhanced Prisma client with user context +function getPrisma(req: NextRequest) { + // getSessionUser extracts the current session user from the request, its + // implementation depends on your auth solution + return enhance(prisma, { user: getSessionUser(req) }); +} + +const handler = NextRequestHandler({ getPrisma, useAppDir: true }); + +export { + handler as GET, + handler as POST, + handler as PUT, + handler as PATCH, + handler as DELETE, +}; +``` + +The Next.js API route handler takes the following options to initialize: + + + + + + + +```ts title='/src/pages/api/model/[...path].ts' +import { NextRequestHandler } from '@zenstackhq/server/next'; +import type { NextApiRequest, NextApiResponse } from 'next'; +import { enhance } from '@zenstackhq/runtime'; +import { prisma } from '~/lib/db.ts'; +import { getSessionUser } from '~/lib/auth.ts'; + +// create an enhanced Prisma client with user context +function getPrisma(req: NextApiRequest, res: NextApiResponse) { + // getSessionUser extracts the current session user from the request, its + // implementation depends on your auth solution + return enhance(prisma, { user: getSessionUser(req, res) }); +} + +// create the request handler with the `getPrisma` hook +export default NextRequestHandler({ getPrisma }); +``` + +The Next.js API route handler takes the following options to initialize: + + + + + + + +### Controlling what endpoints to expose + +You can use a [Next.js middleware](https://nextjs.org/docs/pages/building-your-application/routing/middleware) to further control what endpoints to expose. For example, if you're using a RESTful API handler installed at "/api/model", you can disallow listing all `User` entities by adding a middleware like: + +```ts title='/src/middleware.ts' +import { type NextRequest, NextResponse } from 'next/server'; + +export function middleware(request: NextRequest) { + const url = new URL(request.url); + if ( + request.method === 'GET' && + url.pathname.match(/^\/api\/model\/user\/?$/) + ) { + return NextResponse.json({ error: 'Not allowed' }, { status: 405 }); + } +} + +export const config = { + matcher: '/api/model/:path*', +}; +``` + +### Using the API + + + + + +### Fully working example + +You can find the fully working examples below: +- [Pages router](https://github.com/zenstackhq/docs-tutorial-nextjs) +- [Apps router](https://github.com/zenstackhq/docs-tutorial-nextjs-app-dir) diff --git a/versioned_docs/version-3.x/reference/server-adapters/nuxt.mdx b/versioned_docs/version-3.x/reference/server-adapters/nuxt.mdx new file mode 100644 index 00000000..c254ccf1 --- /dev/null +++ b/versioned_docs/version-3.x/reference/server-adapters/nuxt.mdx @@ -0,0 +1,44 @@ +--- +title: Nuxt +description: Adapter for integrating with Nuxt +sidebar_position: 2 +--- + +import ErrorHandling from './_error-handling.md'; +import AdapterOptions from './_options.mdx'; +import UsingAPI from './_using-api.mdx'; + +# Nuxt Server Adapter + +The `@zenstackhq/server/nuxt` module provides a quick way to install API endpoints onto a [Nuxt V3](https://nuxt.com/) project for database CRUD operations. Combined with ZenStack's power of enhancing Prisma with access policies, it's surprisingly simple to achieve a secure data backend without manually coding it. + +### Mounting the API + +You can mount the API by creating a Nuxt server event handler like: + +```ts title='/server/api/model/[...].ts' +import { enhance } from '@zenstackhq/runtime'; +import { createEventHandler } from '@zenstackhq/server/nuxt'; +import { getSessionUser } from '~/server/auth'; +import { prisma } from '~/server/prisma'; + +export default createEventHandler({ + getPrisma: async (event) => { + return enhance(prisma, { user: getSessionUser(event) }); + }, +}); +``` + +The Nuxt event handler takes the following options to initialize: + + + +### Using the API + + + + + +### Fully working example + +You can find a fully working example [here](https://github.com/zenstackhq/sample-todo-nuxt). diff --git a/versioned_docs/version-3.x/reference/server-adapters/sveltekit.mdx b/versioned_docs/version-3.x/reference/server-adapters/sveltekit.mdx new file mode 100644 index 00000000..2976149f --- /dev/null +++ b/versioned_docs/version-3.x/reference/server-adapters/sveltekit.mdx @@ -0,0 +1,63 @@ +--- +title: SvelteKit +description: Adapter for integrating with SvelteKit +sidebar_position: 3 +--- + +import ErrorHandling from './_error-handling.md'; +import AdapterOptions from './_options.mdx'; +import UsingAPI from './_using-api.mdx'; + +# SvelteKit Server Adapter + +The `@zenstackhq/server/sveltekit` module provides a quick way to install API endpoints onto a [SvelteKit](https://kit.svelte.dev/) project for database CRUD operations. Combined with ZenStack's power of enhancing Prisma with access policies, it's surprisingly simple to achieve a secure data backend without manually coding it. + +### Installation + +```bash +npm install @zenstackhq/server +``` + +### Mounting the API + +You can mount the API by creating a SvelteKit server hooks like: + +```ts title='/src/hooks.server.ts' +import { SvelteKitHandler } from '@zenstackhq/server/sveltekit'; +import { enhance } from '@zenstackhq/runtime'; +import { getSessionUser } from '$lib/auth.ts'; + +// create an enhanced Prisma client with user context +function getPrisma(event: RequestEvent) { + // getSessionUser extracts the current session user from the request, its + // implementation depends on your auth solution + return enhance({ user: getSessionUser(event) }); +} + +// create the hooks handler with the `getPrisma` hook +export const handle = SvelteKitHandler({ prefix: '/api/model', getPrisma }); +``` + +:::tip +You can use the [sequence helper](https://kit.svelte.dev/docs/modules#sveltejs-kit-hooks) to compose multiple server hooks. +::: + +The SvelteKit hooks handler takes the following options to initialize: + +- prefix + +
string
+ + Prefix for the mounted API endpoints. E.g.: /api/model. + + + +### Using the API + + + + + +### Fully working example + +You can find a fully working example [here](https://github.com/zenstackhq/sample-todo-sveltekit). diff --git a/versioned_docs/version-3.x/reference/zmodel-language.md b/versioned_docs/version-3.x/reference/zmodel-language.md new file mode 100644 index 00000000..49988dce --- /dev/null +++ b/versioned_docs/version-3.x/reference/zmodel-language.md @@ -0,0 +1,1987 @@ +--- +description: ZModel language references +sidebar_position: 1 +sidebar_label: ZModel Language +toc_max_heading_level: 3 +--- + +# ZModel Language Reference + +## Overview + +**ZModel**, the modeling DSL of ZenStack, is the main concept you'll deal with when using this toolkit. The ZModel syntax is a superset of [Prisma Schema](https://www.prisma.io/docs/concepts/components/prisma-schema). Therefore, every valid Prisma schema is a valid ZModel. + +:::info + +We made that choice to extend the Prisma schema for several reasons: + +- Creating a new ORM adds little value to the community. Instead, extending Prisma - the overall best ORM toolkit for Typescript - sounds more sensible. + +- Prisma's schema language is simple and intuitive. + +- Extending an existing popular language lowers the learning curve compared to inventing a new one. + +::: + +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 + +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. + +This section provides detailed descriptions of all aspects of the ZModel language, so you don't have to jump over to Prisma's documentation for extra learning. + +## Import +ZModel allows to import other ZModel files. This is useful when you want to split your schema into multiple files for better organization. Under the hood, it will recursively merge all the imported schemas, and generate a single Prisma schema file for the Prisma CLI to consume. + +### Syntax + +```zmodel +import [IMPORT_SPECIFICATION] +``` + +- **[IMPORT_SPECIFICATION]**: + Path to the ZModel file to be imported. It can be: + + - An absolute path, e.g., "/path/to/user". + - A relative path, e.g., "./user". + - A module resolved to an installed NPM package, e.g., "my-package/base". + + If the import specification doesn't end with ".zmodel", the resolver will automatically append it. Once a file is imported, all the declarations in that file will be included in the building process. + +### Examples + +```zmodel +// there is a file called "user.zmodel" in the same directory +import "user" +``` + + +## Data source + +Every model needs to include exactly one `datasource` declaration, providing information on how to connect to the underlying database. + +### Syntax + +```zmodel +datasource [NAME] { + provider = [PROVIDER] + url = [DB_URL] +} +``` + +- **[NAME]**: + + Name of the data source. Needs to be a valid identifier matching regular expression `[A-Za-z][a-za-z0-9_]\*`. Name is only informational and serves no other purposes. + +- **[PROVIDER]**: + + Name of database connector. Valid values: + + - sqlite + - postgresql + - mysql + - sqlserver + - cockroachdb + +- **[DB_URL]**: + + Database connection string. Either a plain string or an invocation of `env` function to fetch from an environment variable. + +### Examples + +```zmodel +datasource db { + provider = "postgresql" + url = "postgresql://postgres:abc123@localhost:5432/todo?schema=public" +} +``` + +It's highly recommended that you not commit sensitive database connection strings into source control. Alternatively, you can load it from an environment variable: + +```zmodel +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} +``` + +### Supported databases + +ZenStack uses [Prisma](https://prisma.io ':target=_blank') to talk to databases, so all relational databases supported by Prisma are also supported by ZenStack. + +Here's a list for your reference: + +| Database | Version | +| --------------------- | ------- | +| PostgreSQL | 9.6 | +| PostgreSQL | 10 | +| PostgreSQL | 11 | +| PostgreSQL | 12 | +| PostgreSQL | 13 | +| PostgreSQL | 14 | +| PostgreSQL | 15 | +| MySQL | 5.6 | +| MySQL | 5.7 | +| MySQL | 8 | +| MariaDB | 10 | +| SQLite | \* | +| AWS Aurora | \* | +| AWS Aurora Serverless | \* | +| Microsoft SQL Server | 2022 | +| Microsoft SQL Server | 2019 | +| Microsoft SQL Server | 2017 | +| Azure SQL | \* | +| CockroachDB | 21.2.4+ | + +You can find the orignal list [here](https://www.prisma.io/docs/reference/database-reference/supported-databases ':target=_blank'). + +## Generator + +Generators are used for creating assets (usually code) from a Prisma schema. Check [here](https://www.prisma.io/docs/concepts/components/prisma-schema/generators) for a list of official and community generators. + +### Syntax + +```zmodel +generator [GENERATOR_NAME] { + [OPTION]* +} +``` + +- **[GENERATOR_NAME]** + + Name of the generator. Needs to be unique in the entire model. Needs to be a valid identifier matching regular expression `[A-Za-z][a-za-z0-9_]\*`. + +- **[OPTION]** + + A generator configuration option, in form of "[NAME] = [VALUE]". A generator needs to have at least a "provider" option that specify its provider. + +### Example + +```zmodel +generator client { + provider = "prisma-client-js" + output = "./generated/prisma-client-js" +``` + +## Plugin + +Plugins are ZenStack's extensibility mechanism. It's usage is similar to [Generator](#generator). Users can define their own plugins to generate artifacts from the ZModel schema. Plugins differ from generators mainly in the following ways: + +- They have a cleaner interface without the complexity of JSON-RPC. +- They use an easier-to-program AST representation than generators. +- They have access to language features that ZenStack adds to Prisma, like custom attributes and functions. + +### Syntax + +```zmodel +plugin [PLUGIN_NAME] { + [OPTION]* +} +``` + +- **[PLUGIN_NAME]** + + Name of the plugin. Needs to be unique in the entire model. Needs to be a valid identifier matching regular expression `[A-Za-z][a-za-z0-9_]\*`. + +- **[OPTION]** + + A plugin configuration option, in form of "[NAME] = [VALUE]". A plugin needs to have at least a "provider" option that specify its provider. + +### Example + +```zmodel +plugin swr { + provider = '@zenstackhq/swr' + output = 'lib/hooks' +} +``` + +## Enum + +Enums are container declarations for grouping constant identifiers. You can use them to express concepts like user roles, product categories, etc. + +### Syntax + +```prsima +enum [ENUM_NAME] { + [FIELD]* +} +``` + +- **[ENUM_NAME]** + + Name of the enum. 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]** + + Field identifier. Needs to be unique in the model. Needs to be a valid identifier matching regular expression `[A-Za-z][a-za-z0-9_]\*`. + +### Example + +```zmodel +enum UserRole { + USER + ADMIN +} +``` + +## Model + +Models represent the business entities of your application. A model inherits all fields and attributes from extended abstract models. Abstract models are eliminated in the generated prisma schema file. + +### Syntax +```zmodel +(abstract)? model [NAME] (extends [ABSTRACT_MODEL_NAME](,[ABSTRACT_MODEL_NAME])*)? { + [FIELD]* +} +``` +- **[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 [Field](#field) for details. + +- **[ABSTRACT_MODEL_NAME]**: + + Name of an abstract model. + +### Note + +A model must include a field marked with `@id` attribute. The `id` field serves as a unique identifier for a model entity and is mapped to the database table's primary key. + +See [here](#attribute) for more details about attributes. + +### Example + +```zmodel +abstract model Basic { + id String @id + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model User extends Basic { + name String +} +``` + +The generated prisma file only contains one `User` model: +```zmodel +model User { + id String @id + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + name String @id +} +``` + +## 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 + +Attributes decorate fields and models and attach extra behaviors or constraints to them. + +### Syntax + +#### Field attribute + +Field attribute name is prefixed by a single `@`. + +```zmodel +id String @[ATTR_NAME](ARGS)? +``` + +- **[ATTR_NAME]** + +Attribute name. See [below](#predefined-attributes) for a full list of attributes. + +- **[ARGS]** + +See [attribute arguments](#arguments). + +#### Model attribute + +Field attribute name is prefixed double `@@`. + +```zmodel +model Model { + @@[ATTR_NAME](ARGS)? +} +``` + +- **[ATTR_NAME]** + +Attribute name. See [below](#predefined-attributes) for a full list of attributes. + +- **[ARGS]** + +See [attribute arguments](#arguments). + +### Arguments + +Attribute can be declared with a list of parameters and applied with a comma-separated list of arguments. + +Arguments are mapped to parameters by position or by name. For example, for the `@default` attribute declared as: + +```zmodel +attribute @default(_ value: ContextType) +``` + +, the following two ways of applying it are equivalent: + +```zmodel +published Boolean @default(value: false) +``` + +```zmodel +published Boolean @default(false) +``` + +### Parameter types + +Attribute parameters are typed. The following types are supported: + +- Int + + Integer literal can be passed as argument. + + E.g., declaration: + + ```zmodel + attribute @password(saltLength: Int?, salt: String?) + + ``` + + application: + + ```zmodel + password String @password(saltLength: 10) + ``` + +- String + + String literal can be passed as argument. + + E.g., declaration: + + ```zmodel + attribute @id(map: String?) + ``` + + application: + + ```zmodel + id String @id(map: "_id") + ``` + +- Boolean + + Boolean literal or expression can be passed as argument. + + E.g., declaration: + + ```zmodel + attribute @@allow(_ operation: String, _ condition: Boolean) + ``` + + application: + + ```zmodel + @@allow("read", true) + @@allow("update", auth() != null) + ``` + +- ContextType + + A special type that represents the type of the field onto which the attribute is attached. + + E.g., declaration: + + ```zmodel + attribute @default(_ value: ContextType) + ``` + + application: + + ```zmodel + f1 String @default("hello") + f2 Int @default(1) + ``` + +- FieldReference + + References to fields defined in the current model. + + E.g., declaration: + + ```zmodel + attribute @relation( + _ name: String?, + fields: FieldReference[]?, + references: FieldReference[]?, + onDelete: ReferentialAction?, + onUpdate: ReferentialAction?, + map: String?) + ``` + + application: + + ```zmodel + model Model { + ... + // [ownerId] is a list of FieldReference + owner Owner @relation(fields: [ownerId], references: [id]) + ownerId + } + ``` + +- Enum + + Attribute parameter can also be typed as predefined enum. + + E.g., declaration: + + ```zmodel + attribute @relation( + _ name: String?, + fields: FieldReference[]?, + references: FieldReference[]?, + // ReferentialAction is a predefined enum + onDelete: ReferentialAction?, + onUpdate: ReferentialAction?, + map: String?) + ``` + + application: + + ```zmodel + model Model { + // 'Cascade' is a predefined enum value + owner Owner @relation(..., onDelete: Cascade) + } + ``` + +An attribute parameter can be typed as any of the types above, a list of the above type, or an optional of the types above. + +```zmodel + model Model { + ... + f1 String + f2 String + // a list of FieldReference + @@unique([f1, f2]) + } +``` + +### Attribute functions + +Attribute functions are used for providing values for attribute arguments, e.g., current `DateTime`, an autoincrement `Int`, etc. They can be used in place of attribute arguments, like: + +```zmodel +model Model { + ... + serial Int @default(autoincrement()) + createdAt DateTime @default(now()) +} +``` + +You can find a list of predefined attribute functions [here](#predefined-attribute-functions). + +### Predefined attributes + +#### Field attributes + +##### @id + +```zmodel +attribute @id(map: String?) +``` + +Defines an ID on the model. + +_Params_: + +| Name | Description | +| ---- | ----------------------------------------------------------------- | +| map | The name of the underlying primary key constraint in the database | + +##### @default + +```zmodel +attribute @default(_ value: ContextType) +``` + +Defines a default value for a field. + +_Params_: + +| Name | Description | +| ----- | ---------------------------- | +| value | The default value expression | + +##### @unique + +```zmodel +attribute @unique(map: String?) +``` + +Defines a unique constraint for this field. + +_Params_: + +| Name | Description | +| ---- | ----------------------------------------------------------------- | +| map | The name of the underlying primary key constraint in the database | + +##### @relation + +```zmodel +attribute @relation( + _ name: String?, + fields: FieldReference[]?, + references: FieldReference[]?, + onDelete: ReferentialAction?, + onUpdate: ReferentialAction?, + map: String?) +``` + +Defines meta information about a relation. + +_Params_: + +| Name | Description | +| ---------- | ------------------------------------------------------------------------------ | +| name | The name of the relationship | +| fields | A list of fields defined in the current model | +| references | A list of fields of the model on the other side of the relation | +| onDelete | Referential action to take on delete. See details [here](#referential-action). | +| onUpdate | Referential action to take on update. See details [here](#referential-action). | + +##### @map + +```zmodel +attribute @map(_ name: String) +``` + +Maps a field name or enum value from the schema to a column with a different name in the database. + +_Params_: + +| Name | Description | +| ---- | ------------------------------------------------- | +| map | The name of the underlying column in the database | + +##### @updatedAt + +```zmodel +attribute @updatedAt() +``` + +Automatically stores the time when a record was last updated. + +##### @ignore + +```zmodel +attribute @ignore() +``` + +Exclude a field from the Prisma Client (for example, a field that you do not want Prisma users to update). + +##### @allow + +```zmodel +attribute @allow(_ operation: String, _ condition: Boolean) +``` + +Defines an access policy that allows the annotated field to be read or updated. Read more about access policies [here](#access-policy). + +_Params_: + +| Name | Description | +| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| operation | Comma separated list of operations to control, including `"read"` and `"update"`. Pass` "all"` as an abbreviation for including all operations. | +| condition | Boolean expression indicating if the operations should be allowed | + +##### @deny + +```zmodel +attribute @deny(_ operation: String, _ condition: Boolean) +``` + +Defines an access policy that denies the annotated field to be read or updated. Read more about access policies [here](#access-policy). + +_Params_: + +| Name | Description | +| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| operation | Comma separated list of operations to control, including `"read"` and `"update"`. Pass` "all"` as an abbreviation for including all operations. | +| condition | Boolean expression indicating if the operations should be denied + +##### @password + +```zmodel +attribute @password(saltLength: Int?, salt: String?) +``` + +Indicates that the field is a password field and needs to be hashed before persistence. + +_NOTE_: ZenStack uses the "bcryptjs" library to hash passwords. You can use the `saltLength` parameter to configure the cost of hashing or use `salt` parameter to provide an explicit salt. By default, a salt length of 12 is used. See [here](https://www.npmjs.com/package/bcryptjs ':target=blank') for more details. + +_Params_: + +| Name | Description | +| ---------- | ------------------------------------------------------------- | +| saltLength | The length of salt to use (cost factor for the hash function) | +| salt | The salt to use (a pregenerated valid salt) | + +##### @omit + +```zmodel +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() +``` + +##### @encrypted + +```zmodel +attribute @encrypted() +``` + +Marks a field to be encrypted during write and decrypted when read. + +```zmodel +model User { + id Int @id + someSecret String @encrypted +} +``` + +Using the attribute requires the "encryption" enhancement kind to be enabled when calling `enhance`, and the `encryption` settings provided in the enhancement options: + +```ts + +function getEncryptionKey(): Uint8Array { + // return a 32-byte key +} + +const db = enhance(prisma, { user }, { + encryption: { + encryptionKey: getEncryptionKey() + } +}); +``` + +See [Encrypting Field](../guides/field-encryption.md) for more details. + +##### @prisma.passthrough + +```zmodel +attribute @prisma.passthrough(_ text: String) +``` + +A utility attribute for passing arbitrary text to the generated Prisma schema. This is useful as a workaround for dealing with discrepancies between Prisma schema and ZModel. + +_Params_: + +| Name | Description | +| ---- | ------------------------------------ | +| text | Text to passthrough to Prisma schema | + +E.g., the following ZModel content: + +```zmodel +model User { + id Int @id @default(autoincrement()) + name String @prisma.passthrough("@unique") +} +``` + +will be translated to the following Prisma schema: + +```zmodel +model User { + id Int @id @default(autoincrement()) + name String @unique +} +``` + +##### @meta + +```zmodel +attribute @meta(_ name: String, _ value: Any) +``` + +Adds arbitrary metadata to a field. The metadata can be accessed by custom plugins for code generation, or at runtime from the `modelMeta` object exported from `@zenstackhq/runtime/model-meta`. The `value` parameter can be an arbitrary literal expression, including object literals. + +```zmodel +model User { + id Int @id + name String @meta(name: "description", value: "The name of the user") +} +``` + +#### Model attributes + +##### @@id + +```zmodel +attribute @@id(_ fields: FieldReference[], name: String?, map: String?) +``` + +Defines a multi-field ID (composite ID) on the model. + +_Params_: + +| Name | Description | +| ------ | ----------------------------------------------------------------------------- | +| fields | A list of fields defined in the current model | +| name | The name that the Client API will expose for the argument covering all fields | +| map | The name of the underlying primary key constraint in the database | + +##### @@unique + +```zmodel +attribute @@unique(_ fields: FieldReference[], name: String?, map: String?) +``` + +Defines a compound unique constraint for the specified fields. + +_Params_: + +| Name | Description | +| ------ | ------------------------------------------------------------ | +| fields | A list of fields defined in the current model | +| name | The name of the unique combination of fields | +| map | The name of the underlying unique constraint in the database | + +##### @@schema + +```zmodel +attribute @@schema(_ name: String) +``` + +Specifies the database schema to use in a [multi-schema setup](https://www.prisma.io/docs/guides/database/multi-schema). + +_Params_: + +| Name | Description | +| ---- | ------------------------------- | +| name | The name of the database schema | + +##### @@index + +```zmodel +attribute @@index(_ fields: FieldReference[], map: String?) +``` + +Defines an index in the database. + +_Params_: + +| Name | Description | +| ------ | ------------------------------------------------ | +| fields | A list of fields defined in the current model | +| map | The name of the underlying index in the database | + +##### @@map + +```zmodel +attribute @@map(_ name: String) +``` + +Maps the schema model name to a table with a different name, or an enum name to a different underlying enum in the database. + +_Params_: + +| Name | Description | +| ---- | -------------------------------------------------------- | +| name | The name of the underlying table or enum in the database | + +##### @@ignore + +```zmodel +attribute @@ignore() +``` + +Exclude a model from the Prisma Client (for example, a model that you do not want Prisma users to update). + +##### @@allow + +```zmodel +attribute @@allow(_ operation: String, _ condition: Boolean) +``` + +Defines an access policy that allows a set of operations when the given condition is true. Read more about access policies [here](#access-policy). + +_Params_: + +| Name | Description | +| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| operation | Comma separated list of operations to control, including `"create"`, `"read"`, `"update"`, and `"delete"`. Pass` "all"` as an abbriviation for including all operations. | +| condition | Boolean expression indicating if the operations should be allowed | + +##### @@deny + +```zmodel +attribute @@deny(_ operation: String, _ condition: Boolean) +``` + +Defines an access policy that denies a set of operations when the given condition is true. Read more about access policies [here](#access-policy). + +_Params_: + +| Name | Description | +| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| operation | Comma separated list of operations to control, including `"create"`, `"read"`, `"update"`, and `"delete"`. Pass` "all"` as an abbreviation for including all operations. | +| condition | Boolean expression indicating if the operations should be denied | + +##### @@auth + +```zmodel +attribute @@auth() +``` + +Specify the model for resolving `auth()` function call in access policies. By default, the model named "User" is used. You can use this attribute to override the default behavior. A Zmodel can have at most one model with this attribute. + +##### @@auth + +```zmodel +attribute @@delegate(_ discriminator: FieldReference) +``` + +Marks a model to be a delegated type. Used for [modeling a polymorphic hierarchy](../guides/polymorphism). + +_Params_: + +| Name | Description | +| ------------- | ---------------------------------------------------------------------------------------------------------------------------- | +| discriminator | A `String` or `enum` field in the same model used to store the name of the concrete model that inherit from this base model. | + +##### @@prisma.passthrough + +```zmodel +attribute @@prisma.passthrough(_ text: String) +``` + +A utility attribute for passing arbitrary text to the generated Prisma schema. This is useful as a workaround for dealing with discrepancies between Prisma schema and ZModel. + +_Params_: + +| Name | Description | +| ---- | ------------------------------------ | +| text | Text to passthrough to Prisma schema | + +E.g., the following ZModel content: + +```zmodel +model User { + id Int @id @default(autoincrement()) + name String + @@prisma.passthrough("@@unique([name])") +} +``` + +will be translated to the following Prisma schema: + +```zmodel +model User { + id Int @id @default(autoincrement()) + name String + @@unique([name]) +} +``` + +##### @@meta + +```zmodel +attribute @@meta(_ name: String, _ value: Any) +``` + +Adds arbitrary metadata to a model. The metadata can be accessed by custom plugins for code generation, or at runtime from the `modelMeta` object exported from `@zenstackhq/runtime/model-meta`. The `value` parameter can be an arbitrary literal expression, including object literals. + +```zmodel +model User { + id Int @id + @@meta('description', 'This is a user model') +} +``` + +### Predefined attribute functions + +##### uuid() + +```zmodel +function uuid(): String {} +``` + +Generates a globally unique identifier based on the UUID spec. + +##### cuid() + +```zmodel +function cuid(version: Int?): String {} +``` + +Generates a unique identifier based on the [CUID](https://github.com/ericelliott/cuid) spec. Pass `2` as an argument to use [cuid2](https://github.com/paralleldrive/cuid2). + +##### nanoid() + +```zmodel +function nanoid(length: Int?): String {} +``` + +Generates an identifier based on the [nanoid](https://github.com/ai/nanoid) spec. + +##### ulid() + +```zmodel +function ulid(): String {} +``` + +Generates a unique identifier based on the [ULID](https://github.com/ulid/spec) spec. + +##### now() + +```zmodel +function now(): DateTime {} +``` + +Gets current date-time. + +##### autoincrement() + +```zmodel +function autoincrement(): Int {} +``` + +Creates a sequence of integers in the underlying database and assign the incremented +values to the ID values of the created records based on the sequence. + +##### dbgenerated() + +```zmodel +function dbgenerated(expr: String): Any {} +``` + +Represents default values that cannot be expressed in the Prisma schema (such as random()). + +##### auth() + +```zmodel +function auth(): User {} +``` + +Gets the current login user. The return type of the function is the `User` model defined in the current ZModel. + +##### future() + +```zmodel +function future(): Any {} +``` + +Gets the "post-update" state of an entity. Only valid when used in a "update" access policy. Read more about access policies [here](#access-policy). + +##### check() + +```zmodel +function check(field: FieldReference, operation String?): Boolean {} +``` + +Checks if the current user can perform the given operation on the given field. + +_Params_ + +- `field`: The field to check access for. Must be a relation field. +- `operation`: The operation to check access for. Can be "read", "create", "update", or "delete". If the operation is not provided, it defaults the operation of the containing policy rule. + +_Example_ + +```zmodel +// delegating a single operation kind +model Post { + id Int @id + author User @relation(fields: [authorId], references: [id]) + authorId Int + + // delegate "read" check to the author, equivalent to + // @@allow('read', check(author)) + @@allow('read', check(author, 'read')) +} +``` + +```zmodel +// delegating all operations +model Post { + id Int @id + author User @relation(fields: [authorId], references: [id]) + authorId Int + + // delegate all access policies to the author, equivalent to: + // @@allow('read', check(author)) + // @@allow('create', check(author)) + // @@allow('update', check(author)) + // @@allow('delete', check(author)) + @@allow('all', check(author)) +} +``` + +```zmodel +// delegating field access control +model Post { + id Int @id + title String @allow('update', check(author)) + author User @relation(fields: [authorId], references: [id]) + authorId Int +} +``` + +:::info +The `check()` function only supports singular relation fields and cannot be used with "to-many" relations. We may add support for it in the future. +::: + +##### contains() + +```zmodel + function contains(field: String, search: String, caseInSensitive: Boolean?): Boolean {} +``` + +Checks if the given field contains the search string. The search string is case-sensitive by default. Use `caseInSensitive` to toggle the case sensitivity. + +Equivalent to Prisma's [contains](https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#contains) operator. + +##### search() + +```zmodel +function search(field: String, search: String): Boolean {} +``` + +Checks if the given field contains the search string using [full-text-search](https://www.prisma.io/docs/concepts/components/prisma-client/full-text-search). + +Equivalent to Prisma's [search](https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#search) operator. + +##### startsWith() + +```zmodel +function startsWith(field: String, search: String): Boolean {} +``` + +Checks if the given field starts with the search string. + +Equivalent to Prisma's [startsWith](https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#startswith) operator. + +##### endsWith() + +```zmodel +function endsWith(field: String, search: String): Boolean {} +``` + +Checks if the given field ends with the search string. + +Equivalent to Prisma's [endsWith](https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#endswith) operator. + +##### has() + +```zmodel +function has(field: Any[], search: Any): Boolean {} +``` + +Check if the given field (list) contains the search value. + +Equivalent to Prisma's [has](https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#has) operator. + +##### hasEvery() + +```zmodel +function hasEvery(field: Any[], search: Any[]): Boolean {} +``` + +Check if the given field (list) contains every element of the search list. + +Equivalent to Prisma's [hasEvery](https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#hasevery) operator. + +##### hasSome() + +```zmodel +function hasSome(field: Any[], search: Any[]): Boolean {} +``` + +Check if the given field (list) contains at least one element of the search list. + +Equivalent to Prisma's [hasSome](https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#hassome) operator. + +##### isEmpty() + +```zmodel +function isEmpty(field: Any[]): Boolean {} +``` + +Check if the given field (list) is empty. + +Equivalent to Prisma's [isEmpty](https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#isempty) operator. + +##### currentModel() + +```zmodel +function currentModel(casing: String?): String {} +``` + +Can only be used in access policy expressions. Return the name of the model for which the policy rule is defined. If the rule is inherited to a sub model, this function returns the name of the sub model. + +The optional "casing" parameter can be used to control the casing of the returned name. Valid values are "original", "upper", "lower", "capitalize", "uncapitalize". Defaults to "original". + +##### currentOperation() + +```zmodel +function currentOperation(casing: String?): String {} +``` + +Can only be used in access policy expressions. Return the operation for which the policy rule is defined for - "create", "read", "update", or "delete". Note that a rule with "all" operation is expanded to "create", "read", "update", and "delete" rules, and the function returns corresponding value for each expanded version. + +The optional "casing" parameter can be used to control the casing of the returned name. Valid values are "original", "upper", "lower", "capitalize", "uncapitalize". Defaults to "original". + +### Examples + +Here're some examples on using field and model attributes: + +```zmodel +model User { + // unique id field with a default UUID value + id String @id @default(uuid()) + + // require email field to be unique + email String @unique + + // password is hashed with bcrypt with length of 16, omitted when returned from the CRUD services + password String @password(saltLength: 16) @omit + + // default to current date-time + createdAt DateTime @default(now()) + + // auto-updated when the entity is modified + updatedAt DateTime @updatedAt + + // mapping to a different column name in database + description String @map("desc") + + // mapping to a different table name in database + @@map("users") + + // use @@index to specify fields to create database index for + @@index([email]) + + // use @@allow to specify access policies + @@allow("create,read", true) + + // use auth() to reference the current user + // use future() to access the "post-update" state + @@allow("update", auth() == this && future().email == email) +} +``` + +### Custom attributes and functions + +You can find examples of custom attributes and functions in [ZModel Standard Library](https://github.com/zenstackhq/zenstack/blob/main/packages/schema/src/res/stdlib.zmodel). + +## Field + +Fields are typed members of models and types. + +### Syntax + +```zmodel +model Model { + [FIELD_NAME] [FIELD_TYPE] (FIELD_ATTRIBUTES)? +} +``` + +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, 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: + + - String + - Boolean + - Int + - BigInt + - Float + - Decimal + - Json + - Bytes + - [Unsupported types](https://www.prisma.io/docs/concepts/components/prisma-schema/data-model#unsupported-types) + + A field's type can be any of the scalar or reference type, a list of the aforementioned type (suffixed with `[]`), or an optional of the aforementioned type (suffixed with `?`). + +- **[FIELD_ATTRIBUTES]** + + Field attributes attach extra behaviors or constraints to the field. See [Attribute](#attribute) for more information. + +### Example + +```zmodel +model Post { + // "id" field is a mandatory unique identifier of this model + 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[] + + // and can reference another model too + comments Comment[] +} +``` + +## Relation + +Relations are connections among models. There're three types of relations: + +- One-to-one +- One-to-many +- Many-to-many + +Relations are expressed with a pair of fields and together with the special `@relation` field attribute. One side of the relation field carries the `@relation` attribute to indicate how the connection is established. + +### One-to-one relation + +The _owner_ side of the relation declares an optional field typed as the model of the _owned_ side of the relation. + +On the _owned_ side, a reference field is declared with `@relation` attribute, together with a **foreign key** field storing the id of the owner entity. + +```zmodel +model User { + id String @id + profile Profile? +} + +model Profile { + id String @id + user User @relation(fields: [userId], references: [id]) + userId String @unique +} +``` + +### One-to-many relation + +The _owner_ side of the relation declares a list field typed as the model of the _owned_ side of the relation. + +On the _owned_ side, a reference field is declared with `@relation` attribute, together with a **foreign key** field storing the id of the owner entity. + +```zmodel +model User { + id String @id + posts Post[] +} + +model Post { + id String @id + author User? @relation(fields: [authorId], references: [id]) + authorId String? +} +``` + +### Many-to-many relation + +A _join model_ is declared to connect the two sides of the relation using two one-to-one relations. + +Each side of the relation then establishes a one-to-many relation with the _join model_. + +```zmodel +model Space { + id String @id + // one-to-many with the "join-model" + 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 + + // a user can be member of a space for only once + @@unique([userId, spaceId]) +} + +model User { + id String @id + // one-to-many with the "join-model" + membership Membership[] +} + +``` + +### Self-relations + +A relation field referencing its own model is called "self-relation". ZModel's represents self-relation in the same way as Prisma does. Please refer to the [Prisma documentation](https://www.prisma.io/docs/concepts/components/prisma-schema/relations/self-relations) for more details. + +### Referential action + +When defining a relation, you can specify what happens when one side of a relation is updated or deleted. See [Referential action](#referential-action) for details. + +## Access policy + +### Model-level policy + +Model-level access policies are defined with `@@allow` and `@@deny` attributes. They specify the eligibility of an operation over a model entity. The signatures of the attributes are: + +- `@@allow` + + ```zmodel + attribute @@allow(_ operation: String, _ condition: Boolean) + ``` + + _Params_: + + | Name | Description | + | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | + | operation | Comma separated list of operations to control, including `"create"`, `"read"`, `"update"`, and `"delete"`. Pass` "all"` as an abbreviation for including all operations. | + | condition | Boolean expression indicating if the operations should be allowed | + +- `@@deny` + + ```zmodel + attribute @@deny(_ operation: String, _ condition: Boolean) + ``` + + _Params_: + + | Name | Description | + | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | + | operation | Comma separated list of operations to control, including `"create"`, `"read"`, `"update"`, and `"delete"`. Pass` "all"` as an abbreviation for including all operations. | + | condition | Boolean expression indicating if the operations should be denied | + +### Field-level policy + +Field-level access policies are defined with `@allow` and `@deny` attributes. They control whether the annotated field can be read or updated. If a field fails "read" check, it'll be deleted when returned. If a field is set to be updated but fails "update" check, the update operation will be rejected. + +Note that it's not allowed to put "update" rule on relation fields, because whether an entity can be updated shouldn't be determined indirectly by a relation, but directly by the entity itself. However, you can put "update" rule on a foreign key field to control how a a relation can be updated. + +The signatures of the attributes are: + +- `@allow` + + ```zmodel + attribute @allow(_ operation: String, _ condition: Boolean, _ override: Boolean?) + ``` + + _Params_: + + | Name | Description | Default | + | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------- | + | operation | Comma separated list of operations to control, including `"read"` and `"update"`. Pass` "all"` as an abbreviation for including all operations. | | + | condition | Boolean expression indicating if the operations should be allowed | | + | override | Boolean indicating if the field-level policy should override model-level ones. See [here](../the-complete-guide/part1/access-policy/field-level#overriding-model-level-policies) for more details. | false | + +- `@deny` + + ```zmodel + attribute @deny(_ operation: String, _ condition: Boolean) + ``` + + _Params_: + + | Name | Description | + | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | + | operation | Comma separated list of operations to control, including ``"read"` and `"update"`. Pass` "all"` as an abbreviation for including all operations. | + | condition | Boolean expression indicating if the operations should be denied | + +### Policy expressions + +Policy rules use boolean expressions to make verdicts. ZModel provides a set of literals and operators for constructing expressions of arbitrary complexity. + +``` + +Expression ::= Literal | Array | This | Null | Reference | MemberAccess | Invocation | Binary | Unary | CollectionPredicate + +Literal ::= String | Number | Boolean + +Array ::= "[" Expression [, Expression]* "]" + +This ::= "this" + +Null ::= "null" + +Reference ::= Identifier + +MemberAccess ::= Expression "." Identifier + +Operator_Precedence#table +Binary ::= Expression ("==" | "!=" | ">" | "<" | ">=" | "<=" | "&&" | "||" || "in") + +Unary ::= "!" Expression + +CollectionPredicate ::= Expression ("?" | "!" | "^") "[" Expression "]" + +``` + +Binary operator precedence follows [Javascript's rules](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/). + +Collection predicate expressions are used for reaching into relation fields. You can find more details [here](#collection-predicate-expressions). + +### 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` model or a model marked with the `@@auth` attribute. + +You can use `auth()` to: + +- Check if a user is logged in + + ```zmodel + @@deny('all', auth() == null) + ``` + +- Access user's fields + + ```zmodel + @@allow('update', auth().role == 'ADMIN') + ``` + +- Compare user identity + + ```zmodel + // owner is a relation field to User model + @@allow('update', auth() == owner) + ``` + +### Accessing relation fields in policy + +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. + +```zmodel + @@allow('read', membership?[space.members?[user == auth()]]) +``` + +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 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: + +- Any + + ``` + ?[condition] + ``` + + Any element in `collection` matches `condition` + +- All + + ``` + ![condition] + ``` + + All elements in `collection` match `condition` + +- None + + ``` + ^[condition] + ``` + + None element in `collection` matches `condition` + +The `condition` expression has direct access to fields defined in the model of `collection`. E.g.: + +```zmodel + @@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 conditions involving multi-level relation lookup. E.g.: + +```zmodel + @@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. + +### Combining multiple rules + +A model can contain an arbitrary number of policy rules. The logic of combining model-level rules is as follows: + +- 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. + +A field can also contain an arbitrary number of policy rules. The logic of combining field-level rules is as follows: + +- The operation is rejected if any of the conditions in `@deny` rules evaluate to `true`. +- Otherwise, if there exists any `@allow` rule and at least one of them evaluates to `true`, the operation is permitted. +- Otherwise, if there exists any `@allow` rule but none one of them evaluates to `true`, the operation is rejected. +- Otherwise, the operation is permitted. + +Please note the difference between model-level and field-level rules. Model-level access are by-default denied, while field-level access are by-default allowed. + +### "Create" rules + +The "create" policy rules should be understood as: **if an entity were to be created, would it satisfy the rules**. Or, in other words, the rules are checked "post create". + +An entity creating process works like the following: + +1. Initiate a transaction and create the entity. +2. In the same transaction, try to read the created entity with the "create" rules as filter, and see if it succeeds. +3. If the read fails, the transaction is rolled back; otherwise it's committed. + +The "post-create check" semantic allows the rules to access relations of the entity being created since they are only accessible after the create happens. For simple cases, ZenStack may apply optimizations to reject a create request without initiating a transaction, but generally speaking the "post-create check" semantic is the correct way to think about it. + +We may introduce a "pre-create" policy type in the future. + +### "Pre-update" vs." post-update" rules + +When an access policy rule is applied to a mutate operation, the entities under operation have a "pre" and "post" state. For a "create" rule, its "pre" state is empty, so the rule implicitly refers to the "post" state. For a "delete" rule, its "post" state is empty, so the rule implicitly refers to the "pre" state. + +However, for "update" rules it is ambiguous; both the "pre" and the "post" states exist. By default, for "update" rules, fields referenced in the expressions refer to the "pre" state, and you can use the `future()` function to refer to the "post" state explicitly. + +In the following example, the "update" rule uses `future()` to ensure an update cannot alter the post's owner. + +```zmodel +model Post { + id String @id @default(uuid()) + title String @length(1, 100) + author User @relation(fields: [authorId], references: [id], onDelete: Cascade) + authorId String + + // update can only be done by the author, and is not allowed to change author + @@allow('update', author == auth() && future().author == author) +} +``` + +:::info +The `future()` function is not supported in field-level access policies. To express post-update rules, put them into model-level policies. +::: + +### Examples + +#### A simple example with Post model + +```zmodel +model Post { + // reject all operations if user's not logged in + @@deny('all', auth() == null) + + // allow all operations if the entity's owner matches the current user + @@allow('all', auth() == owner) + + // posts are readable to anyone + @allow('read', true) +} +``` + +#### A more complex example with multi-user spaces + +```zmodel +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 + // the current entity + @@allow('all', auth() == this) + + // can be read by users sharing a space + @@allow('read', membership?[space.members?[user == auth()]]) +} + +``` + +## Data validation + +### Overview + +Data validation is used for attaching constraints to field values. Unlike access policies, field validation rules cannot access the current user with the `auth()` function and are only checked for 'create' and 'update' operations. The main purpose of field validation is to ensure data integrity and consistency, not for access control. + +The [`@core/zod`](./plugins/zod) plugin recognizes the validation attributes and includes them into the generated Zod schemas. + +### Field-level validation attributes + +The following attributes can be used to attach validation rules to individual fields: + +#### String + +- `@length(_ min: Int?, _ max: Int?, _ message: String?)` + + Validates length of a string field. + +- `@startsWith(_ text: String, _ message: String?)` + + Validates a string field value starts with the given text. + +- `@endsWith(_ text: String, _ message: String?)` + + Validates a string field value ends with the given text. + +- `@contains(_text: String, _ message: String?)` + + Validates a string field value contains the given text. + +- `@email(_ message: String?)` + + Validates a string field value is a valid email address. + +- `@url(_ message: String?)` + + Validates a string field value is a valid url. + +- `@datetime(_ message: String?)` + + Validates a string field value is a valid ISO datetime. + +- `@regex(_ regex: String, _ message: String?)` + + Validates a string field value matches a regex. + +- `@trim(_ value: String)` + + Trims whitespace. + +- `@lower(_ value: String)` + + Converts to lowercase. + +- `@upper(_ value: String)` + + Converts to uppercase. + +:::info +Attributes `@trim`, `@lower`, and `@upper` are actually "transformation" instead of "validation". They make sure the values are transformed before storing into the database. +::: + +#### Number + +- `@gt(_ value: Int, _ message: String?)` + + Validates a number field is greater than the given value. + +- `@gte(_ value: Int, _ message: String?)` + + Validates a number field is greater than or equal to the given value. + +- `@lt(_ value: Int, _ message: String?)` + + Validates a number field is less than the given value. + +- `@lte(_ value: Int, _ message: String?)` + + Validates a number field is less than or equal to the given value. + +### Model-level validation attributes + +You can use the `@@validate` attribute to attach validation rules to a model. Use the `message` parameter to provide an optional custom error message, and the `path` parameter to provide an optional path to the field that caused the error. + +``` +@@validate(_ value: Boolean, _ message: String?, _ path: String[]?) +``` + +Model-level rules can reference multiple fields, use relation operators (`==`, `!=`, `>`, `>=`, `<`, `<=`) to compare fields, use boolean operators (`&&`, `||`, and `!`) to compose conditions, and can use the following functions to evaluate conditions for fields: + +- `function length(field: String, min: Int, max: Int?): Boolean` + + Validates length of a string field. + +- `function regex(field: String, regex: String): Boolean` + + Validates a string field value matches a regex. + +- `function email(field: String): Boolean` + + Validates a string field value is a valid email address. + +- `function datetime(field: String): Boolean` + + Validates a string field value is a valid ISO datetime. + +- `function url(field: String)` + + Validates a string field value is a valid url. + +- `function contains(field: String, search: String, caseInSensitive: Boolean?): Boolean` + + Validates a string field contains the search string. + +- `function startsWith(field: String, search: String): Boolean` + + Validates a string field starts with the search string. + +- `function endsWith(field: String, search: String): Boolean` + + Validates a string field ends with the search string. + +- `function has(field: Any[], search: Any): Boolean` + + Validates a list field contains the search value. + +- `function hasEvery(field: Any[], search: Any[]): Boolean` + + Validates a list field contains every element in the search list. + +- `function hasSome(field: Any[], search: Any[]): Boolean` + + Validates a list field contains some elements in the search list. + +- `function isEmpty(field: Any[]): Boolean` + + Validates a list field is null or empty. + +### Example + +```zmodel +model User { + id String @id + handle String @regex("^[0-9a-zA-Z]{4,16}$") + email String? @email @endsWith("@myorg.com", "must be an email from myorg.com") + profileImage String? @url + age Int @gte(18) + activated Boolean @default(false) + + @@validate(!activated || email != null, "activated user must have an email") +} +``` + +## Referential action + +### Overview + +When defining a relation, you can use referential action to control what happens when one side of a relation is updated or deleted by setting the `onDelete` and `onUpdate` parameters in the `@relation` attribute. + +```zmodel +attribute @relation( + _ name: String?, + fields: FieldReference[]?, + references: FieldReference[]?, + onDelete: ReferentialAction?, + onUpdate: ReferentialAction?, + map: String?) +``` + +The `ReferentialAction` enum is defined as: + +```zmodel +enum ReferentialAction { + Cascade + Restrict + NoAction + SetNull + SetDefault +} +``` + +- `Cascade` + + - **onDelete**: deleting a referenced record will trigger the deletion of referencing record. + + - **onUpdate**: updates the relation scalar fields if the referenced scalar fields of the dependent record are updated. + +- `Restrict` + + - **onDelete**: prevents the deletion if any referencing records exist. + - **onUpdate**: prevents the identifier of a referenced record from being changed. + +- `NoAction` + + Similar to 'Restrict', the difference between the two is dependent on the database being used. + + See details [here](https://www.prisma.io/docs/concepts/components/prisma-schema/relations/referential-actions#noaction ':target=blank') + +- `SetNull` + + - **onDelete**: the scalar field of the referencing object will be set to NULL. + - **onUpdate**: when updating the identifier of a referenced object, the scalar fields of the referencing objects will be set to NULL. + +- `SetDefault` + - **onDelete**: the scalar field of the referencing object will be set to the fields default value. + - **onUpdate**: the scalar field of the referencing object will be set to the fields default value. + +### Example + +```zmodel +model User { + id String @id + profile Profile? +} + +model Profile { + id String @id + user @relation(fields: [userId], references: [id], onUpdate: Cascade, onDelete: Cascade) + 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/versioned_docs/version-3.x/samples.md b/versioned_docs/version-3.x/samples.md new file mode 100644 index 00000000..2b78bb7b --- /dev/null +++ b/versioned_docs/version-3.x/samples.md @@ -0,0 +1,8 @@ +--- +sidebar_position: 7 +sidebar_label: Sample Projects +--- + +# A Catalog of Sample Projects + +The ZenStack team maintains the following three series of sample projects. diff --git a/versioned_docs/version-3.x/service/_category_.yml b/versioned_docs/version-3.x/service/_category_.yml new file mode 100644 index 00000000..d66b3af7 --- /dev/null +++ b/versioned_docs/version-3.x/service/_category_.yml @@ -0,0 +1,7 @@ +position: 4 +label: Query as a Service +collapsible: true +collapsed: true +link: + type: generated-index + title: Query as a Service diff --git a/versioned_docs/version-3.x/service/introduction.md b/versioned_docs/version-3.x/service/introduction.md new file mode 100644 index 00000000..261e992b --- /dev/null +++ b/versioned_docs/version-3.x/service/introduction.md @@ -0,0 +1,3 @@ +# Introduction + +Coming soon 🚧 \ No newline at end of file diff --git a/versioned_docs/version-3.x/upgrade.md b/versioned_docs/version-3.x/upgrade.md new file mode 100644 index 00000000..cfeb1049 --- /dev/null +++ b/versioned_docs/version-3.x/upgrade.md @@ -0,0 +1,10 @@ +--- +description: How to upgrade to ZenStack v3 + +slug: /upgrade-v3 +sidebar_label: Upgrading to V3 +sidebar_position: 8 +--- + +# Upgrading to V3 + diff --git a/versioned_docs/version-3.x/utilities/_category_.yml b/versioned_docs/version-3.x/utilities/_category_.yml new file mode 100644 index 00000000..9dd0322c --- /dev/null +++ b/versioned_docs/version-3.x/utilities/_category_.yml @@ -0,0 +1,7 @@ +position: 5 +label: Utilities +collapsible: true +collapsed: true +link: + type: generated-index + title: Utilities diff --git a/versioned_docs/version-3.x/utilities/zod.md b/versioned_docs/version-3.x/utilities/zod.md new file mode 100644 index 00000000..5bfd1865 --- /dev/null +++ b/versioned_docs/version-3.x/utilities/zod.md @@ -0,0 +1,6 @@ +--- +sidebar_position: 1 +description: Zod integration +--- + +# Zod Integration diff --git a/versioned_docs/version-3.x/welcome.md b/versioned_docs/version-3.x/welcome.md new file mode 100644 index 00000000..8309ace6 --- /dev/null +++ b/versioned_docs/version-3.x/welcome.md @@ -0,0 +1,10 @@ +--- +description: Welcome to ZenStack +slug: /welcome +sidebar_label: Welcome +sidebar_position: 1 +--- + +# Welcome to ZenStack V3 + +The modern data layer for TypeScript applications. diff --git a/versioned_sidebars/version-3.x-sidebars.json b/versioned_sidebars/version-3.x-sidebars.json new file mode 100644 index 00000000..fc4fe139 --- /dev/null +++ b/versioned_sidebars/version-3.x-sidebars.json @@ -0,0 +1,8 @@ +{ + "mySidebar": [ + { + "type": "autogenerated", + "dirName": "." + } + ] +} diff --git a/versions.json b/versions.json index c339c072..bbcb99fe 100644 --- a/versions.json +++ b/versions.json @@ -1 +1 @@ -["1.x"] +["1.x", "3.x"] From 4a3bc1899ebaee0d6516ef88e9bd83345f311ff1 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Fri, 1 Aug 2025 21:32:23 +0800 Subject: [PATCH 02/13] fix build --- .../version-3.x/reference/runtime-api.md | 122 - .../reference/server-adapters/_category_.yml | 7 - .../server-adapters/_error-handling.md | 3 - .../reference/server-adapters/_options.mdx | 66 - .../reference/server-adapters/_using-api.mdx | 137 -- .../api-handlers/_data_type_serialization.md | 16 - .../server-adapters/api-handlers/index.mdx | 31 - .../server-adapters/api-handlers/rest.mdx | 982 -------- .../server-adapters/api-handlers/rpc.mdx | 275 --- .../reference/server-adapters/elysia.mdx | 69 - .../reference/server-adapters/express.mdx | 66 - .../reference/server-adapters/fastify.mdx | 56 - .../reference/server-adapters/hono.mdx | 57 - .../reference/server-adapters/nestjs.mdx | 268 --- .../reference/server-adapters/next.mdx | 124 -- .../reference/server-adapters/nuxt.mdx | 44 - .../reference/server-adapters/sveltekit.mdx | 63 - .../version-3.x/reference/zmodel-language.md | 1979 ----------------- 18 files changed, 4365 deletions(-) delete mode 100644 versioned_docs/version-3.x/reference/server-adapters/_category_.yml delete mode 100644 versioned_docs/version-3.x/reference/server-adapters/_error-handling.md delete mode 100644 versioned_docs/version-3.x/reference/server-adapters/_options.mdx delete mode 100644 versioned_docs/version-3.x/reference/server-adapters/_using-api.mdx delete mode 100644 versioned_docs/version-3.x/reference/server-adapters/api-handlers/_data_type_serialization.md delete mode 100644 versioned_docs/version-3.x/reference/server-adapters/api-handlers/index.mdx delete mode 100644 versioned_docs/version-3.x/reference/server-adapters/api-handlers/rest.mdx delete mode 100644 versioned_docs/version-3.x/reference/server-adapters/api-handlers/rpc.mdx delete mode 100644 versioned_docs/version-3.x/reference/server-adapters/elysia.mdx delete mode 100644 versioned_docs/version-3.x/reference/server-adapters/express.mdx delete mode 100644 versioned_docs/version-3.x/reference/server-adapters/fastify.mdx delete mode 100644 versioned_docs/version-3.x/reference/server-adapters/hono.mdx delete mode 100644 versioned_docs/version-3.x/reference/server-adapters/nestjs.mdx delete mode 100644 versioned_docs/version-3.x/reference/server-adapters/next.mdx delete mode 100644 versioned_docs/version-3.x/reference/server-adapters/nuxt.mdx delete mode 100644 versioned_docs/version-3.x/reference/server-adapters/sveltekit.mdx diff --git a/versioned_docs/version-3.x/reference/runtime-api.md b/versioned_docs/version-3.x/reference/runtime-api.md index 10d7cf39..22d84bdd 100644 --- a/versioned_docs/version-3.x/reference/runtime-api.md +++ b/versioned_docs/version-3.x/reference/runtime-api.md @@ -6,125 +6,3 @@ sidebar_label: Runtime API # Runtime API Reference -This document provides references for runtime APIs exported from the `@zenstackhq/runtime` package. - -### enhance - -#### Description - -Creates an enhanced wrapper for a `PrismaClient`. The return value has the same APIs as the original `PrismaClient`. - -#### Signature - -```ts -function enhance( - prisma: DbClient, - context?: EnhancementContext, - options?: EnhancementOptions -): DbClient; -``` - -##### Parameter `prisma` - -The PrismaClient instance to enhance. - -##### Parameter `context` - -The context to for evaluating access policies with the following typing. - -```ts -type EnhancementContext = { - user?: Record -}; -``` - -| Field | Description | -| ----- | ----------- | -| user | The user object that provides value for the `auth()` function call in access policies. If provided. Its shape should be consistent with the `User` model in your ZModel, with all fields optional except for id field(s). Pass `undefined` to represent an anonymous user, and the `auth()` function call will evaluate to `null` in that case. | - -##### Parameter `options` - -Options with the following typing. - -```ts -type TransactionIsolationLevel = - | 'ReadUncommitted' - | 'ReadCommitted' - | 'RepeatableRead' - | 'Snapshot' - | 'Serializable'; - -type SimpleEncryption = { - encryptionKey: Uint8Array; - decryptionKeys?: Uint8Array[]; -} - -type CustomEncryption = { - encrypt: (model: string, field: FieldInfo, plain: string) => Promise; - decrypt: (model: string, field: FieldInfo, cipher: string) => Promise; -}; - -type ValidationOptions = { - inputOnlyValidationForUpdate?: boolean; -}; - -type EnhancementOptions = { - kinds?: EnhancementKind[]; - logPrismaQuery?: boolean; - errorTransformer?: ErrorTransformer; - transactionMaxWait?: number; - transactionTimeout?: number; - transactionIsolationLevel?: TransactionIsolationLevel; - encryption?: SimpleEncryption | CustomEncryption; - validation?: ValidationOptions; -}; -``` - -| Field | Description | Default | -| ------------------------- | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------- | -| kinds | The kinds of enhancements to apply. By default all enhancements are applied. See [the next section](#enhancement-kinds) for more details. | All enhancement kinds | -| logPrismaQuery | Whether to log queries sent to Prisma client. Log will be emitted with "info" level, so please make sure you [turn that level on](https://www.prisma.io/docs/concepts/components/prisma-client/working-with-prismaclient/logging#log-to-stdout) when creating Prisma client | false | -| errorTransformer | A function for transforming error thrown by the enhanced `PrismaClient` into a custom one. | | -| transactionMaxWait | The `maxWait` option (in ms) passed to `prisma.$transaction()` call for transactions initiated by ZenStack. | Database default | -| transactionTimeout | The `timeout` option (in ms) passed to `prisma.$transaction()` call for transactions initiated by ZenStack. | Database default | -| transactionIsolationLevel | The `isolationLevel` option passed to `prisma.$transaction()` call for transactions initiated by ZenStack. | Database default | -| encryption | Field encryption settings. Only required when using the [field encryption](../guides/field-encryption.md) feature. | | -| validation.inputOnlyValidationForUpdate | By default, ZenStack validates an entity after "update" operation to ensure the final result satisfies validation rules as a whole. This implies if the record under update doesn't satisfy the rules prior to update, the update operation will fail even if the fields causing validation errors are not affected by the operation. You can set this option to `true` to let ZenStack only validate data contained in the input args. | false | - -#### Enhancement Kinds - -Here are the kinds of enhancements available: - -- `policy` - - Enforces model-level and field-level access policies defined with `@@allow`, `@@deny`, `@allow`, and `@deny`. - -- `validation` - - Validates create and update input data against rules defined with [data validation attributes](../reference/zmodel-language#data-validation). - -- `delegate` - - Support for modeling [polymorphic relations](../guides/polymorphism) with delegated types pattern. - -- `password` - - Automatically hashes fields marked with the `@password` attribute using `bcryptjs` before saving to the database. - -- `omit` - - Automatically omits fields marked with the `@omit` attribute from read results. - -- `encryption` - - Transparently encrypt and decrypt fields marked with the `@encrypted` attribute. See [this guide](../guides/field-encryption.md) for more details. - -#### Example - -```ts -const session = getSession(); -const enhancedClient = enhance(prisma, - { user: session.user }, - { kinds: ['policy', 'password']} -); -``` diff --git a/versioned_docs/version-3.x/reference/server-adapters/_category_.yml b/versioned_docs/version-3.x/reference/server-adapters/_category_.yml deleted file mode 100644 index f6e05f3e..00000000 --- a/versioned_docs/version-3.x/reference/server-adapters/_category_.yml +++ /dev/null @@ -1,7 +0,0 @@ -position: 7 -label: Server Adapters -collapsible: true -collapsed: true -link: - type: generated-index - title: Server Adapters diff --git a/versioned_docs/version-3.x/reference/server-adapters/_error-handling.md b/versioned_docs/version-3.x/reference/server-adapters/_error-handling.md deleted file mode 100644 index db2a9b05..00000000 --- a/versioned_docs/version-3.x/reference/server-adapters/_error-handling.md +++ /dev/null @@ -1,3 +0,0 @@ -### Error Handling - -Refer to the specific sections for [RPC Handler](./api-handlers/rpc#http-status-code-and-error-responses) and [RESTful Handler](./api-handlers/rest#error-handling). diff --git a/versioned_docs/version-3.x/reference/server-adapters/_options.mdx b/versioned_docs/version-3.x/reference/server-adapters/_options.mdx deleted file mode 100644 index dee02d69..00000000 --- a/versioned_docs/version-3.x/reference/server-adapters/_options.mdx +++ /dev/null @@ -1,66 +0,0 @@ -import CodeBlock from '@theme/CodeBlock'; - -- getPrisma (required) - -
{props.getPrisma}
- - A callback for getting a PrismaClient instance for talking to the database. Usually you'll use an enhanced instance created with ZenStack's [`enhance`](../runtime-api#enhance) API to ensure access policies are enforced. - -- logger (optional) - -
{'LoggerConfig'}
- - Configuration for customizing logging behavior. - -- modelMeta (optional) - -
{'ModelMeta'}
- - Model metadata. By default loaded from the `node_module/.zenstack/model-meta` module. You can pass it in explicitly if you configured ZenStack to output to a different location. E.g.: `require('output/model-meta').default`. - -- zodSchemas (optional) - -
{'ModelZodSchema | boolean | undefined'}
- - Provides the Zod schemas used for validating CRUD request input. The Zod schemas can be generated with the `@core/zod` plugin. Pass `true` for this option to load the schemas from the default location. If you configured `@core/zod` plugin to output to a custom location, you can load the schemas explicitly and pass the loaded module to this option. E.g.: - - ```ts - factory({ - ... - zodSchemas: require('./zod'), - }); - ``` - - Not passing this option or passing in `undefined` disables input validation. - -- handler (optional) - -
{'(req: RequestContext) => Promise'}
- - The request handler function. This option determines the API endpoints and its input and output formats. Currently ZenStack supports two styles of APIs: RPC (the default) and RESTful. - - - RPC - - The goal of the RPC-style API handler is to fully mirror [PrismaClient's API](https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#model-queries) across the network, so that developers can continue enjoying the convenience and flexibility of Prisma's query syntax. This is the default choice for the `handler` option. - - The RPC-style handler can be created like: - - ```ts - import { RPCApiHandler } from '@zenstackhq/server/api'; - const handler = RPCApiHandler(); - ``` - - For more details, please check out [RPC API Handler](./api-handlers/rpc). - - - RESTful - - The goal of RESTful-style API handler is to provide a resource-centric RESTful API using [JSON:API](https://jsonapi.org/) as transportation format. - - The RESTful-style handler can be created like: - - ```ts - import { RestApiHandler } from '@zenstackhq/server/api'; - const handler = RestApiHandler({ endpoint: 'http://myhost/api' }); - ``` - - For more details, please check out [RESTful API Handler](./api-handlers/rest). diff --git a/versioned_docs/version-3.x/reference/server-adapters/_using-api.mdx b/versioned_docs/version-3.x/reference/server-adapters/_using-api.mdx deleted file mode 100644 index 1d16241c..00000000 --- a/versioned_docs/version-3.x/reference/server-adapters/_using-api.mdx +++ /dev/null @@ -1,137 +0,0 @@ -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -The APIs can be used in the following three ways: - -1. With generated client hooks - - ZenStack provides plugins to generate client hooks from the ZModel targeting the most popular frontend data fetching libraries: [TanStack Query](https://tanstack.com/query/latest) and [SWR](https://swr.vercel.app/). The generated hooks can be used to make API calls to the server adapters. Refer to the follow docs for detailed usage: - - - [`@zenstackhq/tanstack-query`](../plugins/tanstack-query) - - [`@zenstackhq/swr`](../plugins/swr) -

- - :::info - The generated client hooks assumes the server adapter uses [RPC-style API handler](./api-handlers/rpc) (which is the default setting). - ::: - -1. With direct HTTP calls - - You can make direct HTTP calls to the server adapter using your favorite client libraries like `fetch` or `axios`. Refer to the documentation of the [API Handlers](./api-handlers/) for the API endpoints and data formats. - - Here's an example using `fetch`: - - - - - ```ts - // create a user with two posts - const r = await fetch(`/api/user/create`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - include: { posts: true }, - data: { - email: 'user1@abc.com', - posts: { - create: [{ title: 'Post 1' }, { title: 'Post 2' }], - }, - }, - }), - }); - - console.log(await r.json()); - ``` - - Output: - - ```json - { - "id": 1, - "email": "user1@abc.com", - "posts": [ - { - "id": 1, - "createdAt": "2023-03-14T07:45:04.036Z", - "updatedAt": "2023-03-14T07:45:04.036Z", - "title": "Post 1", - "authorId": 1 - }, - { - "id": 2, - "createdAt": "2023-03-14T07:45:04.036Z", - "updatedAt": "2023-03-14T07:45:04.036Z", - "title": "Post 2", - "authorId": 1 - } - ] - } - ``` - - - - - - ```ts - // create a user and attach two posts - const r = await fetch(`/api/user`, { - method: 'POST', - headers: { 'Content-Type': 'application/vnd.api+json' }, - body: JSON.stringify({ - data: { - type: 'user', - attributes: { - email: 'user1@abc.com' - }, - relationships: { - posts: { - data: [ - { type: 'post', id: 1 }, - { type: 'post', id: 2 } - ] - } - } - } - }) - }); - - console.log(await r.json()); - ``` - - Output: - - ```json - { - "jsonapi": { "version": "1.1" }, - "data": { - "type": "user", - "id": 1, - "attributes": { - "email": "user1@abc.com", - }, - "links": { - "self": "http://localhost/api/user/1", - }, - "relationships": { - "posts": { - "links": { - "self": "http://localhost/api/user/1/relationships/posts", - "related": "http://localhost/api/user/1/posts", - }, - "data": [ - { "type": "post", "id": 1 }, - { "type": "post", "id": 2 }, - ], - }, - }, - }, - } - ``` - - - - - -1. With third-party client generators - - ZenStack provides an [OpenAPI](../plugins/openapi) plugin for generating Open API 3.x specification from the ZModel. The generated OpenAPI spec can be used to generate client libraries for various languages and frameworks. For example, you can use [openapi-typescript](https://github.com/drwpow/openapi-typescript) to generate a typescript client. diff --git a/versioned_docs/version-3.x/reference/server-adapters/api-handlers/_data_type_serialization.md b/versioned_docs/version-3.x/reference/server-adapters/api-handlers/_data_type_serialization.md deleted file mode 100644 index d22a602f..00000000 --- a/versioned_docs/version-3.x/reference/server-adapters/api-handlers/_data_type_serialization.md +++ /dev/null @@ -1,16 +0,0 @@ -- `DateTime` - - ISO 8601 string - -- `Bytes` - - Base64-encoded string - -- `BigInt` - - String representation - -- `Decimal` - - String representation - \ No newline at end of file diff --git a/versioned_docs/version-3.x/reference/server-adapters/api-handlers/index.mdx b/versioned_docs/version-3.x/reference/server-adapters/api-handlers/index.mdx deleted file mode 100644 index df4074ec..00000000 --- a/versioned_docs/version-3.x/reference/server-adapters/api-handlers/index.mdx +++ /dev/null @@ -1,31 +0,0 @@ ---- -sidebar_position: 100 ---- - -import useBaseUrl from '@docusaurus/useBaseUrl'; -import ThemedImage from '@theme/ThemedImage'; - -# API Handlers - -ZenStack supports two API styles: ***rpc*** and ***rest***: - -- ***rpc*** style API directly mirrors Prisma client's API, thus provides endpoints like `[model]/findMany`, `[model]/create`, etc. Its input and output data format also aligns with that of Prisma client. - -- ***rest*** style API is designed to be more RESTful, thus provides endpoints like `[model]`, `[model]/[id]`, `[model]/[id]/relationships/[relationship]`, etc. It uses [JSON:API](https://jsonapi.org/) as its data format. - -API handlers are framework-agnostic and deals with canonicalized request and response objects. It's the responsibility of the server adapters to translate the framework-specific request and response types. You can use either of these two API handlers with any server adapter. - -The following diagram illustrates their relationships with server adapters. - - - -Checkout the reference pages for detailed information about these API handlers: - -- [RPC API Handler](./rpc) -- [RESTful API Handler](./rest) diff --git a/versioned_docs/version-3.x/reference/server-adapters/api-handlers/rest.mdx b/versioned_docs/version-3.x/reference/server-adapters/api-handlers/rest.mdx deleted file mode 100644 index 67365c6f..00000000 --- a/versioned_docs/version-3.x/reference/server-adapters/api-handlers/rest.mdx +++ /dev/null @@ -1,982 +0,0 @@ ---- -description: RESTful-style API handler that provides resource-centric endpoints -sidebar_position: 2 -title: RESTful API Handler ---- - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; -import DataTypeSerialization from './_data_type_serialization.md'; - -# RESTful API Handler - -## Introduction - -The RESTful-style API handler exposes CRUD APIs as RESTful endpoints using [JSON:API](https://jsonapi.org/) as transportation format. The API handler is not meant to be used directly; instead, you should use it together with a [server adapter](../../../category/server-adapters) which handles the request and response API for a specific framework. - -It can be created as the following: - - - - -```ts title='/src/app/api/model/[...path]/route.ts' -import { NextRequestHandler } from '@zenstackhq/server/next'; -import { RestApiHandler } from '@zenstackhq/server/api'; -import { getPrisma } from '~/lib/db'; - -const handler = NextRequestHandler({ - getPrisma, - useAppDir: true, - handler: RestApiHandler({ endpoint: 'http://myhost/api' }) -}); - -export { - handler as GET, - handler as POST, - handler as PUT, - handler as PATCH, - handler as DELETE, -}; -``` - - - - - -```ts title='/src/hooks.server.ts' -import { SvelteKitHandler } from '@zenstackhq/server/sveltekit'; -import { RestApiHandler } from '@zenstackhq/server/api'; -import { getPrisma } from './lib/db'; - -export const handle = SvelteKitHandler({ - prefix: '/api/model', - getPrisma, - handler: RestApiHandler({ endpoint: 'http://myhost/api/model' }) -}); -``` - - - - - -```ts title='/server/api/model/[...].ts' -import { createEventHandler } from '@zenstackhq/server/nuxt'; -import { RestApiHandler } from '@zenstackhq/server/api'; -import { getPrisma } from './lib/db'; - -export default createEventHandler({ - handler: RestApiHandler({ endpoint: 'http://myhost/api/model' }) - getPrisma -}); -``` - - - - - -The factory function accepts an options object with the following fields: - -- endpoint - - Required. A `string` field representing the base URL of the RESTful API, used for generating resource links. - -- pageSize - - Optional. A `number` field representing the default page size for listing resources and relationships. Defaults to 100. Set to Infinity to disable pagination. - -- modelNameMapping - - Optional. An `Record` value that provides a mapping from model names (as defined in ZModel) to URL path names. This is useful for example when you want to use plural names in URL endpoints: - - ```ts - // endpoint for accessing User model will then be ".../users" - RestApiHandler({ - modelNameMapping: { - User: 'users' - } - }) - ``` - - The mapping can be partial. You only need to specify the model names that you want to override. If a mapping is provided, only the mapped url path is valid, and accessing to unmapped path will be denied. - - -## Endpoints and Features - -The RESTful API handler conforms to the the [JSON:API](https://jsonapi.org/format/) v1.1 specification for its URL design and input/output format. The following sections list the endpoints and features are implemented. The examples refer to the following schema modeling a blogging app: - -```zmodel -model User { - id Int @id @default(autoincrement()) - email String - posts Post[] -} - -model Profile { - id Int @id @default(autoincrement()) - gender String - user User @relation(fields: [userId], references: [id]) - userId Int @unique -} - -model Post { - id Int @id @default(autoincrement()) - title String - published Boolean @default(false) - viewCount Int @default(0) - author User @relation(fields: [authorId], references: [id]) - authorId Int - comments Comment[] -} - -model Comment { - id Int @id @default(autoincrement()) - content String - post Post @relation(fields: [postId], references: [id]) - postId Int -} -``` - -### Listing resources - -A specific type of resource can be listed using the following endpoint: - -``` -GET /:type -``` - -#### Status codes - -- 200: The request was successful and the response body contains the requested resources. -- 400: The request was malformed. -- 403: The request was forbidden. -- 404: The requested resource type does not exist. -- 422: The request violated data validation rules. - -#### Examples - -```ts -GET /post -``` - -```json -{ - "meta": { - "total": 1 - }, - "data": [ - { - "attributes": { - "authorId": 1, - "published": true, - "title": "My Awesome Post", - "viewCount": 0 - }, - "id": 1, - "links": { - "self": "http://myhost/api/post/1" - }, - "relationships": { - "author": { - "data": { "id": 1, "type": "user" }, - "links": { - "related": "http://myhost/api/post/1/author/1", - "self": "http://myhost/api/post/1/relationships/author/1" - } - } - }, - "type": "post" - } - ], - "jsonapi": { - "version": "1.1" - }, - "links": { - "first": "http://myhost/api/post?page%5Blimit%5D=100", - "last": "http://myhost/api/post?page%5Boffset%5D=0", - "next": null, - "prev": null, - "self": "http://myhost/api/post" - } -} -``` - -### Fetching a resource - -A unique resource can be fetched using the following endpoint: - -```ts -GET /:type/:id -``` - -#### Status codes - -- 200: The request was successful and the response body contains the requested resource. -- 400: The request was malformed. -- 403: The request was forbidden. -- 404: The requested resource type or ID does not exist. - -#### Examples - -```ts -GET /post/1 -``` - -```json -{ - "data": { - "attributes": { - "authorId": 1, - "published": true, - "title": "My Awesome Post", - "viewCount": 0 - }, - "id": 1, - "links": { - "self": "http://myhost/api/post/1" - }, - "relationships": { - "author": { - "data": { "id": 1, "type": "user" }, - "links": { - "related": "http://myhost/api/post/1/author/1", - "self": "http://myhost/api/post/1/relationships/author/1" - } - } - }, - "type": "post" - }, - "jsonapi": { - "version": "1.1" - }, - "links": { - "self": "http://myhost/api/post/1" - } -} -``` - -### Fetching relationships - -A resource's relationships can be fetched using the following endpoint: - -```ts -GET /:type/:id/relationships/:relationship -``` - -#### Status codes - -- 200: The request was successful and the response body contains the requested relationships. -- 400: The request was malformed. -- 403: The request was forbidden. -- 404: The requested resource type, ID, or relationship does not exist. - -#### Examples - -1. Fetching a to-one relationship - - ```ts - GET /post/1/relationships/author - ``` - - ```json - { - "data" : { "id" : 1, "type" : "user" }, - "jsonapi" : { - "version" : "1.1" - }, - "links" : { - "self" : "http://myhost/api/post/1/relationships/author" - } - } - ``` - -1. Fetching a to-many relationship - - ```ts - GET /user/1/relationships/posts - ``` - - ```json - { - "data" : [ - { "id" : 1, "type" : "post" }, - { "id" : 2, "type" : "post" } - ], - "jsonapi" : { - "version" : "1.1" - }, - "links" : { - "first" : "http://myhost/api/user/1/relationships/posts?page%5Blimit%5D=100", - "last" : "http://myhost/api/user/1/relationships/posts?page%5Boffset%5D=0", - "next" : null, - "prev" : null, - "self" : "http://myhost/api/user/1/relationships/posts" - } - } - ``` - -### Fetching related resources - -```ts -GET /:type/:id/:relationship -``` - -#### Status codes - -- 200: The request was successful and the response body contains the requested relationship. -- 400: The request was malformed. -- 403: The request was forbidden. -- 404: The requested resource type, ID, or relationship does not exist. - -#### Examples - -```ts -GET /post/1/author -``` - -```json -{ - "data" : { - "attributes" : { - "email" : "emily@zenstack.dev", - "name" : "Emily" - }, - "id" : 1, - "links" : { - "self" : "http://myhost/api/user/1" - }, - "relationships" : { - "posts" : { - "links" : { - "related" : "http://myhost/api/user/1/posts", - "self" : "http://myhost/api/user/1/relationships/posts" - } - } - }, - "type" : "user" - }, - "jsonapi" : { - "version" : "1.1" - }, - "links" : { - "self" : "http://myhost/api/post/1/author" - } -} -``` - -### Fine-grained data fetching - -#### Filtering - -You can use the `filter[:selector1][:selector2][...]=value` [query parameter family](https://jsonapi.org/format/#query-parameters-families) to filter resource collections or relationship collections. - -##### Examples - -1. Equality filter against plain field - - ```ts - GET /api/post?filter[published]=false - ``` - -1. Equality filter against relationship - - Relationship field can be filtered directly by its id. - - ```ts - GET /api/post?filter[author]=1 - ``` - - If the relationship is to-many, the filter has "some" semantic and evaluates to `true` if any of the items in the relationship matches. - - ```ts - GET /api/user?filter[posts]=1 - ``` - -1. Filtering with multiple values - - Multiple filter values can be separated by comma. Items statisfying any of the values will be returned. - - ```ts - GET /api/post?filter[author]=1,2 - ``` - -1. Multiple filters - - A request can carry multiple filters. Only items statisfying all filters will be returned. - - ```ts - GET /api/post?filter[author]=1&filter[published]=true - ``` - -1. Deep filtering - - A filter can carry multiple field selectors to reach into relationships. - - ```ts - GET /api/post?filter[author][name]=Emily - ``` - - When reaching into a to-many relationship, the filter has "some" semantic and evaluates to `true` if any of the items in the relationship matches. - - ``` - GET /api/user?filter[posts][published]=true - ``` - -1. Filtering with comparison operators - - Filters can go beyond equality by appending an "operator suffix". - - ```ts - GET /api/post?filter[viewCount$gt]=100 - ``` - - The following operators are supported: - - - **$lt** - - Less than - - - **$lte** - - Less than or equal to - - - **$gt** - - Greater than - - - **$gte** - - Greater than or equal to - - - **$contains** - - String contains - - - **$icontains** - - Case-insensitive string contains - - - **$search** - - String full-text search - - - **$startsWith** - - String starts with - - - **$endsWith** - - String ends with - - - **$has** - - Collection has value - - - **$hasEvery** - - Collection has every element in value - - - **$hasSome** - - Collection has some elements in value - - - **$isEmpty** - - Collection is empty - -#### Sorting - -You can use the `sort` query parameter to sort resource collections or relationship collections. The value of the parameter is a comma-separated list of fields names. The order of the fields in the list determines the order of sorting. By default, sorting is done in ascending order. To sort in descending order, prefix the field name with a minus sign. - -##### Examples - -```ts -GET /api/post?sort=createdAt,-viewCount -``` - -#### Pagination - -When creating a RESTful API handler, you can pass in a `pageSize` option to control pagination behavior of fetching a collection of resources, related resources, and relationships. By default the page size is 100, and you can disable pagination by setting `pageSize` option to `Infinity`. - -When fetching a collection resource or relationship, you can use the `page[offset]=value` and `page[limit]=value` [query parameter family](https://jsonapi.org/format/#query-parameters-families) to fetch a specific page. They're mapped to `skip` and `take` parameters in the query arguments sent to PrismaClient. - -The response data of collection fetching contains pagination links that facilitate navigating through the collection. The "meta" section also contains the total count available. E.g.: - -```json -{ - "meta": { - "total": 10 - }, - "data" : [ - ... - ], - "links" : { - "first" : "http://myhost/api/post?page%5Blimit%5D=2", - "last" : "http://myhost/api/post?page%5Boffset%5D=4", - "next" : "http://myhost/api/post?page%5Boffset%5D=4&page%5Blimit%5D=2", - "prev" : "http://myhost/api/post?page%5Boffset%5D=0&page%5Blimit%5D=2", - "self" : "http://myhost/api/post" - } -} -``` - -##### Examples - -1. Fetching a specific page of resources - - ```ts - GET /api/post?page[offset]=10&page[limit]=5 - ``` - -1. Fetching a specific page of relationships - - ```ts - GET /api/user/1/relationships/posts?page[offset]=10&page[limit]=5 - ``` - -1. Fetching a specific page of related resources - - ```ts - GET /api/user/1/posts?page[offset]=10&page[limit]=5 - ``` - -#### Including related resources - -You can use the `include` query parameter to include related resources in the response. The value of the parameter is a comma-separated list of fields names. Field names can contain dots to reach into nested relationships. - -When including related resources, the response data takes the form of [Compound Documents](https://jsonapi.org/format/#document-compound-documents) and contains a `included` field carrying normalized related resources. E.g.: - -```json -{ - "data" : [ - { - "attributes" : { - ... - }, - "id" : 1, - "relationships" : { - "author" : { - "data" : { "id" : 1, "type" : "user" } - } - }, - "type" : "post" - } - ], - "included" : [ - { - "attributes" : { - "email" : "emily@zenstack.dev", - "name" : "Emily" - }, - "id" : 1, - "links" : { - "self" : "http://myhost/api/user/1" - }, - "relationships" : { - "posts" : { - "links" : { - "related" : "http://myhost/api/user/1/posts", - "self" : "http://myhost/api/user/1/relationships/posts" - } - } - }, - "type" : "user" - } - ] -} -``` - -##### Examples - -1. Including a direct relationship - - ```ts - GET /api/post?include=author - ``` - -1. Including a deep relationship - - ```ts - GET /api/post?include=author.profile - ``` - -1. Including multiple relationships - - ```ts - GET /api/post?include=author,comments - ``` - -### Creating a resource - -A new resource can be created using the following endpoint: - -``` -POST /:type -``` - -#### Status codes - -- 201: The request was successful and the resource was created. -- 400: The request was malformed. -- 403: The request was forbidden. -- 404: The requested resource type does not exist. - -#### Examples - -1. Creating a resource - ```json - POST /user - { - "data": { - "type": "user", - "attributes": { - "name": "Emily", - "email": "emily@zenstack.dev" - } - } - } - ``` - -1. Creating a resource with relationships attached - - ```json - POST /user - { - "data": { - "type": "user", - "attributes": { - "name": "Emily", - "email": "emily@zenstack.dev" - }, - "relationships": { - "posts": { - "data": [{ "type": "post", "id": 1 }] - } - } - } - } - ``` - -### Updating a resource - -A resource can be updated using the following endpoints: - -```ts -PUT /:type/:id -PATCH /:type/:id -``` - -Both `PUT` and `PATCH` do partial update and has exactly the same behavior. - -:::info -Besides plain fields, you can also include relationships in the request body. Please note that this won't update the related resource; instead if only replaces the relationships. If you update a to-many relationship, the new collection will entirely replace the old one. - -Relationships can also be manipulated directly. See [Manipulating Relationships](#manipulating-relationships) for more details. -::: - -#### Status codes - -- 200: The request was successful and the resource was updated. -- 400: The request was malformed. -- 403: The request was forbidden. -- 404: The requested resource type or ID does not exist. - -#### Examples - -1. Updating a resource - - ```json - PUT /post/1 - { - "data": { - "type": "post", - "attributes": { - "title": "My Awesome Post" - } - } - } - ``` - -1. Updating a resource's relationships - - ```json - PUT /user/1 - { - "data": { - "type": "user", - "relationships": { - "posts": { - "data": [{ "type": "post", "id": 2 }] - } - } - } - } - ``` - - -### Upserting a resource - -JSON:API didn't specify a convention for "upsert" operations. ZenStack uses a variation of the "create" operation to represent "upsert", and uses the request meta to indicate the intention. See details in [examples](#examples-10). - -``` -POST /:type -``` - -#### Status codes - -- 201: The request was successful and the resource was created. -- 200: The request was successful and the resource was updated. -- 400: The request was malformed. -- 403: The request was forbidden. -- 404: The requested resource type does not exist. - -#### Examples - -```json -POST /user -{ - "data": { - "type": "user", - "attributes": { - "id": 1, - "name": "Emily", - "email": "emily@zenstack.dev" - } - }, - "meta": { - "operation": "upsert", - "matchFields": ["id"], - } -} -``` - -The `meta.operation` field must be "upsert", and the `meta.matchFields` field must be an array of field names that are used to determine if the resource already exists. If an existing resource is found, "update" operation is conducted, otherwise "create". The `meta.matchFields` fields must be unique fields, and they must have corresponding entries in `data.attributes`. - -### Deleting a resource - -A resource can be deleted using the following endpoint: - -#### Status codes - -- 204: The request was successful and the resource was deleted. -- 403: The request was forbidden. -- 404: The requested resource type or ID does not exist. - -```ts -DELETE /:type/:id -``` - -### Manipulating relationships - -Relationships can be manipulated using the following endpoints: - -#### Adding to a to-many relationship - -```ts -POST /:type/:id/relationships/:relationship -``` - -##### Status codes - -- 200: The request was successful and the relationship was updated. -- 403: The request was forbidden. -- 404: The requested resource type, ID, or relationship does not exist. - -##### Examples - -```json -POST /user/1/relationships/posts -{ - "data": [ - { "type": "post", "id": "1" }, - { "type": "post", "id": "2" } - ] -} -``` - -#### Updating a relationship (to-one or to-many) - -```ts -PUT /:type/:id/relationships/:relationship -PATCH /:type/:id/relationships/:relationship -``` - -:::info -`PUT` and `PATCH` has exactly the same behavior and both relace the existing relationships with the new ones entirely. -::: - -##### Status codes - -- 200: The request was successful and the relationship was updated. -- 403: The request was forbidden. -- 404: The requested resource type, ID, or relationship does not exist. - -##### Examples - -1. Replacing a to-many relationship - - ```json - PUT /user/1/relationships/posts - { - "data": [ - { "type": "post", "id": "1" }, - { "type": "post", "id": "2" } - ] - } - ``` - -1. Replacing a to-one relationship - - ```json - PUT /post/1/relationships/author - { - "data": { "type": "user", "id": "2" } - } - ``` - -1. Clearing a to-many relationship - - ```json - PUT /user/1/relationships/posts - { - "data": [] - } - ``` - -1. Clearing a to-one relationship - - ```json - PUT /post/1/relationships/author - { - "data": null - } - ``` - -## Compound ID Fields - -Prisma allows a model to have compound ID fields, e.g.: - -```zmodel -model Post { - id1 Int - id2 Int - @@id([id1, id2]) -} -``` - -The JSON:API specification doesn't have a native way to represent compound IDs. To mitigate this limitation, when returning an entity with compound IDs, ZenStack synthesizes an "id" field to carry the ID values joined with underscore: - -```json -{ - "data": { - "id": "1_2", - "attributes": { - "id1": 1, - "id2": 2, - ... - }, - "links" : { - "self" : "http://localhost:3100/api/model/post/1_2" - }, - ... - } -} -``` - -You can use this ID value convension in places where an ID is needed, e.g., reading a single entity. - -```ts -GET /post/1_2 -``` - -Limitations: - -1. Joining ID values with underscore implies that the ID values themselves cannot contain underscores. We'll make the separator configurable in the future. -1. Prisma allows you to create a name for the compound ID field. Such usage is not yet supported by the RESTful API handler. - -> *Special thanks to [Thomas Sunde Nielsen](https://github.com/thomassnielsen) for implementing this feature!* - -## Serialization - -ZenStack uses [superjson](https://github.com/blitz-js/superjson) to serialize and deserialize data. Superjson generates two parts during serialization: - -- json: - - The JSON-compatible serialization result. - -- meta: - - The serialization metadata including information like field types that facilitates deserialization. - -If the data only involves simple data types, the serialization result is the same as regular `JSON.stringify`, and no `meta` part is generated. However, for complex data types (like `Bytes`, `Decimal`, etc.), a `meta` object will be generated, which needs to be carried along when sending the request, and will also be included in the response. - -When sending requests, if superjson-serializing the request body results in a `meta` object, it should be put into a ```{ "serialization": meta }``` object and included in the `meta` field of the request body. For example, if you have a `bytes` field of type `Bytes`, the request body should look like: - -```json -POST /post -{ - "data": { - "type": "post", - "attributes": { - ... - "bytes": "AQID" // base64-encoded bytes - } - }, - "meta": { - "serialization": {"values": { "data.attributes.bytes": [[ "custom", "Bytes"]] } } - } -} -``` - -Correspondingly, the response body of a query may look like: - -```json -GET /post/1 -{ - "data": { - "id": "1", - "type": "post", - "attributes": { - ... - "bytes": "AQID" // base64-encoded bytes - } - }, - "meta": { - "serialization": {"values": { "data.attributes.bytes": [[ "custom", "Bytes"]] } } - } -} -``` - -You should use the `meta.serialization` field value to superjson-deserialize the response body. - -### Data Type Serialization Format - - - -## Error Handling - -An error response is an object containing the following fields: - -- errors - - An array of error objects, each containing the following fields: - - - code: `string`, error code - - status: `number`, HTTP status code - - title: `string`, error title - - detail: `string`, error detail - - prismaCode: `string`, Prisma error code, if the error is thrown by Prisma - -### Example - -```json -{ - "errors" : [ - { - "code" : "unsupported-model", - "detail" : "Model foo doesn't exist", - "status" : 404, - "title" : "Unsupported model type" - } - ] -} -``` diff --git a/versioned_docs/version-3.x/reference/server-adapters/api-handlers/rpc.mdx b/versioned_docs/version-3.x/reference/server-adapters/api-handlers/rpc.mdx deleted file mode 100644 index ecfd1c96..00000000 --- a/versioned_docs/version-3.x/reference/server-adapters/api-handlers/rpc.mdx +++ /dev/null @@ -1,275 +0,0 @@ ---- -description: RPC-style API handler that fully mirrors PrismaClient's query API -sidebar_position: 1 -title: RPC API Handler ---- - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; -import DataTypeSerialization from './_data_type_serialization.md'; - -# RPC API Handler - -## Introduction - -The RPC-style API handler exposes CRUD endpoints that fully mirror [PrismaClient's query API](https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#model-queries). Consuming the APIs feels like making RPC calls to a PrismaClient then. The API handler is not meant to be used directly; instead, you should use it together with a [server adapter](../../../category/server-adapters) which handles the request and response API for a specific framework. - -It can be created and used as the following: - - - - -```ts title='/src/app/api/model/[...path]/route.ts' -import { NextRequestHandler } from '@zenstackhq/server/next'; -import { RestApiHandler } from '@zenstackhq/server/api'; -import { getPrisma } from '~/lib/db'; - -const handler = NextRequestHandler({ - getPrisma, - useAppDir: true, - handler: RPCApiHandler() // you can also omit it since `RPCApiHandler` is the default -}); - -export { - handler as GET, - handler as POST, - handler as PUT, - handler as PATCH, - handler as DELETE, -}; -``` - - - - - -```ts title='/src/hooks.server.ts' -import { SvelteKitHandler } from '@zenstackhq/server/sveltekit'; -import { RPCApiHandler } from '@zenstackhq/server/api'; -import { getPrisma } from './lib/db'; - -export const handle = SvelteKitHandler({ - prefix: '/api/model', - handler: RPCApiHandler(), // you can also omit it since `RPCApiHandler` is the default - getPrisma -}); -``` - - - - - -```ts title='/server/api/model/[...].ts' -import { createEventHandler } from '@zenstackhq/server/nuxt'; -import { RPCApiHandler } from '@zenstackhq/server/api'; -import { getPrisma } from './lib/db'; - -export default createEventHandler({ - handler: RPCApiHandler(), // you can also omit it since `RPCApiHandler` is the default - getPrisma -}); -``` - - - - - -## Wire Format - -### Input - -For endpoints using `GET` and `DELETE` Http verbs, the query body is serialized and passed as the `q` query parameter. E.g.: - -```ts -GET /api/post/findMany?q=%7B%22where%22%3A%7B%22public%22%3Atrue%7D%7D -``` - -- Endpoint: /api/post/findMany -- Query parameters: `q` -> `{ "where" : { "public": true } }` - -For endpoints using other HTTP verbs, the query body is passed as `application/json` in the request body. E.g.: - -```json -POST /api/post/create -{ "data": { "title": "Hello World" } } -``` - -### Output - -The output shape conforms to the data structure returned by the corresponding PrismaClient API, wrapped into a `data` field. E.g.: - -```json -GET /api/post/findMany - -{ - "data": [ { "id": 1, "title": "Hello World" } ] -} -``` - -### Serialization - -This section explains the details about data serialization. If you're using generated hooks to consume the API, the generated code already automatically deals with serialization for you, and you don't need to do any further processing. - -ZenStack uses [superjson](https://github.com/blitz-js/superjson) to serialize and deserialize data - including the `q` query parameter, the request body, and the response body. Superjson generates two parts during serialization: - -- json: - - The JSON-compatible serialization result. - -- meta: - - The serialization metadata including information like field types that facilitates deserialization. - -If the data only involves simple data types, the serialization result is the same as regular `JSON.stringify`, and no `meta` part is generated. However, for complex data types (like `Bytes`, `Decimal`, etc.), a `meta` object will be generated, which needs to be carried along when sending the request, and will also be included in the response. - -The following part explains how the `meta` information is included for different situations: - -- The `q` query parameter - - If during superjson-serialization of the `q` parameter, a `meta` object is generated, it should be put into an object `{ serialization: meta }`, JSON-stringified, and included as an additional query parameter `meta`. For example, if you have a field named `bytes` of `Bytes` type, and you may want to query with a filter like ```{ where: { bytes: Buffer.from([1,2,3]) } }```. Superjson-serializing the query object results in: - ```json - { - "json": { "where": { "bytes": "AQID" } }, // base-64 encoded bytes - "meta": { "values": { "where.bytes": [["custom","Bytes"]] } } - } - ``` - Your query URL should look like: - ```json - GET /api/post/findMany?q={"where":{"bytes":"AQID"}}&meta={"serialization":{"values":{"where.bytes":[["custom","Bytes"]]}}} - ``` - -- The request body - - If during superjson-serialization of the request body, a `meta` object is generated, it should be put into an object `{ serialization: meta }`, and included as an additional field `meta` field in the request body. For example, if you have a field named `bytes` of `Bytes` type, and you may want to create a record with a value like ```{ data: { bytes: Buffer.from([1,2,3]) } }```. Superjson-serializing the request body results in: - ```json - { - "json": { "bytes": "AQID" }, // base-64 encoded bytes - "meta": { "values": { "bytes": [[ "custom", "Bytes" ]] } } - } - ``` - Your request body should look like: - ```json - POST /api/post/create - - { - "data": { "bytes": "AQID" }, - "meta": { "serialization": {"values": { "bytes": [[ "custom","Bytes" ]] } } } - } - ``` - -- The response body - - If during superjson-serialization of the response body, a `meta` object is generated, it will be put into an object `{ serialization: meta }`, and included as an additional field `meta` field in the response body. For example, if you have a field named `bytes` of `Bytes` type, and a `findFirst` query returns ```{ id: 1, bytes: Buffer.from([1,2,3]) }```. Superjson-serializing the request body results in: - ```json - { - "json": { "id": 1, "bytes":"AQID" }, // base-64 encoded bytes - "meta": { "values": { "bytes": [[ "custom", "Bytes" ]] } } - } - ``` - Your response body will look like: - ```json - GET /api/post/findFirst - - { - "data": { "id": 1, "bytes": "AQID" }, - "meta": { "serialization": {"values": { "bytes": [[ "custom","Bytes"]] } } } - } - ``` - - You should use the meta.serialization field value to superjson-deserialize the response body. - -#### Data Type Serialization Format - - - -## Endpoints - -- **[model]/findMany** - - _Http method:_ `GET` - -- **[model]/findUnique** - - _Http method:_ `GET` - -- **[model]/findFirst** - - _Http method:_ `GET` - -- **[model]/count** - - _Http method:_ `GET` - -- **[model]/aggregate** - - _Http method:_ `GET` - -- **[model]/groupBy** - - _Http method:_ `GET` - -- **[model]/create** - - _Http method:_ `POST` - -- **[model]/createMany** - - _Http method:_ `POST` - -- **[model]/update** - - _Http method:_ `PATCH` or `PUT` - -- **[model]/updateMany** - - _Http method:_ `PATCH` or `PUT` - -- **[model]/upsert** - - _Http method:_ `POST` - -- **[model]/delete** - - _Http method:_ `DELETE` - -- **[model]/deleteMany** - - _Http method:_ `DELETE` - -- **[model]/check** - - _Http method:_ `GET` - -## HTTP Status Code and Error Responses - -### Status code - -The HTTP status code used by the endpoints follows the following rules: - -- `create` and `createMany` use `201` for success. Other endpoints use `200`. -- `403` is used for to indicate the request is denied due to lack of permissions, usually caused by access policy violation. -- `400` is used for invalid requests, e.g., malformed request body. -- `422` is used for data validation errors. -- `500` is used for other unexpected errors. - -### Error response format - -```ts -{ - // true to indicate the failure is due to a Prisma error - prisma?: boolean; - - // true to indicate the failure is due to access policy violation - rejectedByPolicy?: boolean; - - // original Prisma error code, available when `prisma` is true - code?: string; - - // error message - message: string; - - // extra reason about why a failure happened (e.g., 'RESULT_NOT_READABLE' indicates - // a mutation succeeded but the result cannot be read back due to access policy) - reason?: string; -} -``` diff --git a/versioned_docs/version-3.x/reference/server-adapters/elysia.mdx b/versioned_docs/version-3.x/reference/server-adapters/elysia.mdx deleted file mode 100644 index 41582343..00000000 --- a/versioned_docs/version-3.x/reference/server-adapters/elysia.mdx +++ /dev/null @@ -1,69 +0,0 @@ ---- -title: Elysia -description: Adapter for integrating with Elysia -sidebar_position: 8 ---- - -import ErrorHandling from './_error-handling.md'; -import AdapterOptions from './_options.mdx'; -import UsingAPI from './_using-api.mdx' - -# Elysia Adapter - -The `@zenstackhq/server/elysia` module provides a quick way to install a CRUD middleware onto an [Elysia](https://elysiajs.com/) app. Combined with ZenStack's power of enhancing Prisma with access policies, you can achieve a secure data backend without manually coding it. - -This feature is contributed by [@rodrigoburigool](https://github.com/rodrigoburigool). - -### Installation - -```bash -bun install @zenstackhq/server -``` - -### Mounting the API - -You can use the `createElysiaHandler` API to create an Elysia request handler that handles CRUD requests automatically: - -```ts -import { PrismaClient } from '@prisma/client'; -import { Elysia, Context } from 'elysia'; -import { enhance } from '@zenstackhq/runtime'; -import { createElysiaHandler } from '@zenstackhq/server/elysia'; - -const prisma = new PrismaClient(); - -const app = new Elysia({ prefix: '/api' }); - -// install the CRUD middleware under route "/api/crud" -app.group('/crud', (app) => - app.use( - createElysiaHandler({ - getPrisma: (context) => enhance(prisma, { user: getCurrentUser(context) }), - basePath: '/api/crud', - }) - ) -); - -function getCurrentUser(context: Context) { - // the implementation depends on your authentication mechanism - ... -} - -app.listen(3000); -``` - -The middleware factory takes the following options to initialize: - - - -- basePath (optional) - -
string
- - Optional base path to strip from the request path before passing to the API handler. E.g., if your CRUD handler is mounted at `/api/crud`, set this field to `'/api/crud'`. - -### Using the API - - - - diff --git a/versioned_docs/version-3.x/reference/server-adapters/express.mdx b/versioned_docs/version-3.x/reference/server-adapters/express.mdx deleted file mode 100644 index abc413ec..00000000 --- a/versioned_docs/version-3.x/reference/server-adapters/express.mdx +++ /dev/null @@ -1,66 +0,0 @@ ---- -title: Express.js -description: Adapter for integrating with Express.js -sidebar_position: 4 ---- - -import ErrorHandling from './_error-handling.md'; -import AdapterOptions from './_options.mdx'; -import UsingAPI from './_using-api.mdx' - -# Express.js Adapter - -The `@zenstackhq/server/express` module provides a quick way to install API routes onto a [Express.js](https://expressjs.com/) project for database CRUD operations. Combined with ZenStack's power of enhancing Prisma with access policies, it's surprisingly simple to achieve a secure data backend without manually coding it. - -### Installation - -```bash -npm install @zenstackhq/server -``` - -### Mounting the API - -You can integrate ZenStack into your project with the `ZenStackMiddleware` [express middleware](https://expressjs.com/en/guide/using-middleware.html): - -```ts -import { PrismaClient } from '@prisma/client'; -import { enhance } from '@zenstackhq/runtime'; -import { ZenStackMiddleware } from '@zenstackhq/server/express'; -import express from 'express'; - -const prisma = new PrismaClient(); -const app = express(); - -app.use(express.json()); - -app.use( - '/api/model', - ZenStackMiddleware({ - // getSessionUser extracts the current session user from the request, its - // implementation depends on your auth solution - getPrisma: (request) => enhance(prisma, { user: getSessionUser(request) }), - }) -); -``` - -The Express.js adapter takes the following options to initialize: - - - -- sendResponse (optional) - -
boolean
- - Controls if the middleware directly sends a response. If set to false, the response is stored in the `res.locals` object and then the middleware calls the `next()` function to pass the control to the next middleware. Subsequent middleware or request handlers need to make sure to send a response. - - Defaults to `true`. - -### Using the API - - - - - -### Fully working example - -You can find a fully working example [here](https://github.com/zenstackhq/docs-tutorial-express). diff --git a/versioned_docs/version-3.x/reference/server-adapters/fastify.mdx b/versioned_docs/version-3.x/reference/server-adapters/fastify.mdx deleted file mode 100644 index be568e77..00000000 --- a/versioned_docs/version-3.x/reference/server-adapters/fastify.mdx +++ /dev/null @@ -1,56 +0,0 @@ ---- -title: Fastify -description: Adapter for integrating with Fastify -sidebar_position: 5 ---- - -import ErrorHandling from './_error-handling.md'; -import AdapterOptions from './_options.mdx'; -import UsingAPI from './_using-api.mdx'; - -# Fastify Adapter - -The `@zenstackhq/server/fastify` module provides a quick way to install API routes onto a [Fastify](https://www.fastify.io/) project for database CRUD operations. Combined with ZenStack's power of enhancing Prisma with access policies, it's surprisingly simple to achieve a secure data backend without manually coding it. - -### Installation - -```bash -npm install @zenstackhq/server -``` - -### Mounting the API - -You can integrate ZenStack into your project with the `ZenStackFastifyPlugin` [fastify plugin](https://www.fastify.io/docs/latest/Reference/Plugins/): - -```ts -import { enhance } from '@zenstackhq/runtime'; -import { ZenStackFastifyPlugin } from '@zenstackhq/server/fastify'; -import { prisma } from './db.ts'; -import { getSessionUser } from './auth.ts'; - -const server = fastify(); - -// serve OpenAPI at /api/model -server.register(ZenStackFastifyPlugin, { - prefix: '/api/model', - // getSessionUser extracts the current session user from the request, its - // implementation depends on your auth solution - getPrisma: (request) => enhance(prisma, { user: getSessionUser(request) }), -}); -``` - -The Fastify adapter takes the following options to initialize: - -- prefix - -
string
- - Prefix for the mounted API endpoints. E.g.: /api/model. - - - -### Using the API - - - - diff --git a/versioned_docs/version-3.x/reference/server-adapters/hono.mdx b/versioned_docs/version-3.x/reference/server-adapters/hono.mdx deleted file mode 100644 index 67fdea3e..00000000 --- a/versioned_docs/version-3.x/reference/server-adapters/hono.mdx +++ /dev/null @@ -1,57 +0,0 @@ ---- -title: Hono -description: Adapter for integrating with Hono -sidebar_position: 7 ---- - -import ErrorHandling from './_error-handling.md'; -import AdapterOptions from './_options.mdx'; -import UsingAPI from './_using-api.mdx' - -# Hono Adapter - -The `@zenstackhq/server/hono` module provides a quick way to install API routes onto a [Hono](https://hono.dev/) project for database CRUD operations. Combined with ZenStack's power of enhancing Prisma with access policies, it's surprisingly simple to achieve a secure data backend without manually coding it. - -### Installation - -```bash -npm install @zenstackhq/server -``` - -### Mounting the API - -You can use the `createHonoHandler` API to create a [Hono middleware](https://hono.dev/docs/getting-started/basic#using-middleware) that handles CRUD requests automatically: - -```ts -import { PrismaClient } from '@prisma/client'; -import { enhance } from '@zenstackhq/runtime'; -import { createHonoHandler } from '@zenstackhq/server/hono'; -import { Context, Hono } from 'hono'; - -const prisma = new PrismaClient(); -const app = new Hono(); - -app.use( - '/api/model/*', - createHonoHandler({ - getPrisma: (ctx) => { - return enhance(prisma, { user: getCurrentUser(ctx) }); - }, - }) -); - -function getCurrentUser(ctx: Context) { - // the implementation depends on your authentication mechanism - ... -} -``` - -The middleware factory takes the following options to initialize: - - - -### Using the API - - - - diff --git a/versioned_docs/version-3.x/reference/server-adapters/nestjs.mdx b/versioned_docs/version-3.x/reference/server-adapters/nestjs.mdx deleted file mode 100644 index 1d558d3d..00000000 --- a/versioned_docs/version-3.x/reference/server-adapters/nestjs.mdx +++ /dev/null @@ -1,268 +0,0 @@ ---- -title: NestJS -description: Adapter for integrating with NestJS -sidebar_position: 6 ---- - -import ErrorHandling from './_error-handling.md'; -import AdapterOptions from './_options.mdx'; -import UsingAPI from './_using-api.mdx'; - -# NestJS Adapter - -The `@zenstackhq/server/nestjs` module provides a quick way to install a ZenStack-enhanced Prisma service as a dependency injection provider onto a [NestJS](https://nestjs.com/) application. - -### Installation - -```bash -npm install @zenstackhq/server -``` - -### Registering the provider - -You can register the enhanced Prisma service by importing the `ZenStackModule` NestJS module. - -```ts -import { ZenStackModule } from '@zenstackhq/server/nestjs'; -import { enhance } from '@zenstackhq/runtime'; -import { PrismaService } from './prisma.service'; - -@Module({ - imports: [ - ZenStackModule.registerAsync({ - useFactory: (prisma: PrismaService) => { - return { - getEnhancedPrisma: () => enhance(prisma, { user: ... }), - }; - }, - inject: [PrismaService], - extraProviders: [PrismaService], - }), - ], -}) -export class AppModule {} -``` - -The `registerAsync` API takes as input a factory function that returns a config used for creating an enhanced prisma service. The config contains a callback function where you should create and return an enhanced `PrismaClient`. It'll be called each time a Prisma method is invoked. - -You'll usually pass in a user context when calling `enhance` inside the callback. The way how the user context is fetched depends on your authentication mechanism. You can check the [NestJS quick start guide](../../quick-start/nestjs) for a reference solution. - -### Using the enhanced Prisma service - -Inside your NestJS controllers or services, you can inject the enhanced Prisma service and use it as you would with the regular Prisma service. Just use the special token name `ENHANCED_PRISMA` when injecting the service. - -```ts -import { ENHANCED_PRISMA } from '@zenstackhq/server/nestjs'; - -@Controller() -export class MyController { - constructor( - @Inject(ENHANCED_PRISMA) private readonly prismaService: PrismaService, - ) {} - - ... -} -``` - -You can still use the regular Prisma service by injecting as usual. - -### Using the API handler service to automate request handling - -This module also provides an `ApiHandlerService` that can be used to automate the request handling using API handlers. This is useful if you want to handle requests in a more structured way, such as using a controller or middleware. Before using it, you should ensure `ENHANCED_PRISMA` is registered in the module. For example: - -```ts -import { ZenStackModule, ApiHandlerService } from '@zenstackhq/server/nestjs'; -import { enhance } from '@zenstackhq/runtime'; -import { PrismaService } from './prisma.service'; - -@Module({ - imports: [ - // Register the ZenStack module - // The module exports the ENHANCED_PRISMA token and could be used in the ApiHandlerService - ZenStackModule.registerAsync({ - useFactory: (prisma: PrismaService) => { - return { - getEnhancedPrisma: () => enhance(prisma, { user: ... }), - }; - }, - inject: [PrismaService], - extraProviders: [PrismaService], - }), - ], - providers: [ApiHandlerService] -}) -export class AppModule {} -``` - -Then, you can inject the `ApiHandlerService` into your code and use it to handle requests. The service provides a method `handleRequest` to handle request using API handler automatically. - -RPC API Handler: - -```ts -import { Controller, Get, Post } from '@nestjs/common'; -import { ApiHandlerService } from '@zenstackhq/server/nestjs'; - -@Controller('post') -export class PostController { - constructor(private readonly apiHandlerService: ApiHandlerService) {} - - // should align with the route generated by API handler - @Get('findMany') - async findMany() { - return this.apiHandlerService.handleRequest() - } - - @Post('create') - async create() { - return this.apiHandlerService.handleRequest() - } -} -``` - -RESTful API Handler: - -```ts -import { Controller, Get, Post } from '@nestjs/common'; -import { ApiHandlerService } from '@zenstackhq/server/nestjs'; -import RESTApiHandler from '@zenstackhq/server/api/rest'; - -const ENDPOINT = 'http://localhost'; - -@Controller('post') -export class PostController { - constructor(private readonly apiHandlerService: ApiHandlerService) {} - - // should align with the route generated by API handler - @Get() - async list() { - return this.apiHandlerService.handleRequest( - { - handler: RESTApiHandler({ - endpoint: ENDPOINT, - }), - } - ) - } - - @Post() - async create() { - return this.apiHandlerService.handleRequest( - { - handler: RESTApiHandler({ - endpoint: ENDPOINT, - }), - } - ) - } -} -``` - -### API reference - -#### `ZenStackModule.registerAsync` - -##### Signature - -```ts -registerAsync(options: ZenStackModuleAsyncOptions): DynamicModule; -``` - -##### Parameter `options` - -```ts -interface ZenStackModuleAsyncOptions { - /** - * Optional list of imported modules that export the providers which are - * required in this module. - */ - imports?: Array | DynamicModule | Promise | ForwardReference>; - - /** - * Whether the module is global-scoped. - */ - global?: boolean; - - /** - * The token to export the enhanced Prisma service. Default is `'ENHANCED_PRISMA'`. - */ - exportToken?: string; - - /** - * The factory function to create the enhancement options. - */ - useFactory: (...args: unknown[]) => Promise | ZenStackModuleOptions; - - /** - * The dependencies to inject into the factory function. - */ - inject?: FactoryProvider['inject']; - - /** - * Extra providers to facilitate dependency injection. - */ - extraProviders?: Provider[]; -} -``` - -```ts -interface ZenStackModuleOptions { - /** - * A callback for getting an enhanced `PrismaClient`. - */ - getEnhancedPrisma: (model?: string | symbol) => unknown; -} -``` - -#### `ApiHandlerService.handleRequest` - -##### Signature - -```ts -handleRequest(options?: ApiHandlerOptions): Promise; -``` - -##### Parameter `options` - -```ts -interface ApiHandlerOptions { - /** - * Logger settings - */ - logger?: LoggerConfig; - - /** - * Model metadata. By default loaded from the `node_module/.zenstack/model-meta` - * module. You can pass it in explicitly if you configured ZenStack to output to - * a different location. - */ - modelMeta?: ModelMeta; - - /** - * Zod schemas for validating request input. Pass `true` to load from standard location - * (need to enable `@core/zod` plugin in schema.zmodel) or omit to disable input validation. - */ - zodSchemas?: ZodSchemas | boolean; - - /** - * Api request handler function. Can be created using `@zenstackhq/server/api/rest` or `@zenstackhq/server/api/rpc` factory functions. - * Defaults to RPC-style API handler. - */ - handler?: HandleRequestFn; - - /** - * The base URL for the API handler. This is used to determine the base path for the API requests. - * If you are using the ApiHandlerService in a route with a prefix, you should set this to the prefix. - * - * e.g. - * without baseUrl(API handler default route): - * - RPC API handler: [model]/findMany - * - RESTful API handler: /:type - * - * with baseUrl(/api/crud): - * - RPC API handler: /api/crud/[model]/findMany - * - RESTful API handler: /api/crud/:type - */ - baseUrl?: string; -} -``` - diff --git a/versioned_docs/version-3.x/reference/server-adapters/next.mdx b/versioned_docs/version-3.x/reference/server-adapters/next.mdx deleted file mode 100644 index 48647a64..00000000 --- a/versioned_docs/version-3.x/reference/server-adapters/next.mdx +++ /dev/null @@ -1,124 +0,0 @@ ---- -title: Next.js -description: Adapter for integrating with Next.js -sidebar_position: 1 ---- - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; -import ErrorHandling from './_error-handling.md'; -import AdapterOptions from './_options.mdx'; -import UsingAPI from './_using-api.mdx'; - -# Next.js Server Adapter - -The `@zenstackhq/server/next` module provides a quick way to install API endpoints onto a [Next.js](https://nextjs.org/) project for database CRUD operations. Combined with ZenStack's power of enhancing Prisma with access policies, it's surprisingly simple to achieve a secure data backend without manually coding it. - -The server adapter supports both the "pages" routes for older versions of Next.js and the new "app" routes for Next.js 13. - -### Installation - -```bash -npm install @zenstackhq/server -``` - -### Mounting the API - -You can use it to create a request handler in an API endpoint like: - - - - - -```ts title='/src/app/api/model/[...path]/route.ts' -import { NextRequestHandler } from '@zenstackhq/server/next'; -import type { NextRequest } from "next/server"; -import { enhance } from '@zenstackhq/runtime'; -import { prisma } from '~/lib/db.ts'; -import { getSessionUser } from '~/lib/auth.ts'; - -// create an enhanced Prisma client with user context -function getPrisma(req: NextRequest) { - // getSessionUser extracts the current session user from the request, its - // implementation depends on your auth solution - return enhance(prisma, { user: getSessionUser(req) }); -} - -const handler = NextRequestHandler({ getPrisma, useAppDir: true }); - -export { - handler as GET, - handler as POST, - handler as PUT, - handler as PATCH, - handler as DELETE, -}; -``` - -The Next.js API route handler takes the following options to initialize: - - - - - - - -```ts title='/src/pages/api/model/[...path].ts' -import { NextRequestHandler } from '@zenstackhq/server/next'; -import type { NextApiRequest, NextApiResponse } from 'next'; -import { enhance } from '@zenstackhq/runtime'; -import { prisma } from '~/lib/db.ts'; -import { getSessionUser } from '~/lib/auth.ts'; - -// create an enhanced Prisma client with user context -function getPrisma(req: NextApiRequest, res: NextApiResponse) { - // getSessionUser extracts the current session user from the request, its - // implementation depends on your auth solution - return enhance(prisma, { user: getSessionUser(req, res) }); -} - -// create the request handler with the `getPrisma` hook -export default NextRequestHandler({ getPrisma }); -``` - -The Next.js API route handler takes the following options to initialize: - - - - - - - -### Controlling what endpoints to expose - -You can use a [Next.js middleware](https://nextjs.org/docs/pages/building-your-application/routing/middleware) to further control what endpoints to expose. For example, if you're using a RESTful API handler installed at "/api/model", you can disallow listing all `User` entities by adding a middleware like: - -```ts title='/src/middleware.ts' -import { type NextRequest, NextResponse } from 'next/server'; - -export function middleware(request: NextRequest) { - const url = new URL(request.url); - if ( - request.method === 'GET' && - url.pathname.match(/^\/api\/model\/user\/?$/) - ) { - return NextResponse.json({ error: 'Not allowed' }, { status: 405 }); - } -} - -export const config = { - matcher: '/api/model/:path*', -}; -``` - -### Using the API - - - - - -### Fully working example - -You can find the fully working examples below: -- [Pages router](https://github.com/zenstackhq/docs-tutorial-nextjs) -- [Apps router](https://github.com/zenstackhq/docs-tutorial-nextjs-app-dir) diff --git a/versioned_docs/version-3.x/reference/server-adapters/nuxt.mdx b/versioned_docs/version-3.x/reference/server-adapters/nuxt.mdx deleted file mode 100644 index c254ccf1..00000000 --- a/versioned_docs/version-3.x/reference/server-adapters/nuxt.mdx +++ /dev/null @@ -1,44 +0,0 @@ ---- -title: Nuxt -description: Adapter for integrating with Nuxt -sidebar_position: 2 ---- - -import ErrorHandling from './_error-handling.md'; -import AdapterOptions from './_options.mdx'; -import UsingAPI from './_using-api.mdx'; - -# Nuxt Server Adapter - -The `@zenstackhq/server/nuxt` module provides a quick way to install API endpoints onto a [Nuxt V3](https://nuxt.com/) project for database CRUD operations. Combined with ZenStack's power of enhancing Prisma with access policies, it's surprisingly simple to achieve a secure data backend without manually coding it. - -### Mounting the API - -You can mount the API by creating a Nuxt server event handler like: - -```ts title='/server/api/model/[...].ts' -import { enhance } from '@zenstackhq/runtime'; -import { createEventHandler } from '@zenstackhq/server/nuxt'; -import { getSessionUser } from '~/server/auth'; -import { prisma } from '~/server/prisma'; - -export default createEventHandler({ - getPrisma: async (event) => { - return enhance(prisma, { user: getSessionUser(event) }); - }, -}); -``` - -The Nuxt event handler takes the following options to initialize: - - - -### Using the API - - - - - -### Fully working example - -You can find a fully working example [here](https://github.com/zenstackhq/sample-todo-nuxt). diff --git a/versioned_docs/version-3.x/reference/server-adapters/sveltekit.mdx b/versioned_docs/version-3.x/reference/server-adapters/sveltekit.mdx deleted file mode 100644 index 2976149f..00000000 --- a/versioned_docs/version-3.x/reference/server-adapters/sveltekit.mdx +++ /dev/null @@ -1,63 +0,0 @@ ---- -title: SvelteKit -description: Adapter for integrating with SvelteKit -sidebar_position: 3 ---- - -import ErrorHandling from './_error-handling.md'; -import AdapterOptions from './_options.mdx'; -import UsingAPI from './_using-api.mdx'; - -# SvelteKit Server Adapter - -The `@zenstackhq/server/sveltekit` module provides a quick way to install API endpoints onto a [SvelteKit](https://kit.svelte.dev/) project for database CRUD operations. Combined with ZenStack's power of enhancing Prisma with access policies, it's surprisingly simple to achieve a secure data backend without manually coding it. - -### Installation - -```bash -npm install @zenstackhq/server -``` - -### Mounting the API - -You can mount the API by creating a SvelteKit server hooks like: - -```ts title='/src/hooks.server.ts' -import { SvelteKitHandler } from '@zenstackhq/server/sveltekit'; -import { enhance } from '@zenstackhq/runtime'; -import { getSessionUser } from '$lib/auth.ts'; - -// create an enhanced Prisma client with user context -function getPrisma(event: RequestEvent) { - // getSessionUser extracts the current session user from the request, its - // implementation depends on your auth solution - return enhance({ user: getSessionUser(event) }); -} - -// create the hooks handler with the `getPrisma` hook -export const handle = SvelteKitHandler({ prefix: '/api/model', getPrisma }); -``` - -:::tip -You can use the [sequence helper](https://kit.svelte.dev/docs/modules#sveltejs-kit-hooks) to compose multiple server hooks. -::: - -The SvelteKit hooks handler takes the following options to initialize: - -- prefix - -
string
- - Prefix for the mounted API endpoints. E.g.: /api/model. - - - -### Using the API - - - - - -### Fully working example - -You can find a fully working example [here](https://github.com/zenstackhq/sample-todo-sveltekit). diff --git a/versioned_docs/version-3.x/reference/zmodel-language.md b/versioned_docs/version-3.x/reference/zmodel-language.md index 49988dce..8b540c21 100644 --- a/versioned_docs/version-3.x/reference/zmodel-language.md +++ b/versioned_docs/version-3.x/reference/zmodel-language.md @@ -6,1982 +6,3 @@ toc_max_heading_level: 3 --- # ZModel Language Reference - -## Overview - -**ZModel**, the modeling DSL of ZenStack, is the main concept you'll deal with when using this toolkit. The ZModel syntax is a superset of [Prisma Schema](https://www.prisma.io/docs/concepts/components/prisma-schema). Therefore, every valid Prisma schema is a valid ZModel. - -:::info - -We made that choice to extend the Prisma schema for several reasons: - -- Creating a new ORM adds little value to the community. Instead, extending Prisma - the overall best ORM toolkit for Typescript - sounds more sensible. - -- Prisma's schema language is simple and intuitive. - -- Extending an existing popular language lowers the learning curve compared to inventing a new one. - -::: - -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 - -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. - -This section provides detailed descriptions of all aspects of the ZModel language, so you don't have to jump over to Prisma's documentation for extra learning. - -## Import -ZModel allows to import other ZModel files. This is useful when you want to split your schema into multiple files for better organization. Under the hood, it will recursively merge all the imported schemas, and generate a single Prisma schema file for the Prisma CLI to consume. - -### Syntax - -```zmodel -import [IMPORT_SPECIFICATION] -``` - -- **[IMPORT_SPECIFICATION]**: - Path to the ZModel file to be imported. It can be: - - - An absolute path, e.g., "/path/to/user". - - A relative path, e.g., "./user". - - A module resolved to an installed NPM package, e.g., "my-package/base". - - If the import specification doesn't end with ".zmodel", the resolver will automatically append it. Once a file is imported, all the declarations in that file will be included in the building process. - -### Examples - -```zmodel -// there is a file called "user.zmodel" in the same directory -import "user" -``` - - -## Data source - -Every model needs to include exactly one `datasource` declaration, providing information on how to connect to the underlying database. - -### Syntax - -```zmodel -datasource [NAME] { - provider = [PROVIDER] - url = [DB_URL] -} -``` - -- **[NAME]**: - - Name of the data source. Needs to be a valid identifier matching regular expression `[A-Za-z][a-za-z0-9_]\*`. Name is only informational and serves no other purposes. - -- **[PROVIDER]**: - - Name of database connector. Valid values: - - - sqlite - - postgresql - - mysql - - sqlserver - - cockroachdb - -- **[DB_URL]**: - - Database connection string. Either a plain string or an invocation of `env` function to fetch from an environment variable. - -### Examples - -```zmodel -datasource db { - provider = "postgresql" - url = "postgresql://postgres:abc123@localhost:5432/todo?schema=public" -} -``` - -It's highly recommended that you not commit sensitive database connection strings into source control. Alternatively, you can load it from an environment variable: - -```zmodel -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") -} -``` - -### Supported databases - -ZenStack uses [Prisma](https://prisma.io ':target=_blank') to talk to databases, so all relational databases supported by Prisma are also supported by ZenStack. - -Here's a list for your reference: - -| Database | Version | -| --------------------- | ------- | -| PostgreSQL | 9.6 | -| PostgreSQL | 10 | -| PostgreSQL | 11 | -| PostgreSQL | 12 | -| PostgreSQL | 13 | -| PostgreSQL | 14 | -| PostgreSQL | 15 | -| MySQL | 5.6 | -| MySQL | 5.7 | -| MySQL | 8 | -| MariaDB | 10 | -| SQLite | \* | -| AWS Aurora | \* | -| AWS Aurora Serverless | \* | -| Microsoft SQL Server | 2022 | -| Microsoft SQL Server | 2019 | -| Microsoft SQL Server | 2017 | -| Azure SQL | \* | -| CockroachDB | 21.2.4+ | - -You can find the orignal list [here](https://www.prisma.io/docs/reference/database-reference/supported-databases ':target=_blank'). - -## Generator - -Generators are used for creating assets (usually code) from a Prisma schema. Check [here](https://www.prisma.io/docs/concepts/components/prisma-schema/generators) for a list of official and community generators. - -### Syntax - -```zmodel -generator [GENERATOR_NAME] { - [OPTION]* -} -``` - -- **[GENERATOR_NAME]** - - Name of the generator. Needs to be unique in the entire model. Needs to be a valid identifier matching regular expression `[A-Za-z][a-za-z0-9_]\*`. - -- **[OPTION]** - - A generator configuration option, in form of "[NAME] = [VALUE]". A generator needs to have at least a "provider" option that specify its provider. - -### Example - -```zmodel -generator client { - provider = "prisma-client-js" - output = "./generated/prisma-client-js" -``` - -## Plugin - -Plugins are ZenStack's extensibility mechanism. It's usage is similar to [Generator](#generator). Users can define their own plugins to generate artifacts from the ZModel schema. Plugins differ from generators mainly in the following ways: - -- They have a cleaner interface without the complexity of JSON-RPC. -- They use an easier-to-program AST representation than generators. -- They have access to language features that ZenStack adds to Prisma, like custom attributes and functions. - -### Syntax - -```zmodel -plugin [PLUGIN_NAME] { - [OPTION]* -} -``` - -- **[PLUGIN_NAME]** - - Name of the plugin. Needs to be unique in the entire model. Needs to be a valid identifier matching regular expression `[A-Za-z][a-za-z0-9_]\*`. - -- **[OPTION]** - - A plugin configuration option, in form of "[NAME] = [VALUE]". A plugin needs to have at least a "provider" option that specify its provider. - -### Example - -```zmodel -plugin swr { - provider = '@zenstackhq/swr' - output = 'lib/hooks' -} -``` - -## Enum - -Enums are container declarations for grouping constant identifiers. You can use them to express concepts like user roles, product categories, etc. - -### Syntax - -```prsima -enum [ENUM_NAME] { - [FIELD]* -} -``` - -- **[ENUM_NAME]** - - Name of the enum. 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]** - - Field identifier. Needs to be unique in the model. Needs to be a valid identifier matching regular expression `[A-Za-z][a-za-z0-9_]\*`. - -### Example - -```zmodel -enum UserRole { - USER - ADMIN -} -``` - -## Model - -Models represent the business entities of your application. A model inherits all fields and attributes from extended abstract models. Abstract models are eliminated in the generated prisma schema file. - -### Syntax -```zmodel -(abstract)? model [NAME] (extends [ABSTRACT_MODEL_NAME](,[ABSTRACT_MODEL_NAME])*)? { - [FIELD]* -} -``` -- **[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 [Field](#field) for details. - -- **[ABSTRACT_MODEL_NAME]**: - - Name of an abstract model. - -### Note - -A model must include a field marked with `@id` attribute. The `id` field serves as a unique identifier for a model entity and is mapped to the database table's primary key. - -See [here](#attribute) for more details about attributes. - -### Example - -```zmodel -abstract model Basic { - id String @id - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt -} - -model User extends Basic { - name String -} -``` - -The generated prisma file only contains one `User` model: -```zmodel -model User { - id String @id - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - name String @id -} -``` - -## 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 - -Attributes decorate fields and models and attach extra behaviors or constraints to them. - -### Syntax - -#### Field attribute - -Field attribute name is prefixed by a single `@`. - -```zmodel -id String @[ATTR_NAME](ARGS)? -``` - -- **[ATTR_NAME]** - -Attribute name. See [below](#predefined-attributes) for a full list of attributes. - -- **[ARGS]** - -See [attribute arguments](#arguments). - -#### Model attribute - -Field attribute name is prefixed double `@@`. - -```zmodel -model Model { - @@[ATTR_NAME](ARGS)? -} -``` - -- **[ATTR_NAME]** - -Attribute name. See [below](#predefined-attributes) for a full list of attributes. - -- **[ARGS]** - -See [attribute arguments](#arguments). - -### Arguments - -Attribute can be declared with a list of parameters and applied with a comma-separated list of arguments. - -Arguments are mapped to parameters by position or by name. For example, for the `@default` attribute declared as: - -```zmodel -attribute @default(_ value: ContextType) -``` - -, the following two ways of applying it are equivalent: - -```zmodel -published Boolean @default(value: false) -``` - -```zmodel -published Boolean @default(false) -``` - -### Parameter types - -Attribute parameters are typed. The following types are supported: - -- Int - - Integer literal can be passed as argument. - - E.g., declaration: - - ```zmodel - attribute @password(saltLength: Int?, salt: String?) - - ``` - - application: - - ```zmodel - password String @password(saltLength: 10) - ``` - -- String - - String literal can be passed as argument. - - E.g., declaration: - - ```zmodel - attribute @id(map: String?) - ``` - - application: - - ```zmodel - id String @id(map: "_id") - ``` - -- Boolean - - Boolean literal or expression can be passed as argument. - - E.g., declaration: - - ```zmodel - attribute @@allow(_ operation: String, _ condition: Boolean) - ``` - - application: - - ```zmodel - @@allow("read", true) - @@allow("update", auth() != null) - ``` - -- ContextType - - A special type that represents the type of the field onto which the attribute is attached. - - E.g., declaration: - - ```zmodel - attribute @default(_ value: ContextType) - ``` - - application: - - ```zmodel - f1 String @default("hello") - f2 Int @default(1) - ``` - -- FieldReference - - References to fields defined in the current model. - - E.g., declaration: - - ```zmodel - attribute @relation( - _ name: String?, - fields: FieldReference[]?, - references: FieldReference[]?, - onDelete: ReferentialAction?, - onUpdate: ReferentialAction?, - map: String?) - ``` - - application: - - ```zmodel - model Model { - ... - // [ownerId] is a list of FieldReference - owner Owner @relation(fields: [ownerId], references: [id]) - ownerId - } - ``` - -- Enum - - Attribute parameter can also be typed as predefined enum. - - E.g., declaration: - - ```zmodel - attribute @relation( - _ name: String?, - fields: FieldReference[]?, - references: FieldReference[]?, - // ReferentialAction is a predefined enum - onDelete: ReferentialAction?, - onUpdate: ReferentialAction?, - map: String?) - ``` - - application: - - ```zmodel - model Model { - // 'Cascade' is a predefined enum value - owner Owner @relation(..., onDelete: Cascade) - } - ``` - -An attribute parameter can be typed as any of the types above, a list of the above type, or an optional of the types above. - -```zmodel - model Model { - ... - f1 String - f2 String - // a list of FieldReference - @@unique([f1, f2]) - } -``` - -### Attribute functions - -Attribute functions are used for providing values for attribute arguments, e.g., current `DateTime`, an autoincrement `Int`, etc. They can be used in place of attribute arguments, like: - -```zmodel -model Model { - ... - serial Int @default(autoincrement()) - createdAt DateTime @default(now()) -} -``` - -You can find a list of predefined attribute functions [here](#predefined-attribute-functions). - -### Predefined attributes - -#### Field attributes - -##### @id - -```zmodel -attribute @id(map: String?) -``` - -Defines an ID on the model. - -_Params_: - -| Name | Description | -| ---- | ----------------------------------------------------------------- | -| map | The name of the underlying primary key constraint in the database | - -##### @default - -```zmodel -attribute @default(_ value: ContextType) -``` - -Defines a default value for a field. - -_Params_: - -| Name | Description | -| ----- | ---------------------------- | -| value | The default value expression | - -##### @unique - -```zmodel -attribute @unique(map: String?) -``` - -Defines a unique constraint for this field. - -_Params_: - -| Name | Description | -| ---- | ----------------------------------------------------------------- | -| map | The name of the underlying primary key constraint in the database | - -##### @relation - -```zmodel -attribute @relation( - _ name: String?, - fields: FieldReference[]?, - references: FieldReference[]?, - onDelete: ReferentialAction?, - onUpdate: ReferentialAction?, - map: String?) -``` - -Defines meta information about a relation. - -_Params_: - -| Name | Description | -| ---------- | ------------------------------------------------------------------------------ | -| name | The name of the relationship | -| fields | A list of fields defined in the current model | -| references | A list of fields of the model on the other side of the relation | -| onDelete | Referential action to take on delete. See details [here](#referential-action). | -| onUpdate | Referential action to take on update. See details [here](#referential-action). | - -##### @map - -```zmodel -attribute @map(_ name: String) -``` - -Maps a field name or enum value from the schema to a column with a different name in the database. - -_Params_: - -| Name | Description | -| ---- | ------------------------------------------------- | -| map | The name of the underlying column in the database | - -##### @updatedAt - -```zmodel -attribute @updatedAt() -``` - -Automatically stores the time when a record was last updated. - -##### @ignore - -```zmodel -attribute @ignore() -``` - -Exclude a field from the Prisma Client (for example, a field that you do not want Prisma users to update). - -##### @allow - -```zmodel -attribute @allow(_ operation: String, _ condition: Boolean) -``` - -Defines an access policy that allows the annotated field to be read or updated. Read more about access policies [here](#access-policy). - -_Params_: - -| Name | Description | -| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| operation | Comma separated list of operations to control, including `"read"` and `"update"`. Pass` "all"` as an abbreviation for including all operations. | -| condition | Boolean expression indicating if the operations should be allowed | - -##### @deny - -```zmodel -attribute @deny(_ operation: String, _ condition: Boolean) -``` - -Defines an access policy that denies the annotated field to be read or updated. Read more about access policies [here](#access-policy). - -_Params_: - -| Name | Description | -| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| operation | Comma separated list of operations to control, including `"read"` and `"update"`. Pass` "all"` as an abbreviation for including all operations. | -| condition | Boolean expression indicating if the operations should be denied - -##### @password - -```zmodel -attribute @password(saltLength: Int?, salt: String?) -``` - -Indicates that the field is a password field and needs to be hashed before persistence. - -_NOTE_: ZenStack uses the "bcryptjs" library to hash passwords. You can use the `saltLength` parameter to configure the cost of hashing or use `salt` parameter to provide an explicit salt. By default, a salt length of 12 is used. See [here](https://www.npmjs.com/package/bcryptjs ':target=blank') for more details. - -_Params_: - -| Name | Description | -| ---------- | ------------------------------------------------------------- | -| saltLength | The length of salt to use (cost factor for the hash function) | -| salt | The salt to use (a pregenerated valid salt) | - -##### @omit - -```zmodel -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() -``` - -##### @encrypted - -```zmodel -attribute @encrypted() -``` - -Marks a field to be encrypted during write and decrypted when read. - -```zmodel -model User { - id Int @id - someSecret String @encrypted -} -``` - -Using the attribute requires the "encryption" enhancement kind to be enabled when calling `enhance`, and the `encryption` settings provided in the enhancement options: - -```ts - -function getEncryptionKey(): Uint8Array { - // return a 32-byte key -} - -const db = enhance(prisma, { user }, { - encryption: { - encryptionKey: getEncryptionKey() - } -}); -``` - -See [Encrypting Field](../guides/field-encryption.md) for more details. - -##### @prisma.passthrough - -```zmodel -attribute @prisma.passthrough(_ text: String) -``` - -A utility attribute for passing arbitrary text to the generated Prisma schema. This is useful as a workaround for dealing with discrepancies between Prisma schema and ZModel. - -_Params_: - -| Name | Description | -| ---- | ------------------------------------ | -| text | Text to passthrough to Prisma schema | - -E.g., the following ZModel content: - -```zmodel -model User { - id Int @id @default(autoincrement()) - name String @prisma.passthrough("@unique") -} -``` - -will be translated to the following Prisma schema: - -```zmodel -model User { - id Int @id @default(autoincrement()) - name String @unique -} -``` - -##### @meta - -```zmodel -attribute @meta(_ name: String, _ value: Any) -``` - -Adds arbitrary metadata to a field. The metadata can be accessed by custom plugins for code generation, or at runtime from the `modelMeta` object exported from `@zenstackhq/runtime/model-meta`. The `value` parameter can be an arbitrary literal expression, including object literals. - -```zmodel -model User { - id Int @id - name String @meta(name: "description", value: "The name of the user") -} -``` - -#### Model attributes - -##### @@id - -```zmodel -attribute @@id(_ fields: FieldReference[], name: String?, map: String?) -``` - -Defines a multi-field ID (composite ID) on the model. - -_Params_: - -| Name | Description | -| ------ | ----------------------------------------------------------------------------- | -| fields | A list of fields defined in the current model | -| name | The name that the Client API will expose for the argument covering all fields | -| map | The name of the underlying primary key constraint in the database | - -##### @@unique - -```zmodel -attribute @@unique(_ fields: FieldReference[], name: String?, map: String?) -``` - -Defines a compound unique constraint for the specified fields. - -_Params_: - -| Name | Description | -| ------ | ------------------------------------------------------------ | -| fields | A list of fields defined in the current model | -| name | The name of the unique combination of fields | -| map | The name of the underlying unique constraint in the database | - -##### @@schema - -```zmodel -attribute @@schema(_ name: String) -``` - -Specifies the database schema to use in a [multi-schema setup](https://www.prisma.io/docs/guides/database/multi-schema). - -_Params_: - -| Name | Description | -| ---- | ------------------------------- | -| name | The name of the database schema | - -##### @@index - -```zmodel -attribute @@index(_ fields: FieldReference[], map: String?) -``` - -Defines an index in the database. - -_Params_: - -| Name | Description | -| ------ | ------------------------------------------------ | -| fields | A list of fields defined in the current model | -| map | The name of the underlying index in the database | - -##### @@map - -```zmodel -attribute @@map(_ name: String) -``` - -Maps the schema model name to a table with a different name, or an enum name to a different underlying enum in the database. - -_Params_: - -| Name | Description | -| ---- | -------------------------------------------------------- | -| name | The name of the underlying table or enum in the database | - -##### @@ignore - -```zmodel -attribute @@ignore() -``` - -Exclude a model from the Prisma Client (for example, a model that you do not want Prisma users to update). - -##### @@allow - -```zmodel -attribute @@allow(_ operation: String, _ condition: Boolean) -``` - -Defines an access policy that allows a set of operations when the given condition is true. Read more about access policies [here](#access-policy). - -_Params_: - -| Name | Description | -| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| operation | Comma separated list of operations to control, including `"create"`, `"read"`, `"update"`, and `"delete"`. Pass` "all"` as an abbriviation for including all operations. | -| condition | Boolean expression indicating if the operations should be allowed | - -##### @@deny - -```zmodel -attribute @@deny(_ operation: String, _ condition: Boolean) -``` - -Defines an access policy that denies a set of operations when the given condition is true. Read more about access policies [here](#access-policy). - -_Params_: - -| Name | Description | -| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| operation | Comma separated list of operations to control, including `"create"`, `"read"`, `"update"`, and `"delete"`. Pass` "all"` as an abbreviation for including all operations. | -| condition | Boolean expression indicating if the operations should be denied | - -##### @@auth - -```zmodel -attribute @@auth() -``` - -Specify the model for resolving `auth()` function call in access policies. By default, the model named "User" is used. You can use this attribute to override the default behavior. A Zmodel can have at most one model with this attribute. - -##### @@auth - -```zmodel -attribute @@delegate(_ discriminator: FieldReference) -``` - -Marks a model to be a delegated type. Used for [modeling a polymorphic hierarchy](../guides/polymorphism). - -_Params_: - -| Name | Description | -| ------------- | ---------------------------------------------------------------------------------------------------------------------------- | -| discriminator | A `String` or `enum` field in the same model used to store the name of the concrete model that inherit from this base model. | - -##### @@prisma.passthrough - -```zmodel -attribute @@prisma.passthrough(_ text: String) -``` - -A utility attribute for passing arbitrary text to the generated Prisma schema. This is useful as a workaround for dealing with discrepancies between Prisma schema and ZModel. - -_Params_: - -| Name | Description | -| ---- | ------------------------------------ | -| text | Text to passthrough to Prisma schema | - -E.g., the following ZModel content: - -```zmodel -model User { - id Int @id @default(autoincrement()) - name String - @@prisma.passthrough("@@unique([name])") -} -``` - -will be translated to the following Prisma schema: - -```zmodel -model User { - id Int @id @default(autoincrement()) - name String - @@unique([name]) -} -``` - -##### @@meta - -```zmodel -attribute @@meta(_ name: String, _ value: Any) -``` - -Adds arbitrary metadata to a model. The metadata can be accessed by custom plugins for code generation, or at runtime from the `modelMeta` object exported from `@zenstackhq/runtime/model-meta`. The `value` parameter can be an arbitrary literal expression, including object literals. - -```zmodel -model User { - id Int @id - @@meta('description', 'This is a user model') -} -``` - -### Predefined attribute functions - -##### uuid() - -```zmodel -function uuid(): String {} -``` - -Generates a globally unique identifier based on the UUID spec. - -##### cuid() - -```zmodel -function cuid(version: Int?): String {} -``` - -Generates a unique identifier based on the [CUID](https://github.com/ericelliott/cuid) spec. Pass `2` as an argument to use [cuid2](https://github.com/paralleldrive/cuid2). - -##### nanoid() - -```zmodel -function nanoid(length: Int?): String {} -``` - -Generates an identifier based on the [nanoid](https://github.com/ai/nanoid) spec. - -##### ulid() - -```zmodel -function ulid(): String {} -``` - -Generates a unique identifier based on the [ULID](https://github.com/ulid/spec) spec. - -##### now() - -```zmodel -function now(): DateTime {} -``` - -Gets current date-time. - -##### autoincrement() - -```zmodel -function autoincrement(): Int {} -``` - -Creates a sequence of integers in the underlying database and assign the incremented -values to the ID values of the created records based on the sequence. - -##### dbgenerated() - -```zmodel -function dbgenerated(expr: String): Any {} -``` - -Represents default values that cannot be expressed in the Prisma schema (such as random()). - -##### auth() - -```zmodel -function auth(): User {} -``` - -Gets the current login user. The return type of the function is the `User` model defined in the current ZModel. - -##### future() - -```zmodel -function future(): Any {} -``` - -Gets the "post-update" state of an entity. Only valid when used in a "update" access policy. Read more about access policies [here](#access-policy). - -##### check() - -```zmodel -function check(field: FieldReference, operation String?): Boolean {} -``` - -Checks if the current user can perform the given operation on the given field. - -_Params_ - -- `field`: The field to check access for. Must be a relation field. -- `operation`: The operation to check access for. Can be "read", "create", "update", or "delete". If the operation is not provided, it defaults the operation of the containing policy rule. - -_Example_ - -```zmodel -// delegating a single operation kind -model Post { - id Int @id - author User @relation(fields: [authorId], references: [id]) - authorId Int - - // delegate "read" check to the author, equivalent to - // @@allow('read', check(author)) - @@allow('read', check(author, 'read')) -} -``` - -```zmodel -// delegating all operations -model Post { - id Int @id - author User @relation(fields: [authorId], references: [id]) - authorId Int - - // delegate all access policies to the author, equivalent to: - // @@allow('read', check(author)) - // @@allow('create', check(author)) - // @@allow('update', check(author)) - // @@allow('delete', check(author)) - @@allow('all', check(author)) -} -``` - -```zmodel -// delegating field access control -model Post { - id Int @id - title String @allow('update', check(author)) - author User @relation(fields: [authorId], references: [id]) - authorId Int -} -``` - -:::info -The `check()` function only supports singular relation fields and cannot be used with "to-many" relations. We may add support for it in the future. -::: - -##### contains() - -```zmodel - function contains(field: String, search: String, caseInSensitive: Boolean?): Boolean {} -``` - -Checks if the given field contains the search string. The search string is case-sensitive by default. Use `caseInSensitive` to toggle the case sensitivity. - -Equivalent to Prisma's [contains](https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#contains) operator. - -##### search() - -```zmodel -function search(field: String, search: String): Boolean {} -``` - -Checks if the given field contains the search string using [full-text-search](https://www.prisma.io/docs/concepts/components/prisma-client/full-text-search). - -Equivalent to Prisma's [search](https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#search) operator. - -##### startsWith() - -```zmodel -function startsWith(field: String, search: String): Boolean {} -``` - -Checks if the given field starts with the search string. - -Equivalent to Prisma's [startsWith](https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#startswith) operator. - -##### endsWith() - -```zmodel -function endsWith(field: String, search: String): Boolean {} -``` - -Checks if the given field ends with the search string. - -Equivalent to Prisma's [endsWith](https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#endswith) operator. - -##### has() - -```zmodel -function has(field: Any[], search: Any): Boolean {} -``` - -Check if the given field (list) contains the search value. - -Equivalent to Prisma's [has](https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#has) operator. - -##### hasEvery() - -```zmodel -function hasEvery(field: Any[], search: Any[]): Boolean {} -``` - -Check if the given field (list) contains every element of the search list. - -Equivalent to Prisma's [hasEvery](https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#hasevery) operator. - -##### hasSome() - -```zmodel -function hasSome(field: Any[], search: Any[]): Boolean {} -``` - -Check if the given field (list) contains at least one element of the search list. - -Equivalent to Prisma's [hasSome](https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#hassome) operator. - -##### isEmpty() - -```zmodel -function isEmpty(field: Any[]): Boolean {} -``` - -Check if the given field (list) is empty. - -Equivalent to Prisma's [isEmpty](https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#isempty) operator. - -##### currentModel() - -```zmodel -function currentModel(casing: String?): String {} -``` - -Can only be used in access policy expressions. Return the name of the model for which the policy rule is defined. If the rule is inherited to a sub model, this function returns the name of the sub model. - -The optional "casing" parameter can be used to control the casing of the returned name. Valid values are "original", "upper", "lower", "capitalize", "uncapitalize". Defaults to "original". - -##### currentOperation() - -```zmodel -function currentOperation(casing: String?): String {} -``` - -Can only be used in access policy expressions. Return the operation for which the policy rule is defined for - "create", "read", "update", or "delete". Note that a rule with "all" operation is expanded to "create", "read", "update", and "delete" rules, and the function returns corresponding value for each expanded version. - -The optional "casing" parameter can be used to control the casing of the returned name. Valid values are "original", "upper", "lower", "capitalize", "uncapitalize". Defaults to "original". - -### Examples - -Here're some examples on using field and model attributes: - -```zmodel -model User { - // unique id field with a default UUID value - id String @id @default(uuid()) - - // require email field to be unique - email String @unique - - // password is hashed with bcrypt with length of 16, omitted when returned from the CRUD services - password String @password(saltLength: 16) @omit - - // default to current date-time - createdAt DateTime @default(now()) - - // auto-updated when the entity is modified - updatedAt DateTime @updatedAt - - // mapping to a different column name in database - description String @map("desc") - - // mapping to a different table name in database - @@map("users") - - // use @@index to specify fields to create database index for - @@index([email]) - - // use @@allow to specify access policies - @@allow("create,read", true) - - // use auth() to reference the current user - // use future() to access the "post-update" state - @@allow("update", auth() == this && future().email == email) -} -``` - -### Custom attributes and functions - -You can find examples of custom attributes and functions in [ZModel Standard Library](https://github.com/zenstackhq/zenstack/blob/main/packages/schema/src/res/stdlib.zmodel). - -## Field - -Fields are typed members of models and types. - -### Syntax - -```zmodel -model Model { - [FIELD_NAME] [FIELD_TYPE] (FIELD_ATTRIBUTES)? -} -``` - -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, 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: - - - String - - Boolean - - Int - - BigInt - - Float - - Decimal - - Json - - Bytes - - [Unsupported types](https://www.prisma.io/docs/concepts/components/prisma-schema/data-model#unsupported-types) - - A field's type can be any of the scalar or reference type, a list of the aforementioned type (suffixed with `[]`), or an optional of the aforementioned type (suffixed with `?`). - -- **[FIELD_ATTRIBUTES]** - - Field attributes attach extra behaviors or constraints to the field. See [Attribute](#attribute) for more information. - -### Example - -```zmodel -model Post { - // "id" field is a mandatory unique identifier of this model - 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[] - - // and can reference another model too - comments Comment[] -} -``` - -## Relation - -Relations are connections among models. There're three types of relations: - -- One-to-one -- One-to-many -- Many-to-many - -Relations are expressed with a pair of fields and together with the special `@relation` field attribute. One side of the relation field carries the `@relation` attribute to indicate how the connection is established. - -### One-to-one relation - -The _owner_ side of the relation declares an optional field typed as the model of the _owned_ side of the relation. - -On the _owned_ side, a reference field is declared with `@relation` attribute, together with a **foreign key** field storing the id of the owner entity. - -```zmodel -model User { - id String @id - profile Profile? -} - -model Profile { - id String @id - user User @relation(fields: [userId], references: [id]) - userId String @unique -} -``` - -### One-to-many relation - -The _owner_ side of the relation declares a list field typed as the model of the _owned_ side of the relation. - -On the _owned_ side, a reference field is declared with `@relation` attribute, together with a **foreign key** field storing the id of the owner entity. - -```zmodel -model User { - id String @id - posts Post[] -} - -model Post { - id String @id - author User? @relation(fields: [authorId], references: [id]) - authorId String? -} -``` - -### Many-to-many relation - -A _join model_ is declared to connect the two sides of the relation using two one-to-one relations. - -Each side of the relation then establishes a one-to-many relation with the _join model_. - -```zmodel -model Space { - id String @id - // one-to-many with the "join-model" - 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 - - // a user can be member of a space for only once - @@unique([userId, spaceId]) -} - -model User { - id String @id - // one-to-many with the "join-model" - membership Membership[] -} - -``` - -### Self-relations - -A relation field referencing its own model is called "self-relation". ZModel's represents self-relation in the same way as Prisma does. Please refer to the [Prisma documentation](https://www.prisma.io/docs/concepts/components/prisma-schema/relations/self-relations) for more details. - -### Referential action - -When defining a relation, you can specify what happens when one side of a relation is updated or deleted. See [Referential action](#referential-action) for details. - -## Access policy - -### Model-level policy - -Model-level access policies are defined with `@@allow` and `@@deny` attributes. They specify the eligibility of an operation over a model entity. The signatures of the attributes are: - -- `@@allow` - - ```zmodel - attribute @@allow(_ operation: String, _ condition: Boolean) - ``` - - _Params_: - - | Name | Description | - | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | - | operation | Comma separated list of operations to control, including `"create"`, `"read"`, `"update"`, and `"delete"`. Pass` "all"` as an abbreviation for including all operations. | - | condition | Boolean expression indicating if the operations should be allowed | - -- `@@deny` - - ```zmodel - attribute @@deny(_ operation: String, _ condition: Boolean) - ``` - - _Params_: - - | Name | Description | - | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | - | operation | Comma separated list of operations to control, including `"create"`, `"read"`, `"update"`, and `"delete"`. Pass` "all"` as an abbreviation for including all operations. | - | condition | Boolean expression indicating if the operations should be denied | - -### Field-level policy - -Field-level access policies are defined with `@allow` and `@deny` attributes. They control whether the annotated field can be read or updated. If a field fails "read" check, it'll be deleted when returned. If a field is set to be updated but fails "update" check, the update operation will be rejected. - -Note that it's not allowed to put "update" rule on relation fields, because whether an entity can be updated shouldn't be determined indirectly by a relation, but directly by the entity itself. However, you can put "update" rule on a foreign key field to control how a a relation can be updated. - -The signatures of the attributes are: - -- `@allow` - - ```zmodel - attribute @allow(_ operation: String, _ condition: Boolean, _ override: Boolean?) - ``` - - _Params_: - - | Name | Description | Default | - | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------- | - | operation | Comma separated list of operations to control, including `"read"` and `"update"`. Pass` "all"` as an abbreviation for including all operations. | | - | condition | Boolean expression indicating if the operations should be allowed | | - | override | Boolean indicating if the field-level policy should override model-level ones. See [here](../the-complete-guide/part1/access-policy/field-level#overriding-model-level-policies) for more details. | false | - -- `@deny` - - ```zmodel - attribute @deny(_ operation: String, _ condition: Boolean) - ``` - - _Params_: - - | Name | Description | - | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | - | operation | Comma separated list of operations to control, including ``"read"` and `"update"`. Pass` "all"` as an abbreviation for including all operations. | - | condition | Boolean expression indicating if the operations should be denied | - -### Policy expressions - -Policy rules use boolean expressions to make verdicts. ZModel provides a set of literals and operators for constructing expressions of arbitrary complexity. - -``` - -Expression ::= Literal | Array | This | Null | Reference | MemberAccess | Invocation | Binary | Unary | CollectionPredicate - -Literal ::= String | Number | Boolean - -Array ::= "[" Expression [, Expression]* "]" - -This ::= "this" - -Null ::= "null" - -Reference ::= Identifier - -MemberAccess ::= Expression "." Identifier - -Operator_Precedence#table -Binary ::= Expression ("==" | "!=" | ">" | "<" | ">=" | "<=" | "&&" | "||" || "in") - -Unary ::= "!" Expression - -CollectionPredicate ::= Expression ("?" | "!" | "^") "[" Expression "]" - -``` - -Binary operator precedence follows [Javascript's rules](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/). - -Collection predicate expressions are used for reaching into relation fields. You can find more details [here](#collection-predicate-expressions). - -### 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` model or a model marked with the `@@auth` attribute. - -You can use `auth()` to: - -- Check if a user is logged in - - ```zmodel - @@deny('all', auth() == null) - ``` - -- Access user's fields - - ```zmodel - @@allow('update', auth().role == 'ADMIN') - ``` - -- Compare user identity - - ```zmodel - // owner is a relation field to User model - @@allow('update', auth() == owner) - ``` - -### Accessing relation fields in policy - -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. - -```zmodel - @@allow('read', membership?[space.members?[user == auth()]]) -``` - -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 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: - -- Any - - ``` - ?[condition] - ``` - - Any element in `collection` matches `condition` - -- All - - ``` - ![condition] - ``` - - All elements in `collection` match `condition` - -- None - - ``` - ^[condition] - ``` - - None element in `collection` matches `condition` - -The `condition` expression has direct access to fields defined in the model of `collection`. E.g.: - -```zmodel - @@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 conditions involving multi-level relation lookup. E.g.: - -```zmodel - @@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. - -### Combining multiple rules - -A model can contain an arbitrary number of policy rules. The logic of combining model-level rules is as follows: - -- 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. - -A field can also contain an arbitrary number of policy rules. The logic of combining field-level rules is as follows: - -- The operation is rejected if any of the conditions in `@deny` rules evaluate to `true`. -- Otherwise, if there exists any `@allow` rule and at least one of them evaluates to `true`, the operation is permitted. -- Otherwise, if there exists any `@allow` rule but none one of them evaluates to `true`, the operation is rejected. -- Otherwise, the operation is permitted. - -Please note the difference between model-level and field-level rules. Model-level access are by-default denied, while field-level access are by-default allowed. - -### "Create" rules - -The "create" policy rules should be understood as: **if an entity were to be created, would it satisfy the rules**. Or, in other words, the rules are checked "post create". - -An entity creating process works like the following: - -1. Initiate a transaction and create the entity. -2. In the same transaction, try to read the created entity with the "create" rules as filter, and see if it succeeds. -3. If the read fails, the transaction is rolled back; otherwise it's committed. - -The "post-create check" semantic allows the rules to access relations of the entity being created since they are only accessible after the create happens. For simple cases, ZenStack may apply optimizations to reject a create request without initiating a transaction, but generally speaking the "post-create check" semantic is the correct way to think about it. - -We may introduce a "pre-create" policy type in the future. - -### "Pre-update" vs." post-update" rules - -When an access policy rule is applied to a mutate operation, the entities under operation have a "pre" and "post" state. For a "create" rule, its "pre" state is empty, so the rule implicitly refers to the "post" state. For a "delete" rule, its "post" state is empty, so the rule implicitly refers to the "pre" state. - -However, for "update" rules it is ambiguous; both the "pre" and the "post" states exist. By default, for "update" rules, fields referenced in the expressions refer to the "pre" state, and you can use the `future()` function to refer to the "post" state explicitly. - -In the following example, the "update" rule uses `future()` to ensure an update cannot alter the post's owner. - -```zmodel -model Post { - id String @id @default(uuid()) - title String @length(1, 100) - author User @relation(fields: [authorId], references: [id], onDelete: Cascade) - authorId String - - // update can only be done by the author, and is not allowed to change author - @@allow('update', author == auth() && future().author == author) -} -``` - -:::info -The `future()` function is not supported in field-level access policies. To express post-update rules, put them into model-level policies. -::: - -### Examples - -#### A simple example with Post model - -```zmodel -model Post { - // reject all operations if user's not logged in - @@deny('all', auth() == null) - - // allow all operations if the entity's owner matches the current user - @@allow('all', auth() == owner) - - // posts are readable to anyone - @allow('read', true) -} -``` - -#### A more complex example with multi-user spaces - -```zmodel -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 - // the current entity - @@allow('all', auth() == this) - - // can be read by users sharing a space - @@allow('read', membership?[space.members?[user == auth()]]) -} - -``` - -## Data validation - -### Overview - -Data validation is used for attaching constraints to field values. Unlike access policies, field validation rules cannot access the current user with the `auth()` function and are only checked for 'create' and 'update' operations. The main purpose of field validation is to ensure data integrity and consistency, not for access control. - -The [`@core/zod`](./plugins/zod) plugin recognizes the validation attributes and includes them into the generated Zod schemas. - -### Field-level validation attributes - -The following attributes can be used to attach validation rules to individual fields: - -#### String - -- `@length(_ min: Int?, _ max: Int?, _ message: String?)` - - Validates length of a string field. - -- `@startsWith(_ text: String, _ message: String?)` - - Validates a string field value starts with the given text. - -- `@endsWith(_ text: String, _ message: String?)` - - Validates a string field value ends with the given text. - -- `@contains(_text: String, _ message: String?)` - - Validates a string field value contains the given text. - -- `@email(_ message: String?)` - - Validates a string field value is a valid email address. - -- `@url(_ message: String?)` - - Validates a string field value is a valid url. - -- `@datetime(_ message: String?)` - - Validates a string field value is a valid ISO datetime. - -- `@regex(_ regex: String, _ message: String?)` - - Validates a string field value matches a regex. - -- `@trim(_ value: String)` - - Trims whitespace. - -- `@lower(_ value: String)` - - Converts to lowercase. - -- `@upper(_ value: String)` - - Converts to uppercase. - -:::info -Attributes `@trim`, `@lower`, and `@upper` are actually "transformation" instead of "validation". They make sure the values are transformed before storing into the database. -::: - -#### Number - -- `@gt(_ value: Int, _ message: String?)` - - Validates a number field is greater than the given value. - -- `@gte(_ value: Int, _ message: String?)` - - Validates a number field is greater than or equal to the given value. - -- `@lt(_ value: Int, _ message: String?)` - - Validates a number field is less than the given value. - -- `@lte(_ value: Int, _ message: String?)` - - Validates a number field is less than or equal to the given value. - -### Model-level validation attributes - -You can use the `@@validate` attribute to attach validation rules to a model. Use the `message` parameter to provide an optional custom error message, and the `path` parameter to provide an optional path to the field that caused the error. - -``` -@@validate(_ value: Boolean, _ message: String?, _ path: String[]?) -``` - -Model-level rules can reference multiple fields, use relation operators (`==`, `!=`, `>`, `>=`, `<`, `<=`) to compare fields, use boolean operators (`&&`, `||`, and `!`) to compose conditions, and can use the following functions to evaluate conditions for fields: - -- `function length(field: String, min: Int, max: Int?): Boolean` - - Validates length of a string field. - -- `function regex(field: String, regex: String): Boolean` - - Validates a string field value matches a regex. - -- `function email(field: String): Boolean` - - Validates a string field value is a valid email address. - -- `function datetime(field: String): Boolean` - - Validates a string field value is a valid ISO datetime. - -- `function url(field: String)` - - Validates a string field value is a valid url. - -- `function contains(field: String, search: String, caseInSensitive: Boolean?): Boolean` - - Validates a string field contains the search string. - -- `function startsWith(field: String, search: String): Boolean` - - Validates a string field starts with the search string. - -- `function endsWith(field: String, search: String): Boolean` - - Validates a string field ends with the search string. - -- `function has(field: Any[], search: Any): Boolean` - - Validates a list field contains the search value. - -- `function hasEvery(field: Any[], search: Any[]): Boolean` - - Validates a list field contains every element in the search list. - -- `function hasSome(field: Any[], search: Any[]): Boolean` - - Validates a list field contains some elements in the search list. - -- `function isEmpty(field: Any[]): Boolean` - - Validates a list field is null or empty. - -### Example - -```zmodel -model User { - id String @id - handle String @regex("^[0-9a-zA-Z]{4,16}$") - email String? @email @endsWith("@myorg.com", "must be an email from myorg.com") - profileImage String? @url - age Int @gte(18) - activated Boolean @default(false) - - @@validate(!activated || email != null, "activated user must have an email") -} -``` - -## Referential action - -### Overview - -When defining a relation, you can use referential action to control what happens when one side of a relation is updated or deleted by setting the `onDelete` and `onUpdate` parameters in the `@relation` attribute. - -```zmodel -attribute @relation( - _ name: String?, - fields: FieldReference[]?, - references: FieldReference[]?, - onDelete: ReferentialAction?, - onUpdate: ReferentialAction?, - map: String?) -``` - -The `ReferentialAction` enum is defined as: - -```zmodel -enum ReferentialAction { - Cascade - Restrict - NoAction - SetNull - SetDefault -} -``` - -- `Cascade` - - - **onDelete**: deleting a referenced record will trigger the deletion of referencing record. - - - **onUpdate**: updates the relation scalar fields if the referenced scalar fields of the dependent record are updated. - -- `Restrict` - - - **onDelete**: prevents the deletion if any referencing records exist. - - **onUpdate**: prevents the identifier of a referenced record from being changed. - -- `NoAction` - - Similar to 'Restrict', the difference between the two is dependent on the database being used. - - See details [here](https://www.prisma.io/docs/concepts/components/prisma-schema/relations/referential-actions#noaction ':target=blank') - -- `SetNull` - - - **onDelete**: the scalar field of the referencing object will be set to NULL. - - **onUpdate**: when updating the identifier of a referenced object, the scalar fields of the referencing objects will be set to NULL. - -- `SetDefault` - - **onDelete**: the scalar field of the referencing object will be set to the fields default value. - - **onUpdate**: the scalar field of the referencing object will be set to the fields default value. - -### Example - -```zmodel -model User { - id String @id - profile Profile? -} - -model Profile { - id String @id - user @relation(fields: [userId], references: [id], onUpdate: Cascade, onDelete: Cascade) - 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 -} -``` From ae008285df3a291cb26d8a65f1d84ddbb1096d18 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Sun, 3 Aug 2025 17:15:05 +0800 Subject: [PATCH 03/13] WIP --- .../version-3.x/_components/ZModelVsPSL.tsx | 16 ++ .../version-3.x/migration/_category_.yml | 2 +- .../version-3.x/migration/introduction.md | 6 - .../version-3.x/migration/overview.md | 6 + .../{orm/zmodel => modeling}/_category_.yml | 6 +- .../version-3.x/modeling/attribute.md | 47 ++++ .../version-3.x/modeling/custom-type.md | 46 ++++ .../version-3.x/modeling/datasource.md | 46 ++++ versioned_docs/version-3.x/modeling/enum.md | 27 +++ versioned_docs/version-3.x/modeling/mixin.md | 47 ++++ versioned_docs/version-3.x/modeling/model.md | 190 +++++++++++++++ .../{orm/zmodel => modeling}/multi-file.md | 4 +- .../version-3.x/modeling/overview.md | 52 +++++ versioned_docs/version-3.x/modeling/plugin.md | 7 + .../version-3.x/modeling/polymorphism.md | 34 +++ .../version-3.x/modeling/relations.md | 219 ++++++++++++++++++ .../version-3.x/modeling/strong-typed-json.md | 50 ++++ versioned_docs/version-3.x/orm/_category_.yml | 2 +- .../orm/{introduction.md => overview.md} | 6 +- .../version-3.x/orm/quick-start.mdx | 2 +- .../version-3.x/orm/zmodel/attributes.md | 6 - .../version-3.x/orm/zmodel/builtin-types.md | 6 - .../version-3.x/orm/zmodel/components.md | 6 - .../version-3.x/orm/zmodel/custom-types.md | 6 - .../version-3.x/orm/zmodel/datasource.md | 7 - .../version-3.x/orm/zmodel/models.md | 7 - .../version-3.x/orm/zmodel/polymorphism.md | 7 - .../version-3.x/orm/zmodel/relations.md | 6 - .../version-3.x/orm/zmodel/reusing-fields.md | 7 - .../version-3.x/reference/_category_.yml | 2 +- versioned_docs/version-3.x/samples.md | 2 +- .../version-3.x/service/_category_.yml | 2 +- .../service/{introduction.md => overview.md} | 2 +- versioned_docs/version-3.x/upgrade.md | 2 +- .../version-3.x/utilities/_category_.yml | 2 +- .../version-3.x/utilities/tanstack-query.md | 6 + versioned_docs/version-3.x/utilities/zod.md | 2 +- versioned_docs/version-3.x/welcome.md | 22 +- 38 files changed, 831 insertions(+), 84 deletions(-) create mode 100644 versioned_docs/version-3.x/_components/ZModelVsPSL.tsx delete mode 100644 versioned_docs/version-3.x/migration/introduction.md create mode 100644 versioned_docs/version-3.x/migration/overview.md rename versioned_docs/version-3.x/{orm/zmodel => modeling}/_category_.yml (52%) create mode 100644 versioned_docs/version-3.x/modeling/attribute.md create mode 100644 versioned_docs/version-3.x/modeling/custom-type.md create mode 100644 versioned_docs/version-3.x/modeling/datasource.md create mode 100644 versioned_docs/version-3.x/modeling/enum.md create mode 100644 versioned_docs/version-3.x/modeling/mixin.md create mode 100644 versioned_docs/version-3.x/modeling/model.md rename versioned_docs/version-3.x/{orm/zmodel => modeling}/multi-file.md (64%) create mode 100644 versioned_docs/version-3.x/modeling/overview.md create mode 100644 versioned_docs/version-3.x/modeling/plugin.md create mode 100644 versioned_docs/version-3.x/modeling/polymorphism.md create mode 100644 versioned_docs/version-3.x/modeling/relations.md create mode 100644 versioned_docs/version-3.x/modeling/strong-typed-json.md rename versioned_docs/version-3.x/orm/{introduction.md => overview.md} (98%) delete mode 100644 versioned_docs/version-3.x/orm/zmodel/attributes.md delete mode 100644 versioned_docs/version-3.x/orm/zmodel/builtin-types.md delete mode 100644 versioned_docs/version-3.x/orm/zmodel/components.md delete mode 100644 versioned_docs/version-3.x/orm/zmodel/custom-types.md delete mode 100644 versioned_docs/version-3.x/orm/zmodel/datasource.md delete mode 100644 versioned_docs/version-3.x/orm/zmodel/models.md delete mode 100644 versioned_docs/version-3.x/orm/zmodel/polymorphism.md delete mode 100644 versioned_docs/version-3.x/orm/zmodel/relations.md delete mode 100644 versioned_docs/version-3.x/orm/zmodel/reusing-fields.md rename versioned_docs/version-3.x/service/{introduction.md => overview.md} (53%) create mode 100644 versioned_docs/version-3.x/utilities/tanstack-query.md diff --git a/versioned_docs/version-3.x/_components/ZModelVsPSL.tsx b/versioned_docs/version-3.x/_components/ZModelVsPSL.tsx new file mode 100644 index 00000000..0ea279d3 --- /dev/null +++ b/versioned_docs/version-3.x/_components/ZModelVsPSL.tsx @@ -0,0 +1,16 @@ +import React, { FC } from 'react'; +import Admonition from '@theme/Admonition'; + +interface ZModelVsPSLProps { + children: React.ReactNode; +} + +const ZModelVsPSL: FC = ({ children }) => { + return ( + + {children} + + ); +}; + +export default ZModelVsPSL; diff --git a/versioned_docs/version-3.x/migration/_category_.yml b/versioned_docs/version-3.x/migration/_category_.yml index be8bc9d1..013a697a 100644 --- a/versioned_docs/version-3.x/migration/_category_.yml +++ b/versioned_docs/version-3.x/migration/_category_.yml @@ -1,4 +1,4 @@ -position: 3 +position: 4 label: Migration collapsible: true collapsed: true diff --git a/versioned_docs/version-3.x/migration/introduction.md b/versioned_docs/version-3.x/migration/introduction.md deleted file mode 100644 index 2e8a25db..00000000 --- a/versioned_docs/version-3.x/migration/introduction.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -sidebar_position: 1 -description: Introduction to ZenStack migration ---- - -# Introduction diff --git a/versioned_docs/version-3.x/migration/overview.md b/versioned_docs/version-3.x/migration/overview.md new file mode 100644 index 00000000..9fe870b2 --- /dev/null +++ b/versioned_docs/version-3.x/migration/overview.md @@ -0,0 +1,6 @@ +--- +sidebar_position: 1 +description: ZenStack migration overview +--- + +# Overview diff --git a/versioned_docs/version-3.x/orm/zmodel/_category_.yml b/versioned_docs/version-3.x/modeling/_category_.yml similarity index 52% rename from versioned_docs/version-3.x/orm/zmodel/_category_.yml rename to versioned_docs/version-3.x/modeling/_category_.yml index 396fcc06..8dbacd32 100644 --- a/versioned_docs/version-3.x/orm/zmodel/_category_.yml +++ b/versioned_docs/version-3.x/modeling/_category_.yml @@ -1,7 +1,7 @@ -position: 3 -label: Writing Schema +position: 2 +label: Data Modeling collapsible: true collapsed: true link: type: generated-index - title: Writing Schema + title: Data Modeling diff --git a/versioned_docs/version-3.x/modeling/attribute.md b/versioned_docs/version-3.x/modeling/attribute.md new file mode 100644 index 00000000..7994aff6 --- /dev/null +++ b/versioned_docs/version-3.x/modeling/attribute.md @@ -0,0 +1,47 @@ +--- +sidebar_position: 4 +description: Attributes in ZModel +--- + +import ZModelVsPSL from '../_components/ZModelVsPSL'; + +# Attribute + +Attributes allow you to attach metadata to models and fields. As you've seen in the previous sections, they are used for many purposes, like adding unique constraints, mapping names, etc. Attributes are also indispensable for modeling relations between models. + +## Naming conventions + +By convention, attributes attached to models use a double `@@` prefix, while those for fields use a single `@` prefix. + +```zmodel +model User { + id Int @id + email String @unique + + @@index([email, name]) +} +``` + +## Defining and applying attributes + + +Prisma schema doesn't allow users to define custom attributes, while ZModel allows it and uses it as a key mechanism for extensibility. + + +ZModel comes with a rich set of attributes that you can use directly. See [ZModel Language Reference](../reference/zmodel-language.md) for a complete list. You can also define your own custom attributes for specific purposes. Attributes are defined with a list of typed parameters. Parameters can named (default) or positional. Positional parameters can be passed with or without an explicit name. Parameters can also be optional. + +Here's an example for how the `@unique` attribute is defined: + +```zmodel +attribute @unique(map: String?, length: Int?, sort: SortOrder?) +``` + +You can apply it in various ways: + +```zmodel +model Foo { + x String @unique() // default application + y String @unique('y_unique') // positional parameter + z String @unique(map: 'z_unique', length: 10) // named parameter +} +``` diff --git a/versioned_docs/version-3.x/modeling/custom-type.md b/versioned_docs/version-3.x/modeling/custom-type.md new file mode 100644 index 00000000..46f966ea --- /dev/null +++ b/versioned_docs/version-3.x/modeling/custom-type.md @@ -0,0 +1,46 @@ +--- +sidebar_position: 7 +description: Custom types in ZModel +--- + +import ZModelVsPSL from '../_components/ZModelVsPSL'; + +# Custom Type + + +Custom type is a ZModel concept and doesn't exist in PSL. + + +Besides models, you can also define custom types to encapsulate complex data structures. The main difference between a model and a custom type is that the latter is not backed by a database table. + +Here's a simple example: + +```zmodel +type Address { + street String + city String + country String + zip Int +} +``` + +Custom types are defined exactly like models, with the exception that they cannot contain fields that are relations to other models. They can, however, contain fields that are other custom types. + +There are two ways to use custom types: + +```zmodel +type Address { + street String + city String + country String + zip Int +} + +type UserProfile { + gender String + address Address? +} +``` + +- [Mixin](./mixin.md) +- [Strongly typed JSON fields](./strong-typed-json.md) diff --git a/versioned_docs/version-3.x/modeling/datasource.md b/versioned_docs/version-3.x/modeling/datasource.md new file mode 100644 index 00000000..6f308fff --- /dev/null +++ b/versioned_docs/version-3.x/modeling/datasource.md @@ -0,0 +1,46 @@ +--- +sidebar_position: 2 +description: Datasource in ZModel +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; +import ZModelVsPSL from '../_components/ZModelVsPSL'; + +# Data Source + +The `datasource` block provides information about the database your application uses. The ORM relies on it to determine the proper SQL dialect to use when generating queries. If you use [Migration](../migration/), it must also have a `url` field that specifies the database connection string, so that the migration engine knows how to connect to the database. The `env` function can be used to reference environment variables so you can keep sensitive information out of the code. + +:::tip +You can use both single quote and double quote for string literals. +::: + +Each ZModel schema must have exactly one `datasource` block. + + + + +```zmodel +datasource db { + provider = 'postgresql' + url = env('DATABASE_URL') +} +``` + + + +```zmodel +datasource db { + provider = 'sqlite' + url = 'file:./dev.db' +} +``` + + + + +Currently only PostgreSQL and SQLite are supported. MySql will be supported in a future release. There's no plan for other relational database types or NoSQL databases. + + +ZenStack's ORM runtime doesn't rely on the `url` information to connect to the database. Instead, you provide the information when constructing an ORM client. More on this in the [ORM](../orm/) section. + diff --git a/versioned_docs/version-3.x/modeling/enum.md b/versioned_docs/version-3.x/modeling/enum.md new file mode 100644 index 00000000..f6410bba --- /dev/null +++ b/versioned_docs/version-3.x/modeling/enum.md @@ -0,0 +1,27 @@ +--- +sidebar_position: 4 +description: Enums in ZModel +--- + +# Enum + +Enums are simple constructs that allow you to define a set of named values. + +```zmodel +enum Role { + USER + ADMIN +} +``` + +They can be used to type model fields: + +```zmodel +model User { + id Int @id + // highlight-next-line + role Role @default(USER) +} +``` + +Enum field names are added to the global scope and are resolved without qualification. You need to make sure they don't collide with other global names. \ No newline at end of file diff --git a/versioned_docs/version-3.x/modeling/mixin.md b/versioned_docs/version-3.x/modeling/mixin.md new file mode 100644 index 00000000..c08b2b95 --- /dev/null +++ b/versioned_docs/version-3.x/modeling/mixin.md @@ -0,0 +1,47 @@ +--- +sidebar_position: 8 +description: Reusing common fields with mixins +--- + +import ZModelVsPSL from '../_components/ZModelVsPSL'; + +# Mixin + + +Mixin is a ZModel concept and don't exist in PSL. + + +:::info +Mixin was previously known as "abstract inheritance" in ZenStack v2. It's renamed and changed to use the `with` keyword to distinguish from polymorphic model inheritance. +::: + +Very often you'll find many of your models share quite a few common fields. It's tedious and error-prone to repeat them. As a rescue, you can put those fields into custom types, and "mix-in" them into your models. + +***Before:*** + +```zmodel +model User { + id String @id + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + email String @unique +} +``` + +***After:*** + +```zmodel +type BaseFieldsMixin { + id String @id + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model User with BaseFieldsMixin { + email String @unique +} +``` + +A model can use multiple mixins as long as their field names don't conflict. + +Mixins don't exist at the database level. The fields defined in the mixin types are conceptually inlined into the models that use them. diff --git a/versioned_docs/version-3.x/modeling/model.md b/versioned_docs/version-3.x/modeling/model.md new file mode 100644 index 00000000..1af5b1a3 --- /dev/null +++ b/versioned_docs/version-3.x/modeling/model.md @@ -0,0 +1,190 @@ +--- +sidebar_position: 3 +description: Models in ZModel +--- + +# Model + +The `model` construct is the core of ZModel. It defines the structure of your data and their relations. A model represents a domain entity, and is mapped to a database table. + +## Defining models + +A typical model looks like this: + +```zmodel +model User { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + email String @unique + name String +} +``` + +The simplest models are just a collection of fields. A model must be uniquely identifiable by some of its fields. For most cases, you'll have a field marked with the `@id` attribute (more about [attributes](./attribute) later). + +```zmodel +model User { + // highlight-next-line + id Int @id +} +``` + +If your model needs a composite id, you can use the `@@id` model-level attribute to specify it: + +```zmodel +model City { + country String + name String + // highlight-next-line + @@id([country, name]) +} +``` + +If no `@id` or `@@id` is specified, the ORM will resort to using a field (or fields) marked with the `@unique` or `@@unique` attribute as the identifier. + +```zmodel +model User { + // highlight-next-line + email String @unique +} + +model City { + country String + name String + // highlight-next-line + @@unique([country, name]) +} +``` + +## Model fields + +Each model field must at least have a name and a type. A field can be typed in one of the following ways: + +1. Built-in types, including: + - String + - Boolean + - Int + - BigInt + - Float + - Decimal + - DateTime + - Json + - Bytes + - Unsupported + + The `Unsupported` type is for defining fields of types not supported by the ORM, however letting the migration engine know how to create the field in the database. + + ```zmodel + // from Prisma docs + model Star { + id Int @id + // highlight-next-line + position Unsupported("circle")? @default(dbgenerated("'<(10,4),11>'::circle")) + } + ``` + +2. Enum + + We'll talk about [enums](./enum) later. + + ```zmodel + enum Role { + USER + ADMIN + } + + model User { + id Int @id + // highlight-next-line + role Role + } + ``` + +3. Model + + It'll then form a relation. We'll cover that topic [later](./relation). + ```zmodel + model Post { + id Int @id + // highlight-next-line + author User @relation(fields: [authorId], references: [id]) + authorId Int + } + ``` +4. Custom type + + ZenStack allows you to define custom types in the schema and use that to type Json fields. This will be covered in more details in the [Custom Types](./custom-type) section. + + ```zmodel + type Address { + street String + city String + country String + zip Int + } + + model User { + id Int @id + // highlight-next-line + address Address @json + } + ``` + +A field can be set optional by adding the `?` suffix to its type, or list by adding the `[]` suffix. However, a field cannot be both optional and a list at the same time. + +```zmodel +model User { + id Int @id + // highlight-next-line + name String? + // highlight-next-line + tags String[] +} +``` + +A default value can be specified for a field with the `@default` attribute. The value can be a literal, or a supported function call, including: + +- `now()`: returns the current timestamp +- `cuid()`: returns a CUID +- `uuid()`: returns a UUID +- `ulid()`: returns a ULID +- `nanoid()`: returns a Nano ID +- `autoincrement()`: returns an auto-incrementing integer (only for integer fields) +- `dbgenerated("...")`: calls a native db function + +```zmodel +model User { + id Int @id @default(autoincrement()) + role Role @default("USER") + createdAt DateTime @default(now()) +} +``` + +## Native type mapping + +Besides giving field a type, you can also specify the native database type to use with the `@db.` series of attributes. + +```zmodel +model User { + ... + // highlight-next-line + name String @db.VarChar(64) +} +``` + +These attributes control what data type is used when the migration engine maps the schema to DDL. You can find a full list of native type attributes in the [ZModel Language Reference](../reference/zmodel-language). + +## Name mapping + +Quite often you want to use a different naming scheme for your models and fields than the database. You can achieve that with the `@map` attribute. The ORM respects the mapping when generation queries, and the migration engine uses it to generate the DDL. + +```zmodel +model User { + // highlight-next-line + id Int @id + name String @map("full_name") + + @@map("users") +} +``` diff --git a/versioned_docs/version-3.x/orm/zmodel/multi-file.md b/versioned_docs/version-3.x/modeling/multi-file.md similarity index 64% rename from versioned_docs/version-3.x/orm/zmodel/multi-file.md rename to versioned_docs/version-3.x/modeling/multi-file.md index 81500291..ad1ba638 100644 --- a/versioned_docs/version-3.x/orm/zmodel/multi-file.md +++ b/versioned_docs/version-3.x/modeling/multi-file.md @@ -1,9 +1,9 @@ --- -sidebar_position: 10 +sidebar_position: 11 description: Breaking down complex schemas into multiple files --- -# Multi-file Schemas +# Multi-file Schema diff --git a/versioned_docs/version-3.x/modeling/overview.md b/versioned_docs/version-3.x/modeling/overview.md new file mode 100644 index 00000000..b2d9e3c1 --- /dev/null +++ b/versioned_docs/version-3.x/modeling/overview.md @@ -0,0 +1,52 @@ +--- +sidebar_position: 1 +description: ZModel overview +--- + +import ZModelVsPSL from '../_components/ZModelVsPSL'; + +# Overview + +ZenStack uses a schema language named **ZModel** to define data models and their related aspects. We know that designing a good schema language is difficult, and we know it's even more difficult to convince people to learn a new one. So we made the decision to design ZModel as a superset of the [Prisma Schema Language (PSL)](https://www.prisma.io/docs/orm/prisma-schema), which is one of the best data modeling language out there. + +If you're already familiar with PSL, you'll find yourself at home with ZModel. However, we'd still recommend that you skim through this section to learn about the important extensions we made to PSL. Please pay attention to callouts like the following one: + + +ZModel uses the explicit `import` syntax for composing multi-file schemas. + + +Don't worry if you've never used Prima before. This section will introduce all aspects of ZModel, so no prior knowledge is required. + +A very simple ZModel schema looks like this: + +```zmodel title='zenstack/schema.zmodel' +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model User { + id Int @id @default(autoincrement()) + email String @unique + name String +} + +model Post { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + title String + content String + published Boolean @default(false) + author User @relation(fields: [authorId], references: [id]) + authorId String +} +``` + + +Prisma has the concept of "generator" which provides a pluggable mechanism to generate artifacts from PSL. Specifically, you need to define a "prisma-client-js" (or "prisma-client") generator to get the ORM client. + +ZenStack CLI always generates a TypeScript schema without needing any configuration. Also, it replaced PSL's "generator" with a more generalized "plugin" construct that allows you to extend the system both at the schema level and the runtime level. Read more in the [Plugin](./plugin) section. + + +Let's dissect it piece by piece. diff --git a/versioned_docs/version-3.x/modeling/plugin.md b/versioned_docs/version-3.x/modeling/plugin.md new file mode 100644 index 00000000..10506f39 --- /dev/null +++ b/versioned_docs/version-3.x/modeling/plugin.md @@ -0,0 +1,7 @@ +--- +sidebar_position: 11 +description: ZenStack plugins +--- + +# Plugin + diff --git a/versioned_docs/version-3.x/modeling/polymorphism.md b/versioned_docs/version-3.x/modeling/polymorphism.md new file mode 100644 index 00000000..c529c030 --- /dev/null +++ b/versioned_docs/version-3.x/modeling/polymorphism.md @@ -0,0 +1,34 @@ +--- +sidebar_position: 10 +description: Polymorphic models in ZModel +--- + +import ZModelVsPSL from '../_components/ZModelVsPSL'; + +# Polymorphism + + +Polymorphism is a ZModel feature and doesn't exist in PSL. + + +## Introduction + +When modeling non-trivial applications, the need of an "Object-Oriented" kind of polymorphism often arises: +- Something **IS-A** more abstract type of thing. +- Something **HAS-A/HAS-many** a more abstract type of thing(s). + +Imagine we're modeling a content library system where users own different types of content: posts, videos, images, etc. They share some common traits like name, creation date, owner, etc., but have different specific fields. + +It may be tempting to use mixins to share the common fields, however it's not an ideal solution because: + +- The `User` table will have relations to each of the content types. +- There's no efficient and clean way to query all content types together (e.g., all content owned by a user). +- Consequently, whenever you add a new content type, you'll need to modify the `User` model, and probably lots of query code too. + +A true solution involves having a in-database model of polymorphism, where we really have a `Content` table that serves as an intermediary between `User` and the concrete content types. This is what ZModel polymorphism is about. + +:::info +There are [two main ways](https://www.prisma.io/docs/orm/prisma-schema/data-model/table-inheritance) to model polymorphism in relational databases: single-table inheritance (STI) and multi-table inheritance (MTI, aka. delegate types). ZModel only supports MTI. +::: + +## Modeling polymorphism diff --git a/versioned_docs/version-3.x/modeling/relations.md b/versioned_docs/version-3.x/modeling/relations.md new file mode 100644 index 00000000..6a74b365 --- /dev/null +++ b/versioned_docs/version-3.x/modeling/relations.md @@ -0,0 +1,219 @@ +--- +sidebar_position: 5 +description: Relations in ZModel +--- + +# Relations + +Relation is a fundamental concept in relational databases. It connect models into a graph, and allows you to query interconnected data efficiently. In ZModel, relations are modeled using the `@relation` attribute. For most cases it involves one side of the relation defining a foreign key field that references the primary key of the other side. By convention, we call the model that defines the foreign key the "owner" side. + +## One-to-one relations + +A typical one-to-one relation looks like this: + +```zmodel +model User { + id Int @id + profile Profile? +} + +model Profile { + id Int @id + user User @relation(fields: [userId], references: [id]) + userId Int @unique +} +``` + +The `Profile` model holds the foreign key `userId` and is the owner of the relation. The pk-fk association is established by the `@relation` attribute, where the `fields` parameters specifies the foreign key field(s) and the `references` parameter specifies the primary key field(s) of the other side. + +In one-to-one relations, the "non-owner" side must declare the relation field as optional (here `User.profile`), because there's no way to guarantee a `User` row always has a corresponding `Profile` row at the database level. The owner side can be either optional or required. + +Relations can also be explicitly named, and it's useful to disambiguate relations when a model has multiple relations to the same model, or to control the constraint name generated by the migration engine. + +```zmodel +model User { + id Int @id + profile Profile? @relation('UserProfile') +} + +model Profile { + id Int @id + user User @relation('UserProfile', fields: [userId], references: [id]) + userId Int @unique +} +``` + +Please note that even though both sides of the relation now have the `@relation` attribute, only the owner side can have the `fields` and `references` parameters. + +If a relation involves a model with composite PK fields, the FK fields must match the PK fields' count and types, and the `fields` and `references` parameters must be specified with those field tuples with matching order. + +```zmodel +model User { + id1 Int + id2 Int + profile Profile? + + @@id([id1, id2]) +} + +model Profile { + id Int @id + user User @relation(fields: [userId1, userId2], references: [id1, id2]) + userId1 Int + userId2 Int +} +``` + +## One-to-many relations + +A typical one-to-many relation looks like this: + +```zmodel +model User { + id Int @id + posts Post[] +} + +model Post { + id Int @id + author User @relation(fields: [authorId], references: [id]) + authorId Int +} +``` + +It's modeled pretty much the same way as one-to-one relations, except that the "non-owner" side (here `User.posts`) is a of list of the other side's model type. + +## Many-to-many relations + +Many-to-many relations are modeled in the database through a join table - which forms a many-to-one relation with each of the two sides. + +In ZModel, there are two ways to model many-to-many relations: implicitly or explicitly. + +### Implicit many-to-many + +An implicit many-to-many relation simply defines both sides of the relation as lists of the other side's model type, without defining a join table explicitly. + +```zmodel +model User { + id Int @id + posts Post[] +} + +model Post { + id Int @id + editors User[] +} +``` + +Under the hood, the migration engine creates a join table named `_PostToUser` (model names are sorted alphabetically), and the ORM runtime transparently handles the join table for you. + +You can also name the join table explicitly by adding the `@relation` attribute to both sides: + +```zmodel +model User { + id Int @id + posts Post[] @relation('UserPosts') +} + +model Post { + id Int @id + editors User[] @relation('UserPosts') +} +``` + +### Explicit many-to-many + +Explicit many-to-many relations are nothing but a join table with foreign keys linking the two sides. + +```zmodel +model User { + id Int @id + posts UserPost[] +} + +model Post { + id Int @id + editors UserPost[] +} + +model UserPost { + userId Int + postId Int + user User @relation(fields: [userId], references: [id]) + post Post @relation(fields: [postId], references: [id]) + + @@id([userId, postId]) +} +``` + +Since the join table is explicitly defined, when using the ORM, you'll need to involve it in your queries with an extra level of nesting. + +## Self relations + +Self relations are cases where a model has a relation to itself. They can be one-to-one, one-to-many, or many-to-many. + +### One-to-one + +```zmodel +model Employee { + id Int @id + mentorId Int? @unique + mentor Employee? @relation('Mentorship', fields: [mentorId], references: [id]) + mentee Employee? @relation('Mentorship') +} +``` + +Quick notes: + +- Both sides of the relation are defined in the same model. +- Both relation fields need to have `@relation` attributes with matching names. +- One side (here `mentor`) has a foreign key field (`mentorId`) that references the primary key. +- The foreign key field is marked `@unique` to guarantee one-to-one. + +### One-to-many + +```zmodel +model Employee { + id Int @id + managerId Int + manager Employee @relation('Management', fields: [managerId], references: [id]) + subordinates Employee[] @relation('Management') +} +``` + +Quick notes: +- Both sides of the relation are defined in the same model. +- Both relation fields need to have `@relation` attributes with matching names. +- One side (here `manager`) has a foreign key field (`managerId`) that references the primary key. +- The owner side (`Employee.manager`) can be either optional or required based on your needs. + +### Many-to-many + +Defining an implicit many-to-many self relation is very straightforward. + +```zmodel +model Employee { + id Int @id + mentors Employee[] @relation('Mentorship') + mentees Employee[] @relation('Mentorship') +} +``` + +You can also define an explicit one by modeling the join table explicitly. + +```zmodel +model Employee { + id Int @id + mentors Mentorship[] @relation('Mentorship') + mentees Mentorship[] @relation('Mentorship') +} + +model Mentorship { + mentorId Int + menteeId Int + mentor Employee @relation('Mentorship', fields: [mentorId], references: [id]) + mentee Employee @relation('Mentorship', fields: [menteeId], references: [id]) + + @@id([mentorId, menteeId]) +} +``` \ No newline at end of file diff --git a/versioned_docs/version-3.x/modeling/strong-typed-json.md b/versioned_docs/version-3.x/modeling/strong-typed-json.md new file mode 100644 index 00000000..2426d926 --- /dev/null +++ b/versioned_docs/version-3.x/modeling/strong-typed-json.md @@ -0,0 +1,50 @@ +--- +sidebar_position: 9 +description: Strongly typed JSON fields +--- + +import ZModelVsPSL from '../_components/ZModelVsPSL'; + +# Strongly Typed JSON + + +Strongly typed JSON is a ZModel feature and doesn't exist in PSL. + + +With relational databases providing better and better JSON support, their usage has become more common. However, in many cases your JSON fields still follow a specific structure, and when so, you can make the fields strongly typed so that: + +- When mutating the field, its structure is validated. +- When querying the field, its result is strongly typed. + +To type a JSON field, define a custom type in ZModel, use it as the field's type, and additionally mark the field with the `@json` attribute. + +***Before:*** + +```zmodel +model User { + id Int @id + address Json +} +``` + +***After:*** + +```zmodel +type Address { + street String + city String + country String + zip Int +} + +model User { + id Int @id + address Address @json +} +``` + +:::info +The `@json` attribute doesn't do anything except to clearly mark the field is a JSON field but not a relation to another model. +::: + +The migration engine still sees the field as a plain Json field. However, the ORM client enforces its structure and takes care of properly typing the query results. We'll revisit the topic in the [ORM](../orm/) part. diff --git a/versioned_docs/version-3.x/orm/_category_.yml b/versioned_docs/version-3.x/orm/_category_.yml index 7e156336..50000096 100644 --- a/versioned_docs/version-3.x/orm/_category_.yml +++ b/versioned_docs/version-3.x/orm/_category_.yml @@ -1,4 +1,4 @@ -position: 2 +position: 3 label: ORM collapsible: true collapsed: true diff --git a/versioned_docs/version-3.x/orm/introduction.md b/versioned_docs/version-3.x/orm/overview.md similarity index 98% rename from versioned_docs/version-3.x/orm/introduction.md rename to versioned_docs/version-3.x/orm/overview.md index 8bcaf843..dc4fe7aa 100644 --- a/versioned_docs/version-3.x/orm/introduction.md +++ b/versioned_docs/version-3.x/orm/overview.md @@ -1,13 +1,13 @@ --- sidebar_position: 1 -description: Introduction to ZenStack ORM +description: ZenStack ORM overview --- -# Introduction +# Overview ZenStack ORM is a schema-first ORM for modern TypeScript applications. It learnt from the prior arts and aims to provide an awesome developer experience by combining the best ingredients into a cohesive package. -## Key Facts +## Key Features ### [Prisma](https://prisma.io/orm)-Compatible Schema Language diff --git a/versioned_docs/version-3.x/orm/quick-start.mdx b/versioned_docs/version-3.x/orm/quick-start.mdx index 3ffcc406..5e738583 100644 --- a/versioned_docs/version-3.x/orm/quick-start.mdx +++ b/versioned_docs/version-3.x/orm/quick-start.mdx @@ -11,7 +11,7 @@ import PackageExec from '../_components/PackageExec.tsx'; # Quick Start :::info -All v3 packages are published under the "@next" tag. +All v3 packages are currently published under the "@next" tag. ::: There are multiple ways to start using ZenStack. diff --git a/versioned_docs/version-3.x/orm/zmodel/attributes.md b/versioned_docs/version-3.x/orm/zmodel/attributes.md deleted file mode 100644 index be676265..00000000 --- a/versioned_docs/version-3.x/orm/zmodel/attributes.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -sidebar_position: 4 -description: Attributes in ZModel ---- - -# Attributes diff --git a/versioned_docs/version-3.x/orm/zmodel/builtin-types.md b/versioned_docs/version-3.x/orm/zmodel/builtin-types.md deleted file mode 100644 index d3142702..00000000 --- a/versioned_docs/version-3.x/orm/zmodel/builtin-types.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -sidebar_position: 6 -description: Built-in types in ZModel ---- - -# Built-in Types diff --git a/versioned_docs/version-3.x/orm/zmodel/components.md b/versioned_docs/version-3.x/orm/zmodel/components.md deleted file mode 100644 index b0635163..00000000 --- a/versioned_docs/version-3.x/orm/zmodel/components.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -sidebar_position: 1 -description: ZModel schema components ---- - -# Schema Components diff --git a/versioned_docs/version-3.x/orm/zmodel/custom-types.md b/versioned_docs/version-3.x/orm/zmodel/custom-types.md deleted file mode 100644 index 7208914f..00000000 --- a/versioned_docs/version-3.x/orm/zmodel/custom-types.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -sidebar_position: 7 -description: Custom types in ZModel ---- - -# Custom Types diff --git a/versioned_docs/version-3.x/orm/zmodel/datasource.md b/versioned_docs/version-3.x/orm/zmodel/datasource.md deleted file mode 100644 index 3078fe6e..00000000 --- a/versioned_docs/version-3.x/orm/zmodel/datasource.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -sidebar_position: 2 -description: Datasource in ZModel ---- - -# Data Source - diff --git a/versioned_docs/version-3.x/orm/zmodel/models.md b/versioned_docs/version-3.x/orm/zmodel/models.md deleted file mode 100644 index f54ac510..00000000 --- a/versioned_docs/version-3.x/orm/zmodel/models.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -sidebar_position: 3 -description: Models in ZModel ---- - -# Models - diff --git a/versioned_docs/version-3.x/orm/zmodel/polymorphism.md b/versioned_docs/version-3.x/orm/zmodel/polymorphism.md deleted file mode 100644 index 62c1797d..00000000 --- a/versioned_docs/version-3.x/orm/zmodel/polymorphism.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -sidebar_position: 9 -description: Polymorphic models in ZModel ---- - -# Polymorphic Models - diff --git a/versioned_docs/version-3.x/orm/zmodel/relations.md b/versioned_docs/version-3.x/orm/zmodel/relations.md deleted file mode 100644 index 96794f67..00000000 --- a/versioned_docs/version-3.x/orm/zmodel/relations.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -sidebar_position: 5 -description: Relations in ZModel ---- - -# Relations diff --git a/versioned_docs/version-3.x/orm/zmodel/reusing-fields.md b/versioned_docs/version-3.x/orm/zmodel/reusing-fields.md deleted file mode 100644 index eed27f91..00000000 --- a/versioned_docs/version-3.x/orm/zmodel/reusing-fields.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -sidebar_position: 8 -description: Reusing common fields across models in ZModel ---- - -# Reusing Common Fields - diff --git a/versioned_docs/version-3.x/reference/_category_.yml b/versioned_docs/version-3.x/reference/_category_.yml index 3e14b45e..1352c105 100644 --- a/versioned_docs/version-3.x/reference/_category_.yml +++ b/versioned_docs/version-3.x/reference/_category_.yml @@ -1,4 +1,4 @@ -position: 6 +position: 7 label: Reference collapsible: true collapsed: true diff --git a/versioned_docs/version-3.x/samples.md b/versioned_docs/version-3.x/samples.md index 2b78bb7b..3e385d1b 100644 --- a/versioned_docs/version-3.x/samples.md +++ b/versioned_docs/version-3.x/samples.md @@ -1,5 +1,5 @@ --- -sidebar_position: 7 +sidebar_position: 8 sidebar_label: Sample Projects --- diff --git a/versioned_docs/version-3.x/service/_category_.yml b/versioned_docs/version-3.x/service/_category_.yml index d66b3af7..9159f276 100644 --- a/versioned_docs/version-3.x/service/_category_.yml +++ b/versioned_docs/version-3.x/service/_category_.yml @@ -1,4 +1,4 @@ -position: 4 +position: 5 label: Query as a Service collapsible: true collapsed: true diff --git a/versioned_docs/version-3.x/service/introduction.md b/versioned_docs/version-3.x/service/overview.md similarity index 53% rename from versioned_docs/version-3.x/service/introduction.md rename to versioned_docs/version-3.x/service/overview.md index 261e992b..d97b5285 100644 --- a/versioned_docs/version-3.x/service/introduction.md +++ b/versioned_docs/version-3.x/service/overview.md @@ -1,3 +1,3 @@ -# Introduction +# Overview Coming soon 🚧 \ No newline at end of file diff --git a/versioned_docs/version-3.x/upgrade.md b/versioned_docs/version-3.x/upgrade.md index cfeb1049..710d5964 100644 --- a/versioned_docs/version-3.x/upgrade.md +++ b/versioned_docs/version-3.x/upgrade.md @@ -3,7 +3,7 @@ description: How to upgrade to ZenStack v3 slug: /upgrade-v3 sidebar_label: Upgrading to V3 -sidebar_position: 8 +sidebar_position: 9 --- # Upgrading to V3 diff --git a/versioned_docs/version-3.x/utilities/_category_.yml b/versioned_docs/version-3.x/utilities/_category_.yml index 9dd0322c..1fdb7623 100644 --- a/versioned_docs/version-3.x/utilities/_category_.yml +++ b/versioned_docs/version-3.x/utilities/_category_.yml @@ -1,4 +1,4 @@ -position: 5 +position: 6 label: Utilities collapsible: true collapsed: true diff --git a/versioned_docs/version-3.x/utilities/tanstack-query.md b/versioned_docs/version-3.x/utilities/tanstack-query.md new file mode 100644 index 00000000..fa0f1bda --- /dev/null +++ b/versioned_docs/version-3.x/utilities/tanstack-query.md @@ -0,0 +1,6 @@ +--- +sidebar_position: 2 +description: TanStack Query integration +--- + +# TanStack Query diff --git a/versioned_docs/version-3.x/utilities/zod.md b/versioned_docs/version-3.x/utilities/zod.md index 5bfd1865..7f16ec55 100644 --- a/versioned_docs/version-3.x/utilities/zod.md +++ b/versioned_docs/version-3.x/utilities/zod.md @@ -3,4 +3,4 @@ sidebar_position: 1 description: Zod integration --- -# Zod Integration +# Zod diff --git a/versioned_docs/version-3.x/welcome.md b/versioned_docs/version-3.x/welcome.md index 8309ace6..60442d84 100644 --- a/versioned_docs/version-3.x/welcome.md +++ b/versioned_docs/version-3.x/welcome.md @@ -5,6 +5,24 @@ sidebar_label: Welcome sidebar_position: 1 --- -# Welcome to ZenStack V3 +# Welcome -The modern data layer for TypeScript applications. +Welcome to ZenStack - the modern data layer for your TypeScript application! + +ZenStack is built with the belief that most applications should use the data model as its center pillar. If that model is well-designed, it can serve as the single source of truth throughout the app's lifecycle, and be used to derive many other aspects of the app. The result is a smaller, more cohesive code base that scales well as your team grows while maintaining a high level of developer experience. + +Inside the package you'll find: + +- #### An intuitive schema language + That helps you model data, relation, access control, and more, in one place. [πŸ”—](./modeling/) + +- #### A powerful ORM + With awesomely-typed API, built-in access control, and unmatched flexibility. [πŸ”—](./orm/) + +- #### A Query-as-a-Service layer + That provides a full-fledged data API without the need to code it up. [πŸ”—](./service/) + +- #### Utilities + For deriving artifacts like Zod schemas, frontend hooks, OpenAPI specs, etc., from the schema. [πŸ”—](./utilities/) + +> *ZenStack originated as an extension to Prisma ORM. V3 is a complete rewrite that removed Prisma as a runtime dependency and replaced it with an implementation built from the scratch ("scratch" = [Kysely](https://kysely.dev/) πŸ˜†). On its surface, it continues to use a "Prisma-superset" schema language and a query API compatible with PrismaClient.* \ No newline at end of file From 2815551802e9ca9efeac92af741310ac106d3c28 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Sun, 3 Aug 2025 18:46:08 +0800 Subject: [PATCH 04/13] WIP --- .../version-3.x/_components/ZModelVsPSL.tsx | 2 +- .../version-3.x/migration/_category_.yml | 3 - .../migration/{overview.md => index.md} | 0 .../version-3.x/modeling/_category_.yml | 3 - .../modeling/{overview.md => index.md} | 0 .../version-3.x/modeling/multi-file.md | 33 +++++++ versioned_docs/version-3.x/modeling/plugin.md | 31 +++++++ .../version-3.x/modeling/polymorphism.md | 90 ++++++++++++++++++- .../modeling/{relations.md => relation.md} | 0 versioned_docs/version-3.x/orm/_category_.yml | 3 - .../version-3.x/orm/{overview.md => index.md} | 0 .../orm/{ts-types.md => inferred-types.md} | 2 +- .../version-3.x/orm/plugins/_category_.yml | 3 - .../orm/plugins/{introduction.md => index.md} | 0 .../version-3.x/reference/error-handling.md | 43 --------- .../version-3.x/reference/plugin-dev.md | 7 ++ .../reference/plugins/_category_.yml | 7 ++ .../version-3.x/reference/runtime-api.md | 2 +- .../version-3.x/service/_category_.yml | 3 - .../service/{overview.md => index.md} | 0 versioned_docs/version-3.x/welcome.md | 2 +- 21 files changed, 171 insertions(+), 63 deletions(-) rename versioned_docs/version-3.x/migration/{overview.md => index.md} (100%) rename versioned_docs/version-3.x/modeling/{overview.md => index.md} (100%) rename versioned_docs/version-3.x/modeling/{relations.md => relation.md} (100%) rename versioned_docs/version-3.x/orm/{overview.md => index.md} (100%) rename versioned_docs/version-3.x/orm/{ts-types.md => inferred-types.md} (79%) rename versioned_docs/version-3.x/orm/plugins/{introduction.md => index.md} (100%) delete mode 100644 versioned_docs/version-3.x/reference/error-handling.md create mode 100644 versioned_docs/version-3.x/reference/plugin-dev.md create mode 100644 versioned_docs/version-3.x/reference/plugins/_category_.yml rename versioned_docs/version-3.x/service/{overview.md => index.md} (100%) diff --git a/versioned_docs/version-3.x/_components/ZModelVsPSL.tsx b/versioned_docs/version-3.x/_components/ZModelVsPSL.tsx index 0ea279d3..46b7a50d 100644 --- a/versioned_docs/version-3.x/_components/ZModelVsPSL.tsx +++ b/versioned_docs/version-3.x/_components/ZModelVsPSL.tsx @@ -7,7 +7,7 @@ interface ZModelVsPSLProps { const ZModelVsPSL: FC = ({ children }) => { return ( - + {children} ); diff --git a/versioned_docs/version-3.x/migration/_category_.yml b/versioned_docs/version-3.x/migration/_category_.yml index 013a697a..6cb70365 100644 --- a/versioned_docs/version-3.x/migration/_category_.yml +++ b/versioned_docs/version-3.x/migration/_category_.yml @@ -2,6 +2,3 @@ position: 4 label: Migration collapsible: true collapsed: true -link: - type: generated-index - title: Migration diff --git a/versioned_docs/version-3.x/migration/overview.md b/versioned_docs/version-3.x/migration/index.md similarity index 100% rename from versioned_docs/version-3.x/migration/overview.md rename to versioned_docs/version-3.x/migration/index.md diff --git a/versioned_docs/version-3.x/modeling/_category_.yml b/versioned_docs/version-3.x/modeling/_category_.yml index 8dbacd32..2df4bdd6 100644 --- a/versioned_docs/version-3.x/modeling/_category_.yml +++ b/versioned_docs/version-3.x/modeling/_category_.yml @@ -2,6 +2,3 @@ position: 2 label: Data Modeling collapsible: true collapsed: true -link: - type: generated-index - title: Data Modeling diff --git a/versioned_docs/version-3.x/modeling/overview.md b/versioned_docs/version-3.x/modeling/index.md similarity index 100% rename from versioned_docs/version-3.x/modeling/overview.md rename to versioned_docs/version-3.x/modeling/index.md diff --git a/versioned_docs/version-3.x/modeling/multi-file.md b/versioned_docs/version-3.x/modeling/multi-file.md index ad1ba638..75aaeb26 100644 --- a/versioned_docs/version-3.x/modeling/multi-file.md +++ b/versioned_docs/version-3.x/modeling/multi-file.md @@ -3,7 +3,40 @@ sidebar_position: 11 description: Breaking down complex schemas into multiple files --- +import ZModelVsPSL from '../_components/ZModelVsPSL'; + # Multi-file Schema + +Prisma uses an implicit approach that simply merges all schema files in a folder. ZModel uses explicit `import` syntax for better clarity and flexibility. + + +When your schema grows large, you can break them down to smaller files and stitch them together using the `import` statement. + +```zmodel title="zenstack/user.zmodel" +import './post' + +model User { + id Int @id + posts Post[] +} +``` + +```zmodel title="zenstack/post.zmodel" +import './user' + +model Post { + id Int @id + content String + author User @relation(fields: [authorId], references: [id]) + authorId Int +} +``` + +```zmodel title="zenstack/schema.zmodel" +import './user' +import './post' +``` +After type-checking, these files are merged into a single schema AST before passed to the downstream tools. diff --git a/versioned_docs/version-3.x/modeling/plugin.md b/versioned_docs/version-3.x/modeling/plugin.md index 10506f39..25af5443 100644 --- a/versioned_docs/version-3.x/modeling/plugin.md +++ b/versioned_docs/version-3.x/modeling/plugin.md @@ -3,5 +3,36 @@ sidebar_position: 11 description: ZenStack plugins --- +import ZModelVsPSL from '../_components/ZModelVsPSL'; + # Plugin + +ZenStack's "plugin" concept replaces PSL's "generator". + + +Plugin is a powerful mechanism that allows you to extend ZenStack at the schema, CLI, and runtime levels. This section only focuses on how to add plugins to your ZModel. Please refer to the [Plugin Development](../reference/plugin-dev.md) section for more details on how to develop plugins. + +## Adding plugins to ZModel + +Let's take a look at the following example: + +```zmodel +plugin myPlugin { + provider = 'my-zenstack-plugin' + output = './generated' +} +``` + +A plugin declaration involves three parts: + +1. A unique name +2. A `provider` field that specifies where to load the plugin from. It can be a built-in plugin (like `@core/prisma` here), a local folder, or an npm package. +3. Plugin-specific configuration options, such as `output` in this case. + +A plugin can have the following effects to ZModel: + +- It can contribute custom attributes that you can use to annotate models and fields. +- It can contribute code generation logic that's executed when you run the `zenstack generate` command. + +Plugins can also contribute to the ORM runtime behavior, and we'll leave it to the [ORM](../orm/plugins/) part to explain it in detail. diff --git a/versioned_docs/version-3.x/modeling/polymorphism.md b/versioned_docs/version-3.x/modeling/polymorphism.md index c529c030..87006ebe 100644 --- a/versioned_docs/version-3.x/modeling/polymorphism.md +++ b/versioned_docs/version-3.x/modeling/polymorphism.md @@ -17,7 +17,7 @@ When modeling non-trivial applications, the need of an "Object-Oriented" kind of - Something **IS-A** more abstract type of thing. - Something **HAS-A/HAS-many** a more abstract type of thing(s). -Imagine we're modeling a content library system where users own different types of content: posts, videos, images, etc. They share some common traits like name, creation date, owner, etc., but have different specific fields. +Imagine we're modeling a content library system where users own different types of content: posts, images, videos, etc. They share some common traits like name, creation date, owner, etc., but have different specific fields. It may be tempting to use mixins to share the common fields, however it's not an ideal solution because: @@ -32,3 +32,91 @@ There are [two main ways](https://www.prisma.io/docs/orm/prisma-schema/data-mode ::: ## Modeling polymorphism + +Modeling polymorphism in ZModel is similar to designing an OOP class hierarchy - you introduce a base model and then extend it with concrete ones. + +Here's how it looks for our content library example: + +```zmodel +model User { + id Int @id + contents Content[] +} + +model Content { + id Int @id + name String + createdAt DateTime @default(now()) + owner User @relation(fields: [ownerId], references: [id]) + ownerId Int + // highlight-next-line + type String + + // highlight-next-line + @@delegate(type) +} + +model Post extends Content { + content String +} + +model Image extends Content { + data Bytes +} + +model Video extends Content { + url String +} +``` + +```mermaid +erDiagram + User { + id Int PK + } + Content { + id Int PK + name String + createdAt Date + ownerId Int FK + type String + } + User ||--o{ Content: owns + Post { + id Int PK + content String + } + Post ||--|| Content: delegates + Image { + id Int PK + data Bytes + } + Image ||--|| Content: delegates + Video { + id Int PK + url String + } + Video ||--|| Content: delegates +``` + +There are two special things about polymorphic base model: + +1. It must have a "discriminator" field that stores the concrete model type that it should "delegate" to. In the example above, the `type` field serves this purpose. It can be named anything you like, but must be of `String` or enum type. +2. It must have a `@@delegate` attribute. The attribute serves two purposes: it indicates that the model is a base model, and it designates the discriminator field with its parameter. + +You can also have a deep hierarchy involving multiple level of base models. Just need to make sure each base model has its own discriminator field and `@@delegate` attribute. Extending from multiple base models directly is not supported. + +## Migration behavior + +The migration engine takes care of mapping both the base model and the concrete ones to tables, and creates one-to-one relations between the base and each of its derivations. + +To simplify query and conserve space, the base and the concrete are assumed to share the same id values (this is guaranteed by the ORM when creating the records), and consequently, the concrete model's id field is also reused as the foreign key to the base model. So, for a `Post` record with id `1`, the base `Content` record also has id `1`. + +## ORM behavior + +The ORM hides the delegate complexities and provides a simple polymorphic view to the developers: + +1. Creating a concrete model record automatically creates the base model record with the same id and proper discriminator field. +2. Querying with the base model will return entities with concrete model fields. + +We'll revisit the topic in details in the [ORM](../orm/) part. diff --git a/versioned_docs/version-3.x/modeling/relations.md b/versioned_docs/version-3.x/modeling/relation.md similarity index 100% rename from versioned_docs/version-3.x/modeling/relations.md rename to versioned_docs/version-3.x/modeling/relation.md diff --git a/versioned_docs/version-3.x/orm/_category_.yml b/versioned_docs/version-3.x/orm/_category_.yml index 50000096..6f69cf50 100644 --- a/versioned_docs/version-3.x/orm/_category_.yml +++ b/versioned_docs/version-3.x/orm/_category_.yml @@ -2,6 +2,3 @@ position: 3 label: ORM collapsible: true collapsed: true -link: - type: generated-index - title: ORM diff --git a/versioned_docs/version-3.x/orm/overview.md b/versioned_docs/version-3.x/orm/index.md similarity index 100% rename from versioned_docs/version-3.x/orm/overview.md rename to versioned_docs/version-3.x/orm/index.md diff --git a/versioned_docs/version-3.x/orm/ts-types.md b/versioned_docs/version-3.x/orm/inferred-types.md similarity index 79% rename from versioned_docs/version-3.x/orm/ts-types.md rename to versioned_docs/version-3.x/orm/inferred-types.md index 9fd5432d..990d91d6 100644 --- a/versioned_docs/version-3.x/orm/ts-types.md +++ b/versioned_docs/version-3.x/orm/inferred-types.md @@ -3,4 +3,4 @@ sidebar_position: 11 description: TypeScript types derived from the ZModel schema --- -# TypeScript Types +# Schema-Inferred Types diff --git a/versioned_docs/version-3.x/orm/plugins/_category_.yml b/versioned_docs/version-3.x/orm/plugins/_category_.yml index 60e83429..12d5462a 100644 --- a/versioned_docs/version-3.x/orm/plugins/_category_.yml +++ b/versioned_docs/version-3.x/orm/plugins/_category_.yml @@ -2,6 +2,3 @@ position: 12 label: Plugins collapsible: true collapsed: true -link: - type: generated-index - title: Plugins diff --git a/versioned_docs/version-3.x/orm/plugins/introduction.md b/versioned_docs/version-3.x/orm/plugins/index.md similarity index 100% rename from versioned_docs/version-3.x/orm/plugins/introduction.md rename to versioned_docs/version-3.x/orm/plugins/index.md diff --git a/versioned_docs/version-3.x/reference/error-handling.md b/versioned_docs/version-3.x/reference/error-handling.md deleted file mode 100644 index 07d31586..00000000 --- a/versioned_docs/version-3.x/reference/error-handling.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -description: Error handling -sidebar_position: 8 ---- - -# Error Handling - -ZenStack's enhancements to Prisma clients are transparent proxies, so normal errors thrown from a Prisma client simply pass through, and you can handle them the same way as you do in a regular Prisma project. It's good to read these references from Prisma: - -- [Handling exceptions and errors](https://www.prisma.io/docs/concepts/components/prisma-client/handling-exceptions-and-errors) -- [Error message reference](https://www.prisma.io/docs/reference/api-reference/error-reference) - -The enhanced Prisma client can throw extra errors when an operation is rejected by [access policies](./zmodel-language#access-policy) or its data fails [validation rules](./zmodel-language#data-validation). To keep a consistent programming experience, a `PrismaClientKnownRequestError` is thrown with code [`P2004`](https://www.prisma.io/docs/reference/api-reference/error-reference#p2004) is used in such cases: - -```ts -throw new PrismaClientKnownRequestError(message, { - clientVersion: getVersion(), - code: 'P2004', - meta: ... -}); -``` - -The error contains a `meta` field providing more information about the error. It contains the following fields: - -- `reason` - - A string field indicating the detailed reason of the rejection. Its value can be one of the following: - - - *ACCESS_POLICY_VIOLATION* - - CRUD failed because of access policy violation. - - - *RESULT_NOT_READABLE* - - CRUD succeeded but the result was not readable (violating "read" access policies). - - - *DATA_VALIDATION_VIOLATION* - - CRUD failed because of a [data validation](./zmodel-language#data-validation) error. - -- `zodErrors` - - An object field containing the raw Zod validation errors. Only present if the `reason` is `DATA_VALIDATION_VIOLATION`. See [Zod documentation](https://zod.dev/?id=error-handling) for more details. diff --git a/versioned_docs/version-3.x/reference/plugin-dev.md b/versioned_docs/version-3.x/reference/plugin-dev.md new file mode 100644 index 00000000..5cd7ddef --- /dev/null +++ b/versioned_docs/version-3.x/reference/plugin-dev.md @@ -0,0 +1,7 @@ +--- +sidebar_position: 5 +sidebar_label: Plugin Development +description: Plugin development guide +--- + +# Plugin Development diff --git a/versioned_docs/version-3.x/reference/plugins/_category_.yml b/versioned_docs/version-3.x/reference/plugins/_category_.yml new file mode 100644 index 00000000..4a2a7dc0 --- /dev/null +++ b/versioned_docs/version-3.x/reference/plugins/_category_.yml @@ -0,0 +1,7 @@ +position: 6 +label: Plugins +collapsible: true +collapsed: true +link: + type: generated-index + title: Plugins diff --git a/versioned_docs/version-3.x/reference/runtime-api.md b/versioned_docs/version-3.x/reference/runtime-api.md index 22d84bdd..2cc8e47c 100644 --- a/versioned_docs/version-3.x/reference/runtime-api.md +++ b/versioned_docs/version-3.x/reference/runtime-api.md @@ -1,6 +1,6 @@ --- description: Runtime API references -sidebar_position: 3 +sidebar_position: 4 sidebar_label: Runtime API --- diff --git a/versioned_docs/version-3.x/service/_category_.yml b/versioned_docs/version-3.x/service/_category_.yml index 9159f276..938fc11d 100644 --- a/versioned_docs/version-3.x/service/_category_.yml +++ b/versioned_docs/version-3.x/service/_category_.yml @@ -2,6 +2,3 @@ position: 5 label: Query as a Service collapsible: true collapsed: true -link: - type: generated-index - title: Query as a Service diff --git a/versioned_docs/version-3.x/service/overview.md b/versioned_docs/version-3.x/service/index.md similarity index 100% rename from versioned_docs/version-3.x/service/overview.md rename to versioned_docs/version-3.x/service/index.md diff --git a/versioned_docs/version-3.x/welcome.md b/versioned_docs/version-3.x/welcome.md index 60442d84..da83a371 100644 --- a/versioned_docs/version-3.x/welcome.md +++ b/versioned_docs/version-3.x/welcome.md @@ -23,6 +23,6 @@ Inside the package you'll find: That provides a full-fledged data API without the need to code it up. [πŸ”—](./service/) - #### Utilities - For deriving artifacts like Zod schemas, frontend hooks, OpenAPI specs, etc., from the schema. [πŸ”—](./utilities/) + For deriving artifacts like Zod schemas, frontend hooks, OpenAPI specs, etc., from the schema. [πŸ”—](./category/utilities) > *ZenStack originated as an extension to Prisma ORM. V3 is a complete rewrite that removed Prisma as a runtime dependency and replaced it with an implementation built from the scratch ("scratch" = [Kysely](https://kysely.dev/) πŸ˜†). On its surface, it continues to use a "Prisma-superset" schema language and a query API compatible with PrismaClient.* \ No newline at end of file From f215f86e5f4c1d48f53f5ac1886df655ba54361d Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Tue, 5 Aug 2025 17:33:31 +0800 Subject: [PATCH 05/13] progress on ORM part --- src/components/StackBlitzGithubEmbed.tsx | 17 ++++- .../_components/ZenStackVsPrisma.tsx | 16 +++++ .../_components/_zmodel-starter.md | 4 +- versioned_docs/version-3.x/faq.md | 5 ++ .../version-3.x/modeling/conclusion.md | 17 +++++ versioned_docs/version-3.x/modeling/index.md | 2 +- .../orm/api/_select-include-omit.md | 18 +++++ .../version-3.x/orm/api/aggregate.md | 2 +- versioned_docs/version-3.x/orm/api/count.md | 2 +- versioned_docs/version-3.x/orm/api/create.md | 39 +++++++++- versioned_docs/version-3.x/orm/api/delete.md | 2 +- versioned_docs/version-3.x/orm/api/filter.md | 9 +++ versioned_docs/version-3.x/orm/api/find.md | 63 +++++++++++++++- .../version-3.x/orm/api/transaction.md | 2 +- versioned_docs/version-3.x/orm/api/update.md | 2 +- versioned_docs/version-3.x/orm/cli.md | 16 +++++ ...database-client.mdx => database-client.md} | 9 ++- versioned_docs/version-3.x/orm/index.md | 71 +++++++++---------- .../orm/{quick-start.mdx => quick-start.md} | 2 +- 19 files changed, 244 insertions(+), 54 deletions(-) create mode 100644 versioned_docs/version-3.x/_components/ZenStackVsPrisma.tsx create mode 100644 versioned_docs/version-3.x/modeling/conclusion.md create mode 100644 versioned_docs/version-3.x/orm/api/_select-include-omit.md create mode 100644 versioned_docs/version-3.x/orm/api/filter.md rename versioned_docs/version-3.x/orm/{database-client.mdx => database-client.md} (78%) rename versioned_docs/version-3.x/orm/{quick-start.mdx => quick-start.md} (97%) diff --git a/src/components/StackBlitzGithubEmbed.tsx b/src/components/StackBlitzGithubEmbed.tsx index 5bddace1..d65fb762 100644 --- a/src/components/StackBlitzGithubEmbed.tsx +++ b/src/components/StackBlitzGithubEmbed.tsx @@ -4,18 +4,29 @@ import sdk from '@stackblitz/sdk'; interface StackBlitzGithubEmbedProps { repoPath: string; height?: string; + openFile?: string; + startScript?: string; + clickToLoad?: boolean; } -const StackBlitzGithubEmbed: React.FC = ({ repoPath, height = '600px' }) => { +const StackBlitzGithubEmbed: React.FC = ({ + repoPath, + height = '600px', + openFile = 'main.ts', + clickToLoad = false, + startScript, +}) => { const containerRef = useRef(null); useEffect(() => { if (containerRef.current) { sdk.embedGithubProject(containerRef.current, repoPath, { - openFile: 'main.ts', + openFile, height, view: 'editor', - forceEmbedLayout: true, + hideNavigation: true, + startScript, + clickToLoad, }); } }, [repoPath, height]); diff --git a/versioned_docs/version-3.x/_components/ZenStackVsPrisma.tsx b/versioned_docs/version-3.x/_components/ZenStackVsPrisma.tsx new file mode 100644 index 00000000..323adb57 --- /dev/null +++ b/versioned_docs/version-3.x/_components/ZenStackVsPrisma.tsx @@ -0,0 +1,16 @@ +import React, { FC } from 'react'; +import Admonition from '@theme/Admonition'; + +interface ZenStackVsPrismaProps { + children: React.ReactNode; +} + +const ZenStackVsPrisma: FC = ({ children }) => { + return ( + + {children} + + ); +}; + +export default ZenStackVsPrisma; diff --git a/versioned_docs/version-3.x/_components/_zmodel-starter.md b/versioned_docs/version-3.x/_components/_zmodel-starter.md index 029ef799..fbf3eaea 100644 --- a/versioned_docs/version-3.x/_components/_zmodel-starter.md +++ b/versioned_docs/version-3.x/_components/_zmodel-starter.md @@ -6,7 +6,7 @@ model User { id String @id @default(cuid()) - email String @unique @email @length(6, 32) + email String @unique posts Post[] } @@ -14,7 +14,7 @@ id String @id @default(cuid()) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - title String @length(1, 256) + title String content String published Boolean @default(false) author User @relation(fields: [authorId], references: [id]) diff --git a/versioned_docs/version-3.x/faq.md b/versioned_docs/version-3.x/faq.md index b0e81a65..410fe76c 100644 --- a/versioned_docs/version-3.x/faq.md +++ b/versioned_docs/version-3.x/faq.md @@ -7,3 +7,8 @@ sidebar_position: 100 --- # πŸ™‹πŸ» FAQ + +## What databases are supported? + +Currently only SQLite and PostgreSQL are supported. MySQL will be added in the future. There's no plan to support other relational databases or NoSQL databases. + diff --git a/versioned_docs/version-3.x/modeling/conclusion.md b/versioned_docs/version-3.x/modeling/conclusion.md new file mode 100644 index 00000000..4c5cc632 --- /dev/null +++ b/versioned_docs/version-3.x/modeling/conclusion.md @@ -0,0 +1,17 @@ +--- +sidebar_position: 100 +description: Data modeling conclusion +--- + +# Conclusion + +Congratulations! You have learned all the essentials of data model with ZenStack. What's next? + +In the following parts, you'll learn how to put the schema into use and let it drive many aspects of your application, including: + +- [Creating an ORM to query the database](../orm/) +- [Evolving database schema with migrations](../migration/) +- [Exposing a data API with Query-as-a-Service](../service/) +- [Driving other useful utilities](../service/) + +Let's continue our journey with ZenStack! diff --git a/versioned_docs/version-3.x/modeling/index.md b/versioned_docs/version-3.x/modeling/index.md index b2d9e3c1..5cdd7f40 100644 --- a/versioned_docs/version-3.x/modeling/index.md +++ b/versioned_docs/version-3.x/modeling/index.md @@ -5,7 +5,7 @@ description: ZModel overview import ZModelVsPSL from '../_components/ZModelVsPSL'; -# Overview +# Data Modeling Overview ZenStack uses a schema language named **ZModel** to define data models and their related aspects. We know that designing a good schema language is difficult, and we know it's even more difficult to convince people to learn a new one. So we made the decision to design ZModel as a superset of the [Prisma Schema Language (PSL)](https://www.prisma.io/docs/orm/prisma-schema), which is one of the best data modeling language out there. diff --git a/versioned_docs/version-3.x/orm/api/_select-include-omit.md b/versioned_docs/version-3.x/orm/api/_select-include-omit.md new file mode 100644 index 00000000..5b57c5f4 --- /dev/null +++ b/versioned_docs/version-3.x/orm/api/_select-include-omit.md @@ -0,0 +1,18 @@ +- `select` + + An object specifying the fields to include in the result. Setting a field to `true` means to include it. If a field is a relation, you can provide an nested object to further specify which fields of the relation to include. + + This field is optional. If not provided, all non-relation fields are included by default. The `include` field is mutually exclusive with the `select` field. + +- `include` + + An object specifying the relations to include in the result. Setting a relation to `true` means to include it. You can pass an object to further choose what fields/relations are included for the relation, and/or a `where` clause to filter the included relation records. + + This field is optional. If not provided, no relations are included by default. The `include` field is mutually exclusive with the `select` field. + +- `omit` + + An object specifying the fields to omit from the result. Setting a field to `true` means to omit it. Only applicable to non-relation fields. + + This field is optional. If not provided, no fields are omitted by default. The `omit` field is mutually exclusive with the `select` field. + \ No newline at end of file diff --git a/versioned_docs/version-3.x/orm/api/aggregate.md b/versioned_docs/version-3.x/orm/api/aggregate.md index 0b545613..f7f12077 100644 --- a/versioned_docs/version-3.x/orm/api/aggregate.md +++ b/versioned_docs/version-3.x/orm/api/aggregate.md @@ -1,5 +1,5 @@ --- -sidebar_position: 6 +sidebar_position: 7 description: Aggregate API --- diff --git a/versioned_docs/version-3.x/orm/api/count.md b/versioned_docs/version-3.x/orm/api/count.md index d22fd667..7402362e 100644 --- a/versioned_docs/version-3.x/orm/api/count.md +++ b/versioned_docs/version-3.x/orm/api/count.md @@ -1,5 +1,5 @@ --- -sidebar_position: 5 +sidebar_position: 6 description: Count API --- diff --git a/versioned_docs/version-3.x/orm/api/create.md b/versioned_docs/version-3.x/orm/api/create.md index 7bd11da3..d6b8326e 100644 --- a/versioned_docs/version-3.x/orm/api/create.md +++ b/versioned_docs/version-3.x/orm/api/create.md @@ -3,4 +3,41 @@ sidebar_position: 2 description: Create API --- -# Create \ No newline at end of file +import StackBlitzGithubEmbed from '@site/src/components/StackBlitzGithubEmbed'; + +# Create + +The `create` series of APIs are used to create new records in the database. It has the following methods: + +- `create` + Create a single record, optionally with nested relations. +- `createMany` + Create multiple records in a single operation. Nested relations are not supported. Only the number of records created is returned. +- `createManyAndReturn` + Similar to `createMany`, but returns the created records. + +## Samples + + + +## Input + +The create APIs accept an input object with the following fields: + +- `data` + + The data used to create the record. It must contain all non-optional fields of the model. It can also include nested object for creating or connecting relation entities. For `create` the data field must be an object. For `createMany` and `createManyAndReturn`, it can be either an object or an array of objects. + + This field is required. + +- `skipDuplicates` + + A boolean flag that indicates whether to skip records that would violate unique constraints. Only applicable to `createMany` and `createManyAndReturn`. + + This field is optional and defaults to `false`. + +You can also control what fields are turned in the result using the `select`, `include`, and `omit` fields. Read more about field selection in the [next section](./find.md#field-selection). + +## Output + +The output shape of `create` and `createManyAndReturn` API is determined by the `select`, `include`, and `omit` fields in the input object. The output of `createMany` is `{ count: number }`, giving the number of records created. diff --git a/versioned_docs/version-3.x/orm/api/delete.md b/versioned_docs/version-3.x/orm/api/delete.md index 11e0142c..358e28e8 100644 --- a/versioned_docs/version-3.x/orm/api/delete.md +++ b/versioned_docs/version-3.x/orm/api/delete.md @@ -1,5 +1,5 @@ --- -sidebar_position: 4 +sidebar_position: 5 description: Delete API --- diff --git a/versioned_docs/version-3.x/orm/api/filter.md b/versioned_docs/version-3.x/orm/api/filter.md new file mode 100644 index 00000000..01a84115 --- /dev/null +++ b/versioned_docs/version-3.x/orm/api/filter.md @@ -0,0 +1,9 @@ +--- +sidebar_position: 3 +description: how to filter entities +--- + +import StackBlitzGithubEmbed from '@site/src/components/StackBlitzGithubEmbed'; + +# Filter + diff --git a/versioned_docs/version-3.x/orm/api/find.md b/versioned_docs/version-3.x/orm/api/find.md index 633a25c8..78fdebf5 100644 --- a/versioned_docs/version-3.x/orm/api/find.md +++ b/versioned_docs/version-3.x/orm/api/find.md @@ -1,6 +1,65 @@ --- -sidebar_position: 1 +sidebar_position: 2 description: Find API --- -# Find \ No newline at end of file +import StackBlitzGithubEmbed from '@site/src/components/StackBlitzGithubEmbed'; +import SelectIncludeOmit from './_select-include-omit.md'; + +# Find + +The `find` series of APIs are used to query records from the database. It has the following methods: + +- `findMany` + + Find multiple records that match the query criteria. + +- `findUnique` + + Find a single record with a unique criteria. + +- `findFirst` + + Find the first record that matches the query criteria. + +- `findUniqueOrThrow` + + Similar to `findUnique`, but throws an error if no record is found. + +- `findFirstOrThrow` + + Similar to `findFirst`, but throws an error if no record is found. + +## Basic usage + + + +## Filtering + +The API provides a very flexible set of filtering options. We've put it into a [dedicated section](./filter.md). + +## Sorting + +Use the `sort` field to control the sort field, direction, and null field placement. Sorting is not supported for `findUnique` and `findUniqueOrThrow`. + + + +## Pagination + +You can use two strategies for pagination: offset-based or cursor-based. Pagination is not supported for `findUnique` and `findUniqueOrThrow`. + + + +## Field selection + +You can use the following fields to control what fields are returned in the result: + + + + + +## Finding distinct rows + +You can use the `distinct` field to find distinct rows based on specific fields. One row for each unique combination of the specified fields will be returned. The implementation uses SQL `DISTINCT ON` if it's supported by the dialect, otherwise falls back to in-memory deduplication. + + diff --git a/versioned_docs/version-3.x/orm/api/transaction.md b/versioned_docs/version-3.x/orm/api/transaction.md index d24f053d..79bbcb5f 100644 --- a/versioned_docs/version-3.x/orm/api/transaction.md +++ b/versioned_docs/version-3.x/orm/api/transaction.md @@ -1,5 +1,5 @@ --- -sidebar_position: 7 +sidebar_position: 8 description: Transaction API --- diff --git a/versioned_docs/version-3.x/orm/api/update.md b/versioned_docs/version-3.x/orm/api/update.md index 468e8cac..383bb752 100644 --- a/versioned_docs/version-3.x/orm/api/update.md +++ b/versioned_docs/version-3.x/orm/api/update.md @@ -1,5 +1,5 @@ --- -sidebar_position: 3 +sidebar_position: 4 description: Update API --- diff --git a/versioned_docs/version-3.x/orm/cli.md b/versioned_docs/version-3.x/orm/cli.md index 98a18870..86de9ffd 100644 --- a/versioned_docs/version-3.x/orm/cli.md +++ b/versioned_docs/version-3.x/orm/cli.md @@ -3,4 +3,20 @@ sidebar_position: 4 description: Using the CLI --- +import StackBlitzGithubEmbed from '@site/src/components/StackBlitzGithubEmbed'; + # Using the CLI + +ZenStack CLI is a command-line tool that takes the ZModel schema as input and complete different tasks for you. It's included in the "@zenstackhq/cli" package, and can be invoked with either `zen` or `zenstack` command. + +In the context of ORM, the CLI compiles ZModel into a TypeScript representation, which can in turn be used to create a type-safe ORM client. + +You can try running the `npx zen generate` command in the following playground and inspect the TypeScript code generated inside the "zenstack" folder. + + + +The `generate` command outputs the following TypeScript files in the same folder of the schema file: + +- `schema.ts`: TypeScript representation of the ZModel schema, used by the ORM client to understand the database's structure and infer types. +- `models.ts`: Exports types for all models, types, and enums defined in the schema. +- `input.ts`: Export types that you can use to type the arguments passed to the ORM client methods, such as `findMany`, `create`, etc. diff --git a/versioned_docs/version-3.x/orm/database-client.mdx b/versioned_docs/version-3.x/orm/database-client.md similarity index 78% rename from versioned_docs/version-3.x/orm/database-client.mdx rename to versioned_docs/version-3.x/orm/database-client.md index ed04be38..166bafb7 100644 --- a/versioned_docs/version-3.x/orm/database-client.mdx +++ b/versioned_docs/version-3.x/orm/database-client.md @@ -6,12 +6,17 @@ description: Creating a database client import TabItem from '@theme/TabItem'; import Tabs from '@theme/Tabs'; import PackageInstall from '../_components/PackageInstall'; +import ZenStackVsPrisma from '../_components/ZenStackVsPrisma'; # Database Client -The `zen generate` command compiles the ZModel schema into TypeScript code, which we can in turn use to initialize a type-safe database client. ZenStack uses Kysely to handle the low-level database operations, so the client is initialize with a Kysely dialect - an object that encapsulates database details. + +Unlike Prisma, ZenStack doesn't bundle any database driver. You're responsible for installing a compatible one. Also it doesn't read database connection string from the schema. Instead, you pass in the connection information when creating the client. + -The samples below only shows creating a client using SQLite (via [better-sqlite3](https://github.com/WiseLibs/better-sqlite3)) and PostgreSQL (via [node-postgres](https://github.com/brianc/node-postgres)), however you can also use any other Kysely dialects. +The `zen generate` command compiles the ZModel schema into TypeScript code, which we can in turn use to initialize a type-safe database client. ZenStack uses Kysely to handle the low-level database operations, so the client is initialize with a [Kysely dialect](https://kysely.dev/docs/dialects) - an object that encapsulates database details. + +The samples below only shows creating a client using SQLite (via [better-sqlite3](https://github.com/WiseLibs/better-sqlite3)) and PostgreSQL (via [node-postgres](https://github.com/brianc/node-postgres)), however you can also use any other Kysely dialects for these two types of databases. diff --git a/versioned_docs/version-3.x/orm/index.md b/versioned_docs/version-3.x/orm/index.md index dc4fe7aa..1096aad7 100644 --- a/versioned_docs/version-3.x/orm/index.md +++ b/versioned_docs/version-3.x/orm/index.md @@ -3,34 +3,17 @@ sidebar_position: 1 description: ZenStack ORM overview --- +import ZenStackVsPrisma from '../_components/ZenStackVsPrisma'; + # Overview -ZenStack ORM is a schema-first ORM for modern TypeScript applications. It learnt from the prior arts and aims to provide an awesome developer experience by combining the best ingredients into a cohesive package. +ZenStack ORM is a schema-first ORM for modern TypeScript applications. It learnt from the prior arts and strives to provide an awesome developer experience by combining the best ingredients into a cohesive package. ## Key Features -### [Prisma](https://prisma.io/orm)-Compatible Schema Language - -The schema language used by ZenStack (called ZModel) is a superset of Prisma schema language. Every valid Prisma schema can be used with ZenStack unchanged. In addition, ZModel added many important extensions to the language to improve the developer experience and enable advanced features. - -```zmodel -model User { - id String @id @default(cuid()) - email String @unique - posts Post[] -} - -model Post { - id String @id @default(cuid()) - title String @length(1, 256) - author User @relation(fields: [authorId], references: [id]) - authorId String -} -``` - -### [Prisma](https://prisma.io/orm)-Compatible Query API +### [Prisma](https://prisma.io/orm)-compatible query API -Although ZenStack has a completely different implementation (based on [Kysely](https://kysely.dev/)), it replicated Prisma ORM's query API so that you can use it pretty much as a drop-in replacement. Even if you're not a Prisma user, the query API is very intuitive and easy to learn. +ZenStack v3 is inspired by Prisma ORM but it has a completely different implementation (based on [Kysely](https://kysely.dev/)). On the surface, it replicated Prisma ORM's query API so that you can use it pretty much as a drop-in replacement. Even if you're not a Prisma user, the query API is very intuitive and easy to learn. ```ts await db.user.findMany({ @@ -46,7 +29,7 @@ await db.user.findMany({ }); ``` -### Low-Level Query-Builder Powered by [Kysely](https://kysely.dev/) +### Low-level query builder powered by [Kysely](https://kysely.dev/) ORM APIs are concise and pleasant, but they have their limitations. When you need extra power, you can fall back to the low-level query builder API powered by [Kysely](https://kysely.dev/) - limitless expressiveness, full type safety, and zero extra setup. @@ -58,17 +41,17 @@ await db.$qb .execute(); ``` -### Access Control +### Access control ZenStack ORM comes with a powerful built-in access control system. You can define access rules right inside the schema. The rules are enforced at runtime via query injection, so it doesn't rely on any database specific row-level security features. -```zmodel +```zmodel" model Post { - id String @id @default(cuid()) - title. String @length(1, 256) + id Int @id + title String @length(1, 256) published Boolean @default(false) - author User @relation(fields: [authorId], references: [id]) - authorId String + author User @relation(fields: [authorId], references: [id]) + authorId Int // no anonymous access @@deny('all', auth() == null) @@ -81,14 +64,14 @@ model Post { } ``` -### Polymorphic Models +### Polymorphic models Real-world applications often involves storing polymorphic data which is notoriously complex to model and query. ZenStack does the heavy-lifting for you so you can model an inheritance hierarchy with simple annotations, and query them with perfect type safety. -```zmodel -model Asset { - id String @id @default(cuid()) - title String @length(1, 256) +```zmodel title="zenstack/schema.zmodel" +model Content { + id Int @id + name String @length(1, 256) type String // the ORM uses the `type` field to determine to which concrete model @@ -96,12 +79,12 @@ model Asset { @@delegate(type) } -model Post extends Asset { +model Post extends Content { content String } ``` -```ts +```ts title="main.ts" const asset = await db.asset.findFirst(); if (asset.type === 'Post') { // asset's type is narrowed down to `Post` @@ -111,10 +94,24 @@ if (asset.type === 'Post') { } ``` -### Straightforward and Light-Weighted +### Straightforward and light-Weighted Compared to Prisma and previous versions of ZenStack, v3 is more straightforward and light-weighted. - No runtime dependency to Prisma, thus no overhead of Rust/WASM query engines. - No magic generating into `node_modules`. You fully control how the generated code is compiled and bundled. - Less code generation, more type inference. + +## Documentation Conventions + +### Sample playground + +Throughout the documentation we'll use [StackBlitz](https://stackblitz.com/) to provide interactive code samples. StackBlitz's [WebContainers](https://webcontainers.io/) is an awesome technology that allows you to run a Node.js environment inside the browser. The embedded samples use the [sql.js](https://github.com/sql-js/sql.js) (a WASM implementation of SQLite) for WebContainers compatibility, which is not suitable for production use. + +### If you already know Prisma + +Although ZenStack ORM has a Prisma-compatible query API, the documentation doesn't assume prior knowledge of using Prisma. However, readers already familiar with Prisma can quickly skim through most of the content and focus on the differences. The documentation uses the following callout to indicate major differences between ZenStack ORM and Prisma: + + +Explanation of some key differences between ZenStack and Prisma ORM. + diff --git a/versioned_docs/version-3.x/orm/quick-start.mdx b/versioned_docs/version-3.x/orm/quick-start.md similarity index 97% rename from versioned_docs/version-3.x/orm/quick-start.mdx rename to versioned_docs/version-3.x/orm/quick-start.md index 5e738583..aa91ee71 100644 --- a/versioned_docs/version-3.x/orm/quick-start.mdx +++ b/versioned_docs/version-3.x/orm/quick-start.md @@ -14,7 +14,7 @@ import PackageExec from '../_components/PackageExec.tsx'; All v3 packages are currently published under the "@next" tag. ::: -There are multiple ways to start using ZenStack. +There are several ways to start using ZenStack ORM. ## 1. Creating a project from scratch From a43c1d805cbe3cf6066cbc3af9a1dce1cb3b469e Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Tue, 5 Aug 2025 17:36:19 +0800 Subject: [PATCH 06/13] update --- versioned_docs/version-3.x/modeling/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/versioned_docs/version-3.x/modeling/index.md b/versioned_docs/version-3.x/modeling/index.md index 5cdd7f40..95211884 100644 --- a/versioned_docs/version-3.x/modeling/index.md +++ b/versioned_docs/version-3.x/modeling/index.md @@ -15,7 +15,7 @@ If you're already familiar with PSL, you'll find yourself at home with ZModel. H ZModel uses the explicit `import` syntax for composing multi-file schemas. -Don't worry if you've never used Prima before. This section will introduce all aspects of ZModel, so no prior knowledge is required. +Don't worry if you've never used Prisma before. This section will introduce all aspects of ZModel, so no prior knowledge is required. A very simple ZModel schema looks like this: From f64946712537e84dd94660a0b4b06324b2ca38a1 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Tue, 5 Aug 2025 22:11:29 +0800 Subject: [PATCH 07/13] update embed mechanism --- package.json | 1 + pnpm-lock.yaml | 8 +++ src/components/GithubCodeBlock.tsx | 40 ++++++++++++++ src/components/StackBlitzGithubEmbed.tsx | 61 ++++++++++++++++++---- versioned_docs/version-3.x/orm/api/find.md | 5 ++ 5 files changed, 105 insertions(+), 10 deletions(-) create mode 100644 src/components/GithubCodeBlock.tsx diff --git a/package.json b/package.json index 85733b49..d5b8de74 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "@stackblitz/sdk": "^1.11.0", "autoprefixer": "^10.4.13", "clsx": "^1.2.1", + "is-mobile": "^5.0.0", "postcss": "^8.4.21", "prism-react-renderer": "^2.3.1", "prism-svelte": "^0.5.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7b0326f0..ec59cc8f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -38,6 +38,9 @@ importers: clsx: specifier: ^1.2.1 version: 1.2.1 + is-mobile: + specifier: ^5.0.0 + version: 5.0.0 postcss: specifier: ^8.4.21 version: 8.4.21 @@ -3016,6 +3019,9 @@ packages: resolution: {integrity: sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==} engines: {node: '>=10'} + is-mobile@5.0.0: + resolution: {integrity: sha512-Tz/yndySvLAEXh+Uk8liFCxOwVH6YutuR74utvOcu7I9Di+DwM0mtdPVZNaVvvBUM2OXxne/NhOs1zAO7riusQ==} + is-npm@6.0.0: resolution: {integrity: sha512-JEjxbSmtPSt1c8XTkVrlujcXdKV1/tvuQ7GwKcAlyiVLeYFQ2VHat8xfrDJsIkhCdF/tZ7CiIR3sy141c6+gPQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -9099,6 +9105,8 @@ snapshots: global-dirs: 3.0.1 is-path-inside: 3.0.3 + is-mobile@5.0.0: {} + is-npm@6.0.0: {} is-number@7.0.0: {} diff --git a/src/components/GithubCodeBlock.tsx b/src/components/GithubCodeBlock.tsx new file mode 100644 index 00000000..5ab8770a --- /dev/null +++ b/src/components/GithubCodeBlock.tsx @@ -0,0 +1,40 @@ +import CodeBlock from '@theme/CodeBlock'; +import { useEffect, useState } from 'react'; + +interface GithubCodeBlockProps { + repoPath: string; + file: string; +} + +const GithubCodeBlock: React.FC = ({ repoPath, file }) => { + const [code, setCode] = useState('Loading...'); + + useEffect(() => { + (async function () { + const response = await fetch(`https://cdn.jsdelivr.net/gh/${repoPath}/${file}`); + if (!response.ok) { + setCode(`Unable to load "${repoPath}/${file}"`); + return; + } + const text = await response.text(); + setCode(text); + })(); + }, [repoPath, file]); + + const getLanguage = (file: string): string => { + if (file.endsWith('.ts')) { + return 'typescript'; + } else if (file.endsWith('.zmodel')) { + return 'zmodel'; + } else { + return 'plaintext'; + } + }; + return ( + + {code} + + ); +}; + +export default GithubCodeBlock; diff --git a/src/components/StackBlitzGithubEmbed.tsx b/src/components/StackBlitzGithubEmbed.tsx index d65fb762..7a3b1d47 100644 --- a/src/components/StackBlitzGithubEmbed.tsx +++ b/src/components/StackBlitzGithubEmbed.tsx @@ -1,10 +1,15 @@ -import React, { useEffect, useRef } from 'react'; import sdk from '@stackblitz/sdk'; +import TabItem from '@theme/TabItem'; +import Tabs from '@theme/Tabs'; +import mobile from 'is-mobile'; +import React, { useEffect, useRef } from 'react'; +import GithubCodeBlock from './GithubCodeBlock'; interface StackBlitzGithubEmbedProps { repoPath: string; height?: string; openFile?: string; + plainCodeFiles?: string[]; startScript?: string; clickToLoad?: boolean; } @@ -13,25 +18,61 @@ const StackBlitzGithubEmbed: React.FC = ({ repoPath, height = '600px', openFile = 'main.ts', + plainCodeFiles = undefined, clickToLoad = false, startScript, }) => { const containerRef = useRef(null); + const options = { + openFile, + height, + view: 'editor', + startScript, + clickToLoad, + } as const; + + if (!plainCodeFiles) { + plainCodeFiles = [openFile]; + } + useEffect(() => { if (containerRef.current) { - sdk.embedGithubProject(containerRef.current, repoPath, { - openFile, - height, - view: 'editor', - hideNavigation: true, - startScript, - clickToLoad, - }); + sdk.embedGithubProject(containerRef.current, repoPath, options); } }, [repoPath, height]); - return
; + const PlainCode = () => ( + <> + {plainCodeFiles.map((file) => ( + + ))} + + ); + + if (mobile()) { + return ; + } else { + return ( + + +
+
+ Click{' '} + sdk.openGithubProject(repoPath, options)}> + here + {' '} + to pop out if the embed doesn't load an interactive terminal. +
+
+
+ + + + + + ); + } }; export default StackBlitzGithubEmbed; diff --git a/versioned_docs/version-3.x/orm/api/find.md b/versioned_docs/version-3.x/orm/api/find.md index 78fdebf5..75e0204e 100644 --- a/versioned_docs/version-3.x/orm/api/find.md +++ b/versioned_docs/version-3.x/orm/api/find.md @@ -4,6 +4,7 @@ description: Find API --- import StackBlitzGithubEmbed from '@site/src/components/StackBlitzGithubEmbed'; +import GithubCodeBlock from '@site/src/components/GithubCodeBlock'; import SelectIncludeOmit from './_select-include-omit.md'; # Find @@ -30,6 +31,10 @@ The `find` series of APIs are used to query records from the database. It has th Similar to `findFirst`, but throws an error if no record is found. +Throughout this section all samples are based on the following ZModel schema: + + + ## Basic usage From 5cfdf6d04e9b9cb2379d9587fa0ff6c17b788440 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Tue, 5 Aug 2025 23:13:29 +0800 Subject: [PATCH 08/13] fix stackblitz embed --- src/components/StackBlitzGithubEmbed.tsx | 28 +++++++++++++--------- versioned_docs/version-3.x/orm/api/find.md | 8 +++---- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/src/components/StackBlitzGithubEmbed.tsx b/src/components/StackBlitzGithubEmbed.tsx index 7a3b1d47..b3ff03eb 100644 --- a/src/components/StackBlitzGithubEmbed.tsx +++ b/src/components/StackBlitzGithubEmbed.tsx @@ -38,9 +38,13 @@ const StackBlitzGithubEmbed: React.FC = ({ useEffect(() => { if (containerRef.current) { - sdk.embedGithubProject(containerRef.current, repoPath, options); + setTimeout(() => { + // docusaurus seems to recreate the tab after mounting, give it a + // chance to complete + sdk.embedGithubProject(containerRef.current, repoPath, options); + }, 0); } - }, [repoPath, height]); + }, [repoPath, height, containerRef]); const PlainCode = () => ( <> @@ -54,9 +58,9 @@ const StackBlitzGithubEmbed: React.FC = ({ return ; } else { return ( - - -
+ <> + +
-
-
- - - -
+ + + + + + +
+ ); } }; diff --git a/versioned_docs/version-3.x/orm/api/find.md b/versioned_docs/version-3.x/orm/api/find.md index 75e0204e..61636451 100644 --- a/versioned_docs/version-3.x/orm/api/find.md +++ b/versioned_docs/version-3.x/orm/api/find.md @@ -47,13 +47,13 @@ The API provides a very flexible set of filtering options. We've put it into a [ Use the `sort` field to control the sort field, direction, and null field placement. Sorting is not supported for `findUnique` and `findUniqueOrThrow`. - + ## Pagination You can use two strategies for pagination: offset-based or cursor-based. Pagination is not supported for `findUnique` and `findUniqueOrThrow`. - + ## Field selection @@ -61,10 +61,10 @@ You can use the following fields to control what fields are returned in the resu - + ## Finding distinct rows You can use the `distinct` field to find distinct rows based on specific fields. One row for each unique combination of the specified fields will be returned. The implementation uses SQL `DISTINCT ON` if it's supported by the dialect, otherwise falls back to in-memory deduplication. - + From 2c526ff61837490577baae04b7e0837bac343694 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Tue, 5 Aug 2025 23:15:24 +0800 Subject: [PATCH 09/13] make code tabs sync --- src/components/StackBlitzGithubEmbed.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/StackBlitzGithubEmbed.tsx b/src/components/StackBlitzGithubEmbed.tsx index b3ff03eb..de68def6 100644 --- a/src/components/StackBlitzGithubEmbed.tsx +++ b/src/components/StackBlitzGithubEmbed.tsx @@ -59,7 +59,7 @@ const StackBlitzGithubEmbed: React.FC = ({ } else { return ( <> - +
Click{' '} From a9d76ec5863b0e14a61636037c169fc516fdea34 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Sat, 9 Aug 2025 18:03:22 +0800 Subject: [PATCH 10/13] complete filter section --- versioned_docs/version-3.x/orm/api/filter.md | 81 ++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/versioned_docs/version-3.x/orm/api/filter.md b/versioned_docs/version-3.x/orm/api/filter.md index 01a84115..46fb196a 100644 --- a/versioned_docs/version-3.x/orm/api/filter.md +++ b/versioned_docs/version-3.x/orm/api/filter.md @@ -3,7 +3,88 @@ sidebar_position: 3 description: how to filter entities --- +import GithubCodeBlock from '@site/src/components/GithubCodeBlock'; import StackBlitzGithubEmbed from '@site/src/components/StackBlitzGithubEmbed'; # Filter +Filtering is an important topic because it's involved in many ORM operations, for example when you find records, selecting relations, and updating or deleting multiple records. + +Throughout this section all samples are based on the following ZModel schema: + + + +## Basic filters + +You can filter on scalar fields with values or operators as supported by the field type. The following filter operators are available. + +- `equals` `not`: all scalar fields. +- `in` `notIn`: all scalar fields +- `contains` `startsWith` `endsWith`: String fields +- `lt` `lte` `gt` `gte`: String, Int, BigInt, Float, Decimal, and Date fields + +A filter object can contain multiple field filters, and they are combined with `AND` semantic. You can also use the `AND`, `OR`, and `NOT` logical operators to combine filter objects to form a complex filter. + + + +## Relation filters + +Filters can be defined on conditions over relations. For one-to-one relations, you can filter on their fields directly. For one-to-many relations, use the "some", "every", or "none" operators to build a condition over a list of records. + + + +## List filters + +List fields allow extra filter operators to filter on the list content: + +- `has`: checks if the list contains a specific value. +- `hasEvery`: checks if the list contains all values in a given array. +- `hasSome`: checks if the list contains at least one value in a given array. +- `isEmpty`: checks if the list is empty. + +:::info +List type is only supported for PostgreSQL. +::: + +```zmodel +model Post { + ... + topics String[] +} +``` + +```ts +await db.post.findMany({ + where: { topics: { has: 'webdev' } } +}); + +await db.post.findMany({ + where: { topics: { hasSome: ['webdev', 'typescript'] } } +}); + +await db.post.findMany({ + where: { topics: { hasEvery: ['webdev', 'typescript'] } } +}); + +await db.post.findMany({ + where: { topics: { hasNone: ['webdev', 'typescript'] } } +}); + +await db.post.findMany({ + where: { topics: { isEmpty: true } } +}); +``` + +## Json filters + +:::info WORK IN PROGRESS +Filtering on Json fields is work in progress and will be available soon. +::: + +## Query builder filters + +ZenStack v3 is implemented on top of [Kysely](https://kysely.dev/), and it leverages Kysely's powerful query builder API to extend the filtering capabilities. You can use the `$expr` operator to define a boolean expression that can express almost everything that can be expressed in SQL. + +The `$expr` operator can be used together with other filter operators, so you can keep most of your filters simple and only reach to the query builder level for complicated components. + + From 09208d32538887d580cc6383033a0721e6b80b3a Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Sat, 9 Aug 2025 21:43:49 +0800 Subject: [PATCH 11/13] update docs --- versioned_docs/version-3.x/orm/api/update.md | 44 ++++++++++++++++++- versioned_docs/version-3.x/reference/api.md | 7 +++ .../version-3.x/reference/limitations.md | 40 ----------------- .../version-3.x/reference/runtime-api.md | 8 ---- versioned_docs/version-3.x/welcome.md | 6 +-- 5 files changed, 53 insertions(+), 52 deletions(-) create mode 100644 versioned_docs/version-3.x/reference/api.md delete mode 100644 versioned_docs/version-3.x/reference/limitations.md delete mode 100644 versioned_docs/version-3.x/reference/runtime-api.md diff --git a/versioned_docs/version-3.x/orm/api/update.md b/versioned_docs/version-3.x/orm/api/update.md index 383bb752..9adcce77 100644 --- a/versioned_docs/version-3.x/orm/api/update.md +++ b/versioned_docs/version-3.x/orm/api/update.md @@ -3,4 +3,46 @@ sidebar_position: 4 description: Update API --- -# Update \ No newline at end of file +import StackBlitzGithubEmbed from '@site/src/components/StackBlitzGithubEmbed'; + +# Update + +Update to records can be done with the following methods: + +- `update` - Update a single, unique record. +- `updateMany` - Update multiple records that match the query criteria. +- `updateManyAndReturn` - Similar to `updateMany`, but returns the updated records +- `upsert` - Update a single, unique record, or create it if it does not exist. + +## Updating scalar fields + + + +In additional to the standard way of updating fields, list fields support the following operators: + +- `push`: Append a value or a list of values to the end of the list. +- `set`: Replace the entire list with a new list (equivalent to setting the field directly). + +```ts +await db.post.update({ + where: { id: '1' }, + data: { + topics: { push: 'webdev'}, + }, +}); + +await db.post.update({ + where: { id: '1' }, + data: { + topics: { set: ['orm', 'typescript'] }, + }, +}); +``` + +## Manipulating relations + +THe `update` and `upsert` methods are very powerful in that they allow you to freely manipulate relations. You can create, connect, disconnect, update, and delete relations in a single operation. You can also reach deeply into indirect relations. + +`updateMany` and `updateManyAndReturn` only support updating scalar fields. + + diff --git a/versioned_docs/version-3.x/reference/api.md b/versioned_docs/version-3.x/reference/api.md new file mode 100644 index 00000000..52b981ba --- /dev/null +++ b/versioned_docs/version-3.x/reference/api.md @@ -0,0 +1,7 @@ +--- +description: API references +sidebar_position: 4 +sidebar_label: API +--- + +# API Reference diff --git a/versioned_docs/version-3.x/reference/limitations.md b/versioned_docs/version-3.x/reference/limitations.md deleted file mode 100644 index 96d02bc0..00000000 --- a/versioned_docs/version-3.x/reference/limitations.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -description: Current limitations -sidebar_position: 100 ---- - -# Limitations - -This section lists the current limitations of ZenStack. - -### Sequential operations transaction - -[Sequential operations transaction](https://www.prisma.io/docs/concepts/components/prisma-client/transactions#sequential-prisma-client-operations) is not supported by enhanced Prisma clients yet. - -As a workaround, use [interactive transactions](https://www.prisma.io/docs/concepts/components/prisma-client/transactions#interactive-transactions) instead. - -### Minimum transaction isolation level - -To ensure access policies are properly enforced, the database's transaction isolation level should be set to at least [Repeatable Read](https://en.wikipedia.org/wiki/Isolation_(database_systems)#Repeatable_reads). This can be done by changing the settings of the database, or providing the `isolationLevel` option when creating `PrismaClient`: - -```ts -const prisma = new PrismaClient({ - transactionOptions: { - isolationLevel: Prisma.TransactionIsolationLevel.RepeatableRead, - }, -}); -``` - -If you don't want to change the global settings, alternatively you can set the `transactionIsolationLevel` option when calling ZenStack's `enhance` API. All the transactions initiated internally by ZenStack will use the specified isolation level. - -```ts -const db = enhance(prisma, { user }, { transactionIsolationLevel: 'RepeatableRead' }); -``` - -### MongoDB is not supported - -Right now, the focus of this project is SQL databases, and there's no plan to support MongoDB in the near future. - -### Cloudflare D1 database is not supported - -Prisma doesn't support interactive transactions with D1 databases. ZenStack relies on the feature to enforce access policies in certain cases. diff --git a/versioned_docs/version-3.x/reference/runtime-api.md b/versioned_docs/version-3.x/reference/runtime-api.md deleted file mode 100644 index 2cc8e47c..00000000 --- a/versioned_docs/version-3.x/reference/runtime-api.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -description: Runtime API references -sidebar_position: 4 -sidebar_label: Runtime API ---- - -# Runtime API Reference - diff --git a/versioned_docs/version-3.x/welcome.md b/versioned_docs/version-3.x/welcome.md index da83a371..29665f23 100644 --- a/versioned_docs/version-3.x/welcome.md +++ b/versioned_docs/version-3.x/welcome.md @@ -13,13 +13,13 @@ ZenStack is built with the belief that most applications should use the data mod Inside the package you'll find: -- #### An intuitive schema language +- #### Intuitive schema language That helps you model data, relation, access control, and more, in one place. [πŸ”—](./modeling/) -- #### A powerful ORM +- #### Powerful ORM With awesomely-typed API, built-in access control, and unmatched flexibility. [πŸ”—](./orm/) -- #### A Query-as-a-Service layer +- #### Query-as-a-Service That provides a full-fledged data API without the need to code it up. [πŸ”—](./service/) - #### Utilities From b61a2e028e155483a8a5c4cde983dd2d328a3099 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Sun, 10 Aug 2025 07:44:48 +0800 Subject: [PATCH 12/13] progress on delete --- versioned_docs/version-3.x/orm/api/delete.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/versioned_docs/version-3.x/orm/api/delete.md b/versioned_docs/version-3.x/orm/api/delete.md index 358e28e8..e16e4dda 100644 --- a/versioned_docs/version-3.x/orm/api/delete.md +++ b/versioned_docs/version-3.x/orm/api/delete.md @@ -3,4 +3,18 @@ sidebar_position: 5 description: Delete API --- -# Delete \ No newline at end of file +import StackBlitzGithubEmbed from '@site/src/components/StackBlitzGithubEmbed'; + +# Delete + +Deleting records can be done with the following methods: + +- `delete` - Delete a single, unique record. +- `deleteMany` - Delete multiple records that match the query criteria. +- `deleteManyAndReturn` - Similar to `deleteMany`, but returns the deleted records + +You can also delete records as part of an `update` operation from a relation. See [Manipulating relations](./update.md#manipulating-relations) for details. + +## Samples + + From 7531a1808953147ab5151734a80ab2f3fe485d2b Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Sun, 10 Aug 2025 21:20:04 +0800 Subject: [PATCH 13/13] more progress on ORM API --- .../version-3.x/orm/api/_category_.yml | 7 -- .../version-3.x/orm/api/aggregate.md | 18 ++- versioned_docs/version-3.x/orm/api/count.md | 10 +- versioned_docs/version-3.x/orm/api/create.md | 21 ---- versioned_docs/version-3.x/orm/api/filter.md | 5 - versioned_docs/version-3.x/orm/api/find.md | 5 - .../version-3.x/orm/api/group-by.md | 6 + versioned_docs/version-3.x/orm/api/index.md | 117 ++++++++++++++++++ .../version-3.x/orm/api/transaction.md | 2 +- 9 files changed, 150 insertions(+), 41 deletions(-) delete mode 100644 versioned_docs/version-3.x/orm/api/_category_.yml create mode 100644 versioned_docs/version-3.x/orm/api/group-by.md create mode 100644 versioned_docs/version-3.x/orm/api/index.md diff --git a/versioned_docs/version-3.x/orm/api/_category_.yml b/versioned_docs/version-3.x/orm/api/_category_.yml deleted file mode 100644 index 2a384497..00000000 --- a/versioned_docs/version-3.x/orm/api/_category_.yml +++ /dev/null @@ -1,7 +0,0 @@ -position: 6 -label: ORM API -collapsible: true -collapsed: true -link: - type: generated-index - title: ORM API diff --git a/versioned_docs/version-3.x/orm/api/aggregate.md b/versioned_docs/version-3.x/orm/api/aggregate.md index f7f12077..27b95a31 100644 --- a/versioned_docs/version-3.x/orm/api/aggregate.md +++ b/versioned_docs/version-3.x/orm/api/aggregate.md @@ -3,4 +3,20 @@ sidebar_position: 7 description: Aggregate API --- -# Aggregate \ No newline at end of file +import StackBlitzGithubEmbed from '@site/src/components/StackBlitzGithubEmbed'; + +# Aggregate + +The `aggregate` method allows you to conduct multiple aggregations on a set of records with one operation. The supported aggregations are: + +- `_count` - equivalent to the [Count API](./count.md). +- `_sum` - sum of a numeric field. +- `_avg` - average of a numeric field. +- `_min` - minimum value of a field. +- `_max` - maximum value of a field. + +You can also use `where`, `orderBy`, `skip`, and `take` to control what records are included in the aggregation. + +## Samples + + diff --git a/versioned_docs/version-3.x/orm/api/count.md b/versioned_docs/version-3.x/orm/api/count.md index 7402362e..d337e632 100644 --- a/versioned_docs/version-3.x/orm/api/count.md +++ b/versioned_docs/version-3.x/orm/api/count.md @@ -3,4 +3,12 @@ sidebar_position: 6 description: Count API --- -# Count \ No newline at end of file +import StackBlitzGithubEmbed from '@site/src/components/StackBlitzGithubEmbed'; + +# Count + +You can use the `count` method to count the number of records that match a query. It also allows to count non-null field values with an `select` clause. + + + +To count relations, please use a `find` API with the special `_count` field as demonstrated in the [Find](./find.md#field-selection) section. diff --git a/versioned_docs/version-3.x/orm/api/create.md b/versioned_docs/version-3.x/orm/api/create.md index d6b8326e..74c8f447 100644 --- a/versioned_docs/version-3.x/orm/api/create.md +++ b/versioned_docs/version-3.x/orm/api/create.md @@ -20,24 +20,3 @@ The `create` series of APIs are used to create new records in the database. It h -## Input - -The create APIs accept an input object with the following fields: - -- `data` - - The data used to create the record. It must contain all non-optional fields of the model. It can also include nested object for creating or connecting relation entities. For `create` the data field must be an object. For `createMany` and `createManyAndReturn`, it can be either an object or an array of objects. - - This field is required. - -- `skipDuplicates` - - A boolean flag that indicates whether to skip records that would violate unique constraints. Only applicable to `createMany` and `createManyAndReturn`. - - This field is optional and defaults to `false`. - -You can also control what fields are turned in the result using the `select`, `include`, and `omit` fields. Read more about field selection in the [next section](./find.md#field-selection). - -## Output - -The output shape of `create` and `createManyAndReturn` API is determined by the `select`, `include`, and `omit` fields in the input object. The output of `createMany` is `{ count: number }`, giving the number of records created. diff --git a/versioned_docs/version-3.x/orm/api/filter.md b/versioned_docs/version-3.x/orm/api/filter.md index 46fb196a..47fc7e6c 100644 --- a/versioned_docs/version-3.x/orm/api/filter.md +++ b/versioned_docs/version-3.x/orm/api/filter.md @@ -3,17 +3,12 @@ sidebar_position: 3 description: how to filter entities --- -import GithubCodeBlock from '@site/src/components/GithubCodeBlock'; import StackBlitzGithubEmbed from '@site/src/components/StackBlitzGithubEmbed'; # Filter Filtering is an important topic because it's involved in many ORM operations, for example when you find records, selecting relations, and updating or deleting multiple records. -Throughout this section all samples are based on the following ZModel schema: - - - ## Basic filters You can filter on scalar fields with values or operators as supported by the field type. The following filter operators are available. diff --git a/versioned_docs/version-3.x/orm/api/find.md b/versioned_docs/version-3.x/orm/api/find.md index 61636451..ce718521 100644 --- a/versioned_docs/version-3.x/orm/api/find.md +++ b/versioned_docs/version-3.x/orm/api/find.md @@ -4,7 +4,6 @@ description: Find API --- import StackBlitzGithubEmbed from '@site/src/components/StackBlitzGithubEmbed'; -import GithubCodeBlock from '@site/src/components/GithubCodeBlock'; import SelectIncludeOmit from './_select-include-omit.md'; # Find @@ -31,10 +30,6 @@ The `find` series of APIs are used to query records from the database. It has th Similar to `findFirst`, but throws an error if no record is found. -Throughout this section all samples are based on the following ZModel schema: - - - ## Basic usage diff --git a/versioned_docs/version-3.x/orm/api/group-by.md b/versioned_docs/version-3.x/orm/api/group-by.md new file mode 100644 index 00000000..7ed21962 --- /dev/null +++ b/versioned_docs/version-3.x/orm/api/group-by.md @@ -0,0 +1,6 @@ +--- +sidebar_position: 8 +description: GroupBy API +--- + +# GroupBy \ No newline at end of file diff --git a/versioned_docs/version-3.x/orm/api/index.md b/versioned_docs/version-3.x/orm/api/index.md new file mode 100644 index 00000000..88eb3a66 --- /dev/null +++ b/versioned_docs/version-3.x/orm/api/index.md @@ -0,0 +1,117 @@ +--- +sidebar_position: 1 +sidebar_label: Query API +title: Query API Overview +--- + +ZenStack ORM's query API provides a powerful and high-level way to interact with your database with awesome type safety. The API is a superset of [Prisma ORM's query API](https://www.prisma.io/docs/orm/prisma-client/queries), so if you are familiar with Prisma, you will feel right at home. If not, it's intuitive and easy to learn. + +The API is organized into several categories covered by the following sections. The API methods share many common input and output patterns, and we'll cover them in this overview section. + +## Common Input Fields + +- `where` + + When an operation can involve filtering records, a `where` clause is used to specify the condition. E.g., `findUnique`, `updateMany`, `delete`, etc. `where` clause also exists in nested payload for filtering relations. + + ```ts + await db.post.findMany({ where: { published: true } }); + ``` + + The [Filter](./filter) section describes the filtering capabilities in detail. + +- `select`, `include`, `omit` + + When an operation returns record(s), you can use these clauses to control the fields and relations returned in the result. The `select` clause is used to specify the fields/relations to return, `omit` to exclude, and `include` to include relations (together with all regular fields). + + When selecting relations, you can nest these clauses to further control fields and relations returned in the nested relations. + + ```ts + // results will include `title` field and `author` relation + await db.post.findMany({ + select: { title: true, author: true }, + }); + + // results will include all fields except `content`, plus `author` relation + await db.post.findMany({ + omit: { content: true }, include: { author: true } + }); + ``` + +- `orderBy`, `take`, `skip` + + When an operation returns multiple records, you can use these clauses to control the sort order, number of records returned, and the offset for pagination. + + ```ts + // results will be sorted by `createdAt` in descending order, and return + // 10 records starting from the 5th record + await db.post.findMany({ orderBy: { createdAt: 'desc' }, skip: 5, take: 10 }); + ``` + +- `data` + + When an operation involves creating or updating records, a `data` clause is used to specify the data to be used. It can include nested objects for manipulating relations. See the [Create](./create) and [Update](./update) sections for details. + + ```ts + // Create a new post and connect it to an author + await db.post.create({ + data: { title: 'New Post', author: { connect: { id: 1 } } } + }); + ``` + +## Output Types + +The output types of the API methods generally fall into three categories: + +1. When the operation returns record(s) + + The output type is "contextual" to the input's shape, meaning that when you specify `select`, `include`, or `omit` clauses, the output type will reflect that. + + ```ts + // result will be `Promise<{ title: string; author: { name: string } }[]>` + await db.post.findMany({ + select: { title: true, author: { select: { name: true } } } + }); + ``` + +2. When the operation returns a batch result + + Some operations only returns a batch result `{ count: number }`, indicating the number of records affected. These include `createMany`, `updateMany`, and `deleteMany`. + +3. Aggregation + + Aggregation operations' output type is contextual to the input's shape as well. See [Count](./count) and [Aggregate](./aggregate) sections for details. + + +## Sample Schema + +Throughout the following sections, we will use the following ZModel schema as the basis for our examples: + +```zmodel title="zenstack/schema.zmodel" +// This is a sample model to get you started. + +datasource db { + provider = 'sqlite' +} + +/// User model +model User { + id Int @id @default(autoincrement()) + email String @unique + posts Post[] +} + +/// Post model +model Post { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + title String + content String? + slug String? @unique + published Boolean @default(false) + viewCount Int @default(0) + author User? @relation(fields: [authorId], references: [id]) + authorId Int? +} +``` diff --git a/versioned_docs/version-3.x/orm/api/transaction.md b/versioned_docs/version-3.x/orm/api/transaction.md index 79bbcb5f..66bf2c9f 100644 --- a/versioned_docs/version-3.x/orm/api/transaction.md +++ b/versioned_docs/version-3.x/orm/api/transaction.md @@ -1,5 +1,5 @@ --- -sidebar_position: 8 +sidebar_position: 9 description: Transaction API ---