Skip to content

Commit 1f5b1cb

Browse files
committed
feat(server): migrate next.js server adapter
1 parent cccd344 commit 1f5b1cb

File tree

10 files changed

+936
-8
lines changed

10 files changed

+936
-8
lines changed

packages/server/package.json

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,14 +66,20 @@
6666
"@zenstackhq/typescript-config": "workspace:*",
6767
"@zenstackhq/vitest-config": "workspace:*",
6868
"body-parser": "^2.2.0",
69-
"supertest": "^7.1.4"
69+
"supertest": "^7.1.4",
70+
"express": "^5.0.0",
71+
"next": "^15.0.0"
7072
},
7173
"peerDependencies": {
72-
"express": "^5.0.0"
74+
"express": "^5.0.0",
75+
"next": "^15.0.0"
7376
},
7477
"peerDependenciesMeta": {
7578
"express": {
7679
"optional": true
80+
},
81+
"next": {
82+
"optional": true
7783
}
7884
}
7985
}

packages/server/src/express/middleware.ts renamed to packages/server/src/adapter/express/middleware.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
import type { ClientContract } from '@zenstackhq/orm';
22
import type { SchemaDef } from '@zenstackhq/orm/schema';
33
import type { Handler, Request, Response } from 'express';
4-
import type { ApiHandler } from '../types';
4+
import type { ApiHandler } from '../../types';
55

66
/**
77
* Express middleware options
88
*/
99
export interface MiddlewareOptions<Schema extends SchemaDef> {
10+
/**
11+
* The API handler to process requests
12+
*/
1013
apiHandler: ApiHandler<Schema>;
1114

1215
/**
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import type { SchemaDef } from '@zenstackhq/orm/schema';
2+
import { NextRequest, NextResponse } from 'next/server';
3+
import type { AppRouteRequestHandlerOptions } from '.';
4+
5+
type Context = { params: Promise<{ path: string[] }> };
6+
7+
/**
8+
* Creates a Next.js "app router" API route request handler that handles ZenStack CRUD requests.
9+
*
10+
* @param options Options for initialization
11+
* @returns An API route request handler
12+
*/
13+
export default function factory<Schema extends SchemaDef>(
14+
options: AppRouteRequestHandlerOptions<Schema>,
15+
): (req: NextRequest, context: Context) => Promise<NextResponse> {
16+
return async (req: NextRequest, context: Context) => {
17+
const client = await options.getClient(req);
18+
if (!client) {
19+
return NextResponse.json({ message: 'unable to get ZenStackClient from request context' }, { status: 500 });
20+
}
21+
22+
let params: Awaited<Context['params']>;
23+
const url = new URL(req.url);
24+
const query = Object.fromEntries(url.searchParams);
25+
26+
try {
27+
params = await context.params;
28+
} catch {
29+
return NextResponse.json({ message: 'Failed to resolve request parameters' }, { status: 500 });
30+
}
31+
32+
if (!params.path) {
33+
return NextResponse.json(
34+
{ message: 'missing path parameter' },
35+
{
36+
status: 400,
37+
},
38+
);
39+
}
40+
const path = params.path.join('/');
41+
42+
let requestBody: unknown;
43+
if (req.body) {
44+
try {
45+
requestBody = await req.json();
46+
} catch {
47+
// noop
48+
}
49+
}
50+
51+
try {
52+
const r = await options.apiHandler.handleRequest({
53+
method: req.method!,
54+
path,
55+
query,
56+
requestBody,
57+
client,
58+
});
59+
return NextResponse.json(r.body, { status: r.status });
60+
} catch (err) {
61+
return NextResponse.json({ message: `An unhandled error occurred: ${err}` }, { status: 500 });
62+
}
63+
};
64+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import type { ClientContract } from '@zenstackhq/orm';
2+
import type { SchemaDef } from '@zenstackhq/orm/schema';
3+
import type { NextApiRequest, NextApiResponse } from 'next';
4+
import type { NextRequest } from 'next/server';
5+
import type { ApiHandler } from '../../types';
6+
import { default as AppRouteHandler } from './app-route-handler';
7+
import { default as PagesRouteHandler } from './pages-route-handler';
8+
9+
interface CommonAdapterOptions<Schema extends SchemaDef> {
10+
/**
11+
* The API handler to process requests
12+
*/
13+
apiHandler: ApiHandler<Schema>;
14+
}
15+
16+
/**
17+
* Options for initializing a Next.js API endpoint request handler.
18+
*/
19+
export interface PageRouteRequestHandlerOptions<Schema extends SchemaDef> extends CommonAdapterOptions<Schema> {
20+
/**
21+
* Callback for getting a ZenStackClient for the given request
22+
*/
23+
getClient: (req: NextApiRequest, res: NextApiResponse) => ClientContract<Schema> | Promise<ClientContract<Schema>>;
24+
25+
/**
26+
* Use app dir or not
27+
*/
28+
useAppDir?: false | undefined;
29+
}
30+
31+
/**
32+
* Options for initializing a Next.js 13 app dir API route handler.
33+
*/
34+
export interface AppRouteRequestHandlerOptions<Schema extends SchemaDef> extends CommonAdapterOptions<Schema> {
35+
/**
36+
* Callback method for getting a Prisma instance for the given request.
37+
*/
38+
getClient: (req: NextRequest) => ClientContract<Schema> | Promise<ClientContract<Schema>>;
39+
40+
/**
41+
* Use app dir or not
42+
*/
43+
useAppDir: true;
44+
}
45+
46+
/**
47+
* Creates a Next.js API route handler.
48+
*/
49+
export function NextRequestHandler<Schema extends SchemaDef>(
50+
options: PageRouteRequestHandlerOptions<Schema>,
51+
): ReturnType<typeof PagesRouteHandler>;
52+
export function NextRequestHandler<Schema extends SchemaDef>(
53+
options: AppRouteRequestHandlerOptions<Schema>,
54+
): ReturnType<typeof AppRouteHandler>;
55+
export function NextRequestHandler<Schema extends SchemaDef>(
56+
options: PageRouteRequestHandlerOptions<Schema> | AppRouteRequestHandlerOptions<Schema>,
57+
) {
58+
if (options.useAppDir === true) {
59+
return AppRouteHandler(options);
60+
} else {
61+
return PagesRouteHandler(options);
62+
}
63+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import type { SchemaDef } from '@zenstackhq/orm/schema';
2+
import type { NextApiRequest, NextApiResponse } from 'next';
3+
import type { PageRouteRequestHandlerOptions } from '.';
4+
5+
/**
6+
* Creates a Next.js API endpoint "pages" router request handler that handles ZenStack CRUD requests.
7+
*
8+
* @param options Options for initialization
9+
* @returns An API endpoint request handler
10+
*/
11+
export default function factory<Schema extends SchemaDef>(
12+
options: PageRouteRequestHandlerOptions<Schema>,
13+
): (req: NextApiRequest, res: NextApiResponse) => Promise<void> {
14+
return async (req: NextApiRequest, res: NextApiResponse) => {
15+
const client = await options.getClient(req, res);
16+
if (!client) {
17+
res.status(500).json({ message: 'unable to get ZenStackClient from request context' });
18+
return;
19+
}
20+
21+
if (!req.query['path']) {
22+
res.status(400).json({ message: 'missing path parameter' });
23+
return;
24+
}
25+
const path = (req.query['path'] as string[]).join('/');
26+
27+
try {
28+
const r = await options.apiHandler.handleRequest({
29+
method: req.method!,
30+
path,
31+
query: req.query as Record<string, string | string[]>,
32+
requestBody: req.body,
33+
client,
34+
});
35+
res.status(r.status).send(r.body);
36+
} catch (err) {
37+
res.status(500).send({ message: `An unhandled error occurred: ${err}` });
38+
}
39+
};
40+
}

packages/server/test/adapter/express.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import bodyParser from 'body-parser';
33
import express from 'express';
44
import request from 'supertest';
55
import { describe, expect, it } from 'vitest';
6+
import { ZenStackMiddleware } from '../../src/adapter/express';
67
import { RPCApiHandler } from '../../src/api';
7-
import { ZenStackMiddleware } from '../../src/express';
88
import { makeUrl, schema } from '../utils';
99

1010
describe('Express adapter tests - rpc handler', () => {

0 commit comments

Comments
 (0)