diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 79c7c0e8..85572d18 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -32,13 +32,19 @@ function Header() { A TypeScript toolkit that enhances Prisma ORM with flexible Authorization and auto-generated, type-safe APIs/hooks, simplifying full-stack development

-
+
Get Started → + + Check V3 Beta +
diff --git a/versioned_docs/version-3.x/migrate-prisma.md b/versioned_docs/version-3.x/migrate-prisma.md index b5a3f638..7b979753 100644 --- a/versioned_docs/version-3.x/migrate-prisma.md +++ b/versioned_docs/version-3.x/migrate-prisma.md @@ -260,7 +260,7 @@ export const db = new ZenStackClient(schema, { 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 +## Feature Gaps Here's a list of Prisma features that are not supported in ZenStack v3: diff --git a/versioned_docs/version-3.x/migrate-v2.md b/versioned_docs/version-3.x/migrate-v2.md new file mode 100644 index 00000000..9e9a90aa --- /dev/null +++ b/versioned_docs/version-3.x/migrate-v2.md @@ -0,0 +1,257 @@ +--- +description: How to migrate from a ZenStack v2 project to v3 +sidebar_position: 11 +--- + +import PackageInstall from './_components/PackageInstall'; + +# Migrating From ZenStack V2 + +## Overview + +ZenStack v3 is a major rewrite of v2, with a focus on simplicity and flexibility. It replaced Prisma ORM with its own ORM component built on top of [Kysely](https://kysely.dev/), resulting in a much more lighter-weight architecture and the level of extensibility that we couldn't achieve in v2. + +A few v3 design decisions should make an upgrade much less painful: + +- The ZModel schema is compatible with v2. +- The ORM query API is compatible with that of `PrismaClient`, thus compatible with v2. + +However, given the architectural changes, some effort is required to adapt to the new system. This guide will help you migrate an existing ZenStack v2 project. + +## Compatibility Check + +Here are a few essential items to verify before preparing your migration: + +- Database support + + V3 currently only supports PostgreSQL and SQLite databases. MySQL will be added later. + + For PostgreSQL, only the traditional TCP-based connection is supported. Newer HTTP-based protocols, such as those supported by providers like Neon and Prisma PG, are not yet supported, but will be in the future. + +- Prisma feature gaps + + A few Prisma ORM's features are not implemented or not planned. Please check the [Prisma Feature Gaps](./migrate-prisma.md#feature-gaps) for details. + +- V2 feature gaps + + A few ZenStack v2 features are not implemented. Some less popular features are planned to be removed. Please check the [Feature Gaps](#feature-gaps) for details. + +## Migrating Prisma + +Since ZenStack v3 is no longer based on Prisma ORM, the first step is to replace Prisma dependencies with ZenStack and update the code where `PrismaClient` is created. Please follow the [Prisma Migration Guide](./migrate-prisma.md) for detailed instructions. + +## Migrating ZModel + +### Access Control + +Access control functionality has been moved into a self-contained plugin. You need to install the package, add a plugin declaration in ZModel, and then use the plugin at runtime. + +1. Install the package + + + +2. Add a plugin declaration in ZModel + + ```zmodel title="schema.zmodel" + + // adding the plugin makes attributes like `@@allow` and `@@deny` work + plugin policy { + provider = '@zenstackhq/plugin-policy' + } + + ``` + +3. Install the plugin at runtime + +```ts title="db.ts" +import { PolicyPlugin } from '@zenstackhq/plugin-policy'; + +// ORM client without access control +export const db = new ZenStackClient({ ... }); + +// ORM client with access control +export const authDb = db.$use(new PolicyPlugin()); +``` + +When you need to query the database with a specific user identity, call the `$setAuth()` method on the ORM client (with the access policy plugin installed) to get a user-bound instance. + +```ts +async function processRequest(request: Request) { + // get the validated user identity from your auth system + const user = await getCurrentUser(request); + + // create a user-bound ORM client + const userDb = authDb.$setAuth(user); + + // process the request with `userDb` + ... +} +``` + +The policy rules in ZModel are mostly backward compatible, except for a breaking change about post-update policies. In v3, post-update rules are expressed with their own "post-update" policy type and separate from regular "update" rules. Inside "post-update" rules, by default, fields refer to the entity's "after-update" state, and you can use the `before()` function to refer to the "before-update" state. See [Post Update Rules](./orm/access-control/post-update.md) for more details. + +Here's a quick example for how to migrate: + +V2: + +```zmodel +model Post { + ... + ownerId Int + owner User @relation(fields: [ownerId], references: [id]) + + // update is not allowed to change the owner + @@deny('update', future().ownerId != ownerId) +} +``` + +V3: +```zmodel +model Post { + ... + ownerId Int + owner User @relation(fields: [ownerId], references: [id]) + + // update is not allowed to change the owner + @@deny('post-update', ownerId != before().ownerId) +} +``` + +### Abstract Base Models + +V2 had the concept of [Abstract Model](/docs/guides/multiple-schema#abstract-model-inheritance) that allows you to define base models that serve purely as base types, but are not mapped to the database. You can use the `extends` keyword to inherit from an abstract model. We found this to be confusing because `extends` is also used for inheriting from a [Polymorphic Model](/docs/guides/polymorphism). The same keyword was used for two very different concepts. + +In v3, abstract models are replaced with "types", and the concept of "abstract inheritance" is replaced with [Mixins](./modeling/mixin.md). What you need to do is to change `abstract model` to `type`, and change the `extends` keyword to `with`. + +V2: + +```zmodel +abstract model Timestamped { + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model Post extends Timestamped { + id Int @id @default(autoincrement()) + title String +} +``` + +V3: + +```zmodel +type Timestamped { + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model Post with Timestamped { + id Int @id @default(autoincrement()) + title String +} +``` + +## Migrating Server Adapters + +Server adapters are mostly backward compatible. One small change needed is that, when creating a server adapter, it's now mandatory to explicitly pass in an API handler instance (RPC or RESTful). The API handlers are now created with the `schema` object as input. See [Server Adapters](./service/server-adapter.md) for more details. + +Here's an example with Express.js: + +```ts +import { schema } from '~/zenstack/schema'; +import { authDb } from '~/db'; + +app.use( + '/api/model', + ZenStackMiddleware({ + // an API handler needs to be explicitly passed in + apiHandler: new RPCApiHandler({ schema }), + + // `getPrisma` is renamed to `getClient` in v3 + getClient: (request) => getClientForRequest(request), + }) +); + +function getClientForRequest(request: Request) { + const user = getCurrentUser(request); + return authDb.$setAuth(user); +} +``` + +## Migrating Client-Side Hooks + +V3 introduces a new implementation of [TanStack Query](https://tanstack.com/query) implementation that doesn't require code generation. Instead, TS types are fully inferred from the schema type at compile time, and the runtime logic is based on the interpretation of the schema object. As a result, the new integration becomes a simple library that you call, and no plugin is involved. + +To support such an architecture change. Query hooks are now grouped into an object that mirrors the API style of the ORM client. You need to adjust the v2 code (that uses the flat `useFindMany[Model]` style hooks) into this new structure. + +V2: +```ts +import { useFindManyUser } from '~/hooks'; + +export function MyComponent() { + const { data } = useFindManyUser({ ... }); + ... +} +``` + +V3: +```ts +import { useClientQueries } from '@zenstackhq/tanstack-query'; +import { schema } from '~/zenstack/schema'; + +export function MyComponent() { + const client = useClientQueries(schema); + const { data } = client.user.useFindMany({ ... }); + ... +} +``` + +[SWR](https://swr.vercel.app/) support has been dropped due to low popularity. + +## Migration Custom Plugins + +V3 comes with a completely revised plugin system that offers greater power and flexibility. You can check the concepts in the [Data Model Plugin](./modeling/plugin.md) and [ORM Plugin](./orm/plugins/) documentation. + +The plugin development document is still WIP. This part of the migration guide will be added later when it's ready. + +## Feature Gaps + +This section lists v2 features that haven't been migrated to v3 yet, or that are planned to be removed in v3. Please feel free to share your thoughts about these decisions in [Discord](https://discord.gg/Ykhr738dUe), and we'll be happy to discuss them. + +### Automatic password hashing + +The `@password` attribute is removed in v3. We believe most people will use a more sophisticated authentication system than a simple id/password mechanism. + +### Field-level access control + +Not supported yet, but will be added soon with some design changes. + +### Data encryption + +Not supported yet, but will be added soon. + +### Zod integration + +Not supported yet, but will be added soon with some design changes. + +### Checking permissions without querying the database + +The [check()](/docs/guides/check-permission) feature is removed due to low popularity. + +### tRPC integration + +[TRPC](https://trpc.io/) is TypeScript-inference heavy, and stacking it over ZenStack generates additional complexities and pressure on the compiler. We're evaluating the best way to integrate it in v3, and no concrete plan is in place yet. At least there's no plan to migrate the code-generation-based approach in v2 directly. + +### OpenAPI spec generation + +The [OpenAPI plugin](/docs/reference/plugins/openapi) has not migrated to v3 yet and will be added later with some redesign. + +### SWR integration + +The [SWR plugin](https://swr.vercel.app/) is removed due to low popularity. + +## FAQ + +### Is data migration needed? + +No. From the database schema point of view, v3 is fully backward-compatible with v2. diff --git a/versioned_docs/version-3.x/orm/errors.md b/versioned_docs/version-3.x/orm/errors.md index 4ce578a9..f761e972 100644 --- a/versioned_docs/version-3.x/orm/errors.md +++ b/versioned_docs/version-3.x/orm/errors.md @@ -5,22 +5,101 @@ description: ORM Errors # Errors -The ORM uses the following error classes from `@zenstackhq/orm` to represent different types of failures: +The ORM throws an `ORMError` in case of failures. The class has the following fields: -## `InputValidationError` +```ts +/** + * ZenStack ORM error. + */ +export class ORMError extends Error { + /** + * The name of the model that the error pertains to. + */ + model?: string; -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. + /** + * The error code given by the underlying database driver. + */ + dbErrorCode?: unknown; -If [input validation](../orm/validation.md) is used, this error is also thrown when the validation rules are violated. + /** + * The error message given by the underlying database driver. + */ + dbErrorMessage?: string; -## `NotFoundError` + /** + * The reason code for policy rejection. Only available when `reason` is `REJECTED_BY_POLICY`. + */ + rejectedByPolicyReason?: RejectedByPolicyReason; -This error is thrown when a requested record is not found in the database, e.g., when calling `findUniqueOrThrow`, `update`, etc. + /** + * The SQL query that was executed. Only available when `reason` is `DB_QUERY_ERROR`. + */ + sql?: string; -## `QueryError` + /** + * The parameters used in the SQL query. Only available when `reason` is `DB_QUERY_ERROR`. + */ + sqlParams?: readonly unknown[]; +} -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. +/** + * Reason code for ORM errors. + */ +export enum ORMErrorReason { + /** + * ORM client configuration error. + */ + CONFIG_ERROR = 'config-error', -## `RejectedByPolicyError` + /** + * Invalid input error. + */ + INVALID_INPUT = 'invalid-input', -This error is thrown when an operation is rejected by [access control policies](../orm/access-control/index.md). + /** + * The specified record was not found. + */ + NOT_FOUND = 'not-found', + + /** + * Operation is rejected by access policy. + */ + REJECTED_BY_POLICY = 'rejected-by-policy', + + /** + * Error was thrown by the underlying database driver. + */ + DB_QUERY_ERROR = 'db-query-error', + + /** + * The requested operation is not supported. + */ + NOT_SUPPORTED = 'not-supported', + + /** + * An internal error occurred. + */ + INTERNAL_ERROR = 'internal-error', +} + +/** + * Reason code for policy rejection. + */ +export enum RejectedByPolicyReason { + /** + * Rejected because the operation is not allowed by policy. + */ + NO_ACCESS = 'no-access', + + /** + * Rejected because the result cannot be read back after mutation due to policy. + */ + CANNOT_READ_BACK = 'cannot-read-back', + + /** + * Other reasons. + */ + OTHER = 'other', +} +``` \ No newline at end of file diff --git a/versioned_docs/version-3.x/roadmap.md b/versioned_docs/version-3.x/roadmap.md index bcb566f6..4689d37b 100644 --- a/versioned_docs/version-3.x/roadmap.md +++ b/versioned_docs/version-3.x/roadmap.md @@ -1,5 +1,5 @@ --- -sidebar_position: 11 +sidebar_position: 12 --- # Roadmap @@ -10,10 +10,10 @@ This is a list of major features that are planned for the future releases of Zen - [x] Data Validation - [x] Performance benchmark - [x] Query-as-a-Service (automatic CRUD API) -- [ ] TanStack Query integration +- [x] TanStack Query integration - [ ] Zod utility - [ ] Custom functions - [ ] Custom procedures - [ ] Field encryption - [ ] Soft delete -- [ ] Audit trail \ No newline at end of file +- [ ] Audit trail diff --git a/versioned_docs/version-3.x/service/api-handler/rest.md b/versioned_docs/version-3.x/service/api-handler/rest.md index cbdc4ed6..a58ab2a1 100644 --- a/versioned_docs/version-3.x/service/api-handler/rest.md +++ b/versioned_docs/version-3.x/service/api-handler/rest.md @@ -949,6 +949,7 @@ An error response is an object containing the following fields: - title: `string`, error title - detail: `string`, detailed error message - reason: `string`, extra reason for the error + - dbErrorCode: `unknown`, the error code given by the underlying database driver ### Example diff --git a/versioned_docs/version-3.x/service/api-handler/rpc.md b/versioned_docs/version-3.x/service/api-handler/rpc.md index b9792d14..b0e2d98d 100644 --- a/versioned_docs/version-3.x/service/api-handler/rpc.md +++ b/versioned_docs/version-3.x/service/api-handler/rpc.md @@ -200,7 +200,7 @@ The HTTP status code used by the endpoints follows the following rules: ### Error response format -When an error occurs, the response body will have the following shape: +When an error occurs, the response body will have the following shape. See [ORM error](../../orm/errors.md) for details of the `ORMErrorReason` and `RejectedByPolicyReason` enums. ```ts { @@ -212,8 +212,8 @@ When an error occurs, the response body will have the following shape: // error message message: string; - // extra details about the error that's causing the failure - cause?: string; + // reason of the error + reason: ORMErrorReason; // the model name involved in the error, if applicable model?: string; @@ -226,7 +226,10 @@ When an error occurs, the response body will have the following shape: // detailed rejection reason, only available when // `rejectedByValidation` or `rejectedByPolicy` is true - rejectReason?: string; + rejectReason?: RejectedByPolicyReason; + + // the error code given by the underlying database driver + dbErrorCode?: unknown; } } }