Skip to content

Commit a266fdd

Browse files
ymc9CopilotCopilot
authored
doc: custom procedures (#548)
* doc: custom procedures * Fix typo: change "URL encoded" to "URL-encoded" (#549) * Add AvailableSince component to custom procedures documentation (#550) * Add anchor link to Custom Procedures section in TanStack Query documentation (#551) * Update docs/service/api-handler/rest.md Co-authored-by: Copilot <[email protected]> * Update docs/service/api-handler/rest.md Co-authored-by: Copilot <[email protected]> * Update docs/modeling/custom-proc.md Co-authored-by: Copilot <[email protected]> * Update docs/orm/custom-proc.md Co-authored-by: Copilot <[email protected]> --------- Co-authored-by: Copilot <[email protected]> Co-authored-by: Copilot <[email protected]>
1 parent 93d913a commit a266fdd

File tree

11 files changed

+18513
-7
lines changed

11 files changed

+18513
-7
lines changed

docs/modeling/custom-proc.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
---
2+
sidebar_position: 12
3+
description: ZenStack custom procedures
4+
---
5+
6+
import ZModelVsPSL from '../_components/ZModelVsPSL';
7+
import PreviewFeature from '../_components/PreviewFeature';
8+
import AvailableSince from '../_components/AvailableSince';
9+
10+
# Custom Procedure
11+
12+
<PreviewFeature name="Custom procedure" />
13+
14+
<AvailableSince version="v3.2.0" />
15+
16+
<ZModelVsPSL>
17+
Custom procedure is a ZModel feature and doesn't exist in PSL.
18+
</ZModelVsPSL>
19+
20+
Custom procedures are like database stored procedures that allow you to define reusable routines encapsulating complex logic.
21+
22+
Use the `procedure` keyword to define a custom procedure in ZModel. Here's an example for a query procedure:
23+
24+
```zmodel title="schema.zmodel"
25+
procedure getUserFeeds(userId: Int, limit: Int?) : Post[]
26+
```
27+
28+
Mutation procedures (that write to the database) should be defined with `mutation procedure`:
29+
30+
```zmodel title="schema.zmodel"
31+
mutation procedure signUp(email: String) : User
32+
```
33+
34+
You can use all types supported by ZModel to define procedure parameters and return types, including:
35+
36+
- Primitive types like `Int`, `String`
37+
- Models
38+
- Enums
39+
- Custom types
40+
- Array of the types above
41+
42+
Parameter types can be marked optional with a `?` suffix. If a procedure doesn't return anything, use `Void` as the return type.
43+
44+
Custom procedures are implemented with TypeScript when constructing the ORM client, and can be invoked via the ORM client in backend code. See [Custom Procedures](../orm/custom-proc.md) in the ORM part for more details.
45+
46+
They are also accessible via Query-as-a-Service (via [RPC-style](../service/api-handler/rpc.md#endpoints) or [RESTful-style](../service/api-handler/rest.md#calling-custom-procedures) API), plus consumable via Client SDKs like [TanStack Query Client](../service/client-sdk/tanstack-query/#custom-procedures).

docs/modeling/plugin.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
sidebar_position: 12
2+
sidebar_position: 13
33
description: ZenStack plugins
44
---
55

docs/orm/custom-proc.md

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
---
2+
sidebar_position: 13
3+
description: ORM custom procedures
4+
---
5+
6+
import PreviewFeature from '../_components/PreviewFeature';
7+
import AvailableSince from '../_components/AvailableSince';
8+
9+
# Custom Procedures
10+
11+
<PreviewFeature name="Custom procedure" />
12+
13+
<AvailableSince version="v3.2.0" />
14+
15+
:::info
16+
Please refer to the [Modeling](../modeling/custom-proc.md) part for how to define custom procedures in ZModel.
17+
:::
18+
19+
The ORM's CRUD API is very flexible and powerful, but in real-world applications you'll often find the need to encapsulate complex logic into more high-level and reusable operations. For example, in a collaborative app, after creating new users, you may want to automatically create a default workspace for them and assign some initial roles.
20+
21+
A conventional approach is to implement a `signUp` API route that orchestrates these steps. However, since the operation is still very much database-centric, it's more natural to have the encapsulation at the ORM level. This is where custom procedures come in. They are type-safe procedures defined in ZModel and implemented with TypeScript, and can be invoked via the ORM client just like the built-in CRUD methods.
22+
23+
## Implementing custom procedures
24+
25+
Suppose you have the following custom procedures defined in ZModel:
26+
27+
```zmodel title="schema.zmodel"
28+
// get blog post feeds for a given user
29+
procedure getUserFeeds(userId: Int, limit: Int?) : Post[]
30+
31+
// sign up a new user
32+
mutation procedure signUp(email: String) : User
33+
```
34+
35+
:::info
36+
Query procedures and mutation procedures currently don't have any semantic differences at the ORM level. However, in the future they may behave differently, for example, when features like cached queries are introduced.
37+
:::
38+
39+
When you construct a `ZenStackClient`, you must provide an implementation for each procedure:
40+
41+
```ts title="db.ts"
42+
const db = new ZenStackClient({
43+
...
44+
procedures: {
45+
getUserFeeds: ({ client, args }) => {
46+
return client.post.findMany({
47+
where: { authorId: args.userId },
48+
orderBy: { createdAt: 'desc' },
49+
take: args.limit,
50+
});
51+
},
52+
53+
signUp: ({ client, args }) => {
54+
return client.user.create({
55+
data: {
56+
email: args.email,
57+
memberships: {
58+
create: {
59+
role: 'OWNER',
60+
workspace: {
61+
create: { name: 'Default Workspace' },
62+
},
63+
},
64+
}
65+
}
66+
});
67+
},
68+
},
69+
});
70+
```
71+
72+
The implementation callbacks are provided with a context argument with the following fields:
73+
74+
- `client`: an instance of `ZenStackClient` used to invoke the procedure.
75+
- `args`: an object that contains the procedure arguments.
76+
77+
At runtime, before passing the args to the callbacks, ZenStack verifies that they conform to the types defined in ZModel. You can implement additional validations in the implementation if needed. ZenStack doesn't verify the return values. It's your responsibility to ensure they match the declared return types.
78+
79+
## Calling custom procedures
80+
81+
The custom procedures methods are grouped under the `$procs` property of the client instance. You must provide arguments as an object under the `args` key:
82+
83+
```ts
84+
const user = await db.$procs.signUp({
85+
args: { email: '[email protected]' }
86+
});
87+
88+
const feeds = await db.$procs.getUserFeeds({
89+
args: { userId: user.id, limit: 20 }
90+
});
91+
```
92+
93+
## Error handling
94+
95+
The `ZenStackClient` always throws an `ORMError` to the caller when an error occurs. To follow this protocol, custom procedure implementations should ensure other types of errors are caught and wrapped into `ORMError` and re-thrown. See [Error Handling](./errors.md) for more details.

docs/orm/logging.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
sidebar_position: 14
2+
sidebar_position: 15
33
description: Setup logging
44
---
55

docs/orm/plugins/_category_.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
position: 13
1+
position: 14
22
label: Plugins
33
collapsible: true
44
collapsed: true

docs/recipe/plugin-dev.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
sidebar_position: 5
2+
sidebar_position: 6
33
description: Plugin development guide
44
---
55

docs/service/api-handler/rest.md

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ The factory function accepts an options object with the following fields:
5050

5151
- externalIdMapping
5252

53-
Optional. An `Record<string, string>` value that provides a mapping from model names (as defined in ZModel) to unique constraint name. This is useful when you for example want to expose natural keys in place of a surrogate keys:
53+
Optional. An `Record<string, string>` value that provides a mapping from model names (as defined in ZModel) to the model's unique field name. This is useful when you for example want to expose natural keys in place of a surrogate keys:
5454

5555
```ts
5656
// Expose tags by unique name and not by ID, ie. /tag/blue intead of /tag/id
@@ -98,6 +98,10 @@ model Comment {
9898
post Post @relation(fields: [postId], references: [id])
9999
postId Int
100100
}
101+
102+
procedure getUserFeeds(userId: Int, limit: Int?) : Post[]
103+
104+
mutation procedure signUp(email: String) : User
101105
```
102106

103107
### Listing resources
@@ -835,6 +839,47 @@ PATCH /:type/:id/relationships/:relationship
835839
}
836840
```
837841

842+
### Calling custom procedures
843+
844+
Custom procedures can be invoked with the special `$procs` resource type.
845+
846+
Use `GET` for query procedures and pass the arguments as a URL-encoded object in the `args` query parameter:
847+
848+
```ts
849+
GET /$procs/:procName?args=<encoded arguments>
850+
```
851+
852+
Use `POST` for mutation procedures and pass the arguments in the request body:
853+
854+
```ts
855+
POST /$procs/:procName
856+
{
857+
"args": { ... }
858+
}
859+
```
860+
861+
#### Status codes
862+
863+
- 200: The request was successful and the response body contains the custom procedure's return value.
864+
- 400: Invalid custom procedure name or arguments.
865+
- 500: An error occurred while executing the custom procedure.
866+
867+
#### Examples
868+
869+
```ts
870+
// for arguments `{"userId":1,"limit":10}`
871+
GET /$procs/getUserFeeds?args=%7B%22userId%22%3A1%2C%22limit%22%3A10%7D
872+
```
873+
874+
```ts
875+
POST /$procs/signUp
876+
{
877+
"args": {
878+
"email": "[email protected]"
879+
}
880+
}
881+
```
882+
838883
## Compound ID Fields
839884

840885
ZModel allows a model to have compound ID fields, e.g.:

docs/service/api-handler/rpc.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,10 +181,18 @@ The following part explains how the `meta` information is included for different
181181
182182
_Http method:_ `DELETE`
183183
184-
- **[model]/check**
184+
- **[$procs]/[custom-procedure-name]**
185+
186+
Invoking a query custom procedure. E.g., `/$procs/getUserFeeds?q=<encoded args>`.
185187
186188
_Http method:_ `GET`
187189
190+
- **[$procs]/[mutation-custom-procedure-name]**
191+
192+
Invoking a mutation custom procedure. E.g., `/$procs/signUp`.
193+
194+
_Http method:_ `POST`
195+
188196
## HTTP Status Code and Error Responses
189197
190198
### Status code

docs/service/client-sdk/tanstack-query/index.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -521,6 +521,12 @@ Here's a quick example of using infinite query to load a list of posts with infi
521521
522522
</Tabs>
523523
524+
## Custom Procedures
525+
526+
[Custom procedures](../../../modeling/custom-proc.md) are grouped under the `$procs` property on the client returned by `useClientQueries`. Query procedures are mapped to query hooks, while mutation procedures are mapped to mutation hooks.
527+
528+
There's no automatic query invalidation or optimistic update support for custom procedures, since their semantics are unknown to the system. You need to implement such behavior manually as needed.
529+
524530
## Advanced Topics
525531
526532
### Query Invalidation

0 commit comments

Comments
 (0)