diff --git a/.gitmodules b/.gitmodules index 54c052af..217cf705 100644 --- a/.gitmodules +++ b/.gitmodules @@ -22,3 +22,6 @@ [submodule "code-repos/zenstackhq/v3-doc-orm-policy"] path = code-repos/zenstackhq/v3-doc-orm-policy url = https://github.com/zenstackhq/v3-doc-orm-policy.git +[submodule "code-repos/zenstackhq/v3-doc-server-adapter"] + path = code-repos/zenstackhq/v3-doc-server-adapter + url = https://github.com/zenstackhq/v3-doc-server-adapter diff --git a/code-repos/zenstackhq/v3-doc-orm b/code-repos/zenstackhq/v3-doc-orm index bafb4695..fc438dc4 160000 --- a/code-repos/zenstackhq/v3-doc-orm +++ b/code-repos/zenstackhq/v3-doc-orm @@ -1 +1 @@ -Subproject commit bafb4695a8c32d875ce2262a8729a853c4e8bdcb +Subproject commit fc438dc4ef636387ac7e5065005cc8100aa849da diff --git a/code-repos/zenstackhq/v3-doc-orm-computed-fields b/code-repos/zenstackhq/v3-doc-orm-computed-fields index 12f895b6..e1e80da9 160000 --- a/code-repos/zenstackhq/v3-doc-orm-computed-fields +++ b/code-repos/zenstackhq/v3-doc-orm-computed-fields @@ -1 +1 @@ -Subproject commit 12f895b61ee3f80fb6a3534c37e020acc3cbb035 +Subproject commit e1e80da9c243d21de4fd30cf9ef40c0b5b34e656 diff --git a/code-repos/zenstackhq/v3-doc-orm-policy b/code-repos/zenstackhq/v3-doc-orm-policy index f4b864b5..ccbde6f8 160000 --- a/code-repos/zenstackhq/v3-doc-orm-policy +++ b/code-repos/zenstackhq/v3-doc-orm-policy @@ -1 +1 @@ -Subproject commit f4b864b5ece8dbff17a4f75db57ab6f157c6c6fd +Subproject commit ccbde6f82a7e0d18455a3b9ce002493baed29ad5 diff --git a/code-repos/zenstackhq/v3-doc-orm-polymorphism b/code-repos/zenstackhq/v3-doc-orm-polymorphism index 74231c1b..69a6ca0d 160000 --- a/code-repos/zenstackhq/v3-doc-orm-polymorphism +++ b/code-repos/zenstackhq/v3-doc-orm-polymorphism @@ -1 +1 @@ -Subproject commit 74231c1b06e7d51968e4966bbd316722a492bb1c +Subproject commit 69a6ca0daeed4d664eb3ad8e9740f35578ba4dc7 diff --git a/code-repos/zenstackhq/v3-doc-orm-typed-json b/code-repos/zenstackhq/v3-doc-orm-typed-json index 710d2cab..1237f4cf 160000 --- a/code-repos/zenstackhq/v3-doc-orm-typed-json +++ b/code-repos/zenstackhq/v3-doc-orm-typed-json @@ -1 +1 @@ -Subproject commit 710d2cabe95d425ed6cc01ac1ece503ef989cc65 +Subproject commit 1237f4cfb59ac6005580934fac1c29e0a42392f5 diff --git a/code-repos/zenstackhq/v3-doc-orm-validation b/code-repos/zenstackhq/v3-doc-orm-validation index c8643932..59b5fc77 160000 --- a/code-repos/zenstackhq/v3-doc-orm-validation +++ b/code-repos/zenstackhq/v3-doc-orm-validation @@ -1 +1 @@ -Subproject commit c8643932d21a8d0f072489f0dda21661b5d51746 +Subproject commit 59b5fc77668b8400caf08e1ff2b3470cf6f158e3 diff --git a/code-repos/zenstackhq/v3-doc-quick-start b/code-repos/zenstackhq/v3-doc-quick-start index bddda1f7..e0372d8d 160000 --- a/code-repos/zenstackhq/v3-doc-quick-start +++ b/code-repos/zenstackhq/v3-doc-quick-start @@ -1 +1 @@ -Subproject commit bddda1f7ba6046fff49e2beccd99dad64cd8252d +Subproject commit e0372d8db250985e48ec68fce0196c9f86fded28 diff --git a/code-repos/zenstackhq/v3-doc-server-adapter b/code-repos/zenstackhq/v3-doc-server-adapter new file mode 160000 index 00000000..24d6599c --- /dev/null +++ b/code-repos/zenstackhq/v3-doc-server-adapter @@ -0,0 +1 @@ +Subproject commit 24d6599c8f9c9e603e1cd038238fea955ad67334 diff --git a/docs/reference/server-adapters/api-handlers/rest.mdx b/docs/reference/server-adapters/api-handlers/rest.mdx index aca6d667..652fc6e2 100644 --- a/docs/reference/server-adapters/api-handlers/rest.mdx +++ b/docs/reference/server-adapters/api-handlers/rest.mdx @@ -101,7 +101,7 @@ The factory function accepts an options object with the following fields: - externalIdMapping - Optional. An `Record` value that provides a mapping from model names (as defined in ZModel) to unique contraint name. This is useful when you for example want to expose natural keys in place of a surrogate keys: + Optional. An `Record` 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: ```ts // Expose tags by unique name and not by ID, ie. /tag/blue intead of /tag/id @@ -112,7 +112,7 @@ The factory function accepts an options object with the following fields: }) ``` - Currentlly it is not possible to use custom index names. This also works for compound unique contraints just like for [compound IDs](#compound-id-fields). + Currently it is not possible to use custom index names. This also works for compound unique constraints just like for [compound IDs](#compound-id-fields). ## Endpoints and Features @@ -408,7 +408,7 @@ You can use the `filter[:selector1][:selector2][...]=value` [query parameter fam 1. Filtering with multiple values - Multiple filter values can be separated by comma. Items statisfying any of the values will be returned. + Multiple filter values can be separated by comma. Items satisfying any of the values will be returned. ```ts GET /api/post?filter[author]=1,2 @@ -416,7 +416,7 @@ You can use the `filter[:selector1][:selector2][...]=value` [query parameter fam 1. Multiple filters - A request can carry multiple filters. Only items statisfying all filters will be returned. + A request can carry multiple filters. Only items satisfying all filters will be returned. ```ts GET /api/post?filter[author]=1&filter[published]=true @@ -700,7 +700,7 @@ PUT /:type/:id PATCH /:type/:id ``` -Both `PUT` and `PATCH` do partial update and has exactly the same behavior. +Both `PUT` and `PATCH` perform partial updates with identical behavior. :::info Besides plain fields, you can also include relationships in the request body. Please note that this won't update the related resource; instead if only replaces the relationships. If you update a to-many relationship, the new collection will entirely replace the old one. @@ -836,7 +836,7 @@ PATCH /:type/:id/relationships/:relationship ``` :::info -`PUT` and `PATCH` has exactly the same behavior and both relace the existing relationships with the new ones entirely. +`PUT` and `PATCH` have identical behavior and both replace the existing relationships with the new ones entirely. ::: ##### Status codes @@ -917,7 +917,7 @@ The JSON:API specification doesn't have a native way to represent compound IDs. } ``` -You can use this ID value convension in places where an ID is needed, e.g., reading a single entity. +You can use this ID value convention in places where an ID is needed, e.g., reading a single entity. ```ts GET /post/1_2 diff --git a/versioned_docs/version-1.x/reference/server-adapters/api-handlers/rest.mdx b/versioned_docs/version-1.x/reference/server-adapters/api-handlers/rest.mdx index 1b73d963..a62a5a5b 100644 --- a/versioned_docs/version-1.x/reference/server-adapters/api-handlers/rest.mdx +++ b/versioned_docs/version-1.x/reference/server-adapters/api-handlers/rest.mdx @@ -360,7 +360,7 @@ You can use the `filter[:selector1][:selector2][...]=value` [query parameter fam 1. Filtering with multiple values - Multiple filter values can be separated by comma. Items statisfying any of the values will be returned. + Multiple filter values can be separated by comma. Items satisfying any of the values will be returned. ```ts GET /api/post?filter[author]=1,2 @@ -368,7 +368,7 @@ You can use the `filter[:selector1][:selector2][...]=value` [query parameter fam 1. Multiple filters - A request can carry multiple filters. Only items statisfying all filters will be returned. + A request can carry multiple filters. Only items satisfying all filters will be returned. ```ts GET /api/post?filter[author]=1&filter[published]=true @@ -632,7 +632,7 @@ PUT /:type/:id PATCH /:type/:id ``` -Both `PUT` and `PATCH` do partial update and has exactly the same behavior. +Both `PUT` and `PATCH` perform partial updates with identical behavior. :::info Besides plain fields, you can also include relationships in the request body. Please note that this won't update the related resource; instead if only replaces the relationships. If you update a to-many relationship, the new collection will entirely replace the old one. @@ -730,7 +730,7 @@ PATCH /:type/:id/relationships/:relationship ``` :::info -`PUT` and `PATCH` has exactly the same behavior and both relace the existing relationships with the new ones entirely. +`PUT` and `PATCH` have identical behavior and both replace the existing relationships with the new ones entirely. ::: ##### Status codes diff --git a/versioned_docs/version-3.x/reference/package.md b/versioned_docs/version-3.x/reference/package.md new file mode 100644 index 00000000..a9cc4ad7 --- /dev/null +++ b/versioned_docs/version-3.x/reference/package.md @@ -0,0 +1,5 @@ +--- +sidebar_position: 5 +--- + +# Packages diff --git a/versioned_docs/version-3.x/reference/plugin-dev.md b/versioned_docs/version-3.x/reference/plugin-dev.md index 658cf50b..275fd325 100644 --- a/versioned_docs/version-3.x/reference/plugin-dev.md +++ b/versioned_docs/version-3.x/reference/plugin-dev.md @@ -1,5 +1,5 @@ --- -sidebar_position: 5 +sidebar_position: 6 description: Plugin development guide --- diff --git a/versioned_docs/version-3.x/reference/plugins/_category_.yml b/versioned_docs/version-3.x/reference/plugins/_category_.yml index d74396e4..b204ec4f 100644 --- a/versioned_docs/version-3.x/reference/plugins/_category_.yml +++ b/versioned_docs/version-3.x/reference/plugins/_category_.yml @@ -1,4 +1,4 @@ -position: 4 +position: 5 label: Plugins collapsible: true collapsed: true diff --git a/versioned_docs/version-3.x/reference/server-adapters/_category_.yml b/versioned_docs/version-3.x/reference/server-adapters/_category_.yml new file mode 100644 index 00000000..f6e05f3e --- /dev/null +++ b/versioned_docs/version-3.x/reference/server-adapters/_category_.yml @@ -0,0 +1,7 @@ +position: 7 +label: Server Adapters +collapsible: true +collapsed: true +link: + type: generated-index + title: Server Adapters diff --git a/versioned_docs/version-3.x/reference/server-adapters/_error-handling.md b/versioned_docs/version-3.x/reference/server-adapters/_error-handling.md new file mode 100644 index 00000000..3cd5fb08 --- /dev/null +++ b/versioned_docs/version-3.x/reference/server-adapters/_error-handling.md @@ -0,0 +1,3 @@ +### Error Handling + +Refer to the specific sections for [RPC Handler](../../service/api-handler/rpc#http-status-code-and-error-responses) and [RESTful Handler](../../service/api-handler/rest#error-handling). diff --git a/versioned_docs/version-3.x/reference/server-adapters/_intro.mdx b/versioned_docs/version-3.x/reference/server-adapters/_intro.mdx new file mode 100644 index 00000000..3f3726a6 --- /dev/null +++ b/versioned_docs/version-3.x/reference/server-adapters/_intro.mdx @@ -0,0 +1 @@ +

The {props.module} module provides a quick way to install a full set of CRUD API onto {props.framework} apps. Combined with ZenStack's powerful access policies, you can achieve a secure data backend without manually coding it.

\ No newline at end of file diff --git a/versioned_docs/version-3.x/reference/server-adapters/_options.mdx b/versioned_docs/version-3.x/reference/server-adapters/_options.mdx new file mode 100644 index 00000000..feaeeed6 --- /dev/null +++ b/versioned_docs/version-3.x/reference/server-adapters/_options.mdx @@ -0,0 +1,11 @@ +- getClient (required) + +
{props.getClient}
+ + A callback for getting a ZenStackClient instance for talking to the database. Usually you'll return a client instance with access policy enabled and user identity bound. + +- apiHandler (required) + +
{'ApiHandler'}
+ + The [API handler](../../service/api-handler/) instance that determines the API specification. diff --git a/versioned_docs/version-3.x/reference/server-adapters/elysia.mdx b/versioned_docs/version-3.x/reference/server-adapters/elysia.mdx new file mode 100644 index 00000000..f63c6691 --- /dev/null +++ b/versioned_docs/version-3.x/reference/server-adapters/elysia.mdx @@ -0,0 +1,67 @@ +--- +title: Elysia +description: Adapter for integrating with Elysia +sidebar_position: 8 +--- + +import PackageInstall from '../../_components/PackageInstall'; +import ErrorHandling from './_error-handling.md'; +import Intro from './_intro.mdx'; +import AdapterOptions from './_options.mdx'; + +# Elysia Adapter + + + +This feature is contributed by [@rodrigoburigool](https://github.com/rodrigoburigool). + +### Installation + + + +### Mounting the API + +You can use the `createElysiaHandler` API to create an Elysia request handler that handles CRUD requests automatically: + +```ts +import { Elysia, Context } from 'elysia'; +import { RPCApiHandler } from '@zenstackhq/server/api'; +import { createElysiaHandler } from '@zenstackhq/server/elysia'; +import { getSessionUser } from '~/auth'; +import { client } from '~/db'; +import { schema } from '~/zenstack/schema'; + +const app = new Elysia({ prefix: '/api' }); + +// install the CRUD middleware under route "/api/crud" +app.group('/crud', (app) => + app.use( + createElysiaHandler({ + apiHandler: new RPCApiHandler({ schema }), + basePath: '/api/model', + // getSessionUser extracts the current session user from the request, + // its implementation depends on your auth solution + getClient: (context) => client.$setAuth(getSessionUser(context)) }), + }) + ) +); + +function getCurrentUser(context: Context) { + // the implementation depends on your authentication mechanism + ... +} + +app.listen(3000); +``` + +The middleware factory takes the following options to initialize: + + + +- basePath (optional) + +
string
+ + Optional base path to strip from the request path before passing to the API handler. E.g., if your CRUD handler is mounted at `/api/crud`, set this field to `'/api/crud'`. + + diff --git a/versioned_docs/version-3.x/reference/server-adapters/express.mdx b/versioned_docs/version-3.x/reference/server-adapters/express.mdx new file mode 100644 index 00000000..82d3942b --- /dev/null +++ b/versioned_docs/version-3.x/reference/server-adapters/express.mdx @@ -0,0 +1,59 @@ +--- +title: Express.js +description: Adapter for integrating with Express.js +sidebar_position: 5 +--- + +import PackageInstall from '../../_components/PackageInstall'; +import ErrorHandling from './_error-handling.md'; +import Intro from './_intro.mdx'; +import AdapterOptions from './_options.mdx'; + +# Express.js Adapter + + + +### Installation + + + +### Mounting the API + +You can integrate ZenStack into your project with the `ZenStackMiddleware` [express middleware](https://expressjs.com/en/guide/using-middleware.html): + +```ts +import express from 'express'; +import { ZenStackMiddleware } from '@zenstackhq/server/express'; +import { RPCApiHandler } from '@zenstackhq/server/api'; +import { getSessionUser } from '~/auth'; +import { client } from '~/db'; +import { schema } from '~/zenstack/schema'; + +const app = express(); + +app.use(express.json()); + +app.use( + '/api/model', + ZenStackMiddleware({ + apiHandler: new RPCApiHandler({ schema }), + // getSessionUser extracts the current session user from the request, its + // implementation depends on your auth solution + getClient: (request) => client.$setAuth(getSessionUser(request)), + }) +); +``` + +The Express.js adapter takes the following options to initialize: + + + +- sendResponse (optional) + +
boolean
+ + Controls if the middleware directly sends a response. If set to false, the response is stored in the `res.locals` object and then the middleware calls the `next()` function to pass the control to the next middleware. Subsequent middleware or request handlers need to make sure to send a response. + + Defaults to `true`. + + diff --git a/versioned_docs/version-3.x/reference/server-adapters/fastify.mdx b/versioned_docs/version-3.x/reference/server-adapters/fastify.mdx new file mode 100644 index 00000000..dce01ce9 --- /dev/null +++ b/versioned_docs/version-3.x/reference/server-adapters/fastify.mdx @@ -0,0 +1,53 @@ +--- +title: Fastify +description: Adapter for integrating with Fastify +sidebar_position: 6 +--- + +import PackageInstall from '../../_components/PackageInstall'; +import ErrorHandling from './_error-handling.md'; +import Intro from './_intro.mdx'; +import AdapterOptions from './_options.mdx'; + +# Fastify Adapter + + + +### Installation + + + +### Mounting the API + +You can integrate ZenStack into your project with the `ZenStackFastifyPlugin` [fastify plugin](https://www.fastify.io/docs/latest/Reference/Plugins/): + +```ts +import fastify from 'fastify' +import { ZenStackFastifyPlugin } from '@zenstackhq/server/fastify'; +import { RPCApiHandler } from '@zenstackhq/server/api'; +import { getSessionUser } from '~/auth'; +import { client } from '~/db'; +import { schema } from '~/zenstack/schema'; + +const server = fastify(); + +server.register(ZenStackFastifyPlugin, { + apiHandler: new RPCApiHandler({ schema }), + prefix: '/api/model', + // getSessionUser extracts the current session user from the request, its + // implementation depends on your auth solution + getClient: (request) => client.$setAuth(getSessionUser(request)), +}); +``` + +The Fastify adapter takes the following options to initialize: + +- prefix + +
string
+ + Prefix for the mounted API endpoints. E.g.: /api/model. + + + + diff --git a/versioned_docs/version-3.x/reference/server-adapters/hono.mdx b/versioned_docs/version-3.x/reference/server-adapters/hono.mdx new file mode 100644 index 00000000..4ac6cab7 --- /dev/null +++ b/versioned_docs/version-3.x/reference/server-adapters/hono.mdx @@ -0,0 +1,49 @@ +--- +title: Hono +description: Adapter for integrating with Hono +sidebar_position: 7 +--- + +import PackageInstall from '../../_components/PackageInstall'; +import ErrorHandling from './_error-handling.md'; +import Intro from './_intro.mdx'; +import AdapterOptions from './_options.mdx'; + +# Hono Adapter + + + +### Installation + + + +### Mounting the API + +You can use the `createHonoHandler` API to create a [Hono middleware](https://hono.dev/docs/getting-started/basic#using-middleware) that handles CRUD requests automatically: + +```ts +import { Context, Hono } from 'hono'; +import { createHonoHandler } from '@zenstackhq/server/hono'; +import { RPCApiHandler } from '@zenstackhq/server/api'; +import { getSessionUser } from '~/auth'; +import { client } from '~/db'; +import { schema } from '~/zenstack/schema'; + +const app = new Hono(); + +app.use( + '/api/model/*', + createHonoHandler({ + apiHandler: new RPCApiHandler({ schema }), + // getSessionUser extracts the current session user from the request, + // its implementation depends on your auth solution + getClient: (ctx) => client.$setAuth(getSessionUser(ctx)), + }) +); +``` + +The middleware factory takes the following options to initialize: + + + + diff --git a/versioned_docs/version-3.x/reference/server-adapters/next.mdx b/versioned_docs/version-3.x/reference/server-adapters/next.mdx new file mode 100644 index 00000000..696a0456 --- /dev/null +++ b/versioned_docs/version-3.x/reference/server-adapters/next.mdx @@ -0,0 +1,110 @@ +--- +title: Next.js +description: Adapter for integrating with Next.js +sidebar_position: 1 +--- + +import TabItem from '@theme/TabItem'; +import Tabs from '@theme/Tabs'; +import PackageInstall from '../../_components/PackageInstall'; +import ErrorHandling from './_error-handling.md'; +import Intro from './_intro.mdx'; +import AdapterOptions from './_options.mdx'; + +# Next.js Server Adapter + + + +The server adapter supports both the traditional "pages" router and the new "app" router. + +### Installation + + + +### Mounting the API + +You can use it to create a request handler in an API endpoint like: + + + + + +```ts title='/src/app/api/model/[...path]/route.ts' +import type { NextRequest } from "next/server"; +import { NextRequestHandler } from '@zenstackhq/server/next'; +import { RPCApiHandler } from '@zenstackhq/server/api'; +import { getSessionUser } from '~/auth'; +import { client } from '~/db'; +import { schema } from '~/zenstack/schema'; + +const handler = NextRequestHandler({ + apiHandler: new RPCApiHandler({ schema }), + // getSessionUser extracts the current session user from the request, its + // implementation depends on your auth solution + getClient: (req: NextRequest) => client.$setAuth(getSessionUser(req)), + useAppDir: true }); + +export { + handler as GET, + handler as POST, + handler as PUT, + handler as PATCH, + handler as DELETE, +}; +``` + +The Next.js API route handler takes the following options to initialize: + + + + + + + +```ts title='/src/pages/api/model/[...path].ts' +import type { NextApiRequest, NextApiResponse } from 'next'; +import { NextRequestHandler } from '@zenstackhq/server/next'; +import { RPCApiHandler } from '@zenstackhq/server/api'; +import { getSessionUser } from '~/auth'; +import { client } from '~/db'; +import { schema } from '~/zenstack/schema'; + +export default NextRequestHandler({ + apiHandler: new RPCApiHandler({ schema }), + // getSessionUser extracts the current session user from the request, its + // implementation depends on your auth solution + getClient: (req: NextApiRequest, res: NextApiResponse) => client.$setAuth(getSessionUser(req, res)), +}); +``` + +The Next.js API route handler takes the following options to initialize: + + + + + + + +### Controlling what endpoints to expose + +You can use a [Next.js middleware](https://nextjs.org/docs/pages/building-your-application/routing/middleware) to further control what endpoints to expose. For example, if you're using a RESTful API handler installed at "/api/model", you can disallow listing all `User` entities by adding a middleware like: + +```ts title='/src/middleware.ts' +import { type NextRequest, NextResponse } from 'next/server'; + +export function middleware(request: NextRequest) { + const url = new URL(request.url); + if ( + request.method === 'GET' && + url.pathname.match(/^\/api\/model\/user\/?$/) + ) { + return NextResponse.json({ error: 'Not allowed' }, { status: 405 }); + } +} + +export const config = { + matcher: '/api/model/:path*', +}; +``` + + diff --git a/versioned_docs/version-3.x/reference/server-adapters/nuxt.mdx b/versioned_docs/version-3.x/reference/server-adapters/nuxt.mdx new file mode 100644 index 00000000..a9f7a9d6 --- /dev/null +++ b/versioned_docs/version-3.x/reference/server-adapters/nuxt.mdx @@ -0,0 +1,43 @@ +--- +title: Nuxt +description: Adapter for integrating with Nuxt +sidebar_position: 2 +--- + +import PackageInstall from '../../_components/PackageInstall'; +import ErrorHandling from './_error-handling.md'; +import Intro from './_intro.mdx'; +import AdapterOptions from './_options.mdx'; + +# Nuxt Server Adapter + + + +### Installation + + + +### Mounting the API + +You can mount the API by creating a Nuxt server event handler like: + +```ts title='/server/api/model/[...].ts' +import { createEventHandler } from '@zenstackhq/server/nuxt'; +import { RPCApiHandler } from '@zenstackhq/server/api'; +import { getSessionUser } from '~/auth'; +import { client } from '~/db'; +import { schema } from '~/zenstack/schema'; + +export default createEventHandler({ + apiHandler: new RPCApiHandler({ schema }), + // getSessionUser extracts the current session user from the request, its + // implementation depends on your auth solution + getClient: (event) => client.$setAuth(getSessionUser(event)), +}); +``` + +The Nuxt event handler takes the following options to initialize: + + + + diff --git a/versioned_docs/version-3.x/reference/server-adapters/sveltekit.mdx b/versioned_docs/version-3.x/reference/server-adapters/sveltekit.mdx new file mode 100644 index 00000000..02f34efc --- /dev/null +++ b/versioned_docs/version-3.x/reference/server-adapters/sveltekit.mdx @@ -0,0 +1,54 @@ +--- +title: SvelteKit +description: Adapter for integrating with SvelteKit +sidebar_position: 3 +--- + +import PackageInstall from '../../_components/PackageInstall'; +import ErrorHandling from './_error-handling.md'; +import Intro from './_intro.mdx'; +import AdapterOptions from './_options.mdx'; + +# SvelteKit Server Adapter + + + +### Installation + + + +### Mounting the API + +You can mount the API by creating SvelteKit server hooks like: + +```ts title='/src/hooks.server.ts' +import { SvelteKitHandler } from '@zenstackhq/server/sveltekit'; +import { RPCApiHandler } from '@zenstackhq/server/api'; +import { getSessionUser } from '~/auth'; +import { client } from '~/db'; +import { schema } from '~/zenstack/schema'; + +export const handle = SvelteKitHandler({ + apiHandler: new RPCApiHandler({ schema }), + prefix: '/api/model', + // getSessionUser extracts the current session user from the request, its + // implementation depends on your auth solution + getClient: (event) => client.$setAuth(getSessionUser(event)), +}); +``` + +:::tip +You can use the [sequence helper](https://svelte.dev/docs/kit/@sveltejs-kit-hooks#sequence) to compose multiple server hooks. +::: + +The SvelteKit hooks handler takes the following options to initialize: + +- prefix + +
string
+ + Prefix for the mounted API endpoints. E.g.: /api/model. + + + + diff --git a/versioned_docs/version-3.x/reference/server-adapters/tanstack-start.mdx b/versioned_docs/version-3.x/reference/server-adapters/tanstack-start.mdx new file mode 100644 index 00000000..922923d2 --- /dev/null +++ b/versioned_docs/version-3.x/reference/server-adapters/tanstack-start.mdx @@ -0,0 +1,61 @@ +--- +title: TanStack Start +description: Adapter for integrating with TanStack Start +sidebar_position: 4 +--- + +import ErrorHandling from './_error-handling.md'; +import AdapterOptions from './_options.mdx'; +import PackageInstall from '../../_components/PackageInstall'; +import Intro from './_intro.mdx'; + +# TanStack Start Adapter + +[TanStack Start](https://tanstack.com/start) is a full-stack React framework powered by TanStack Router, offering full-document SSR, streaming, server functions, and bundling capabilities. + + + +This feature is contributed by [@digoburigo](https://github.com/digoburigo). + +### Installation + + + +### Mounting the API + +You can use the `TanStackStartHandler` to create a handler for your API routes. TanStack Start uses file-based routing, so you'll typically create a catch-all route to handle all CRUD operations: + +```ts title='app/routes/api/$.ts' +import { createFileRoute } from '@tanstack/react-router' +import { TanStackStartHandler } from '@zenstackhq/server/tanstack-start' +import { RPCApiHandler } from '@zenstackhq/server/api'; +import { getSessionUser } from '~/auth'; +import { client } from '~/db'; +import { schema } from '~/zenstack/schema'; + +const handler = TanStackStartHandler({ + apiHandler: new RPCApiHandler({ schema }), + // getSessionUser extracts the current session user from the request, its + // implementation depends on your auth solution + getClient: (request) => client.$setAuth(getSessionUser(request)), +}) + +export const Route = createFileRoute('/api/$')({ + server: { + handlers: { + GET: handler, + POST: handler, + PUT: handler, + PATCH: handler, + DELETE: handler, + } + } +}) +``` + +The TanStack Start handler takes the following options to initialize: + + + + + diff --git a/versioned_docs/version-3.x/roadmap.md b/versioned_docs/version-3.x/roadmap.md index 570d21da..bcb566f6 100644 --- a/versioned_docs/version-3.x/roadmap.md +++ b/versioned_docs/version-3.x/roadmap.md @@ -8,10 +8,11 @@ This is a list of major features that are planned for the future releases of Zen - [x] Access Control - [x] Data Validation -- [ ] Performance benchmark -- [ ] Zod integration -- [ ] Query-as-a-Service (automatic CRUD API) +- [x] Performance benchmark +- [x] Query-as-a-Service (automatic CRUD API) - [ ] TanStack Query integration +- [ ] Zod utility +- [ ] Custom functions - [ ] Custom procedures - [ ] Field encryption - [ ] Soft delete diff --git a/versioned_docs/version-3.x/service/_category_.yml b/versioned_docs/version-3.x/service/_category_.yml index 9f3e3b17..938fc11d 100644 --- a/versioned_docs/version-3.x/service/_category_.yml +++ b/versioned_docs/version-3.x/service/_category_.yml @@ -1,4 +1,4 @@ position: 5 -label: Query as a Service 🚧 +label: Query as a Service collapsible: true collapsed: true diff --git a/versioned_docs/version-3.x/service/api-handler/_category_.yml b/versioned_docs/version-3.x/service/api-handler/_category_.yml new file mode 100644 index 00000000..dce4d1a6 --- /dev/null +++ b/versioned_docs/version-3.x/service/api-handler/_category_.yml @@ -0,0 +1,4 @@ +position: 1 +label: API Handler +collapsible: true +collapsed: true diff --git a/versioned_docs/version-3.x/service/api-handler/_data_type_serialization.md b/versioned_docs/version-3.x/service/api-handler/_data_type_serialization.md new file mode 100644 index 00000000..d22a602f --- /dev/null +++ b/versioned_docs/version-3.x/service/api-handler/_data_type_serialization.md @@ -0,0 +1,16 @@ +- `DateTime` + + ISO 8601 string + +- `Bytes` + + Base64-encoded string + +- `BigInt` + + String representation + +- `Decimal` + + String representation + \ No newline at end of file diff --git a/versioned_docs/version-3.x/service/api-handler/index.md b/versioned_docs/version-3.x/service/api-handler/index.md new file mode 100644 index 00000000..48dcca0a --- /dev/null +++ b/versioned_docs/version-3.x/service/api-handler/index.md @@ -0,0 +1,7 @@ +# API Handler + +API handlers are components that implement different API design specifications, such as REST and RPC, as we'll see in the following sections. An API handler is responsible for understanding the incoming requests, translating them into ORM queries, and formatting the results into proper responses. + +API handlers are framework-agnostic, meaning they only deal with "logical" requests and responses that are independent of any specific web framework. The framework-specific details are handled by [Server Adapters](../server-adapter). This decoupled design allows you to mix and match any API specification with any supported web framework. + +ZenStack currently provides two built-in API handlers: [RPC API Handler](./rpc.md) and [RESTful API Handler](./rest.md). \ No newline at end of file diff --git a/versioned_docs/version-3.x/service/api-handler/rest.md b/versioned_docs/version-3.x/service/api-handler/rest.md new file mode 100644 index 00000000..cbdc4ed6 --- /dev/null +++ b/versioned_docs/version-3.x/service/api-handler/rest.md @@ -0,0 +1,966 @@ +--- +sidebar_position: 2 +--- + +import DataTypeSerialization from './_data_type_serialization.md'; + +# RESTful API Handler + +## Introduction + +The RESTful-style API handler exposes CRUD APIs as RESTful endpoints using [JSON:API](https://jsonapi.org/) as transportation format. The API handler is not meant to be used directly; instead, you should use it together with a [server adapter](../../../category/server-adapters) which handles the request and response API for a specific framework. + +It can be created as the following: + +```ts +import { schema } from '~/zenstack/schema'; +import { RestApiHandler } from '@zenstackhq/server/api'; + +const handler = new RestApiHandler({ schema, endpoint: 'http://localhost/api' }); +``` + +The factory function accepts an options object with the following fields: + +- schema + + Required. The schema object generated by ZenStack CLI. + +- endpoint + + Required. A `string` field representing the base URL of the RESTful API, used for generating resource links. + +- pageSize + + Optional. A `number` field representing the default page size for listing resources and relationships. This value also determines the maximum `limit` value in an API call. Defaults to 100. Set to Infinity to disable pagination. + +- modelNameMapping + + Optional. An `Record` value that provides a mapping from model names (as defined in ZModel) to URL path names. This is useful for example when you want to use plural names in URL endpoints: + + ```ts + // endpoint for accessing User model will then be ".../users" + RestApiHandler({ + modelNameMapping: { + User: 'users' + } + }) + ``` + + The mapping can be partial. You only need to specify the model names that you want to override. If a mapping is provided, only the mapped url path is valid, and accessing to unmapped path will be denied. + +- externalIdMapping + + Optional. An `Record` 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: + + ```ts + // Expose tags by unique name and not by ID, ie. /tag/blue intead of /tag/id + RestApiHandler({ + externalIdMapping: { + Tag: 'name' + } + }) + ``` + + Currently it is not possible to use custom index names. This also works for compound unique constraints just like for [compound IDs](#compound-id-fields). + + +## Endpoints and Features + +The RESTful API handler conforms to the the [JSON:API](https://jsonapi.org/format/) v1.1 specification for its URL design and input/output format. The following sections list the endpoints and features are implemented. The examples refer to the following schema modeling a blogging app: + +```zmodel +model User { + id Int @id @default(autoincrement()) + email String + posts Post[] +} + +model Profile { + id Int @id @default(autoincrement()) + gender String + user User @relation(fields: [userId], references: [id]) + userId Int @unique +} + +model Post { + id Int @id @default(autoincrement()) + title String + published Boolean @default(false) + viewCount Int @default(0) + author User @relation(fields: [authorId], references: [id]) + authorId Int + comments Comment[] +} + +model Comment { + id Int @id @default(autoincrement()) + content String + post Post @relation(fields: [postId], references: [id]) + postId Int +} +``` + +### Listing resources + +A specific type of resource can be listed using the following endpoint: + +``` +GET /:type +``` + +#### Status codes + +- 200: The request was successful and the response body contains the requested resources. +- 400: The request was malformed. +- 403: The request was forbidden. +- 404: The requested resource type does not exist. +- 422: The request violated data validation rules. + +#### Examples + +```ts +GET /post +``` + +```json +{ + "meta": { + "total": 1 + }, + "data": [ + { + "attributes": { + "authorId": 1, + "published": true, + "title": "My Awesome Post", + "viewCount": 0 + }, + "id": 1, + "links": { + "self": "http://myhost/api/post/1" + }, + "relationships": { + "author": { + "data": { "id": 1, "type": "user" }, + "links": { + "related": "http://myhost/api/post/1/author/1", + "self": "http://myhost/api/post/1/relationships/author/1" + } + } + }, + "type": "post" + } + ], + "jsonapi": { + "version": "1.1" + }, + "links": { + "first": "http://myhost/api/post?page%5Blimit%5D=100", + "last": "http://myhost/api/post?page%5Boffset%5D=0", + "next": null, + "prev": null, + "self": "http://myhost/api/post" + } +} +``` + +### Fetching a resource + +A unique resource can be fetched using the following endpoint: + +```ts +GET /:type/:id +``` + +#### Status codes + +- 200: The request was successful and the response body contains the requested resource. +- 400: The request was malformed. +- 403: The request was forbidden. +- 404: The requested resource type or ID does not exist. + +#### Examples + +```ts +GET /post/1 +``` + +```json +{ + "data": { + "attributes": { + "authorId": 1, + "published": true, + "title": "My Awesome Post", + "viewCount": 0 + }, + "id": 1, + "links": { + "self": "http://myhost/api/post/1" + }, + "relationships": { + "author": { + "data": { "id": 1, "type": "user" }, + "links": { + "related": "http://myhost/api/post/1/author/1", + "self": "http://myhost/api/post/1/relationships/author/1" + } + } + }, + "type": "post" + }, + "jsonapi": { + "version": "1.1" + }, + "links": { + "self": "http://myhost/api/post/1" + } +} +``` + +### Fetching relationships + +A resource's relationships can be fetched using the following endpoint: + +```ts +GET /:type/:id/relationships/:relationship +``` + +#### Status codes + +- 200: The request was successful and the response body contains the requested relationships. +- 400: The request was malformed. +- 403: The request was forbidden. +- 404: The requested resource type, ID, or relationship does not exist. + +#### Examples + +1. Fetching a to-one relationship + + ```ts + GET /post/1/relationships/author + ``` + + ```json + { + "data" : { "id" : 1, "type" : "user" }, + "jsonapi" : { + "version" : "1.1" + }, + "links" : { + "self" : "http://myhost/api/post/1/relationships/author" + } + } + ``` + +1. Fetching a to-many relationship + + ```ts + GET /user/1/relationships/posts + ``` + + ```json + { + "data" : [ + { "id" : 1, "type" : "post" }, + { "id" : 2, "type" : "post" } + ], + "jsonapi" : { + "version" : "1.1" + }, + "links" : { + "first" : "http://myhost/api/user/1/relationships/posts?page%5Blimit%5D=100", + "last" : "http://myhost/api/user/1/relationships/posts?page%5Boffset%5D=0", + "next" : null, + "prev" : null, + "self" : "http://myhost/api/user/1/relationships/posts" + } + } + ``` + +### Fetching related resources + +```ts +GET /:type/:id/:relationship +``` + +#### Status codes + +- 200: The request was successful and the response body contains the requested relationship. +- 400: The request was malformed. +- 403: The request was forbidden. +- 404: The requested resource type, ID, or relationship does not exist. + +#### Examples + +```ts +GET /post/1/author +``` + +```json +{ + "data" : { + "attributes" : { + "email" : "emily@zenstack.dev", + "name" : "Emily" + }, + "id" : 1, + "links" : { + "self" : "http://myhost/api/user/1" + }, + "relationships" : { + "posts" : { + "links" : { + "related" : "http://myhost/api/user/1/posts", + "self" : "http://myhost/api/user/1/relationships/posts" + } + } + }, + "type" : "user" + }, + "jsonapi" : { + "version" : "1.1" + }, + "links" : { + "self" : "http://myhost/api/post/1/author" + } +} +``` + +### Fine-grained data fetching + +#### Filtering + +You can use the `filter[:selector1][:selector2][...]=value` [query parameter family](https://jsonapi.org/format/#query-parameters-families) to filter resource collections or relationship collections. + +##### Examples + +1. Equality filter against plain field + + ```ts + GET /api/post?filter[published]=false + ``` + +1. Equality filter against relationship + + Relationship field can be filtered directly by its id. + + ```ts + GET /api/post?filter[author]=1 + ``` + + If the relationship is to-many, the filter has "some" semantic and evaluates to `true` if any of the items in the relationship matches. + + ```ts + GET /api/user?filter[posts]=1 + ``` + +1. Filtering with multiple values + + Multiple filter values can be separated by comma. Items satisfying any of the values will be returned. + + ```ts + GET /api/post?filter[author]=1,2 + ``` + +1. Multiple filters + + A request can carry multiple filters. Only items satisfying all filters will be returned. + + ```ts + GET /api/post?filter[author]=1&filter[published]=true + ``` + +1. Deep filtering + + A filter can carry multiple field selectors to reach into relationships. + + ```ts + GET /api/post?filter[author][name]=Emily + ``` + + When reaching into a to-many relationship, the filter has "some" semantic and evaluates to `true` if any of the items in the relationship matches. + + ``` + GET /api/user?filter[posts][published]=true + ``` + +1. Filtering with comparison operators + + Filters can go beyond equality by appending an "operator suffix". + + ```ts + GET /api/post?filter[viewCount$gt]=100 + ``` + + The following operators are supported: + + - **$lt** + + Less than + + - **$lte** + + Less than or equal to + + - **$gt** + + Greater than + + - **$gte** + + Greater than or equal to + + - **$contains** + + String contains + + - **$icontains** + + Case-insensitive string contains + + - **$search** + + String full-text search + + - **$startsWith** + + String starts with + + - **$endsWith** + + String ends with + + - **$has** + + Collection has value + + - **$hasEvery** + + Collection has every element in value + + - **$hasSome** + + Collection has some elements in value + + - **$isEmpty** + + Collection is empty + +#### Sorting + +You can use the `sort` query parameter to sort resource collections or relationship collections. The value of the parameter is a comma-separated list of fields names. The order of the fields in the list determines the order of sorting. By default, sorting is done in ascending order. To sort in descending order, prefix the field name with a minus sign. + +##### Examples + +```ts +GET /api/post?sort=createdAt,-viewCount +``` + +#### Pagination + +When creating a RESTful API handler, you can pass in a `pageSize` option to control pagination behavior of fetching a collection of resources, related resources, and relationships. By default the page size is 100, and you can disable pagination by setting `pageSize` option to `Infinity`. + +When fetching a collection resource or relationship, you can use the `page[offset]=value` and `page[limit]=value` [query parameter family](https://jsonapi.org/format/#query-parameters-families) to fetch a specific page. They're mapped to `skip` and `take` parameters in the query arguments sent to ZenStack. + +The response data of collection fetching contains pagination links that facilitate navigating through the collection. The "meta" section also contains the total count available. E.g.: + +```json +{ + "meta": { + "total": 10 + }, + "data" : [ + ... + ], + "links" : { + "first" : "http://myhost/api/post?page%5Blimit%5D=2", + "last" : "http://myhost/api/post?page%5Boffset%5D=4", + "next" : "http://myhost/api/post?page%5Boffset%5D=4&page%5Blimit%5D=2", + "prev" : "http://myhost/api/post?page%5Boffset%5D=0&page%5Blimit%5D=2", + "self" : "http://myhost/api/post" + } +} +``` + +##### Examples + +1. Fetching a specific page of resources + + ```ts + GET /api/post?page[offset]=10&page[limit]=5 + ``` + +1. Fetching a specific page of relationships + + ```ts + GET /api/user/1/relationships/posts?page[offset]=10&page[limit]=5 + ``` + +1. Fetching a specific page of related resources + + ```ts + GET /api/user/1/posts?page[offset]=10&page[limit]=5 + ``` + +#### Including related resources + +You can use the `include` query parameter to include related resources in the response. The value of the parameter is a comma-separated list of fields names. Field names can contain dots to reach into nested relationships. + +When including related resources, the response data takes the form of [Compound Documents](https://jsonapi.org/format/#document-compound-documents) and contains a `included` field carrying normalized related resources. E.g.: + +```json +{ + "data" : [ + { + "attributes" : { + ... + }, + "id" : 1, + "relationships" : { + "author" : { + "data" : { "id" : 1, "type" : "user" } + } + }, + "type" : "post" + } + ], + "included" : [ + { + "attributes" : { + "email" : "emily@zenstack.dev", + "name" : "Emily" + }, + "id" : 1, + "links" : { + "self" : "http://myhost/api/user/1" + }, + "relationships" : { + "posts" : { + "links" : { + "related" : "http://myhost/api/user/1/posts", + "self" : "http://myhost/api/user/1/relationships/posts" + } + } + }, + "type" : "user" + } + ] +} +``` + +##### Examples + +1. Including a direct relationship + + ```ts + GET /api/post?include=author + ``` + +1. Including a deep relationship + + ```ts + GET /api/post?include=author.profile + ``` + +1. Including multiple relationships + + ```ts + GET /api/post?include=author,comments + ``` + +#### Sparse Fieldsets + +You can use the `fields[type]` query parameter to only include specified fields in the response. The value of the parameter is a comma-separated list of field names. + +See the JSON:API specification for more details [Sparse Fieldsets](https://jsonapi.org/format/#fetching-sparse-fieldsets) and their [Examples](https://jsonapi.org/examples/#sparse-fieldsets) + +##### Examples + +1. Simple select + + ```ts + GET /api/post?fields[post]=title,viewCount + ``` + +1. Also works with includes or relationships + + ```ts + GET /api/post?include=author&fields[post]=title,viewCount&fields[author]=email + ``` + +### Creating a resource + +A new resource can be created using the following endpoint: + +``` +POST /:type +``` + +#### Status codes + +- 201: The request was successful and the resource was created. +- 400: The request was malformed. +- 403: The request was forbidden. +- 404: The requested resource type does not exist. + +#### Examples + +1. Creating a resource + ```json + POST /user + { + "data": { + "type": "user", + "attributes": { + "name": "Emily", + "email": "emily@zenstack.dev" + } + } + } + ``` + +1. Creating a resource with relationships attached + + ```json + POST /user + { + "data": { + "type": "user", + "attributes": { + "name": "Emily", + "email": "emily@zenstack.dev" + }, + "relationships": { + "posts": { + "data": [{ "type": "post", "id": 1 }] + } + } + } + } + ``` + +### Updating a resource + +A resource can be updated using the following endpoints: + +```ts +PUT /:type/:id +PATCH /:type/:id +``` + +Both `PUT` and `PATCH` perform partial updates with identical behavior. + +:::info +Besides plain fields, you can also include relationships in the request body. Please note that this won't update the related resource; instead if only replaces the relationships. If you update a to-many relationship, the new collection will entirely replace the old one. + +Relationships can also be manipulated directly. See [Manipulating Relationships](#manipulating-relationships) for more details. +::: + +#### Status codes + +- 200: The request was successful and the resource was updated. +- 400: The request was malformed. +- 403: The request was forbidden. +- 404: The requested resource type or ID does not exist. + +#### Examples + +1. Updating a resource + + ```json + PUT /post/1 + { + "data": { + "type": "post", + "attributes": { + "title": "My Awesome Post" + } + } + } + ``` + +1. Updating a resource's relationships + + ```json + PUT /user/1 + { + "data": { + "type": "user", + "relationships": { + "posts": { + "data": [{ "type": "post", "id": 2 }] + } + } + } + } + ``` + + +### Upserting a resource + +JSON:API didn't specify a convention for "upsert" operations. ZenStack uses a variation of the "create" operation to represent "upsert", and uses the request meta to indicate the intention. See details in [examples](#examples-10). + +``` +POST /:type +``` + +#### Status codes + +- 201: The request was successful and the resource was created. +- 200: The request was successful and the resource was updated. +- 400: The request was malformed. +- 403: The request was forbidden. +- 404: The requested resource type does not exist. + +#### Examples + +```json +POST /user +{ + "data": { + "type": "user", + "attributes": { + "id": 1, + "name": "Emily", + "email": "emily@zenstack.dev" + } + }, + "meta": { + "operation": "upsert", + "matchFields": ["id"], + } +} +``` + +The `meta.operation` field must be "upsert", and the `meta.matchFields` field must be an array of field names that are used to determine if the resource already exists. If an existing resource is found, "update" operation is conducted, otherwise "create". The `meta.matchFields` fields must be unique fields, and they must have corresponding entries in `data.attributes`. + +### Deleting a resource + +A resource can be deleted using the following endpoint: + +#### Status codes + +- 204: The request was successful and the resource was deleted. +- 403: The request was forbidden. +- 404: The requested resource type or ID does not exist. + +```ts +DELETE /:type/:id +``` + +### Manipulating relationships + +Relationships can be manipulated using the following endpoints: + +#### Adding to a to-many relationship + +```ts +POST /:type/:id/relationships/:relationship +``` + +##### Status codes + +- 200: The request was successful and the relationship was updated. +- 403: The request was forbidden. +- 404: The requested resource type, ID, or relationship does not exist. + +##### Examples + +```json +POST /user/1/relationships/posts +{ + "data": [ + { "type": "post", "id": "1" }, + { "type": "post", "id": "2" } + ] +} +``` + +#### Updating a relationship (to-one or to-many) + +```ts +PUT /:type/:id/relationships/:relationship +PATCH /:type/:id/relationships/:relationship +``` + +:::info +`PUT` and `PATCH` have identical behavior and both replace the existing relationships with the new ones entirely. +::: + +##### Status codes + +- 200: The request was successful and the relationship was updated. +- 403: The request was forbidden. +- 404: The requested resource type, ID, or relationship does not exist. + +##### Examples + +1. Replacing a to-many relationship + + ```json + PUT /user/1/relationships/posts + { + "data": [ + { "type": "post", "id": "1" }, + { "type": "post", "id": "2" } + ] + } + ``` + +1. Replacing a to-one relationship + + ```json + PUT /post/1/relationships/author + { + "data": { "type": "user", "id": "2" } + } + ``` + +1. Clearing a to-many relationship + + ```json + PUT /user/1/relationships/posts + { + "data": [] + } + ``` + +1. Clearing a to-one relationship + + ```json + PUT /post/1/relationships/author + { + "data": null + } + ``` + +## Compound ID Fields + +ZModel allows a model to have compound ID fields, e.g.: + +```zmodel +model Post { + id1 Int + id2 Int + @@id([id1, id2]) +} +``` + +The JSON:API specification doesn't have a native way to represent compound IDs. To mitigate this limitation, when returning an entity with compound IDs, ZenStack synthesizes an "id" field to carry the ID values joined with underscore: + +```json +{ + "data": { + "id": "1_2", + "attributes": { + "id1": 1, + "id2": 2, + ... + }, + "links" : { + "self" : "http://localhost:3100/api/model/post/1_2" + }, + ... + } +} +``` + +You can use this ID value convention in places where an ID is needed, e.g., reading a single entity. + +```ts +GET /post/1_2 +``` + +Limitations: + +1. Joining ID values with underscore implies that the ID values themselves cannot contain underscores. We'll make the separator configurable in the future. +1. ZModel allows you to create a name for the compound ID field. Such usage is not yet supported by the RESTful API handler. + +> *Special thanks to [Thomas Sunde Nielsen](https://github.com/thomassnielsen) for implementing this feature!* + +## Serialization + +ZenStack uses [superjson](https://github.com/blitz-js/superjson) to serialize and deserialize data. Superjson generates two parts during serialization: + +- json: + + The JSON-compatible serialization result. + +- meta: + + The serialization metadata including information like field types that facilitates deserialization. + +If the data only involves simple data types, the serialization result is the same as regular `JSON.stringify`, and no `meta` part is generated. However, for complex data types (like `Bytes`, `Decimal`, etc.), a `meta` object will be generated, which needs to be carried along when sending the request, and will also be included in the response. + +When sending requests, if superjson-serializing the request body results in a `meta` object, it should be put into a ```{ "serialization": meta }``` object and included in the `meta` field of the request body. For example, if you have a `bytes` field of type `Bytes`, the request body should look like: + +```json +POST /post +{ + "data": { + "type": "post", + "attributes": { + ... + "bytes": "AQID" // base64-encoded bytes + } + }, + "meta": { + "serialization": {"values": { "data.attributes.bytes": [[ "custom", "Bytes"]] } } + } +} +``` + +Correspondingly, the response body of a query may look like: + +```json +GET /post/1 +{ + "data": { + "id": "1", + "type": "post", + "attributes": { + ... + "bytes": "AQID" // base64-encoded bytes + } + }, + "meta": { + "serialization": {"values": { "data.attributes.bytes": [[ "custom", "Bytes"]] } } + } +} +``` + +You should use the `meta.serialization` field value to superjson-deserialize the response body. + +### Data Type Serialization Format + + + +## Error Handling + +An error response is an object containing the following fields: + +- errors + + An array of error objects, each containing the following fields: + + - code: `string`, error code + - status: `number`, HTTP status code + - title: `string`, error title + - detail: `string`, detailed error message + - reason: `string`, extra reason for the error + +### Example + +```json +{ + "errors" : [ + { + "code" : "unsupported-model", + "detail" : "Model foo doesn't exist", + "status" : 404, + "title" : "Unsupported model type" + } + ] +} +``` diff --git a/versioned_docs/version-3.x/service/api-handler/rpc.md b/versioned_docs/version-3.x/service/api-handler/rpc.md new file mode 100644 index 00000000..b9792d14 --- /dev/null +++ b/versioned_docs/version-3.x/service/api-handler/rpc.md @@ -0,0 +1,233 @@ +--- +sidebar_position: 1 +--- + +import DataTypeSerialization from './_data_type_serialization.md'; + +# RPC API Handler + +## Introduction + +The RPC-style API handler exposes CRUD endpoints that fully mirror [ZenStack's ORM API](../../orm/api/). Consuming the APIs feels like making RPC calls to a ZenStackClient then. The API handler is not meant to be used directly; instead, you should use it together with a [server adapter](../../../category/server-adapters) which handles the request and response API for a specific framework. + +It can be created and used as the following: + +```ts +import { schema } from '~/zenstack/schema'; +import { RPCApiHandler } from '@zenstackhq/server/api'; + +const handler = new RPCApiHandler({ schema }); +``` + +## Wire Format + +### Input + +For endpoints using `GET` and `DELETE` Http verbs, the query body is serialized and passed as the `q` query parameter. E.g.: + +```ts +GET /api/post/findMany?q=%7B%22where%22%3A%7B%22public%22%3Atrue%7D%7D +``` + +- Endpoint: /api/post/findMany +- Query parameters: `q` -> `{ "where" : { "public": true } }` + +For endpoints using other HTTP verbs, the query body is passed as `application/json` in the request body. E.g.: + +```json +POST /api/post/create +{ "data": { "title": "Hello World" } } +``` + +### Output + +The output shape conforms to the data structure returned by the corresponding ZenStackClient API, wrapped into a `data` field. E.g.: + +```json +GET /api/post/findMany + +{ + "data": [ { "id": 1, "title": "Hello World" } ] +} +``` + +### Serialization + +This section explains the details about data serialization. If you're using generated hooks to consume the API, the generated code already automatically deals with serialization for you, and you don't need to do any further processing. + +ZenStack uses [superjson](https://github.com/blitz-js/superjson) to serialize and deserialize data - including the `q` query parameter, the request body, and the response body. Superjson generates two parts during serialization: + +- json: + + The JSON-compatible serialization result. + +- meta: + + The serialization metadata including information like field types that facilitates deserialization. + +If the data only involves simple data types, the serialization result is the same as regular `JSON.stringify`, and no `meta` part is generated. However, for complex data types (like `Bytes`, `Decimal`, etc.), a `meta` object will be generated, which needs to be carried along when sending the request, and will also be included in the response. + +The following part explains how the `meta` information is included for different situations: + +- The `q` query parameter + + If during superjson-serialization of the `q` parameter, a `meta` object is generated, it should be put into an object `{ serialization: meta }`, JSON-stringified, and included as an additional query parameter `meta`. For example, if you have a field named `bytes` of `Bytes` type, and you may want to query with a filter like ```{ where: { bytes: Buffer.from([1,2,3]) } }```. Superjson-serializing the query object results in: + ```json + { + "json": { "where": { "bytes": "AQID" } }, // base-64 encoded bytes + "meta": { "values": { "where.bytes": [["custom","Bytes"]] } } + } + ``` + Your query URL should look like: + ```json + GET /api/post/findMany?q={"where":{"bytes":"AQID"}}&meta={"serialization":{"values":{"where.bytes":[["custom","Bytes"]]}}} + ``` + +- The request body + + If during superjson-serialization of the request body, a `meta` object is generated, it should be put into an object `{ serialization: meta }`, and included as an additional field `meta` field in the request body. For example, if you have a field named `bytes` of `Bytes` type, and you may want to create a record with a value like ```{ data: { bytes: Buffer.from([1,2,3]) } }```. Superjson-serializing the request body results in: + ```json + { + "json": { "bytes": "AQID" }, // base-64 encoded bytes + "meta": { "values": { "bytes": [[ "custom", "Bytes" ]] } } + } + ``` + Your request body should look like: + ```json + POST /api/post/create + + { + "data": { "bytes": "AQID" }, + "meta": { "serialization": {"values": { "bytes": [[ "custom","Bytes" ]] } } } + } + ``` + +- The response body + + If during superjson-serialization of the response body, a `meta` object is generated, it will be put into an object `{ serialization: meta }`, and included as an additional field `meta` field in the response body. For example, if you have a field named `bytes` of `Bytes` type, and a `findFirst` query returns ```{ id: 1, bytes: Buffer.from([1,2,3]) }```. Superjson-serializing the request body results in: + ```json + { + "json": { "id": 1, "bytes":"AQID" }, // base-64 encoded bytes + "meta": { "values": { "bytes": [[ "custom", "Bytes" ]] } } + } + ``` + Your response body will look like: + ```json + GET /api/post/findFirst + + { + "data": { "id": 1, "bytes": "AQID" }, + "meta": { "serialization": {"values": { "bytes": [[ "custom","Bytes"]] } } } + } + ``` + + You should use the meta.serialization field value to superjson-deserialize the response body. + +#### Data Type Serialization Format + + + +## Endpoints + +- **[model]/findMany** + + _Http method:_ `GET` + +- **[model]/findUnique** + + _Http method:_ `GET` + +- **[model]/findFirst** + + _Http method:_ `GET` + +- **[model]/count** + + _Http method:_ `GET` + +- **[model]/aggregate** + + _Http method:_ `GET` + +- **[model]/groupBy** + + _Http method:_ `GET` + +- **[model]/create** + + _Http method:_ `POST` + +- **[model]/createMany** + + _Http method:_ `POST` + +- **[model]/update** + + _Http method:_ `PATCH` or `PUT` + +- **[model]/updateMany** + + _Http method:_ `PATCH` or `PUT` + +- **[model]/upsert** + + _Http method:_ `POST` + +- **[model]/delete** + + _Http method:_ `DELETE` + +- **[model]/deleteMany** + + _Http method:_ `DELETE` + +- **[model]/check** + + _Http method:_ `GET` + +## HTTP Status Code and Error Responses + +### Status code + +The HTTP status code used by the endpoints follows the following rules: + +- `create` and `createMany` use `201` for success. Other endpoints use `200`. +- `400` is used for generic invalid requests, e.g., malformed request body. +- `403` is used for to indicate the request is denied due to lack of permissions, usually caused by access policy violation. +- `404` is used to indicate the requested record is not found. +- `422` is used for input validation errors. +- `500` is used for other unexpected errors. + +### Error response format + +When an error occurs, the response body will have the following shape: + +```ts +{ + body: { + error: { + // HTTP status code, same as the response status code + status: number; + + // error message + message: string; + + // extra details about the error that's causing the failure + cause?: string; + + // the model name involved in the error, if applicable + model?: string; + + // indicates if the failure is due to input validation errors + rejectedByValidation?: boolean; + + // indicates if the failure is due to access policy violation + rejectedByPolicy?: boolean; + + // detailed rejection reason, only available when + // `rejectedByValidation` or `rejectedByPolicy` is true + rejectReason?: string; + } + } +} +``` diff --git a/versioned_docs/version-3.x/service/client-sdk.md b/versioned_docs/version-3.x/service/client-sdk.md new file mode 100644 index 00000000..e625ac3d --- /dev/null +++ b/versioned_docs/version-3.x/service/client-sdk.md @@ -0,0 +1,7 @@ +--- +sidebar_position: 3 +--- + +# Client SDK 🚧 + +Coming soon. diff --git a/versioned_docs/version-3.x/service/index.md b/versioned_docs/version-3.x/service/index.md index d97b5285..3e08bda8 100644 --- a/versioned_docs/version-3.x/service/index.md +++ b/versioned_docs/version-3.x/service/index.md @@ -1,3 +1,9 @@ -# Overview +# Query-as-a-Service -Coming soon 🚧 \ No newline at end of file +## Overview + +A powerful ORM with built-in access control enables a wide range of possibilities. The most exciting one is to serve a set of data query web APIs automatically derived from your data model. + +Automatically serving data query APIs is not a new idea. Many Backend-as-a-Service (BaaS) platforms, such as Hasura, Firebase and, Supabase, provide such features. However, ZenStack takes a different approach by providing a set of server adapters that you can plug into your application. You can enjoy the convenience of the query service while still having complete control over your choice of framework and hosting environment. + +Understanding the query service involves two main concepts: [API Handler](./api-handler) and [Server Adapter](./server-adapter.md). diff --git a/versioned_docs/version-3.x/service/server-adapter.md b/versioned_docs/version-3.x/service/server-adapter.md new file mode 100644 index 00000000..2fd84e9a --- /dev/null +++ b/versioned_docs/version-3.x/service/server-adapter.md @@ -0,0 +1,49 @@ +--- +sidebar_position: 2 +--- + +import StackBlitzGithub from '@site/src/components/StackBlitzGithub'; + +# Server Adapter + +## Overview + +Server adapters are components that handle the integration with specific frameworks. They understand how to install API routes and handle framework-specific request and response objects. + +Server adapters need to be configured with an API handler that defines the API specification. The following diagram illustrates the relationship between them: + +```mermaid +flowchart TD + framework["`Web Framework + _Next.js, Express, etc._`"] + adapter[Server Adapter] + api[API Handler] + zenstack[ZenStack ORM] + framework -->|Request/Response| adapter + adapter -->|Framework Agnostic Request/Response| api + api -->|ORM Queries| zenstack +``` + +## Example + +Let's use a real example to see how API handlers and server adapters work together to serve an automatic secured data query API. A few notes about the example: + +- Express.js is used to demonstrate, but the same concept applies to other supported frameworks. +- Authentication is simulated by using the "x-userid" header. In real applications, you would use a proper authentication mechanism. +- ZModel schema is configured with access policies. +- For each request, the `getClient` call back is called to get an ORM client instance bound to the current user. + + + +## Catalog + +ZenStack currently maintains the following server adapters. New ones will be added over time based on popularity of frameworks. + +- [Next.js](../reference/server-adapters/next) +- [Nuxt](../reference/server-adapters/nuxt) +- [SvelteKit](../reference/server-adapters/sveltekit) +- [TanStack Start](../reference/server-adapters/tanstack-start) +- [Express.js](../reference/server-adapters/express) +- [Fastify](../reference/server-adapters/fastify) +- [Hono](../reference/server-adapters/hono) +- [Elysia](../reference/server-adapters/elysia)