From d733db5e119f505bf3940822c9362befc489f52e Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Mon, 20 Apr 2026 17:56:30 -0700 Subject: [PATCH] doc: v3.6.0 release updates - RPC-style API OAPI generation (v3.6.0) - Extract shared OpenAPI spec options into a reusable partial - Custom types now support relation fields (v3.6.0), with mixin example - Zod: document breaking changes and new `optionality` option (PR #2525) - MySQL: remove preview banner Co-Authored-By: Claude Sonnet 4.6 --- docs/modeling/custom-type.md | 30 ++++- docs/recipe/databases/mysql.md | 2 - docs/service/openapi/_openapi_spec_options.md | 21 ++++ docs/service/openapi/restful.md | 25 +---- docs/service/openapi/rpc.md | 104 +++++++++++++++++- docs/utilities/zod.md | 47 ++++++-- 6 files changed, 189 insertions(+), 40 deletions(-) create mode 100644 docs/service/openapi/_openapi_spec_options.md diff --git a/docs/modeling/custom-type.md b/docs/modeling/custom-type.md index 7314678b..524fa0fb 100644 --- a/docs/modeling/custom-type.md +++ b/docs/modeling/custom-type.md @@ -4,6 +4,7 @@ description: Custom types in ZModel --- import ZModelVsPSL from '../_components/ZModelVsPSL'; +import AvailableSince from '../_components/AvailableSince'; # Custom Type @@ -24,7 +25,7 @@ type Address { } ``` -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. +Custom types are defined exactly like models. They can contain fields that are other custom types: ```zmodel type Address { @@ -40,6 +41,33 @@ type UserProfile { } ``` +### Relation Fields + + + +Custom types can also contain relation fields to models. This is particularly useful when used as [mixins](./mixin.md) to share relation field definitions across multiple models. For example, you can define an audit mixin that tracks who created and last updated a record: + +```zmodel +type AuditMixin { + id String @id @default(cuid()) + createdBy User @relation("CreatedBy", fields: [createdById], references: [id]) + createdById String + updatedBy User @relation("UpdatedBy", fields: [updatedById], references: [id]) + updatedById String +} + +model Post with AuditMixin { + title String +} + +model Comment with AuditMixin { + body String +} +``` +:::warning +Custom types with relation fields can only be used as [mixins](./mixin.md). They cannot be used to type [JSON fields](./typed-json.md), since JSON fields cannot hold relational data. +::: + There are two ways to use custom types: - [Mixin](./mixin.md) diff --git a/docs/recipe/databases/mysql.md b/docs/recipe/databases/mysql.md index 7bfb0cd0..a38d9ca0 100644 --- a/docs/recipe/databases/mysql.md +++ b/docs/recipe/databases/mysql.md @@ -7,8 +7,6 @@ import PreviewFeature from '../../_components/PreviewFeature'; # MySQL - - ## Installing driver diff --git a/docs/service/openapi/_openapi_spec_options.md b/docs/service/openapi/_openapi_spec_options.md new file mode 100644 index 00000000..96cb97af --- /dev/null +++ b/docs/service/openapi/_openapi_spec_options.md @@ -0,0 +1,21 @@ +The `generateSpec` method accepts an optional `OpenApiSpecOptions` object: + +```ts +import type { OpenApiSpecOptions } from '@zenstackhq/server/api'; + +const spec = await handler.generateSpec({ + title: 'My Blog API', + version: '2.0.0', + description: 'API for managing blog posts and users', + summary: 'Blog API', + respectAccessPolicies: true, +}); +``` + +| Option | Type | Default | Description | +| --- | --- | --- | --- | +| `title` | `string` | `'ZenStack Generated API'` | The title of the API shown in the spec's `info.title` field. | +| `version` | `string` | `'1.0.0'` | The version of the API shown in the spec's `info.version` field. | +| `description` | `string` | — | A longer description of the API. | +| `summary` | `string` | — | A short summary of the API. | +| `respectAccessPolicies` | `boolean` | `false` | When `true`, adds `403 Forbidden` responses to operations on models that have access policies defined. | diff --git a/docs/service/openapi/restful.md b/docs/service/openapi/restful.md index 60ecdd65..5d0f319e 100644 --- a/docs/service/openapi/restful.md +++ b/docs/service/openapi/restful.md @@ -3,6 +3,7 @@ sidebar_position: 2 --- import AvailableSince from '../../_components/AvailableSince'; +import OpenApiSpecOptions from './_openapi_spec_options.md'; # RESTful API @@ -28,27 +29,7 @@ console.log(JSON.stringify(spec)); ### Options -The `generateSpec` method accepts an optional `OpenApiSpecOptions` object: - -```ts -import type { OpenApiSpecOptions } from '@zenstackhq/server/api'; - -const spec = await handler.generateSpec({ - title: 'My Blog API', - version: '2.0.0', - description: 'API for managing blog posts and users', - summary: 'Blog API', - respectAccessPolicies: true, -}); -``` - -| Option | Type | Default | Description | -| --- | --- | --- | --- | -| `title` | `string` | `'ZenStack Generated API'` | The title of the API shown in the spec's `info.title` field. | -| `version` | `string` | `'1.0.0'` | The version of the API shown in the spec's `info.version` field. | -| `description` | `string` | — | A longer description of the API. | -| `summary` | `string` | — | A short summary of the API. | -| `respectAccessPolicies` | `boolean` | `false` | When `true`, adds `403 Forbidden` responses to operations on models that have access policies defined. | + ## Serving the Spec @@ -100,7 +81,7 @@ The `queryOptions` passed to the handler influence the generated spec in several #### Slicing -If you provide a `slicing` configuration, the spec only includes the models, operations, procedures, and filter kinds that are allowed: +If you provide a [slicing](../../orm/advanced/slicing.md) configuration, the spec only includes the models, operations, procedures, and filter kinds that are allowed: ```ts const handler = new RestApiHandler({ diff --git a/docs/service/openapi/rpc.md b/docs/service/openapi/rpc.md index 14857efe..24a79a59 100644 --- a/docs/service/openapi/rpc.md +++ b/docs/service/openapi/rpc.md @@ -2,8 +2,106 @@ sidebar_position: 3 --- +import AvailableSince from '../../_components/AvailableSince'; +import OpenApiSpecOptions from './_openapi_spec_options.md'; + # RPC-Style API -:::info -OpenAPI spec generation for RPC-style APIs is coming soon. -::: + + +The RPC API handler can generate an OpenAPI v3.1 specification that describes all its endpoints. The spec is generated at runtime from your ZModel schema, so it always stays in sync with your data model. + +## Generating the Spec + +Call the `generateSpec` method on an `RPCApiHandler` instance: + +```ts +import { schema } from '~/zenstack/schema'; +import { RPCApiHandler } from '@zenstackhq/server/api'; + +const handler = new RPCApiHandler({ schema, ... }); + +const spec = await handler.generateSpec(); + +// Use with Swagger UI, write to a file, or serve as a JSON endpoint +console.log(JSON.stringify(spec)); +``` + +### Options + + + +## Serving the Spec + +A common pattern is to expose the spec as a JSON endpoint alongside your API: + +```ts title="Express example" +import express from 'express'; +import { schema } from '~/zenstack/schema'; +import { RPCApiHandler } from '@zenstackhq/server/api'; + +const app = express(); +const handler = new RPCApiHandler({ schema, endpoint: 'http://localhost:3000/api' }); + +// Serve the OpenAPI spec +app.get('/api/openapi.json', async (_req, res) => { + const spec = await handler.generateSpec({ + title: 'My API', + respectAccessPolicies: true, + }); + res.json(spec); +}); +``` + +You can then point tools like Swagger UI to `http://localhost:3000/api/openapi.json`. + +You can also implement a script to wrap the generation logic to output the spec during build. + +## Customization Through Handler Options + +The generated spec is influenced by the `RPCApiHandler` options you provide: + +### Query Options + +The `queryOptions` passed to the handler influence the generated spec in several ways: + +#### Slicing + +If you provide a [slicing](../../orm/advanced/slicing.md) configuration, the spec only includes the models, operations, procedures, and filter kinds that are allowed: + +```ts +const handler = new RPCApiHandler({ + schema, + endpoint: 'http://localhost:3000/api', + queryOptions: { + slicing: { + includedModels: ['User', 'Post'], + models: { + post: { + excludedOperations: ['delete'], + }, + }, + }, + }, +}); +``` + +The generated spec will only contain `User` and `Post` models and will omit the `DELETE /post/delete` endpoint. + +#### Omit + +If you provide an `omit` configuration, the specified fields are excluded from the generated schemas: + +```ts +const handler = new RPCApiHandler({ + schema, + endpoint: 'http://localhost:3000/api', + queryOptions: { + omit: { + user: { password: true }, + }, + }, +}); +``` + +The `password` field will not appear in the `User` resource schema, create request schema, or update request schema. diff --git a/docs/utilities/zod.md b/docs/utilities/zod.md index f26b738c..e8ee5c19 100644 --- a/docs/utilities/zod.md +++ b/docs/utilities/zod.md @@ -34,17 +34,28 @@ The factory exposes the following methods: - `makeModelSchema` - Creates a schema for the full shape of a model. By default, all scalar fields are included, and all relation fields are included as optional fields. + Creates a schema for a model. By default, **only scalar fields are included** — relation fields are excluded. Use `include` or `select` options to opt in to relations, mirroring ORM query API semantics. - You can pass an optional second argument with `select`, `include`, or `omit` options to control which fields and relations are included in the resulting schema. These options work recursively for relation fields, mirroring the ORM's query API semantics. + You can pass an optional second argument with `select`, `include`, `omit`, or `optionality` options: - **`select`** — pick only the listed fields. Set a field to `true` to include it with its default shape, or pass a nested options object for relation fields. Mutually exclusive with `include` and `omit`. - **`include`** — start with all scalar fields, then additionally include the named relation fields. Can be combined with `omit`. - **`omit`** — remove named scalar fields from the default set. Can be combined with `include`, but mutually exclusive with `select`. + - **`optionality`** — controls which fields are made optional at runtime: + - `'all'` — every field in the schema becomes optional. + - `'defaults'` — only fields that have a `@default` attribute or are `@updatedAt` become optional; all other fields remain their original optionality. ```ts + // Default: scalar fields only (no relations) + const schema = factory.makeModelSchema('User'); + + // Include a relation on top of all scalar fields + const schema = factory.makeModelSchema('User', { + include: { posts: true }, + }); + // Select specific fields only const schema = factory.makeModelSchema('User', { select: { id: true, email: true }, @@ -61,11 +72,6 @@ The factory exposes the following methods: }, }); - // Include relations on top of all scalar fields - const schema = factory.makeModelSchema('User', { - include: { posts: true }, - }); - // Omit specific scalar fields const schema = factory.makeModelSchema('User', { omit: { password: true }, @@ -76,15 +82,33 @@ The factory exposes the following methods: include: { posts: true }, omit: { password: true }, }); + + // Make all fields optional (useful for update payloads) + const schema = factory.makeModelSchema('User', { + optionality: 'all', + }); + + // Make only @default / @updatedAt fields optional (useful for create payloads) + const schema = factory.makeModelSchema('User', { + optionality: 'defaults', + }); ``` - The resulting Zod schema is fully typed — the inferred TypeScript type reflects exactly which fields are present based on the options you provide. + The resulting Zod schema is fully typed — the inferred TypeScript type reflects exactly which fields are present and their optionality based on the options you provide. + +- `makeModelCreateSchema` *(deprecated)* + + :::warning Deprecated + Use `makeModelSchema(model, { optionality: 'defaults' })` instead. + ::: -- `makeModelCreateSchema` - Creates a schema for creating new records, with fields that have defaults being optional. The result schema excludes relation fields. -- `makeModelUpdateSchema` +- `makeModelUpdateSchema` *(deprecated)* + + :::warning Deprecated + Use `makeModelSchema(model, { optionality: 'all' })` instead. + ::: Creates a schema for updating records, with all fields being optional. The result schema excludes relation fields. @@ -108,4 +132,3 @@ The created Zod schemas have the following features: ## Samples -