Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion docs/modeling/custom-type.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ description: Custom types in ZModel
---

import ZModelVsPSL from '../_components/ZModelVsPSL';
import AvailableSince from '../_components/AvailableSince';

# Custom Type

Expand All @@ -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 {
Expand All @@ -40,6 +41,33 @@ type UserProfile {
}
```

### Relation Fields
Comment thread
ymc9 marked this conversation as resolved.

<AvailableSince version="v3.6.0" />

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)
Expand Down
2 changes: 0 additions & 2 deletions docs/recipe/databases/mysql.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ import PreviewFeature from '../../_components/PreviewFeature';

# MySQL

<PreviewFeature name="MySQL support" />

## Installing driver

<PackageInstall dependencies={['mysql2']} />
Expand Down
21 changes: 21 additions & 0 deletions docs/service/openapi/_openapi_spec_options.md
Original file line number Diff line number Diff line change
@@ -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. |
25 changes: 3 additions & 22 deletions docs/service/openapi/restful.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ sidebar_position: 2
---

import AvailableSince from '../../_components/AvailableSince';
import OpenApiSpecOptions from './_openapi_spec_options.md';

# RESTful API

Expand All @@ -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. |
<OpenApiSpecOptions />

## Serving the Spec

Expand Down Expand Up @@ -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:
Comment thread
ymc9 marked this conversation as resolved.

```ts
const handler = new RestApiHandler({
Expand Down
104 changes: 101 additions & 3 deletions docs/service/openapi/rpc.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
:::
<AvailableSince version="v3.6.0" />

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

<OpenApiSpecOptions />

## 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.
47 changes: 35 additions & 12 deletions docs/utilities/zod.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<AvailableSince version="v3.5.0" />

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 },
Expand All @@ -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 },
Expand All @@ -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.

Expand All @@ -108,4 +132,3 @@ The created Zod schemas have the following features:
## Samples

<StackBlitzGithub repoPath="zenstackhq/v3-doc-zod" openFile={['zenstack/schema.zmodel', 'main.ts']} />