|
| 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. |
0 commit comments