diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..5b3bd36c --- /dev/null +++ b/.gitmodules @@ -0,0 +1,18 @@ +[submodule "code-repos/zenstackhq/v3-doc-orm"] + path = code-repos/zenstackhq/v3-doc-orm + url = https://github.com/zenstackhq/v3-doc-orm.git +[submodule "code-repos/zenstackhq/v3-doc-orm-computed-fields"] + path = code-repos/zenstackhq/v3-doc-orm-computed-fields + url = https://github.com/zenstackhq/v3-doc-orm-computed-fields.git +[submodule "code-repos/zenstackhq/v3-doc-orm-computed-polymorphism"] + path = code-repos/zenstackhq/v3-doc-orm-computed-polymorphism + url = https://github.com/zenstackhq/v3-doc-orm-polymorphism.git +[submodule "code-repos/zenstackhq/v3-doc-orm-typed-json"] + path = code-repos/zenstackhq/v3-doc-orm-typed-json + url = https://github.com/zenstackhq/v3-doc-orm-typed-json.git +[submodule "code-repos/zenstackhq/v3-doc-quick-start"] + path = code-repos/zenstackhq/v3-doc-quick-start + url = https://github.com/zenstackhq/v3-doc-quick-start.git +[submodule "code-repos/zenstackhq/v3-doc-orm-polymorphism"] + path = code-repos/zenstackhq/v3-doc-orm-polymorphism + url = https://github.com/zenstackhq/v3-doc-orm-polymorphism.git diff --git a/code-repos/zenstackhq/v3-doc-orm b/code-repos/zenstackhq/v3-doc-orm new file mode 160000 index 00000000..bafb4695 --- /dev/null +++ b/code-repos/zenstackhq/v3-doc-orm @@ -0,0 +1 @@ +Subproject commit bafb4695a8c32d875ce2262a8729a853c4e8bdcb diff --git a/code-repos/zenstackhq/v3-doc-orm-computed-fields b/code-repos/zenstackhq/v3-doc-orm-computed-fields new file mode 160000 index 00000000..12f895b6 --- /dev/null +++ b/code-repos/zenstackhq/v3-doc-orm-computed-fields @@ -0,0 +1 @@ +Subproject commit 12f895b61ee3f80fb6a3534c37e020acc3cbb035 diff --git a/code-repos/zenstackhq/v3-doc-orm-polymorphism b/code-repos/zenstackhq/v3-doc-orm-polymorphism new file mode 160000 index 00000000..74231c1b --- /dev/null +++ b/code-repos/zenstackhq/v3-doc-orm-polymorphism @@ -0,0 +1 @@ +Subproject commit 74231c1b06e7d51968e4966bbd316722a492bb1c diff --git a/code-repos/zenstackhq/v3-doc-orm-typed-json b/code-repos/zenstackhq/v3-doc-orm-typed-json new file mode 160000 index 00000000..710d2cab --- /dev/null +++ b/code-repos/zenstackhq/v3-doc-orm-typed-json @@ -0,0 +1 @@ +Subproject commit 710d2cabe95d425ed6cc01ac1ece503ef989cc65 diff --git a/code-repos/zenstackhq/v3-doc-quick-start b/code-repos/zenstackhq/v3-doc-quick-start new file mode 160000 index 00000000..0c878665 --- /dev/null +++ b/code-repos/zenstackhq/v3-doc-quick-start @@ -0,0 +1 @@ +Subproject commit 0c878665fce7c377c11b87c3b5b2c8d186da57f8 diff --git a/docusaurus.config.js b/docusaurus.config.js index 0dd2e501..c68999ad 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 Beta', + banner: 'none', + }, }, }, blog: false, @@ -63,6 +67,12 @@ const config = { themeConfig: /** @type {import('@docusaurus/preset-classic').ThemeConfig} */ { + announcementBar: { + id: 'v3_beta', + content: + 'ZenStack v3 Beta is released πŸš€. The new version has replaced Prisma with a brand new query engine. Check it out', + isCloseable: true, + }, colorMode: { defaultMode: 'light', respectPrefersColorScheme: false, @@ -99,6 +109,11 @@ const config = { position: 'left', label: 'Handbook', }, + { + to: 'v3', + position: 'left', + label: 'V3 Beta πŸš€', + }, { to: '/blog', label: 'Blog', position: 'left' }, { href: 'https://discord.gg/Ykhr738dUe', @@ -193,6 +208,23 @@ const config = { }, ], }, + { + title: 'FlatIcon Credits', + items: [ + { + label: 'Endure', + href: 'https://www.flaticon.com/free-icons/endure', + }, + { + label: 'Diagram by Kiranshastry', + href: 'https://www.flaticon.com/free-icons/diagram', + }, + { + href: 'https://www.flaticon.com/free-icons/database', + label: 'Database by kerismaker', + }, + ], + }, ], copyright: `Copyright Β© ${new Date().getFullYear()} ZenStack, Inc.`, }, diff --git a/package.json b/package.json index fa70489e..e881f2b9 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,8 @@ "serve": "docusaurus serve", "write-translations": "docusaurus write-translations", "write-heading-ids": "docusaurus write-heading-ids", - "typecheck": "tsc" + "typecheck": "tsc", + "pull-submodules": "git submodule foreach git pull origin main" }, "dependencies": { "@algolia/client-search": "^4.22.1", @@ -22,12 +23,15 @@ "@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", + "is-mobile": "^5.0.0", "postcss": "^8.4.21", "prism-react-renderer": "^2.3.1", "prism-svelte": "^0.5.0", "prismjs": "^1.29.0", + "raw-loader": "^4.0.2", "react": "^18.2.0", "react-dom": "^18.2.0", "react-icons": "^5.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 78043273..5631d504 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,12 +29,18 @@ 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) 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 @@ -47,6 +53,9 @@ importers: prismjs: specifier: ^1.29.0 version: 1.29.0 + raw-loader: + specifier: ^4.0.2 + version: 4.0.2(webpack@5.90.1) react: specifier: ^18.2.0 version: 18.2.0 @@ -1113,6 +1122,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'} @@ -3010,6 +3022,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} @@ -4250,6 +4265,12 @@ packages: resolution: {integrity: sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==} engines: {node: '>= 0.8'} + raw-loader@4.0.2: + resolution: {integrity: sha512-ZnScIV3ag9A4wPX/ZayxL/jZH+euYb6FcUinPcgiQW0+UBtEv0O6Q3lGd3cqJ+GHH+rksEv3Pj99oxJ3u3VIKA==} + engines: {node: '>= 10.13.0'} + peerDependencies: + webpack: ^4.0.0 || ^5.0.0 + rc@1.2.8: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true @@ -6914,6 +6935,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 @@ -9091,6 +9114,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: {} @@ -10771,6 +10796,12 @@ snapshots: iconv-lite: 0.4.24 unpipe: 1.0.0 + raw-loader@4.0.2(webpack@5.90.1): + dependencies: + loader-utils: 2.0.4 + schema-utils: 3.3.0 + webpack: 5.90.1 + rc@1.2.8: dependencies: deep-extend: 0.6.0 diff --git a/src/components/GithubCodeBlock.tsx b/src/components/GithubCodeBlock.tsx new file mode 100644 index 00000000..4f3692b7 --- /dev/null +++ b/src/components/GithubCodeBlock.tsx @@ -0,0 +1,27 @@ +import CodeBlock from '@theme/CodeBlock'; + +interface GithubCodeBlockProps { + repoPath: string; + file: string; +} + +const GithubCodeBlock: React.FC = ({ repoPath, file }) => { + const code = require(`!!raw-loader!@site/code-repos/${repoPath}/${file}`).default; + + 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/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/StackBlitzGithub.tsx b/src/components/StackBlitzGithub.tsx new file mode 100644 index 00000000..59a87ab2 --- /dev/null +++ b/src/components/StackBlitzGithub.tsx @@ -0,0 +1,44 @@ +import sdk from '@stackblitz/sdk'; +import React from 'react'; +import GithubCodeBlock from './GithubCodeBlock'; + +interface StackBlitzGithubProps { + repoPath: string; + openFile?: string; + codeFiles?: string[]; + startScript?: string; +} + +const StackBlitzGithub: React.FC = ({ + repoPath, + openFile = 'main.ts', + codeFiles: plainCodeFiles = undefined, + startScript, +}) => { + const options = { + openFile, + view: 'editor', + startScript, + } as const; + + if (!plainCodeFiles) { + plainCodeFiles = [openFile]; + } + + return ( + <> +
+ Click{' '} + sdk.openGithubProject(repoPath, options)}> + here + {' '} + to open an interactive playground. +
+ {plainCodeFiles.map((file) => ( + + ))} + + ); +}; + +export default StackBlitzGithub; diff --git a/src/components/ValueProposition.tsx b/src/components/ValueProposition.tsx index c58a8aef..2593baee 100644 --- a/src/components/ValueProposition.tsx +++ b/src/components/ValueProposition.tsx @@ -33,8 +33,8 @@ const FeatureList: FeatureItem[] = [ img: '/img/ai-friendly.png', description: ( <> - Schema-first reduces code complexity, helping AI understand better with fewer hallucinations. - Schema serves as a single source of truth for AI integration. + Schema-first reduces code complexity, helping AI understand better with fewer hallucinations. Schema + serves as a single source of truth for AI integration. ), }, @@ -47,7 +47,7 @@ function Proposition({ title, img, description }: FeatureItem) { {title}
-

{title}

+

{title}

{description}

diff --git a/src/css/custom.css b/src/css/custom.css index ae6d471d..3559aeea 100644 --- a/src/css/custom.css +++ b/src/css/custom.css @@ -68,3 +68,20 @@ right: 0; pointer-events: mouse; } + +@media (max-width: 1200px) { + .navbar__items--right, + .navbar__items--left { + display: none; + } + + .navbar__toggle { + display: inherit; + } +} + +@media (min-width: 1201px) { + .navbar__toggle { + display: none; + } +} diff --git a/src/lib/prism-zmodel.js b/src/lib/prism-zmodel.js index a5198df6..d26ea9cb 100644 --- a/src/lib/prism-zmodel.js +++ b/src/lib/prism-zmodel.js @@ -1,19 +1,10 @@ Prism.languages.zmodel = Prism.languages.extend('clike', { - keyword: /\b(?:datasource|enum|generator|model|type|abstract|import|extends|attribute|view|plugin|proc)\b/, + function: /@@?[A-Za-z_]\w*/, + keyword: /\b(?:datasource|enum|generator|model|type|abstract|import|extends|attribute|view|plugin|proc|with)\b/, + entity: /\b(?:Int|String|Boolean|DateTime|Float|Decimal|BigInt|Bytes|Json|Unsupported)\b/, 'type-class-name': /(\b()\s+)[\w.\\]+/, }); -Prism.languages.javascript['class-name'][0].pattern = - /(\b(?:model|datasource|enum|generator|type|plugin|abstract)\s+)[\w.\\]+/; - -Prism.languages.insertBefore('zmodel', 'function', { - annotation: { - pattern: /(^|[^.])@+\w+/, - lookbehind: true, - alias: 'punctuation', - }, -}); - Prism.languages.insertBefore('zmodel', 'punctuation', { 'type-args': /\b(?:references|fields|onDelete|onUpdate):/, }); diff --git a/src/pages/v3/_components/AICoding.tsx b/src/pages/v3/_components/AICoding.tsx new file mode 100644 index 00000000..aad73f88 --- /dev/null +++ b/src/pages/v3/_components/AICoding.tsx @@ -0,0 +1,69 @@ +type FeatureItem = { + title: string; + img: string; + description: JSX.Element; +}; + +const FeatureList: FeatureItem[] = [ + { + title: 'Single Source of Truth', + img: '/img/access-control.png', + description: ( + <> + When LLMs see a self-contained, non-ambiguous, and well-defined application model, their inference works + more efficiently and effectively. + + ), + }, + { + title: 'Concise Query API', + img: '/img/auto-api.png', + description: ( + <> + Concise and expressive, while leveraging existing knowledge of Prisma and Kysely, the query API makes it + easy for LLMs to generate high-quality query code. + + ), + }, + { + title: 'Slim Code Base', + img: '/img/ai-friendly.png', + description: ( + <> + By deriving artifacts from the schema instead of implementing them, ZenStack helps you maintain a slim + code base that is easier for AI to digest. + + ), + }, +]; + +function Proposition({ title, img, description }: FeatureItem) { + return ( +
+
+ {title} +
+
+

{title}

+

{description}

+
+
+ ); +} + +export default function AICoding(): JSX.Element { + return ( +
+
+

+ Perfect Match for AI-Assisted Programming +

+
+
+ {FeatureList.map((props, idx) => ( + + ))} +
+
+ ); +} diff --git a/src/pages/v3/_components/Notes.tsx b/src/pages/v3/_components/Notes.tsx new file mode 100644 index 00000000..6bcfbb83 --- /dev/null +++ b/src/pages/v3/_components/Notes.tsx @@ -0,0 +1,22 @@ +import Link from '@docusaurus/Link'; + +export default function Notes(): JSX.Element { + return ( +
+
+

+ Notes to V2 Users +

+
+
+
+ ZenStack V3 has made the bold decision to remove Prisma as a runtime dependency and implement its + own ORM infrastructure on top of Kysely. Albeit the cost of such a + big refactor, we believe this is the right move to gain the flexibility needed to achieve the + project's vision. Please read this blog post for more + thoughts behind the changes. +
+
+
+ ); +} diff --git a/src/pages/v3/_components/ORM.tsx b/src/pages/v3/_components/ORM.tsx new file mode 100644 index 00000000..c67b8d23 --- /dev/null +++ b/src/pages/v3/_components/ORM.tsx @@ -0,0 +1,67 @@ +import CodeBlock from '@theme/CodeBlock'; + +export default function ORM(): JSX.Element { + return ( +
+
+

+
Flexible and Awesomely Typed ORM
+

+
+
+ + {`import { schema } from './zenstack'; +import { + ZenStackClient, + AccessControlPlugin +} from '@zenstackhq/runtime'; + +const db = new ZenStackClient(schema, { ... }) + // install access control plugin to enforce policies + .$use(new AccessControlPlugin()) + // set current user context + .$setAuth(...); + +// high-level query API +const userWithPosts = await db.user.findUnique({ + where: { id: userId }, + include: { posts: true } +}); + +// low-level SQL query builder API +const userPostJoin = await db + .$qb + .selectFrom('User') + .innerJoin('Post', 'Post.authorId', 'User.id') + .select(['User.id', 'User.email', 'Post.title']) + .where('User.id', '=', userId) + .execute(); +`} + +
+

+ An ORM is derived from the schema that gives you +

+
    +
  • πŸ”‹ High-level ORM query API
  • +
  • πŸ”‹ Low-level SQL query builder API
  • +
  • + πŸ”‹ Access control enforcement coming soon +
  • +
  • + πŸ”‹ Runtime data validation coming soon +
  • +
  • πŸ”‹ Computed fields and custom procedures
  • +
  • πŸ”‹ Plugin system for tapping into various lifecycle events
  • +
+ + ZenStack's ORM is built on top of the awesome Kysely SQL query + builder. Its query API is compatible with that of{' '} + Prisma Client, so migrating an + existing Prisma project will require minimal code changes. + +
+
+
+ ); +} diff --git a/src/pages/v3/_components/Schema.tsx b/src/pages/v3/_components/Schema.tsx new file mode 100644 index 00000000..04fc4a59 --- /dev/null +++ b/src/pages/v3/_components/Schema.tsx @@ -0,0 +1,55 @@ +import CodeBlock from '@theme/CodeBlock'; + +export default function SchemaLanguage(): JSX.Element { + return ( +
+
+

+
Intuitive and Expressive Data Modeling
+

+
+
+
+

The modeling language allows you to

+
    +
  • βœ… Define data models and relations
  • +
  • βœ… Define access control policies
  • +
  • βœ… Express data validation rules
  • +
  • βœ… Model polymorphic inheritance
  • +
  • βœ… Add custom attributes and functions to introduce custom semantics
  • +
  • βœ… Implement custom code generators
  • +
+ + The schema language is a superset of{' '} + Prisma Schema Language. + Migrating a Prisma schema is as simple as file renaming. + +
+ + {`model User { + id Int @id + email String @unique @email // constraint and validation + role String + posts Post[] // relation to another model + postCount Int @computed // computed field + + // access control rules colocated with data + @@allow('all', auth().id == id) + @@allow('create, read', true) +} + +model Post { + id Int @id + title String @length(1, 255) + published Boolean @default(false) + author User @relation(fields: [authorId], references: [id]) + authorId Int // relation foreign key + + @@allow('read', published) + @@allow('all', auth().id == authorId || auth().role == 'ADMIN') +}`} + +
+
+ ); +} diff --git a/src/pages/v3/_components/Service.tsx b/src/pages/v3/_components/Service.tsx new file mode 100644 index 00000000..ad14b2d3 --- /dev/null +++ b/src/pages/v3/_components/Service.tsx @@ -0,0 +1,109 @@ +import CodeBlock from '@theme/CodeBlock'; +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +export default function Service(): JSX.Element { + return ( +
+
+

+
Automatic HTTP Query Service
{' '} + + coming soon + +

+
+
+
+

+ Thanks to the ORM's built-in access control, you get an HTTP query service for free +

+
    +
  • πŸš€ Fully mirrors the ORM API
  • +
  • πŸš€ Seamlessly integrates with popular frameworks
  • +
  • πŸš€ Works with any authentication solution
  • +
  • + πŸš€ Type-safe client SDK powered by TanStack Query +
  • +
  • πŸš€ Highly customizable
  • +
+
+

+ Since the ORM is protected with access control, ZenStack can directly map it to an HTTP + service. ZenStack provides out-of-the-box integrations with popular frameworks including + Next.js, Nuxt, Express, etc. +

+

+ Client hooks based on TanStack Query can also be + derived from the schema, allowing you to make type-safe queries to the service without + writing a single line of code. +

+
+
+
+ + + + {`import { NextRequestHandler } from '@zenstackhq/server/next'; +import { db } from './db'; // ZenStackClient instance +import { getSessionUser } from './auth'; + +// callback to provide a per-request ORM client +async function getClient() { + // call a framework-specific helper to get session user + const authUser = await getSessionUser(); + + // return a new ORM client configured with the user, + // the user info will be used to enforce access control + return db.$setAuth(authUser); +} + +// Create a request handler for all requests to this route +// All CRUD requests are forwarded to the underlying ORM +const handler = NextRequestHandler({ getClient }); + +export { + handler as GET, + handler as PUT, + handler as POST, + handler as PATCH, + handler as DELETE, +}; + `} + + + + + {`import { schema } from './zenstack'; +import { useQueryHooks } from '@zenstackhq/query/react'; + +export function UserPosts({ userId }: { userId: number }) { + // use auto-generated hook to query user with posts + const { user: userHooks } = useQueryHooks(schema); + const { data, isLoading } = userHooks.useFindUnique({ + where: { id: userId }, + include: { posts: true } + }); + + if (isLoading) return
Loading...
; + + return ( +
+

{data?.email}'s Posts

+
    + {data?.posts.map((post) => ( +
  • {post.title}
  • + ))} +
+
+ ); +} + `} +
+
+
+
+
+
+ ); +} diff --git a/src/pages/v3/_components/ValueProps.tsx b/src/pages/v3/_components/ValueProps.tsx new file mode 100644 index 00000000..555ecdad --- /dev/null +++ b/src/pages/v3/_components/ValueProps.tsx @@ -0,0 +1,56 @@ +type FeatureItem = { + title: string; + img: string; + description: JSX.Element; +}; + +const FeatureList: FeatureItem[] = [ + { + title: 'Coherent Schema', + img: '/img/diagram.png', + description: ( + <> + Simple schema language to capture the most important aspects of your application in one place: data and + security. + + ), + }, + { + title: 'Powerful ORM', + img: '/img/search.png', + description: ( + <>One-of-a-kind ORM that combines type-safety, query flexibility, and access control in one package. + ), + }, + { + title: 'Limitless Utility', + img: '/img/versatility.png', + description: ( + <>Deriving crucial artifacts that streamline development from backend APIs to frontend components. + ), + }, +]; + +function Proposition({ title, img, description }: FeatureItem) { + return ( +
+
+ {title} +
+
+

{title}

+

{description}

+
+
+ ); +} + +export default function ValueProps(): JSX.Element { + return ( +
+ {FeatureList.map((props, idx) => ( + + ))} +
+ ); +} diff --git a/src/pages/v3/index.module.css b/src/pages/v3/index.module.css new file mode 100644 index 00000000..b3332046 --- /dev/null +++ b/src/pages/v3/index.module.css @@ -0,0 +1,24 @@ +/** + * CSS files with the .module.css suffix will be treated as CSS modules + * and scoped locally. + */ + +.heroBanner { + padding: 8rem 2rem !important; + text-align: center !important; + position: relative !important; + overflow: hidden !important; +} + +@media screen and (max-width: 996px) { + .heroBanner { + padding-top: 4rem !important; + padding-bottom: 4rem !important; + } +} + +.buttons { + display: flex; + align-items: center; + justify-content: center; +} diff --git a/src/pages/v3/index.tsx b/src/pages/v3/index.tsx new file mode 100644 index 00000000..f6e10a41 --- /dev/null +++ b/src/pages/v3/index.tsx @@ -0,0 +1,101 @@ +import Link from '@docusaurus/Link'; +import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; +import Layout from '@theme/Layout'; +import clsx from 'clsx'; +import React from 'react'; +import AICoding from './_components/AICoding'; +import ORM from './_components/ORM'; +import SchemaLanguage from './_components/Schema'; +import Service from './_components/Service'; +import ValueProps from './_components/ValueProps'; +import styles from './index.module.css'; + +const description = `ZenStack v3 is a powerful data layer for modern TypeScript applications. It provides an intuitive data modeling language, a fully type-safe ORM, built-in access control and data validation, and automatic data query service that seamlessly integrates with popular frameworks like Next.js and Nuxt.`; + +function Header() { + return ( +
+
+
+
+

+ + Modern Data Layer for TypeScript Applications + +

+

+ Intuitive data modeling, type-safe ORM, built-in access control, automatic query services, + and more. +

+
+ + Check V3 Docs β†’ + + + Open Playground + +
+
+
+
+
+ ); +} + +function Section({ children, className }: { children: React.ReactNode; className?: string }) { + return ( +
+
{children}
+
+ ); +} + +export default function Home(): JSX.Element { + const { siteConfig } = useDocusaurusContext(); + return ( + +
+
+
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ + Start Building Now β†’ + +
+
+ + {/*
+ +
*/} +
+ + ); +} diff --git a/static/img/diagram.png b/static/img/diagram.png new file mode 100644 index 00000000..850806c0 Binary files /dev/null and b/static/img/diagram.png differ diff --git a/static/img/search.png b/static/img/search.png new file mode 100644 index 00000000..12163b4d Binary files /dev/null and b/static/img/search.png differ diff --git a/static/img/versatility.png b/static/img/versatility.png new file mode 100644 index 00000000..253d37bf Binary files /dev/null and b/static/img/versatility.png differ 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/ZModelVsPSL.tsx b/versioned_docs/version-3.x/_components/ZModelVsPSL.tsx new file mode 100644 index 00000000..46b7a50d --- /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/_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 new file mode 100644 index 00000000..b02e72d3 --- /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 + posts Post[] + } + + model Post { + id String @id @default(cuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + title String + 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..0a788729 --- /dev/null +++ b/versioned_docs/version-3.x/faq.md @@ -0,0 +1,14 @@ +--- +description: ZenStack FAQ. + +slug: /faq +sidebar_label: FAQ +sidebar_position: 100 +--- + +# πŸ™‹πŸ» FAQ + +## What databases are supported? + +Currently only SQLite (with [better-sqlite3](https://github.com/WiseLibs/better-sqlite3) or [sql.js](https://github.com/sql-js/sql.js) driver) and PostgreSQL (with [node-postgres](https://github.com/brianc/node-postgres) driver) 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/index.md b/versioned_docs/version-3.x/index.md new file mode 100644 index 00000000..ab6c42a4 --- /dev/null +++ b/versioned_docs/version-3.x/index.md @@ -0,0 +1,31 @@ +--- +description: Welcome to ZenStack +sidebar_label: Welcome +sidebar_position: 1 +--- + +# Welcome + +Welcome to ZenStack - the data layer for modern TypeScript applications. + +ZenStack is built with the belief that most applications should use the data model as their 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: + +- **Intuitive schema language** + + That helps you model data, relations, access control, and more, in one place. [πŸ”—](./modeling/) + +- **Powerful ORM** + + With awesomely-typed API, built-in access control, and unmatched flexibility. [πŸ”—](./orm/) + +- **Query-as-a-Service** + + 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. [πŸ”—](./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 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. [This blog post](https://zenstack.dev/blog/next-chapter-1) contains more background about the thoughts behind the v3 refactor. \ No newline at end of file diff --git a/versioned_docs/version-3.x/migrate-prisma.md b/versioned_docs/version-3.x/migrate-prisma.md new file mode 100644 index 00000000..1f91aa26 --- /dev/null +++ b/versioned_docs/version-3.x/migrate-prisma.md @@ -0,0 +1,273 @@ +--- +description: How to migrate from a Prisma project to ZenStack v3 +sidebar_position: 10 +--- + +import PackageInstall from './_components/PackageInstall'; +import TabItem from '@theme/TabItem'; +import Tabs from '@theme/Tabs'; + +# Migrating From Prisma + +## Overview + +This guide will help you migrate an existing Prisma project to ZenStack v3. The process is made straightforward by the following design decisions: + +1. The ZModel schema language is a superset of Prisma Schema Language. +2. The ORM provides a Prisma-compatible API. +3. The migration engine is built on top of Prisma Migrate. + +In the following sections, we'll cover various aspects of the migration process where care needs to be taken. + +:::warning +ZenStack v3 currently only supports PostgreSQL and SQLite databases. +::: + +## Migration Steps + +### 1. Project dependencies + +ZenStack v3 doesn't depend on Prisma at runtime. Its CLI has a peer dependency on the `prisma` package for migration related commands. The most straightforward way is to follow these steps: + +- Remove `prisma` and `@prisma/client` from your project dependencies. +- Install ZenStack packages + + + +- Install a database driver + + ZenStack doesn't bundle database drivers. Install the appropriate driver for your database: + + + + + + + + + + + + + + + +You don't need to explicitly install `prisma` package because the `@zenstackhq/cli` has a peer dependency on it. + +### 2. Migrate your schema + +If you have a single `schema.prisma` file, you can move and rename it to `zenstack/schema.zmodel`. No change should be necessary because every valid Prisma schema is also a valid ZModel schema. The only optional change you can consider making is to remove the Prisma client generator block since it doesn't have any effect in ZenStack: + +```zmodel +generator client { + provider = "prisma-client-js" +} +``` + +If you use Prisma's [multi-schema feature](https://www.prisma.io/docs/orm/prisma-schema/overview/location#multi-file-prisma-schema), you'll need to explicitly use the `import` statement to merge related schema files into a whole graph. See [Multi-file Schema](./modeling/multi-file.md) for details. + +### 3. Update generation command + +In your `package.json` scripts, replace the `prisma generate` command with `zen generate`. + +```json +{ + "scripts": { + "generate": "zen generate" + } +} +``` + +### 4. Update database client instantiation + +Replace `new PrismaClient()` with `new ZenStackClient(schema, ...)` where `schema` is imported from the TypeScript code generated by the `zen generate` command. + + + + + +```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, + }), + }), +}); +``` + + + + +```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('./db.sqlite'), + }), +}); +``` + + + + +Since `ZenStackClient` has a PrismaClient-compatible API, you should not need to change your existing code that uses the database client. + +### 5. Update type references + +Prisma generates many TypeScript types that you may have used in your code, such as `User`, `UserCreateArgs`, etc. ZenStack replicates some of the most useful types in its generated code. + +For top-level model types like `User`, you can import from the `models.ts` file. For other detailed input types like `UserCreateArgs`, import them from the `input.ts` file. + +### 6. Update migration command + +In your `package.json` scripts, replace the Prisma `db` and `migrate` commands with ZenStack equivalents: + +```json +{ + "scripts": { + "db:push": "zen db push", + "migrate:dev": "zen migrate dev", + "migrate:deploy": "zen migrate deploy" + } +} +``` + +## Other Considerations + +Now, let's check the areas that are less straightforward to migrate. + +### Prisma custom generators + +ZenStack has its own CLI plugin system and doesn't support Prisma custom generators. However, you can continue running them with the following two steps: + +1. Use the [@core/prisma](./reference/plugins/prisma.md) plugin to generate a Prisma schema from ZModel. + + ```zmodel + plugin prisma { + provider = '@core/prisma' + output = './schema.prisma' + } + ``` + +2. Run a `prisma generate` command after `zen generate` with the prisma schema as input. + + ```json + { + "scripts": { + "generate": "zen generate && prisma generate --schema=zenstack/schema.prisma" + } + } + ``` + +### Prisma client extensions + +ZenStack has its own [runtime plugin mechanism](./orm/plugins/) and doesn't plan to be compatible with Prisma client extensions. However, there are easy migration paths for the two most popular types of Prisma client extensions. + +**1. Query extension** + +[Query extension](https://www.prisma.io/docs/orm/prisma-client/client-extensions/query) allows you to intercept ORM query calls. + +Suppose you have an extension like: + +```ts +const extPrisma = prisma.$extends({ + query: { + user: { + async findMany({ model, operation, args, query }) { + // take incoming `where` and set `age` + args.where = { ...args.where, age: { gt: 18 } } + return query(args) + }, + }, + }, +}); +``` + +You can replace it with an equivalent ZenStack plugin: + +```ts +const extDb = db.$use({ + id: 'my-plugin', + onQuery: { + user: { + async findMany({ model, operation, args, proceed }) { + // take incoming `where` and set `age` + args.where = { ...args.where, age: { gt: 18 } } + return proceed(args) + }, + }, + }, +}); +``` + +You can also use the special `$allModels` and `$allOperations` keys to apply the plugin to all models and operations, like when using Prisma client extensions. + +**2. Result extension** + +[Result extension](https://www.prisma.io/docs/orm/prisma-client/client-extensions/result) allows you to add custom fields to query results. ZenStack provides a mechanism that achieves the same goal but a lot more powerful. + +Suppose you have a result extension like: + +```ts +const extPrisma = prisma.$extends({ + result: { + user: { + fullName: { + // the dependencies + needs: { firstName: true, lastName: true }, + compute(user) { + // the computation logic + return `${user.firstName} ${user.lastName}` + }, + }, + }, + }, +}); +``` + +You can replace it with a ZenStack computed field, which involves changes in ZModel and database client instantiation. + +```zmodel title="zenstack/schema.zmodel" +model User { + ... + firstName String + lastName String + fullName String @computed +} +``` + +```ts +export const db = new ZenStackClient(schema, { + ..., + computedFields: { + User: { + // SQL: CONCAT(firstName, ' ', lastName) + fullName: (eb) => eb.fn('concat', ['firstName', eb.val(' '), 'lastName']) + }, + }, +}); +``` + +A key difference is that ZenStack's computed fields are evaluated on the database side, which much more efficient and flexible than client-side computation. Read more in the [Computed Fields](./orm/computed-fields.md) documentation. + +## Feature Gap + +Here's a list of Prisma features that are not supported in ZenStack v3: + +| Feature | Planned | Notes | +|---------|-------------| --- | +| [Client Extensions](https://www.prisma.io/docs/orm/prisma-client/client-extensions) | No | Replaced with ZenStack runtime plugins | +| [JSON Filters](https://www.prisma.io/docs/orm/reference/prisma-client-reference#json-filters) | Yes | | +| [Full-Text Search](https://www.prisma.io/docs/orm/prisma-client/queries/full-text-search) | Yes | | +| [Comparing Columns](https://www.prisma.io/docs/orm/reference/prisma-client-reference#compare-columns-in-the-same-table) | Yes | | +| [Postgres Multi-Schema](https://www.prisma.io/docs/orm/prisma-schema/data-model/multi-schema) | Yes | | diff --git a/versioned_docs/version-3.x/modeling/_category_.yml b/versioned_docs/version-3.x/modeling/_category_.yml new file mode 100644 index 00000000..9f8d1691 --- /dev/null +++ b/versioned_docs/version-3.x/modeling/_category_.yml @@ -0,0 +1,4 @@ +position: 3 +label: Data Modeling +collapsible: true +collapsed: true 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..e7f0c6f7 --- /dev/null +++ b/versioned_docs/version-3.x/modeling/attribute.md @@ -0,0 +1,49 @@ +--- +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, such as adding unique constraints and mapping names. 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/attribute#predefined-attributes) for a complete list. You can also define your own attributes for specific purposes. Attributes are defined with a list of typed parameters. Parameters can be named (default) or positional. Positional parameters can be passed with or without an explicit name. Parameters can also be optional. + +Here's an example of 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 +} +``` + +Read the [ZModel Language Reference](../reference/zmodel/attribute#syntax) for more details on how to define and use attributes. 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..06890e4a --- /dev/null +++ b/versioned_docs/version-3.x/modeling/conclusion.md @@ -0,0 +1,16 @@ +--- +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: + +- [Using ORM to query the database](../orm/) +- [Exposing a data API with Query-as-a-Service](../service/) +- [Deriving other useful utilities](../service/) + +Let's continue our journey with ZenStack! 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..7314678b --- /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. + +```zmodel +type Address { + street String + city String + country String + zip Int +} + +type UserProfile { + gender String + address Address? +} +``` + +There are two ways to use custom types: + +- [Mixin](./mixin.md) +- [Strongly typed JSON fields](./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..bbdeb7d1 --- /dev/null +++ b/versioned_docs/version-3.x/modeling/datasource.md @@ -0,0 +1,42 @@ +--- +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](../orm/migration.md), 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. + +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/) part. + 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/index.md b/versioned_docs/version-3.x/modeling/index.md new file mode 100644 index 00000000..6aace027 --- /dev/null +++ b/versioned_docs/version-3.x/modeling/index.md @@ -0,0 +1,52 @@ +--- +sidebar_position: 1 +description: ZModel overview +--- + +import ZModelVsPSL from '../_components/ZModelVsPSL'; + +# 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 challenging to convince people to learn a new one. We therefore decided to design ZModel as a superset of the [Prisma Schema Language (PSL)](https://www.prisma.io/docs/orm/prisma-schema), one of the best data modeling languages available. + +If you're already familiar with PSL, you'll find yourself at home with ZModel. However, we recommend that you skim through this section to learn about the essential extensions we made to PSL. Please pay attention to callouts like the following: + + +ZModel allows both single quotes and double quotes for string literals. + + +Don't worry if you've never used Prisma before. This part of documentation will introduce all aspects of ZModel, so no prior knowledge is required. + +A simplest 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 Int +} +``` + + +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 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/mixin.md b/versioned_docs/version-3.x/modeling/mixin.md new file mode 100644 index 00000000..51451c67 --- /dev/null +++ b/versioned_docs/version-3.x/modeling/mixin.md @@ -0,0 +1,58 @@ +--- +sidebar_position: 8 +description: Reusing common fields with mixins +--- + +import ZModelVsPSL from '../_components/ZModelVsPSL'; + +# Mixin + + +Mixin is a ZModel concept and doesn'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 +} + +model Post { + id String @id + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + title String +} +``` + +***After:*** + +```zmodel +type BaseFieldsMixin { + id String @id + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model User with BaseFieldsMixin { + email String @unique +} + +model Post with BaseFieldsMixin { + title String +} +``` + +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..f51c3f8f --- /dev/null +++ b/versioned_docs/version-3.x/modeling/model.md @@ -0,0 +1,188 @@ +--- +sidebar_position: 3 +description: Models in ZModel +--- + +# Model + +The `model` construct is the core of ZModel. It defines the structure of your data and relations. A model represents a domain entity and is backed by 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. In 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. It lets 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 them to type JSON fields. This is covered in more detail in the [Custom Type](./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 as 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, an enum value, 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 a 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](../orm/migration.md) maps the schema to DDL. You can find a complete list of native type attributes in the [ZModel Language Reference](../reference/zmodel/attribute#native-type-mapping-attributes). + +## 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` and `@@map` attribute. The ORM respects the mapping when generating queries, and the migration engine uses it to generate the DDL. + +```zmodel +model User { + id Int @id @map('_id') + @@map('users') +} +``` diff --git a/versioned_docs/version-3.x/modeling/multi-file.md b/versioned_docs/version-3.x/modeling/multi-file.md new file mode 100644 index 00000000..75aaeb26 --- /dev/null +++ b/versioned_docs/version-3.x/modeling/multi-file.md @@ -0,0 +1,42 @@ +--- +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 new file mode 100644 index 00000000..0166b44a --- /dev/null +++ b/versioned_docs/version-3.x/modeling/plugin.md @@ -0,0 +1,51 @@ +--- +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' +} +``` + +:::info +In fact, the `zen generate` command is entirely implemented with plugins. The ZModel -> TypeScript generation is supported by the built-in `@core/typescript` plugin which runs automatically. You can explicitly declare it if you wish: + +```zmodel +plugin typescript { + provider = '@core/typescript' + output = '../generated' +} +``` + +Please refer to the [Plugin References](../category/plugins) for the full list of built-in plugins. +::: + +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 JavaScript module, or an NPM package name. +3. Plugin-specific configuration options, such as `output` in this case. Options are solely interpreted by the plugin implementation. + +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 new file mode 100644 index 00000000..9e36f0e2 --- /dev/null +++ b/versioned_docs/version-3.x/modeling/polymorphism.md @@ -0,0 +1,122 @@ +--- +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, 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: + +- 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 an 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's implementation follows the MTI pattern. +::: + +## 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 DateTime + ownerId Int FK + type String + } + User ||--o{ Content: owns + Post { + id Int PK,FK + content String + } + Post ||--|| Content: delegates + Image { + id Int PK,FK + data Bytes + } + Image ||--|| Content: delegates + Video { + id Int PK,FK + url String + } + Video ||--|| Content: delegates +``` + +There are two special things about a 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 levels 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/polymorphism.md) part. diff --git a/versioned_docs/version-3.x/modeling/relation.md b/versioned_docs/version-3.x/modeling/relation.md new file mode 100644 index 00000000..5b391ac0 --- /dev/null +++ b/versioned_docs/version-3.x/modeling/relation.md @@ -0,0 +1,286 @@ +--- +sidebar_position: 5 +description: Relations in ZModel +--- + +# Relation + +Relations are a fundamental concept in relational databases. They connect models into a graph and allow you to query interconnected data efficiently. In ZModel, relations are modeled using the `@relation` attribute. In 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 holds the foreign key the "owner" side. + +## One-to-one relation + +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` parameter 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') + privateProfile Profile? @relation('UserPrivateProfile') +} + +model Profile { + id Int @id + user User @relation('UserProfile', fields: [userId], references: [id]) + userId Int @unique + userPrivate User @relation('UserPrivateProfile', fields: [userPrivateId], references: [id]) + userPrivateId 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 relation + +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 list of the other side's model type. + +## Many-to-many relation + +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 modeling 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. Albeit the complexity, an explicit join table gives you the flexibility, e.g., to have extra fields. + +## Self relation + +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 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 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('Mentor') + mentees Mentorship[] @relation('Mentee') +} + +model Mentorship { + mentorId Int + menteeId Int + mentor Employee @relation('Mentor', fields: [mentorId], references: [id]) + mentee Employee @relation('Mentee', fields: [menteeId], references: [id]) + + @@id([mentorId, menteeId]) +} +``` + +## Referential Actions + +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. + +- `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 User @relation(fields: [userId], references: [id], onUpdate: Cascade, onDelete: Cascade) + userId String @unique +} +``` diff --git a/versioned_docs/version-3.x/modeling/typed-json.md b/versioned_docs/version-3.x/modeling/typed-json.md new file mode 100644 index 00000000..70694b7f --- /dev/null +++ b/versioned_docs/version-3.x/modeling/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 serves no purpose today other than to indicate (for readability) that the field is a JSON field, not a relation to another model. We may extend it in the future for fine-tuning the JSON field's behavior. +::: + +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 this topic in the [ORM part](../orm/typed-json.md). 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..8e6b7172 --- /dev/null +++ b/versioned_docs/version-3.x/orm/_category_.yml @@ -0,0 +1,4 @@ +position: 4 +label: ORM +collapsible: true +collapsed: true diff --git a/versioned_docs/version-3.x/orm/access-control/index.md b/versioned_docs/version-3.x/orm/access-control/index.md new file mode 100644 index 00000000..0b584167 --- /dev/null +++ b/versioned_docs/version-3.x/orm/access-control/index.md @@ -0,0 +1,7 @@ +--- +sidebar_position: 6 +--- + +# Access Control 🚧 + +Coming soon 🚧 \ No newline at end of file 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 new file mode 100644 index 00000000..7f0856d1 --- /dev/null +++ b/versioned_docs/version-3.x/orm/api/aggregate.md @@ -0,0 +1,22 @@ +--- +sidebar_position: 7 +description: Aggregate API +--- + +import StackBlitzGithub from '@site/src/components/StackBlitzGithub'; + +# 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 new file mode 100644 index 00000000..a463523e --- /dev/null +++ b/versioned_docs/version-3.x/orm/api/count.md @@ -0,0 +1,14 @@ +--- +sidebar_position: 6 +description: Count API +--- + +import StackBlitzGithub from '@site/src/components/StackBlitzGithub'; + +# 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 new file mode 100644 index 00000000..3d1f0371 --- /dev/null +++ b/versioned_docs/version-3.x/orm/api/create.md @@ -0,0 +1,22 @@ +--- +sidebar_position: 2 +description: Create API +--- + +import StackBlitzGithub from '@site/src/components/StackBlitzGithub'; + +# 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 + + + 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..b5e39961 --- /dev/null +++ b/versioned_docs/version-3.x/orm/api/delete.md @@ -0,0 +1,20 @@ +--- +sidebar_position: 5 +description: Delete API +--- + +import StackBlitzGithub from '@site/src/components/StackBlitzGithub'; + +# 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 + + 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..00699bab --- /dev/null +++ b/versioned_docs/version-3.x/orm/api/filter.md @@ -0,0 +1,86 @@ +--- +sidebar_position: 3 +description: how to filter entities +--- + +import ZenStackVsPrisma from '../../_components/ZenStackVsPrisma'; +import StackBlitzGithub from '@site/src/components/StackBlitzGithub'; + +# 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. + +## 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. + + + +## 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: { isEmpty: true } } +}); +``` + +## Json filters + +:::info WORK IN PROGRESS +Filtering on Json fields is work in progress and will be available soon. +::: + +## 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. + + + +## Query builder filters + + +The ability to mix SQL query builder into ORM filters is a major improvement over Prisma. + + +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. + + 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..52a8b00b --- /dev/null +++ b/versioned_docs/version-3.x/orm/api/find.md @@ -0,0 +1,68 @@ +--- +sidebar_position: 2 +description: Find API +--- + +import StackBlitzGithub from '@site/src/components/StackBlitzGithub'; +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 `orderBy` 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 relies on SQL `DISTINCT ON`, so it's not available for SQLite provider. + +```ts +// returns one Post for each unique authorId +await db.post.findMany({ distinct: ['authorId'] }); +``` 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..fb6d8f32 --- /dev/null +++ b/versioned_docs/version-3.x/orm/api/group-by.md @@ -0,0 +1,43 @@ +--- +sidebar_position: 8 +description: GroupBy API +--- + +import StackBlitzGithub from '@site/src/components/StackBlitzGithub'; + +# GroupBy + +The `groupBy` method allows you to group records by one or more fields and perform aggregations on the grouped records - very useful for generating summary statistics or reports. + +Use the `by` field to specify the field(s) to group by, and the following aggregation operators to perform on the grouped records: + +- `_count` - count the number of records in each group. +- `_sum` - sum of a numeric field in each group. +- `_avg` - average of a numeric field in each group. +- `_min` - minimum value of a field in each group. +- `_max` - maximum value of a field in each group. + +You can also use `where`, `orderBy`, `skip`, and `take` to control what records are included in the aggregation. + +The `having` field can be used to filter the aggregated results. Two types of filters can be used in the `having` clause: + +- A regular field that's used in the `by` clause, e.g.: + + ```ts + await db.post.groupBy({ by: 'published', having: { published: true } }); + ``` + +- An aggregation, e.g.: + + In this case, the fields of aggregation doesn't need to be in the `by` clause. + + ```ts + await db.post.groupBy({ + by: 'authorId', + having: { viewCount: { _sum: { gt: 100 }} } + }); + ``` + +## Samples + + 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..6bd18a7d --- /dev/null +++ b/versioned_docs/version-3.x/orm/api/index.md @@ -0,0 +1,117 @@ +--- +sidebar_position: 4 +sidebar_label: Query API +title: Query API +--- + +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 new file mode 100644 index 00000000..6b97d1e5 --- /dev/null +++ b/versioned_docs/version-3.x/orm/api/transaction.md @@ -0,0 +1,43 @@ +--- +sidebar_position: 9 +description: Transaction API +--- + +import StackBlitzGithub from '@site/src/components/StackBlitzGithub'; + +# Transaction + +You can use the `$transaction` method to run multiple operations in a transaction. There are two overloads of this method: + +## Sequential Transaction + +This overload takes an array of promises as input. The operations are executed sequentially in the order they are provided. The operations are independent of each other, because there's no way to access the result of a previous operation and use it to influence the later operations. + +```ts +// Note that the `db.user.create` and `db.post.create` calls are not awaited. They +// are passed to the `$transaction` method to execute. +const [user, post] = await db.$transaction([ + db.user.create({ data: { name: 'Alice' } }), + db.user.create({ data: { name: 'Bob' } }), +]); +``` + +The result of each operation is returned in the same order as the input. + +## Interactive Transaction + +This overload takes an async callback function as input. The callback receives a transaction client that can be used to perform database operations within the transaction. + +Interactive transactions allows you to write imperative code that can access the results of previous operations and use them to influence later operations. Albeit it's flexibility, you should make the transaction callback run as fast as possible so as to reduce the performance impact of the transaction on the database. + +```ts +const [user, post] = await db.$transaction(async (tx) => { + const user = await tx.user.create({ data: { name: 'Alice' } }); + const post = await tx.post.create({ data: { title: 'Hello World', authorId: user.id } }); + return [user, post]; +}); +``` + +## Samples + + 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..c0b7e87d --- /dev/null +++ b/versioned_docs/version-3.x/orm/api/update.md @@ -0,0 +1,48 @@ +--- +sidebar_position: 4 +description: Update API +--- + +import StackBlitzGithub from '@site/src/components/StackBlitzGithub'; + +# 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/orm/cli.md b/versioned_docs/version-3.x/orm/cli.md new file mode 100644 index 00000000..a6944b0b --- /dev/null +++ b/versioned_docs/version-3.x/orm/cli.md @@ -0,0 +1,18 @@ +--- +sidebar_position: 2 +description: Using the CLI +--- + +import StackBlitzGithub from '@site/src/components/StackBlitzGithub'; + +# 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 (they are equivalent). + +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 generates several TypeScript files from the ZModel schema that support both development-time typing and runtime access to the schema. For more details of the generated code, please refer to the [@core/typescript plugin](../reference/plugins/typescript.md) documentation. diff --git a/versioned_docs/version-3.x/orm/client.md b/versioned_docs/version-3.x/orm/client.md new file mode 100644 index 00000000..c190ebb9 --- /dev/null +++ b/versioned_docs/version-3.x/orm/client.md @@ -0,0 +1,70 @@ +--- +sidebar_position: 3 +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 + + +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 `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 show 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. + + + + + + + +```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/computed-fields.md b/versioned_docs/version-3.x/orm/computed-fields.md new file mode 100644 index 00000000..561555e5 --- /dev/null +++ b/versioned_docs/version-3.x/orm/computed-fields.md @@ -0,0 +1,72 @@ +--- +sidebar_position: 10 +description: Computed fields in ZModel +--- + +import ZenStackVsPrisma from '../_components/ZenStackVsPrisma'; +import StackBlitzGithub from '@site/src/components/StackBlitzGithub'; + +# Computed Fields + + +Prisma client extensions allow you to define computed fields. ZenStack's approach is very different in two aspects: +1. Computed fields are evaluated on the database side, not in the client. +2. Computed fields are defined in the schema and can be used in most places where regular fields are used. + + +Computed fields are "virtual" fields that do not physically exist in the database. They are computed on the fly, but other than that, they behave like regular fields. They are returned as part of the query results, can be used for filtering, sorting, etc., and can be used to define access policies. + +## Defining Computed Fields + +Defining a computed field involves two steps. First, add the field in the ZModel schema to a model and annotate it with the `@computed` attribute. + +```zmodel +model User { + ... + postCount Int @computed +} +``` + +Then, when creating a `ZenStackClient`, provide the implementation of the field using the Kysely query builder. + +```ts +const db = new ZenStackClient(schema, { + ... + computedFields: { + User: { + // equivalent SQL: + // `(SELECT COUNT(*) AS "count" FROM "Post" WHERE "Post"."authorId" = "User"."id")` + postCount: (eb) => + eb.selectFrom('Post') + .whereRef('Post.authorId', '=', 'id') + // the `as('count')` part is required because every Kysely selection + // needs to have a name + .select(({fn}) => fn.countAll().as('count')), + }, + }, +}); +``` + +The computed field callback is also passed with a second `context` argument containing other useful information related to the current query. For example, you can use the `currentModel` property to refer to the containing model and use it to qualify field names in case of conflicts. + +```ts +import { sql } from 'kysely'; + +const db = new ZenStackClient(schema, { + ... + computedFields: { + User: { + postCount: (eb, { currentModel }) => + eb.selectFrom('Post') + // the `currentModel` context property gives you a name that you can + // use to address the containing model (here `User`) at runtime + .whereRef('Post.authorId', '=', sql.ref(currentModel, 'id')) + .select(({fn}) => fn.countAll().as('count')), + }, + }, +}); +``` + +## Samples + + diff --git a/versioned_docs/version-3.x/orm/errors.md b/versioned_docs/version-3.x/orm/errors.md new file mode 100644 index 00000000..2c8db21a --- /dev/null +++ b/versioned_docs/version-3.x/orm/errors.md @@ -0,0 +1,20 @@ +--- +sidebar_position: 16 +description: ORM Errors +--- + +# Errors + +The ORM uses the following error classes from `@zenstackhq/runtime` to represent different types of failures: + +## `ValidationError` + +This error is thrown when the argument passed to the ORM methods is invalid, e.g., missing required fields, or containing unknown fields. The `cause` property is set to the original error thrown during validation. + +## `NotFoundError` + +This error is thrown when a requested record is not found in the database, e.g., when calling `findUniqueOrThrow`, `update`, etc. + +## `QueryError` + +This error is used to encapsulate all other errors thrown from the underlying database driver. The `cause` property is set to the original error thrown. diff --git a/versioned_docs/version-3.x/orm/index.md b/versioned_docs/version-3.x/orm/index.md new file mode 100644 index 00000000..b8a0f92f --- /dev/null +++ b/versioned_docs/version-3.x/orm/index.md @@ -0,0 +1,122 @@ +--- +sidebar_position: 1 +description: ZenStack ORM overview +--- + +import ZenStackVsPrisma from '../_components/ZenStackVsPrisma'; + +# ORM Overview + +ZenStack ORM is a schema-first ORM for modern TypeScript applications. It learnt from the prior arts and strives to provide an excellent developer experience and incredible flexibility by combining the best ingredients into a cohesive package. + +## Key Features + +### [Prisma](https://prisma.io/orm)-compatible query API + +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({ + 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 Int @id + title String + published Boolean @default(false) + author User @relation(fields: [authorId], references: [id]) + authorId Int + + // 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 Content { + id Int @id + name String + type String + + // the ORM uses the `type` field to determine to which concrete model + // a query should be delegated + @@delegate(type) +} + +model Post extends Content { + content String +} + +model Video extends Content { + url String +} +``` + +```ts title="main.ts" +const content = await db.content.findFirstOrThrow(); +if (content.type === 'Post') { + // content's type is narrowed down to `Post` + console.log(content.content); +} else { + // other asset type +} +``` + +### Straightforward, light-weighted, flexible + +Compared to Prisma and previous versions of ZenStack, v3 is more straightforward, light-weighted, and flexible. + +- 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. +- A plugin system that allows you to extend ZenStack at the schema, CLI, and runtime levels. + +## Documentation Conventions + +### Sample playground + +Throughout the documentation we'll use [StackBlitz](https://stackblitz.com/) to provide interactive samples alongside with static code snippets. 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. Feel free to make changes and try things out in the playground. + +### 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/inferred-types.md b/versioned_docs/version-3.x/orm/inferred-types.md new file mode 100644 index 00000000..0b347ecc --- /dev/null +++ b/versioned_docs/version-3.x/orm/inferred-types.md @@ -0,0 +1,26 @@ +--- +sidebar_position: 15 +description: TypeScript types derived from the ZModel schema +--- + +import StackBlitzGithub from '@site/src/components/StackBlitzGithub'; + +# Schema-Inferred Types + +Most of the time, you don't need to explicitly type the input and output of the ORM methods, thanks to TypeScript's powerful inference capabilities. However, when you do have the need, you can rely on the following utilities to type things: + +- `zenstack/models` + + The `zen generate` command generates a `models` module that exports types for all models, types, and enums. The model types include all scalar fields (including computed ones). + +- `zenstack/input` + + The `zen generate` command generates an `input` module that exports types for input arguments of the ORM methods, such as `UserCreateArgs`, `PostUpsertArgs`, etc. You can use them to type intermediary variables that are later passed to the ORM methods. + +- `ModelResult` + + The `ModelResult` generic type from `@zenstackhq/runtime` allows you to infer the exact model type given field selection and relation inclusion information. + +## Samples + + diff --git a/versioned_docs/version-3.x/orm/logging.md b/versioned_docs/version-3.x/orm/logging.md new file mode 100644 index 00000000..357ecad4 --- /dev/null +++ b/versioned_docs/version-3.x/orm/logging.md @@ -0,0 +1,44 @@ +--- +sidebar_position: 14 +description: Setup logging +--- + +# Logging + +Logging can be enabled by passing a `log` option when creating a `ZenStackClient` instance. The log option can be a list of log levels, causing the client to log messages at those levels to the console: + +```ts +const db = new ZenStackClient(..., { + log: ['query', 'error'], +}); +``` + +Or it can be a logging function that receives a log object with the following type: + +```ts +// https://kysely.dev/docs/recipes/logging + +interface LogEvent { + // log level + level: 'query' | 'error'; + + // the query compiled by Kysely, including the SQL AST, raw SQL string, and parameters. + query: CompiledQuery; + + // time taken to execute the query + queryDurationMillis: number; + + // error information, only present if `level` is `'error'` + error: unknown; +} +``` + +```ts +const db = new ZenStackClient(..., { + log: (event) => { + console.log(`[${event.level}] ${event.queryDurationMillis}ms`); + }, +}); +``` + +The `log` option is passed through to the underlying Kysely instance. diff --git a/versioned_docs/version-3.x/orm/migration.md b/versioned_docs/version-3.x/orm/migration.md new file mode 100644 index 00000000..90bac71e --- /dev/null +++ b/versioned_docs/version-3.x/orm/migration.md @@ -0,0 +1,61 @@ +--- +sidebar_position: 17 +--- + +# Database Migration + +Database schema migration is a crucial aspect of application development. It helps you keep your database schema in sync with your data model and ensures deployments are smooth and predictable. ZenStack provides migration tools that create migration scripts based on the ZModel schema and apply them to the database. + +## Introduction + +ZenStack migration is built on top of [Prisma Migrate](https://www.prisma.io/docs/concepts/components/prisma-migrate). The `zen` CLI provides a set of migration-related commands that are simple wrappers around Prisma CLI. When running these commands, the CLI automatically generates a Prisma schema from ZModel and then executes the corresponding Prisma CLI. + +Basing on Prisma Migrate brings the following benefits: + +- **No reinventing the wheel**: Prisma Migrate is a mature and widely used migration tool, ensuring reliability and stability. + +- **Backward compatibility**: When migrating from Prisma to ZenStack, you can continue using your existing migration scripts without any changes. + +If you are already familiar with Prisma Migrate, you can continue using your current workflow with ZenStack, simply swapping the `prisma` CLI with `zen`. If not, this section will guide you through the commands and common workflows, but it's strongly recommended to check [Prisma Migrate documentation](https://www.prisma.io/docs/orm/prisma-migrate/understanding-prisma-migrate/overview) for in-depth information. + +## Commands + +Please refer to the [CLI Reference](../reference/cli.md) for the complete list of commands and options. + +### db push + +The `zen db push` command is used to push your ZModel schema changes to the database without creating a migration file. It's useful for development and testing purposes, but should never be used in production. + +### migrate dev + +The `zen migrate dev` command is used to create a new migration file based on your ZModel schema changes. It will also apply the migration to your database. The command is for development only and should never be used in production. + +### migrate deploy + +The `zen migrate deploy` command is used to apply all pending migrations to your production database. It's typically used in your deployment pipeline. + +### migrate reset + +The `zen migrate reset` command is used to reset your database to a clean state by dropping all tables and reapplying all migrations. It's useful for testing and development purposes, but should never be used in production. + +### migrate status + +The `zen migrate status` command is used to check the status of your migrations. It will show you which migrations have been applied and which are pending. + +### migrate resolve + +The `zen migrate resolve` command is used to mark a migration as applied or rolled back without changing the database schema. This is useful for situations where you need to manually intervene in the migration process. + +## Workflow + +### Typical development workflow + +1. Make changes to your ZModel schema. +2. Run `zen db push` to push the changes to your database without creating a migration file. +3. Test the changes locally. +4. Run `zen migrate dev` to create a migration file. You'll be promoted if a full reset is needed. +5. Carefully review the migration file, make changes as needed, and commit it to source control. + +### Typical deployment workflow + +The most common practice is to run `zen migrate deploy` as part of your production deployment pipeline, which simply applies all pending migrations to the database. 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..5effe808 --- /dev/null +++ b/versioned_docs/version-3.x/orm/plugins/_category_.yml @@ -0,0 +1,4 @@ +position: 13 +label: Plugins +collapsible: true +collapsed: true diff --git a/versioned_docs/version-3.x/orm/plugins/entity-mutation-hooks.md b/versioned_docs/version-3.x/orm/plugins/entity-mutation-hooks.md new file mode 100644 index 00000000..41a88eef --- /dev/null +++ b/versioned_docs/version-3.x/orm/plugins/entity-mutation-hooks.md @@ -0,0 +1,52 @@ +--- +sidebar_position: 2 +--- + +import StackBlitzGithub from '@site/src/components/StackBlitzGithub'; + +# Entity Mutation Hooks + +## Introduction + +Entity mutation hooks allow you to intercept entity mutation operations, i.e., "create", "update", and "delete". They are triggered regardless of whether the operations are performed through the ORM queries or the query builder API. + +To create an entity mutation hook plugin, call the `$use` method with an `onEntityMutation` key containing an object with the following fields (all optional): + +- `beforeEntityMutation` + A callback function that is called before the entity mutation operation. It receives a context object containing: + - The model. + - The action (`create`, `update`, `delete`). + - The Kysely query node (SQL AST). + - An async loader to load the entities to be mutated. + - An `ZenStackClient` instance to perform further queries or mutations. Mutation operations initiated with this client will not trigger the entity mutation hooks again. + - A unique query ID to correlate data between `beforeEntityMutation` and `afterEntityMutation` hooks. + +- `afterEntityMutation` + A callback function that is called after the entity mutation operation. It receives a context object containing: + - The model. + - The action (`create`, `update`, `delete`). + - The Kysely query node (SQL AST). + - An async loader to load the entities after the mutation. + - An `ZenStackClient` instance to perform further queries or mutations. Mutation operations initiated with this client will not trigger the entity mutation hooks again. + - A unique query ID to correlate data between `beforeEntityMutation` and `afterEntityMutation` hooks. + +- `runAfterMutationWithinTransaction` + + A boolean option that controls whether to run after-mutation hooks within the transaction that performs the mutation. + - If set to `true`, if the mutation already runs inside a transaction, the callbacks are executed immediately after the mutation within the transaction boundary. If the mutation is not running inside a transaction, a new transaction is created to wrap both the mutation and the callbacks. If your hooks make further mutations, they will succeed or fail atomically with the original mutation. + - If set to `false`, the callbacks are executed after the mutation transaction is committed. + + Defaults to `false`. + + +:::info +Update and delete triggered by cascading operations are not captured by the entity mutation hooks. +::: + +:::warning +Be very careful about loading before and after mutation entities. Batch mutations can result in a large number of entities being loaded and incur significant performance overhead. +::: + +## Samples + + diff --git a/versioned_docs/version-3.x/orm/plugins/index.md b/versioned_docs/version-3.x/orm/plugins/index.md new file mode 100644 index 00000000..c2b299bb --- /dev/null +++ b/versioned_docs/version-3.x/orm/plugins/index.md @@ -0,0 +1,42 @@ +--- +sidebar_position: 1 +description: ORM plugin introduction +--- + +import ZenStackVsPrisma from '../../_components/ZenStackVsPrisma'; + +# Plugin Overview + + +ZenStack's plugin system aims to provide a more flexible extensibility solution than [Prisma Client Extensions](https://www.prisma.io/docs/orm/prisma-client/client-extensions), allowing you to tap into the ORM runtime at different levels. Some parts of the plugin design resemble client extensions, but overall, it's not meant to be compatible. + + +As you go deeper using an ORM, you'll find the need to tap into its engine for different purposes. For example, you may want to: + +- Log the time cost of each query operation. +- Block certain CRUD operations. +- Execute code when an entity is created, updated, or deleted. +- Modify the SQL query before it's sent to the database. +- ... + +ZenStack ORM provides three ways for you to tap into its runtime: + +1. **Query API hooks** + + Query API hooks allow you to intercept ORM query operations (`create`, `findUnique`, etc.). You can execute arbitrary code before or after the query operation, or even block the operation altogether. See [Query API Hooks](./query-api-hooks.md) for details. + +2. **Entity mutation hooks** + + Entity mutation hooks allow you to execute code before or after an entity is created, updated, or deleted. It's very useful when you only care about entity changes instead of how the mutations are triggered. See [Entity Mutation Hooks](./entity-mutation-hooks.md) for details. + +3. **Kysely query hooks** + + Kysely query hooks give you the ultimate power to inspect and alter the SQL query (in AST form) before it's sent to the database. It's a very powerful low-level extensibility that should be used with care. See [Kysely Query Hooks](./kysely-query-hooks.md) for details. + +All three types of plugins are installed via the unified `$use` method on the ORM client. The `$use` method returns a new ORM client with the plugin applied, without modifying the original client. You can use the `$unuse` or `$unuseAll` methods to remove plugin(s) from a client. + +```ts +const db = new ZenStackClient({ ... }); +const withPlugin = $db.use({ ... }); +const noPlugin = withPlugin.$unuseAll(); +``` diff --git a/versioned_docs/version-3.x/orm/plugins/kysely-query-hooks.md b/versioned_docs/version-3.x/orm/plugins/kysely-query-hooks.md new file mode 100644 index 00000000..eac671f3 --- /dev/null +++ b/versioned_docs/version-3.x/orm/plugins/kysely-query-hooks.md @@ -0,0 +1,28 @@ +--- +sidebar_position: 3 +--- + +import StackBlitzGithub from '@site/src/components/StackBlitzGithub'; + +# Kysely Query Hooks + +## Introduction + +Kysely query hooks are the lowest level of interceptors in the plugin system. Since ZenStack eventually delegates all database access to Kysely, these hooks allow you to inspect and modify all SQL queries before they are sent to the database, regardless of whether they originate from the ORM query API or the query builder API. + +This mechanism gives you great power to control the ORM's behavior entirely. One good example is the [access policy](../access-control/) - the access policy enforcement is entirely achieved via intercepting the Kysely queries. + +To create a Kysely query hook plugin, call the `$use` method with an object containing a `onKyselyQuery` callback. The callback is triggered before each Kysely query is executed. It receives a context object containing: + +- The Kysely instance +- The Kysely query node (SQL AST) +- The ORM client that triggered the query +- A "proceed query" function, which you can call to send the query to the database + +## Samples + +:::info +Kysely's `QueryNode` objects are low-level and not easy to process. ZenStack will provide helpers to facilitate common tasks in the future. +::: + + diff --git a/versioned_docs/version-3.x/orm/plugins/query-api-hooks.md b/versioned_docs/version-3.x/orm/plugins/query-api-hooks.md new file mode 100644 index 00000000..53996e4d --- /dev/null +++ b/versioned_docs/version-3.x/orm/plugins/query-api-hooks.md @@ -0,0 +1,25 @@ +--- +sidebar_position: 1 +--- + +import StackBlitzGithub from '@site/src/components/StackBlitzGithub'; + +# Query API Hooks + +## Introduction + +Query API hooks allow you to intercept ORM queries, like `create`, `findUnique`, etc. You can execute arbitrary code before or after the query operation, modify query args, or even block the operation altogether. + +To create a query API hook plugin, call the `$use` method with an object with the `onQuery` key providing a callback. The callback is invoked with an argument containing the following fields: + +- The model +- The operation +- The query args +- The ORM client that triggered the query +- A "proceed query" function, which you can call to continue executing the operation + +As its name suggests, query API hooks are only triggered by ORM query calls, not by query builder API calls. + +## Samples + + diff --git a/versioned_docs/version-3.x/orm/polymorphism.md b/versioned_docs/version-3.x/orm/polymorphism.md new file mode 100644 index 00000000..f01bc7a4 --- /dev/null +++ b/versioned_docs/version-3.x/orm/polymorphism.md @@ -0,0 +1,33 @@ +--- +sidebar_position: 11 +description: Polymorphic models +--- + +import ZenStackVsPrisma from '../_components/ZenStackVsPrisma'; +import StackBlitzGithub from '@site/src/components/StackBlitzGithub'; + +# Polymorphic Models + + +Polymorphic models is a major feature that sets ZenStack apart from Prisma. + + +ZenStack natively supports polymorphic models. As we have seen in the [Polymorphism](../modeling/polymorphism.md) section in the data modeling part, the ZModel language allows you to define models with Object-Oriented style inheritance. This section will describe the ORM runtime behavior of polymorphic models. + +## CRUD behavior + +Polymorphic models' CRUD behavior is similar to that of regular models, with two major differences: + +1. Base model entities cannot be created directly as they cannot exist without an associated concrete model entity. +2. When querying a base model (either top-level or nested), the result will include all fields of the associated concrete model (unless fields are explicitly selected). The result's type is a discriminated union, so you can use TypeScript's type narrowing to access the concrete model's specific fields. + +The ORM query API hides all the complexity of managing polymorphic models for you: +- When creating a concrete model entity, its base entity is automatically created. +- When querying a base entity, the ORM fetches the associated concrete entity and merges the results. +- When deleting a base or concrete entity, the ORM automatically deletes the counterpart entity. + +## Samples + +The schema used in the sample involves a base model and three concrete models: + + 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..0d909d8a --- /dev/null +++ b/versioned_docs/version-3.x/orm/query-builder.md @@ -0,0 +1,27 @@ +--- +sidebar_position: 5 +description: Query builder API +--- + +import ZenStackVsPrisma from '../_components/ZenStackVsPrisma'; +import StackBlitzGithub from '@site/src/components/StackBlitzGithub'; + +# Query Builder API + + +Query builder API is a major feature that sets ZenStack apart from Prisma. + + +The [Query API](./api/) introduced in the previous sections provide a powerful and intuitive way to query databases. However, complex applications usually have use cases that outgrow its capabilities. For typical ORMs, this is where you leave the comfort zone and resort to writing SQL. + +The unique advantage of ZenStack is that it's built above [Kysely](https://kysely.dev) - a highly popular, well-designed and type-safe SQL query builder. This means we can easily expose the full power of Kysely to you as a much better alternative to writing raw SQL. + +No extra setup is needed to use the query builder API. The ORM client has a `$qb` property that provides the Kysely query builder, and it's typing is inferred from the ZModel schema. + +Besides building full queries, the query builder API can also be embedded inside the ORM query API with a `$expr` key inside a `where` clause. See [Filter](./api/filter.md) section for details. + +## Samples + +The samples assume you have a basic understanding of Kysely. + + diff --git a/versioned_docs/version-3.x/orm/quick-start.md b/versioned_docs/version-3.x/orm/quick-start.md new file mode 100644 index 00000000..52ded564 --- /dev/null +++ b/versioned_docs/version-3.x/orm/quick-start.md @@ -0,0 +1,67 @@ +--- +sidebar_position: 1 +description: Quick start guide +--- + +import StackBlitzGithub from '@site/src/components/StackBlitzGithub'; +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 currently published under the "@next" tag. +::: + +There are several ways to start using ZenStack ORM. + +## 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 [interactive playground](https://stackblitz.com/~/github.com/zenstackhq/v3-doc-quick-start) 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. Optionally, run `zen db push` to push the schema to the database. + + + +## 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 + + + +:::info + +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. + +::: \ No newline at end of file diff --git a/versioned_docs/version-3.x/orm/typed-json.md b/versioned_docs/version-3.x/orm/typed-json.md new file mode 100644 index 00000000..a96f1475 --- /dev/null +++ b/versioned_docs/version-3.x/orm/typed-json.md @@ -0,0 +1,23 @@ +--- +sidebar_position: 12 +description: Strongly typed JSON fields +--- + +import ZenStackVsPrisma from '../_components/ZenStackVsPrisma'; +import StackBlitzGithub from '@site/src/components/StackBlitzGithub'; + +# Strongly Typed JSON + + +Strongly typed JSON is a ZModel feature and doesn't exist in Prisma. + + +ZModel allows you to define custom types and use them to [type JSON fields](../modeling/typed-json.md). The ORM respects such fields in two ways: + +1. The return type of such fields is typed as TypeScript types derived from the ZModel custom type definition. +2. When creating or updating such fields, the ORM validates the input against the custom type definition. The engine "loosely" validates the mutation input and doesn't prevent you from including fields not defined in the custom type. + +## Samples + + + 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..d505d5a9 --- /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 🚧 + +Coming soon 🚧 diff --git a/versioned_docs/version-3.x/prerequisite.md b/versioned_docs/version-3.x/prerequisite.md new file mode 100644 index 00000000..5d9bd1bb --- /dev/null +++ b/versioned_docs/version-3.x/prerequisite.md @@ -0,0 +1,31 @@ +--- +sidebar_position: 2 +--- + +# Prerequisite + +## Node.js + +Node.js v20 or above. + +## TypeScript + +TypeScript v5.8.0 or above. + +## IDE Extension + +If you use VSCode, please install the [ZenStack V3 VSCode Extension](https://marketplace.visualstudio.com/items?itemName=zenstack.zenstack-v3) for syntax highlighting, auto-completion, and error reporting. + +![VSCode Extension](./vscode.png) + +If you use both ZenStack v2 and v3 in different projects, you can install the original [ZenStack VSCode Extension](https://marketplace.visualstudio.com/items?itemName=zenstack.zenstack) side-by-side with the v3 extension. The two extensions have different language ids (v2: `zmodel`, v3: `zmodel-v3`) but handle the same `.zmodel` file extension. To avoid conflicts, make sure you specify the language id explicitly in the `.vscode/settings.json` file in your project: + +```json +{ + "files.associations": { + "*.zmodel": "zmodel-v3" // use "zmodel" for ZenStack v2 projects + } +} +``` + +Other IDEs are not supported at this time. 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..1352c105 --- /dev/null +++ b/versioned_docs/version-3.x/reference/_category_.yml @@ -0,0 +1,7 @@ +position: 7 +label: Reference +collapsible: true +collapsed: true +link: + type: generated-index + title: Reference 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..591870dd --- /dev/null +++ b/versioned_docs/version-3.x/reference/api.md @@ -0,0 +1,48 @@ +--- +description: API references +sidebar_position: 4 +sidebar_label: API +--- + +# API Reference + +## `@zenstackhq/runtime` + +### `class ZenStackClient` + +```ts +/** + * ZenStack ORM client. + */ +export const ZenStackClient = function ( + this: any, + schema: Schema, + options: ClientOptions, +); + +/** + * ZenStack client options. + */ +export type ClientOptions = { + /** + * Kysely dialect. + */ + dialect: Dialect; + + /** + * Plugins. + */ + plugins?: RuntimePlugin[]; + + /** + * Logging configuration. + */ + log?: KyselyConfig['log']; + + // only required when using computed fields + /** + * Computed field definitions. + */ + computedFields: ComputedFieldsOptions; +}; +``` \ No newline at end of file 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..ee3bde1b --- /dev/null +++ b/versioned_docs/version-3.x/reference/cli.md @@ -0,0 +1,270 @@ +--- +description: CLI references +sidebar_position: 2 +sidebar_label: CLI +--- + +# ZenStack CLI Reference + +## Usage + +```text +zen [options] [command] + +ΞΆ ZenStack is the data layer for modern TypeScript apps. + +Documentation: https://zenstack.dev/docs/3.x + +Options: + -v --version display CLI version + -h, --help display help for command + +Commands: + generate [options] Run code generation. + migrate Run database schema migration related tasks. + db Manage your database schema during development. + info [path] Get information of installed ZenStack packages. + init [path] Initialize an existing project for ZenStack. + check [options] Check a ZModel schema for syntax or semantic errors. + help [command] display help for command +``` + +## Sub Commands + +### generate + +Run code generation plugins. + +```bash +Usage: zen generate [options] + +Run code generation plugins. + +Options: + --schema schema file (with extension .zmodel). Defaults to "zenstack/schema.zmodel" unless + specified in package.json. + -o, --output default output directory for code generation + --silent suppress all output except errors (default: false) + -h, --help display help for command +``` + +### migrate + +Run database schema migration related tasks. + +```bash +Usage: zen migrate [options] [command] + +Run database schema migration related tasks. + +Options: + -h, --help display help for command + +Commands: + dev [options] Create a migration from changes in schema and apply it to the database. + reset [options] Reset your database and apply all migrations, all data will be lost. + deploy [options] Deploy your pending migrations to your production/staging database. + status [options] Check the status of your database migrations. + resolve [options] Resolve issues with database migrations in deployment databases + help [command] display help for command +``` + +#### migrate dev + +Create a migration from changes in schema and apply it to the database. + +:::warning +For development only. Do not use this command in production. +::: + +```bash +Usage: zen migrate dev [options] + +Create a migration from changes in schema and apply it to the database. + +Options: + --schema schema file (with extension .zmodel). Defaults to "zenstack/schema.zmodel" unless + specified in package.json. + -n, --name migration name + --create-only only create migration, do not apply + --migrations path that contains the "migrations" directory + -h, --help display help for command +``` + +#### migrate reset + +Reset your database and apply all migrations, all data will be lost. + +:::danger +Never run this command in production. It will drop all data in the database. +::: + +```bash +Usage: zen migrate reset [options] + +Reset your database and apply all migrations, all data will be lost. + +Options: + --schema schema file (with extension .zmodel). Defaults to "zenstack/schema.zmodel" unless + specified in package.json. + --force skip the confirmation prompt + --migrations path that contains the "migrations" directory + -h, --help display help for command +``` + +#### migrate deploy + +Deploy your pending migrations to your production/staging database. + +```bash +Usage: zen migrate deploy [options] + +Deploy your pending migrations to your production/staging database. + +Options: + --schema schema file (with extension .zmodel). Defaults to "zenstack/schema.zmodel" unless + specified in package.json. + --migrations path that contains the "migrations" directory + -h, --help display help for command +``` + +#### migrate status + +Check the status of your database migrations. + +```bash +Usage: zen migrate status [options] + +Check the status of your database migrations. + +Options: + --schema schema file (with extension .zmodel). Defaults to "zenstack/schema.zmodel" unless + specified in package.json. + --migrations path that contains the "migrations" directory + -h, --help display help for command +``` + +#### migrate resolve + +Resolve issues with database migrations in deployment databases. + +```bash +Usage: zen migrate resolve [options] + +Resolve issues with database migrations in deployment databases. + +Options: + --schema schema file (with extension .zmodel). Defaults to "zenstack/schema.zmodel" unless + specified in package.json. + --migrations path that contains the "migrations" directory + --applied record a specific migration as applied + --rolled-back record a specific migration as rolled back + -h, --help display help for command +``` + +### db + +Manage your database schema during development. + +```bash +Usage: zen db [options] [command] + +Manage your database schema during development. + +Options: + -h, --help display help for command + +Commands: + push [options] Push the state from your schema to your database + help [command] display help for command +``` + +#### db push + +Push the state from your schema to your database. + +```bash +Usage: zen db push [options] + +Push the state from your schema to your database. + +Options: + --schema schema file (with extension .zmodel). Defaults to "zenstack/schema.zmodel" + unless specified in package.json. + --accept-data-loss ignore data loss warnings + --force-reset force a reset of the database before push + -h, --help display help for command +``` + +### info + +Get information of installed ZenStack packages. + +```bash +Usage: zen info [options] [path] + +Get information of installed ZenStack. + +Arguments: + path project path (default: ".") + +Options: + -h, --help display help for command +``` + +### init + +Initialize an existing project for ZenStack. + +```bash +Usage: zen init [options] [path] + +Initialize an existing project for ZenStack. + +Arguments: + path project path (default: ".") + +Options: + -h, --help display help for command +``` + +### check + +Check a ZModel schema for syntax or semantic errors. + +```bash +Usage: zen check [options] + +Check a ZModel schema for syntax or semantic errors. + +Options: + --schema schema file (with extension .zmodel). Defaults to "zenstack/schema.zmodel" unless + specified in package.json. + -h, --help display help for command +``` + +## Overriding Default Options + +### Default Schema Location + +You can override the default path that the CLI loads the schema by adding the following key to your `package.json`: + +```json +{ + "zenstack": { + "schema": "path/to/your/schema.zmodel" + } +} +``` + +### Default Output Directory + +You can override the default code generation output path that the CLI uses by adding the following key to your `package.json`: + +```json +{ + "zenstack": { + "output": "path/to/your/output/directory" + } +} +``` 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..658cf50b --- /dev/null +++ b/versioned_docs/version-3.x/reference/plugin-dev.md @@ -0,0 +1,8 @@ +--- +sidebar_position: 5 +description: Plugin development guide +--- + +# Plugin Development 🚧 + +Coming soon 🚧 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..d74396e4 --- /dev/null +++ b/versioned_docs/version-3.x/reference/plugins/_category_.yml @@ -0,0 +1,7 @@ +position: 4 +label: Plugins +collapsible: true +collapsed: true +link: + type: generated-index + title: Plugin Reference diff --git a/versioned_docs/version-3.x/reference/plugins/prisma.md b/versioned_docs/version-3.x/reference/plugins/prisma.md new file mode 100644 index 00000000..2e40f1b6 --- /dev/null +++ b/versioned_docs/version-3.x/reference/plugins/prisma.md @@ -0,0 +1,24 @@ +--- +sidebar_position: 2 +sidebar_label: "@core/prisma" +description: Generating Prisma schema from ZModel +--- + +# @core/prisma + +The `@core/prisma` plugin generates a Prisma schema file from ZModel. + +Please note that ZenStack's ORM runtime doesn't depend on Prisma, so you don't need to use this plugin to use the ORM. However, you can use it to generate a Prisma schema and then run custom Prisma generators or other tools that consumes a Prisma schema. + +## Options + +- `output`: Specifies the path of the generated Prisma schema file. If a relative path is provided, it will be resolved relative to the ZModel schema. + +## Example + +```zmodel +plugin prisma { + provider = '@core/prisma' + output = '../prisma/schema.prisma' +} +``` diff --git a/versioned_docs/version-3.x/reference/plugins/typescript.md b/versioned_docs/version-3.x/reference/plugins/typescript.md new file mode 100644 index 00000000..d14d5a98 --- /dev/null +++ b/versioned_docs/version-3.x/reference/plugins/typescript.md @@ -0,0 +1,30 @@ +--- +sidebar_position: 1 +sidebar_label: "@core/typescript" +description: Generating TypeScript code from ZModel +--- + +# @core/typescript + +The `@core/typescript` plugin generates TypeScript code from ZModel. The generated code is used to access the schema at runtime, as well as type declarations at development time. + +## Options + +- `output`: Specifies the output directory for the generated TypeScript code. If a relative path is provided, it will be resolved relative to the ZModel schema. + +## Output + +The plugin generates the following TypeScript files: + +- `schema.ts`: TypeScript object representation of the ZModel schema. +- `models.ts`: Exports types for all models, types, and enums defined in the schema. +- `input.ts`: Exports types that you can use to type the arguments passed to the ORM query API, such as `findMany`, `create`, etc. + +## Example + +```zmodel +plugin ts { + provider = '@core/typescript' + output = '../generated' +} +``` diff --git a/versioned_docs/version-3.x/reference/zmodel/_category_.yml b/versioned_docs/version-3.x/reference/zmodel/_category_.yml new file mode 100644 index 00000000..aeb71fb9 --- /dev/null +++ b/versioned_docs/version-3.x/reference/zmodel/_category_.yml @@ -0,0 +1,6 @@ +position: 1 +label: ZModel Language +collapsible: true +collapsed: true +link: + type: generated-index diff --git a/versioned_docs/version-3.x/reference/zmodel/attribute.md b/versioned_docs/version-3.x/reference/zmodel/attribute.md new file mode 100644 index 00000000..e8af70c7 --- /dev/null +++ b/versioned_docs/version-3.x/reference/zmodel/attribute.md @@ -0,0 +1,539 @@ +--- +sidebar_position: 6 +--- + +# Attribute + +Attributes decorate fields and models and attach extra behaviors or constraints to them. + +ZModel's standard library provides a set of predefined attributes, plugins can provide additional attributes, and you can also define your own attributes in the schema. + +## Syntax + +### Field attribute + +#### Definition + +Field attribute name is prefixed by a single `@`. + +```zmodel +attribute NAME(PARAMS) +``` + +- **NAME** + + Attribute name. Field attribute's name must start with '@'. + +- **PARAMS** + + Attribute parameters. See [Parameters](#parameters) for details. + +Example: + +```zmodel +attribute @id(map: String?, length: Int?) +``` + +#### Application + +```zmodel +id String ATTR_NAME(ARGS)? +``` + +- **ATTR_NAME** + + Attribute name. + +- **ARGS** + + Argument list. See [Parameters](#parameters) for details. + +Example: + +```zmodel +model User { + id Int @id(map: "_id") +} +``` + +### Model attribute + +#### Definition + +Field attribute name is prefixed by double `@@`. + +```zmodel +attribute @@NAME(PARAMS) +``` + +- **NAME** + + Attribute name. Model attribute's name must start with '@@'. + +- **PARAMS** + + Attribute parameters. See [Parameters](#parameters) for details. + +Example: + +```zmodel +attribute @@unique(_ fields: FieldReference[], name: String?, map: String?) +``` + +#### Application + +```zmodel +model Model { + ATTR_NAME(ARGS)? +} +``` + +- **ATTR_NAME** + + Attribute name. + +- **ARGS** + + Argument list. See [Parameters](#parameters) for details. + +Example: + +```zmodel +model User { + org String + userName String + @@unique([org, userName]) +} +``` + +### Parameters + +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. Parameter names prefixed with `_ ` are positional and arguments for such parameters can be provided without their names. 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]) +} +``` + +## Predefined 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 | + +### @@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 ORM Client. + +### @@auth + +```zmodel +attribute @@auth() +``` + +Specify the model for resolving `auth()` function call. + +### @@delegate + +```zmodel +attribute @@delegate(_ discriminator: FieldReference) +``` + +Marks a model to be a delegated base. Used for [modeling a polymorphic hierarchy](../../modeling/polymorphism.md). + +_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. | + +### @@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') +} +``` + +### @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 ORM Client (so that the field is not included in the input and output of ORM queries). + +### @json + +```zmodel +attribute @json() +``` + +### @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") +} +``` + +## Native type mapping attributes + +Native type mapping attributes are a special set of field attributes that allow you to specify the native database type for a field. They are prefixed with `@db.`. + +Without native type mappings, ZModel types are by default mapped to the database types as follows: + +| ZModel Type | SQLite Type | PostgreSQL Type | +|-------------|-------------|----------------------| +| `String` | `TEXT` | `text` | +| `Boolean` | `INTEGER` | `boolean` | +| `Int` | `INTEGER` | `integer` | +| `BigInt` | `INTEGER` | `bigint` | +| `Float` | `REAL` | `double precision` | +| `Decimal` | `DECIMAL` | `decimal(65,30)` | +| `DateTime` | `NUMERIC` | `timestamp(3)` | +| `Json` | `JSONB` | `jsonb` | +| `Bytes` | `BLOB` | `bytea` | + + +The following native type mapping attributes can be used to override the default mapping: + +| Attribute | ZModel Type | SQLite Type | PostgreSQL Type | +|--------------------------|-------------------------------|-------------|----------------------| +| `@db.Text` | `String` | - | `text` | +| `@db.Char(x)` | `String` | - | `char(x)` | +| `@db.VarChar(x)` | `String` | - | `varchar(x)` | +| `@db.Bit(x)` | `String` `Boolean` `Bytes` | - | `bit(x)` | +| `@db.VarBit` | `String` | - | `varbit` | +| `@db.Uuid` | `String` | - | `uuid` | +| `@db.Xml` | `String` | - | `xml` | +| `@db.Inet` | `String` | - | `inet` | +| `@db.Citext` | `String` | - | `citext` | +| `@db.Boolean` | `Boolean` | - | `boolean` | +| `@db.Int` | `Int` | - | `serial` `serial4` | +| `@db.Integer` | `Int` | - | `integer` `int`,`int4` | +| `@db.SmallInt` | `Int` | - | `smallint` `int2` | +| `@db.Oid` | `Int` | - | `oid` | +| `@db.BigInt` | `BigInt` | - | `bigint` `int8` | +| `@db.DoublePrecision` | `Float` `Decimal` | - | `double precision` | +| `@db.Real` | `Float` `Decimal` | - | `real` | +| `@db.Decimal` | `Float` `Decimal` | - | `decimal` `numeric` | +| `@db.Money` | `Float` `Decimal` | - | `money` | +| `@db.Timestamp(x)` | `DateTime` | - | `timestamp(x)` | +| `@db.Timestamptz(x)` | `DateTime` | - | `timestampz(x)` | +| `@db.Date` | `DateTime` | - | `date` | +| `@db.Time(x)` | `DateTime` | - | `time(x)` | +| `@db.Timetz` | `DateTime` | - | `timez(x)` | +| `@db.Json` | `Json` | - | `json` | +| `@db.JsonB` | `Json` | - | `jsonb` | +| `@db.ByteA` | `Bytes` | - | `bytea` | \ No newline at end of file diff --git a/versioned_docs/version-3.x/reference/zmodel/data-field.md b/versioned_docs/version-3.x/reference/zmodel/data-field.md new file mode 100644 index 00000000..82d984d0 --- /dev/null +++ b/versioned_docs/version-3.x/reference/zmodel/data-field.md @@ -0,0 +1,77 @@ +--- +sidebar_position: 5 +--- + +# Data Field + +Data 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 or type. Must be a valid identifier. + +- **FIELD_TYPE** + + Type of the field. Can be a scalar type, a reference to another model or type 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 + + 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.md) 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[] +} +``` \ No newline at end of file diff --git a/versioned_docs/version-3.x/reference/zmodel/datasource.md b/versioned_docs/version-3.x/reference/zmodel/datasource.md new file mode 100644 index 00000000..2213ed6f --- /dev/null +++ b/versioned_docs/version-3.x/reference/zmodel/datasource.md @@ -0,0 +1,49 @@ +--- +sidebar_position: 1 +--- + +# 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. Must be a valid identifier. Name is only informational and serves no other purposes. + +- **`provider`**: + + Name of database connector. Valid values: + + - sqlite + - postgresql + +- **`url`**: + + Database connection string. Either a plain string or an invocation of `env` function to fetch from an environment variable. For SQLite provider, the URL should be a file protocol, like `file:./dev.db`. For PostgreSQL provider, it should be a postgres connection string, like `postgresql://user:password@localhost:5432/dbname`. + + The `url` option is only used by the migration engine to connect to the database. The ORM runtime doesn't rely on it. Instead, you provide the connection information when constructing an ORM client. + +## Example + +```zmodel +datasource db { + provider = 'postgresql' + url = env('DATABASE_URL') +} +``` + +```zmodel +datasource db { + provider = 'sqlite' + url = 'file:./dev.db' +} +``` diff --git a/versioned_docs/version-3.x/reference/zmodel/enum.md b/versioned_docs/version-3.x/reference/zmodel/enum.md new file mode 100644 index 00000000..56e9ac94 --- /dev/null +++ b/versioned_docs/version-3.x/reference/zmodel/enum.md @@ -0,0 +1,32 @@ +--- +sidebar_position: 3 +--- + +# Enum + +Enums are container declarations for grouping constant identifiers. You can use them to express concepts like user roles, product categories, etc. + +### Syntax + +```zmodel +enum NAME { + FIELD* +} +``` + +- **ENUM_NAME** + + Name of the enum. Needs to be unique in the entire model. Must be a valid identifier. + +- **FIELD** + + Field identifier. Needs to be unique in the model. Must be a valid identifier. + +### Example + +```zmodel +enum UserRole { + USER + ADMIN +} +``` \ No newline at end of file diff --git a/versioned_docs/version-3.x/reference/zmodel/function.md b/versioned_docs/version-3.x/reference/zmodel/function.md new file mode 100644 index 00000000..1e91123b --- /dev/null +++ b/versioned_docs/version-3.x/reference/zmodel/function.md @@ -0,0 +1,148 @@ +--- +sidebar_position: 7 +--- + +# Function + +Functions are used to provide values for attribute arguments, e.g., current `DateTime`, an auto-increment `Int`, etc. They can be used in place of attribute arguments, like: + +```zmodel +model Model { + ... + serial Int @default(autoincrement()) + createdAt DateTime @default(now()) +} +``` + +ZModel's standard library provides a set of predefined functions, plugins can provide additional functions, and you can also define your own functions in the schema. + +## Syntax + +### Definition + +```zmodel +function NAME(PARAMS): RETURN_TYPE {} +``` + +- **NAME** + + Function name. Must be a valid identifier. + +- **PARAMS** + + Parameters. See [Parameters](#parameters) for details. + +- **RETURN_TYPE** + + Return type. Must be a valid type as described in [Parameters](#parameters). + +Example: + +```zmodel +function uuid(version: Int?): String {} +``` + +### Application + +```zmodel +id String @default(FUNC_NAME(ARGS)) +``` +- **FUNC_NAME** + + Function name. + +- **ARGS** + + Argument list. See [Parameters](#parameters) for details. + +Example: + +```zmodel +id String @default(uuid(4)) +``` + +### Parameters + +A function can have zero or more parameters. A parameter has a name and a type. + +Valid parameter types include: + - `String` + - `Boolean` + - `Int` + - `BigInt` + - `Float` + - `Decimal` + - `DateTime` + - `Bytes` + - `Any` + +Parameter's type can also carry the following suffix: + - `[]` to indicate it's a list type + - `?` to indicate it's optional + +## Predefined 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 ZModel (such as random()). + +### auth() + +```zmodel +function auth(): AUTH_TYPE {} +``` + +Gets the current login user. The return type is resolved to a model or type annotated with the `@@auth` attribute, and if not available, a model or type named `User`. diff --git a/versioned_docs/version-3.x/reference/zmodel/import.md b/versioned_docs/version-3.x/reference/zmodel/import.md new file mode 100644 index 00000000..6d0cab02 --- /dev/null +++ b/versioned_docs/version-3.x/reference/zmodel/import.md @@ -0,0 +1,30 @@ +--- +sidebar_position: 8 +--- + +# Import + +ZModel allows to import other ZModel files. This is useful when you want to split your schema into multiple files for better organization. + +## Syntax + +```zmodel +import IMPORT_SPEC +``` + +- **IMPORT_SPEC**: + + 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" +``` diff --git a/versioned_docs/version-3.x/reference/zmodel/model.md b/versioned_docs/version-3.x/reference/zmodel/model.md new file mode 100644 index 00000000..aa0624f6 --- /dev/null +++ b/versioned_docs/version-3.x/reference/zmodel/model.md @@ -0,0 +1,56 @@ +--- +sidebar_position: 2 +--- + +# Model + +Models represent the business entities of your application. A model can have zero or more [mixins](../../modeling/mixin.md), and zero or one [polymorphic base models](../../modeling/polymorphism.md). + +## Syntax + +```zmodel +model NAME (with MIXIN_NAME(,MIXIN_NAME)*)? (extends BASE_NAME)? { + FIELD* + ATTRIBUTE* +} +``` +- **NAME**: + + Name of the model. Needs to be unique in the entire schema. Must be a valid identifier. + +- **FIELD**: + + Arbitrary number of fields. See [Field](./data-field.md) for details. + +- **ATTRIBUTE**: + + Arbitrary number of attributes. See [Attribute](./attribute.md) for details. + +- **MIXIN_NAME**: + + Name of a custom type used as a mixin. + +- **BASE_NAME**: + + Name of a polymorphic base model. + +## Note + +A model must be uniquely identifiable by one or several of its fields. In most cases, you'll have a field marked with the `@id` attribute. If needed, you can use multiple fields as unique identifier by using the `@@id` model-level attribute. + +If no `@id` or `@@id` is specified, the field(s) marked with the `@unique` or `@@unique` attribute will be used as fallback identifier. + +## Example + +```zmodel +type CommonFields { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model User with CommonFields { + email String @unique + name String +} +``` diff --git a/versioned_docs/version-3.x/reference/zmodel/plugin.md b/versioned_docs/version-3.x/reference/zmodel/plugin.md new file mode 100644 index 00000000..d2350ab2 --- /dev/null +++ b/versioned_docs/version-3.x/reference/zmodel/plugin.md @@ -0,0 +1,37 @@ +--- +sidebar_position: 9 +--- + +# Plugin + +Plugins allow you to extend the ZModel language and add custom code generation logic. + +## Syntax + +```zmodel +plugin PLUGIN_NAME { + provider = PROVIDER + OPTION* +} +``` + +- **PLUGIN_NAME** + + Name of the plugin. Needs to be unique in the entire model. Must be a valid identifier. + +- **`provider`** + + The JavaScript module that provides the plugin's implementation. It can be a built-in plugin (like `@core/typescript`), a local JavaScript file, or an installed NPM package that exports a plugin. + +- **OPTION** + + A plugin configuration option, in form of "NAME = VALUE". Option values can be literal, array, or object. + +## Example + +```zmodel +plugin custom { + provider = './my-plugin.js' + output = './generated' +} +``` \ No newline at end of file diff --git a/versioned_docs/version-3.x/reference/zmodel/type.md b/versioned_docs/version-3.x/reference/zmodel/type.md new file mode 100644 index 00000000..22b78387 --- /dev/null +++ b/versioned_docs/version-3.x/reference/zmodel/type.md @@ -0,0 +1,52 @@ +--- +sidebar_position: 4 +--- + +# Type + +Types provide a way to define complex data structures that are not backed by a database table. They server two purposes: + +1. Types can be used as [mixins](../../modeling/mixin.md) to contain common fields and attributes shared by multiple models. +2. Types can be used to define [strongly typed JSON fields](../../modeling/typed-json.md) in models. + +## Syntax + +```zmodel +type NAME { + FIELD* + ATTRIBUTE* +} +``` + +- **NAME**: + + Name of the model. Needs to be unique in the entire model. Must be a valid identifier. + +- **FIELD**: + + Arbitrary number of fields. See [Field](./data-field.md) for details. + +- **ATTRIBUTE**: + + Arbitrary number of attributes. See [Attribute](./attribute.md) for details. + +## Example + +```zmodel +type Profile { + age Int + gender String +} + +type CommonFields { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model User with CommonFields { + email String @unique + name String + profile Profile? @json +} +``` \ No newline at end of file diff --git a/versioned_docs/version-3.x/roadmap.md b/versioned_docs/version-3.x/roadmap.md new file mode 100644 index 00000000..f46b901d --- /dev/null +++ b/versioned_docs/version-3.x/roadmap.md @@ -0,0 +1,13 @@ +--- +sidebar_position: 11 +--- + +# Roadmap + +- [ ] Access Control +- [ ] Data Validation +- [ ] Zod integration +- [ ] Performance benchmark +- [ ] Query-as-a-Service (automatic CRUD API) +- [ ] Json filter +- [ ] Custom procedures \ No newline at end of file diff --git a/versioned_docs/version-3.x/samples.md b/versioned_docs/version-3.x/samples.md new file mode 100644 index 00000000..2e2a6f1a --- /dev/null +++ b/versioned_docs/version-3.x/samples.md @@ -0,0 +1,15 @@ +--- +sidebar_position: 8 +sidebar_label: Sample Projects +--- + +# Sample Projects + +You can find sample projects in the [samples](https://github.com/zenstackhq/zenstack-v3/tree/main/samples) folder of the GitHub repository. New samples will continue to be added. + +## Blog + +https://github.com/zenstackhq/zenstack-v3/tree/main/samples/blog + +A simple TypeScript application that demonstrates how to model a minimum blog app and how to use the ORM client to query the database. + 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..9f3e3b17 --- /dev/null +++ b/versioned_docs/version-3.x/service/_category_.yml @@ -0,0 +1,4 @@ +position: 5 +label: Query as a Service 🚧 +collapsible: true +collapsed: true diff --git a/versioned_docs/version-3.x/service/index.md b/versioned_docs/version-3.x/service/index.md new file mode 100644 index 00000000..d97b5285 --- /dev/null +++ b/versioned_docs/version-3.x/service/index.md @@ -0,0 +1,3 @@ +# Overview + +Coming soon 🚧 \ No newline at end of file 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..1fdb7623 --- /dev/null +++ b/versioned_docs/version-3.x/utilities/_category_.yml @@ -0,0 +1,7 @@ +position: 6 +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..a35591cd --- /dev/null +++ b/versioned_docs/version-3.x/utilities/zod.md @@ -0,0 +1,8 @@ +--- +sidebar_position: 1 +description: Zod integration +--- + +# Zod 🚧 + +Coming soon 🚧 diff --git a/versioned_docs/version-3.x/vscode.png b/versioned_docs/version-3.x/vscode.png new file mode 100644 index 00000000..38dbf54f Binary files /dev/null and b/versioned_docs/version-3.x/vscode.png differ 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"]