Skip to content

Commit e12fc5a

Browse files
authored
feat: express.js adapter (#271)
1 parent b7548d1 commit e12fc5a

File tree

25 files changed

+629
-76
lines changed

25 files changed

+629
-76
lines changed

README.md

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,12 +104,37 @@ The following diagram gives a high-level overview of how it works.
104104
## Features
105105

106106
- Access control and data validation rules right inside your Prisma schema
107-
- Auto-generated RESTful API and client library
107+
- Auto-generated OpenAPI (RESTful) specifications, services, and client libraries
108108
- End-to-end type safety
109109
- Extensible: custom attributes, functions, and a plugin system
110-
- Framework agnostic
110+
- A framework-agnostic core with framework-specific adapters
111111
- Uncompromised performance
112112

113+
### Plugins
114+
115+
- Prisma schema generator
116+
- Zod schema generator
117+
- React hooks generator
118+
- OpenAPI specification generator
119+
- [tRPC](https://trpc.io) router generator
120+
- 🙋🏻 [Request for a plugin](https://go.zenstack.dev/chat)
121+
122+
### Framework adapters
123+
124+
- [Next.js](https://zenstack.dev/docs/reference/server-adapters/next)
125+
- [Fastify](https://zenstack.dev/docs/reference/server-adapters/fastify)
126+
- [ExpressJS](https://zenstack.dev/docs/reference/server-adapters/express)
127+
- Nuxt.js (Future)
128+
- SvelteKit (Future)
129+
- 🙋🏻 [Request for an adapter](https://go.zenstack.dev/chat)
130+
131+
### Prisma schema extensions
132+
133+
- [Custom attributes and functions](https://zenstack.dev/docs/reference/zmodel-language#custom-attributes-and-functions)
134+
- Multi-file schema (coming soon)
135+
- String-typed JSON field (coming soon)
136+
- 🙋🏻 [Request for an extension](https://go.zenstack.dev/chat)
137+
113138
## Examples
114139

115140
Check out the [Collaborative Todo App](https://zenstack-todo.vercel.app/) for a running example. You can find the source code below:

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "zenstack-monorepo",
3-
"version": "1.0.0-alpha.73",
3+
"version": "1.0.0-alpha.74",
44
"description": "",
55
"scripts": {
66
"build": "pnpm -r build",

packages/language/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@zenstackhq/language",
3-
"version": "1.0.0-alpha.73",
3+
"version": "1.0.0-alpha.74",
44
"displayName": "ZenStack modeling language compiler",
55
"description": "ZenStack modeling language compiler",
66
"homepage": "https://zenstack.dev",

packages/next/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@zenstackhq/next",
3-
"version": "1.0.0-alpha.73",
3+
"version": "1.0.0-alpha.74",
44
"displayName": "ZenStack Next.js integration",
55
"description": "ZenStack Next.js integration",
66
"homepage": "https://zenstack.dev",

packages/plugins/openapi/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@zenstackhq/openapi",
33
"displayName": "ZenStack Plugin and Runtime for OpenAPI",
4-
"version": "1.0.0-alpha.73",
4+
"version": "1.0.0-alpha.74",
55
"description": "ZenStack plugin and runtime supporting OpenAPI",
66
"main": "index.js",
77
"repository": {

packages/plugins/react/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@zenstackhq/react",
33
"displayName": "ZenStack plugin and runtime for ReactJS",
4-
"version": "1.0.0-alpha.73",
4+
"version": "1.0.0-alpha.74",
55
"description": "ZenStack plugin and runtime for ReactJS",
66
"main": "index.js",
77
"repository": {

packages/plugins/trpc/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@zenstackhq/trpc",
33
"displayName": "ZenStack plugin for tRPC",
4-
"version": "1.0.0-alpha.73",
4+
"version": "1.0.0-alpha.74",
55
"description": "ZenStack plugin for tRPC",
66
"main": "index.js",
77
"repository": {

packages/runtime/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@zenstackhq/runtime",
33
"displayName": "ZenStack Runtime Library",
4-
"version": "1.0.0-alpha.73",
4+
"version": "1.0.0-alpha.74",
55
"description": "Runtime of ZenStack for both client-side and server-side environments.",
66
"repository": {
77
"type": "git",

packages/runtime/src/zod.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ export function getModelZodSchemas(): ModelZodSchema {
1414
// eslint-disable-next-line @typescript-eslint/no-var-requires
1515
return require('.zenstack/zod').default;
1616
} catch {
17-
throw new Error('Model meta cannot be loaded');
17+
throw new Error(
18+
'Zod schemas cannot be loaded. Please make sure "@core/zod" plugin is enabled in schema.zmodel.'
19+
);
1820
}
1921
}

packages/schema/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"publisher": "zenstack",
44
"displayName": "ZenStack Language Tools",
55
"description": "A toolkit for building secure CRUD apps with Next.js + Typescript",
6-
"version": "1.0.0-alpha.73",
6+
"version": "1.0.0-alpha.74",
77
"author": {
88
"name": "ZenStack Team"
99
},

packages/schema/src/cli/plugin-runner.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export class PluginRunner {
3030
}> = [];
3131

3232
const pluginDecls = context.schema.declarations.filter((d): d is Plugin => isPlugin(d));
33-
const prereqPlugins = ['@core/prisma', '@core/model-meta', '@core/access-policy', '@core/zod'];
33+
const prereqPlugins = ['@core/prisma', '@core/model-meta', '@core/access-policy'];
3434
const allPluginProviders = prereqPlugins.concat(
3535
pluginDecls
3636
.map((p) => this.getPluginProvider(p))

packages/schema/src/res/stdlib.zmodel

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ attribute @map(_ name: String) @@@prisma
154154
attribute @@map(_ name: String) @@@prisma
155155

156156
/*
157-
* Exclude a field from the Prisma Client (for example, a model that you do not want Prisma users to update).
157+
* Exclude a field from the Prisma Client (for example, a field that you do not want Prisma users to update).
158158
*/
159159
attribute @ignore() @@@prisma
160160

packages/sdk/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@zenstackhq/sdk",
3-
"version": "1.0.0-alpha.73",
3+
"version": "1.0.0-alpha.74",
44
"description": "ZenStack plugin development SDK",
55
"main": "index.js",
66
"scripts": {

packages/server/package.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@zenstackhq/server",
3-
"version": "1.0.0-alpha.73",
3+
"version": "1.0.0-alpha.74",
44
"displayName": "ZenStack Server-side Adapters",
55
"description": "ZenStack server-side adapters",
66
"homepage": "https://zenstack.dev",
@@ -31,13 +31,19 @@
3131
"zod-validation-error": "^0.2.1"
3232
},
3333
"devDependencies": {
34+
"@types/body-parser": "^1.19.2",
35+
"@types/express": "^4.17.17",
3436
"@types/jest": "^29.4.0",
37+
"@types/supertest": "^2.0.12",
3538
"@zenstackhq/testtools": "workspace:*",
39+
"body-parser": "^1.20.2",
3640
"copyfiles": "^2.4.1",
41+
"express": "^4.18.2",
3742
"fastify": "^4.14.1",
3843
"fastify-plugin": "^4.5.0",
3944
"jest": "^29.4.3",
4045
"rimraf": "^3.0.2",
46+
"supertest": "^6.3.3",
4147
"ts-jest": "^29.0.5",
4248
"typescript": "^4.9.4"
4349
}

packages/server/src/express/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default as ZenStackMiddleware } from './middleware';
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/* eslint-disable @typescript-eslint/no-explicit-any */
2+
import { DbClientContract } from '@zenstackhq/runtime';
3+
import { getModelZodSchemas, ModelZodSchema } from '@zenstackhq/runtime/zod';
4+
import type { Handler, Request, Response } from 'express';
5+
import { handleRequest, LoggerConfig } from '../openapi';
6+
7+
/**
8+
* Express middleware options
9+
*/
10+
export interface MiddlewareOptions {
11+
/**
12+
* Callback for getting a PrismaClient for the given request
13+
*/
14+
getPrisma: (req: Request, res: Response) => unknown | Promise<unknown>;
15+
16+
/**
17+
* Logger settings
18+
*/
19+
logger?: LoggerConfig;
20+
21+
/**
22+
* Zod schemas for validating request input. Pass `true` to load from standard location (need to enable `@core/zod` plugin in schema.zmodel).
23+
*/
24+
zodSchemas?: ModelZodSchema | boolean;
25+
}
26+
27+
/**
28+
* Creates an Express middleware for handling CRUD requests.
29+
*/
30+
const factory = (options: MiddlewareOptions): Handler => {
31+
let schemas: ModelZodSchema | undefined;
32+
if (typeof options.zodSchemas === 'object') {
33+
schemas = options.zodSchemas;
34+
} else if (options.zodSchemas === true) {
35+
schemas = getModelZodSchemas();
36+
}
37+
38+
return async (request, response) => {
39+
const prisma = (await options.getPrisma(request, response)) as DbClientContract;
40+
if (!prisma) {
41+
throw new Error('unable to get prisma from request context');
42+
}
43+
44+
const r = await handleRequest({
45+
method: request.method,
46+
path: request.path,
47+
query: request.query as Record<string, string | string[]>,
48+
requestBody: request.body,
49+
prisma,
50+
logger: options.logger,
51+
zodSchemas: schemas,
52+
});
53+
54+
response.status(r.status).json(r.body);
55+
};
56+
};
57+
58+
export default factory;

packages/server/src/fastify/plugin.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* eslint-disable @typescript-eslint/no-explicit-any */
22
import { DbClientContract } from '@zenstackhq/runtime';
3-
import { ModelZodSchema } from '@zenstackhq/runtime/zod';
3+
import { getModelZodSchemas, ModelZodSchema } from '@zenstackhq/runtime/zod';
44
import { FastifyPluginCallback, FastifyReply, FastifyRequest } from 'fastify';
55
import fp from 'fastify-plugin';
66
import { handleRequest, LoggerConfig } from '../openapi';
@@ -15,7 +15,7 @@ export interface PluginOptions {
1515
prefix: string;
1616

1717
/**
18-
* Callback for gettign a PrismaClient for the given request
18+
* Callback for getting a PrismaClient for the given request
1919
*/
2020
getPrisma: (request: FastifyRequest, reply: FastifyReply) => unknown | Promise<unknown>;
2121

@@ -25,11 +25,14 @@ export interface PluginOptions {
2525
logger?: LoggerConfig;
2626

2727
/**
28-
* Path to the generated zod schemas
28+
* Zod schemas for validating request input. Pass `true` to load from standard location (need to enable `@core/zod` plugin in schema.zmodel).
2929
*/
30-
zodSchemas?: ModelZodSchema;
30+
zodSchemas?: ModelZodSchema | boolean;
3131
}
3232

33+
/**
34+
* Fastify plugin for handling CRUD requests.
35+
*/
3336
const pluginHandler: FastifyPluginCallback<PluginOptions> = (fastify, options, done) => {
3437
const prefix = options.prefix ?? '';
3538

@@ -39,6 +42,13 @@ const pluginHandler: FastifyPluginCallback<PluginOptions> = (fastify, options, d
3942
options.logger?.info?.(`ZenStackPlugin installing routes at prefix: ${prefix}`);
4043
}
4144

45+
let schemas: ModelZodSchema | undefined;
46+
if (typeof options.zodSchemas === 'object') {
47+
schemas = options.zodSchemas;
48+
} else if (options.zodSchemas === true) {
49+
schemas = getModelZodSchemas();
50+
}
51+
4252
fastify.all(`${prefix}/*`, async (request, reply) => {
4353
const prisma = (await options.getPrisma(request, reply)) as DbClientContract;
4454
if (!prisma) {
@@ -53,7 +63,7 @@ const pluginHandler: FastifyPluginCallback<PluginOptions> = (fastify, options, d
5363
requestBody: request.body,
5464
prisma,
5565
logger: options.logger,
56-
zodSchemas: options.zodSchemas,
66+
zodSchemas: schemas,
5767
});
5868

5969
reply.status(response.status).send(response.body);

packages/server/src/openapi/index.ts

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
isPrismaClientUnknownRequestError,
66
isPrismaClientValidationError,
77
} from '@zenstackhq/runtime';
8-
import { getModelZodSchemas, ModelZodSchema } from '@zenstackhq/runtime/zod';
8+
import type { ModelZodSchema } from '@zenstackhq/runtime/zod';
99
import { capitalCase } from 'change-case';
1010
import invariant from 'tiny-invariant';
1111
import { fromZodError } from 'zod-validation-error';
@@ -55,10 +55,7 @@ export type Response = {
5555
body: unknown;
5656
};
5757

58-
function getZodSchema(zodSchemas: ModelZodSchema | undefined, model: string, operation: keyof DbOperations) {
59-
if (!zodSchemas) {
60-
zodSchemas = getModelZodSchemas();
61-
}
58+
function getZodSchema(zodSchemas: ModelZodSchema, model: string, operation: keyof DbOperations) {
6259
if (zodSchemas[model]) {
6360
return zodSchemas[model][operation];
6461
} else if (zodSchemas[capitalCase(model)]) {
@@ -74,7 +71,7 @@ function zodValidate(
7471
operation: keyof DbOperations,
7572
args: unknown
7673
) {
77-
const zodSchema = getZodSchema(zodSchemas, model, operation);
74+
const zodSchema = zodSchemas && getZodSchema(zodSchemas, model, operation);
7875
if (zodSchema) {
7976
const parseResult = zodSchema.safeParse(args);
8077
if (parseResult.success) {
@@ -168,11 +165,13 @@ export async function handleRequest({
168165
return { status: 400, body: { message: 'invalid operation: ' + op } };
169166
}
170167

171-
const { data, error } = zodValidate(zodSchemas, model, dbOp, args);
172-
if (error) {
173-
return { status: 400, body: { message: error } };
174-
} else {
175-
args = data;
168+
if (zodSchemas) {
169+
const { data, error } = zodValidate(zodSchemas, model, dbOp, args);
170+
if (error) {
171+
return { status: 400, body: { message: error } };
172+
} else {
173+
args = data;
174+
}
176175
}
177176

178177
try {

0 commit comments

Comments
 (0)