Skip to content

Commit a078b23

Browse files
authored
feat: make nextjs adapter support next 13 app dir (#483)
1 parent 8693852 commit a078b23

File tree

6 files changed

+339
-320
lines changed

6 files changed

+339
-320
lines changed

.github/workflows/build-test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ jobs:
1717

1818
strategy:
1919
matrix:
20-
node-version: [16.x]
20+
node-version: [18.x]
2121

2222
steps:
2323
- uses: actions/checkout@v3

packages/server/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151
"fastify-plugin": "^4.5.0",
5252
"isomorphic-fetch": "^3.0.0",
5353
"jest": "^29.5.0",
54-
"next": "^12.3.1",
54+
"next": "^13.4.5",
5555
"rimraf": "^3.0.2",
5656
"supertest": "^6.3.3",
5757
"ts-jest": "^29.0.5",
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/* eslint-disable @typescript-eslint/no-non-null-assertion */
2+
3+
import { DbClientContract } from '@zenstackhq/runtime';
4+
import { ModelZodSchema, getModelZodSchemas } from '@zenstackhq/runtime/zod';
5+
import { NextRequest, NextResponse } from 'next/server';
6+
import { AppRouteRequestHandlerOptions } from '.';
7+
import RPCAPIHandler from '../api/rpc';
8+
import { buildUrlQuery, marshalToObject, unmarshalFromObject } from '../utils';
9+
10+
type Context = { params: { path: string[] } };
11+
12+
/**
13+
* Creates a Next.js 13 "app dir" API route request handler which encapsulates Prisma CRUD operations.
14+
*
15+
* @param options Options for initialization
16+
* @returns An API route request handler
17+
*/
18+
export default function factory(
19+
options: AppRouteRequestHandlerOptions
20+
): (req: NextRequest, context: Context) => Promise<NextResponse> {
21+
let zodSchemas: ModelZodSchema | undefined;
22+
if (typeof options.zodSchemas === 'object') {
23+
zodSchemas = options.zodSchemas;
24+
} else if (options.zodSchemas === true) {
25+
zodSchemas = getModelZodSchemas();
26+
}
27+
28+
const requestHandler = options.handler || RPCAPIHandler();
29+
const useSuperJson = options.useSuperJson === true;
30+
31+
return async (req: NextRequest, context: Context) => {
32+
const prisma = (await options.getPrisma(req)) as DbClientContract;
33+
if (!prisma) {
34+
return NextResponse.json(
35+
marshalToObject({ message: 'unable to get prisma from request context' }, useSuperJson),
36+
{ status: 500 }
37+
);
38+
}
39+
40+
const url = new URL(req.url);
41+
let query: Record<string, string | string[]> = Object.fromEntries(url.searchParams);
42+
try {
43+
query = buildUrlQuery(query, useSuperJson);
44+
} catch {
45+
return NextResponse.json(marshalToObject({ message: 'invalid query parameters' }, useSuperJson), {
46+
status: 400,
47+
});
48+
}
49+
50+
if (!context.params.path) {
51+
return NextResponse.json(marshalToObject({ message: 'missing path parameter' }, useSuperJson), {
52+
status: 400,
53+
});
54+
}
55+
const path = context.params.path.join('/');
56+
57+
let requestBody: unknown;
58+
if (req.body) {
59+
try {
60+
requestBody = await req.json();
61+
} catch {
62+
// noop
63+
}
64+
}
65+
66+
try {
67+
const r = await requestHandler({
68+
method: req.method!,
69+
path,
70+
query,
71+
requestBody: unmarshalFromObject(requestBody, useSuperJson),
72+
prisma,
73+
modelMeta: options.modelMeta,
74+
zodSchemas,
75+
logger: options.logger,
76+
});
77+
return NextResponse.json(marshalToObject(r.body, useSuperJson), { status: r.status });
78+
} catch (err) {
79+
return NextResponse.json(
80+
marshalToObject({ message: `An unhandled error occurred: ${err}` }, useSuperJson),
81+
{ status: 500 }
82+
);
83+
}
84+
};
85+
}

packages/server/src/next/index.ts

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,52 @@
1-
export { default as NextRequestHandler } from './request-handler';
2-
export * from './request-handler';
1+
import { NextApiRequest, NextApiResponse } from 'next';
2+
import type { NextRequest } from 'next/server';
3+
import type { AdapterBaseOptions } from '../types';
4+
import { default as AppRouteHandler } from './app-route-handler';
5+
import { default as PagesRouteHandler } from './pages-route-handler';
6+
7+
/**
8+
* Options for initializing a Next.js API endpoint request handler.
9+
*/
10+
export interface PagesRouteRequestHandlerOptions extends AdapterBaseOptions {
11+
/**
12+
* Callback method for getting a Prisma instance for the given request/response pair.
13+
*/
14+
getPrisma: (req: NextApiRequest, res: NextApiResponse) => Promise<unknown> | unknown;
15+
16+
/**
17+
* Use Next.js 13 app dir or not
18+
*/
19+
useAppDir?: false | undefined;
20+
}
21+
22+
/**
23+
* Options for initializing a Next.js 13 app dir API route handler.
24+
*/
25+
export interface AppRouteRequestHandlerOptions extends AdapterBaseOptions {
26+
/**
27+
* Callback method for getting a Prisma instance for the given request.
28+
*/
29+
getPrisma: (req: NextRequest) => Promise<unknown> | unknown;
30+
31+
/**
32+
* Use Next.js 13 app dir or not
33+
*/
34+
useAppDir: true;
35+
}
36+
37+
/**
38+
* Creates a Next.js API route handler.
39+
* @see https://zenstack.dev/docs/reference/server-adapters/next
40+
*/
41+
export function NextRequestHandler(options: PagesRouteRequestHandlerOptions): ReturnType<typeof PagesRouteHandler>;
42+
export function NextRequestHandler(options: AppRouteRequestHandlerOptions): ReturnType<typeof AppRouteHandler>;
43+
export function NextRequestHandler(options: PagesRouteRequestHandlerOptions | AppRouteRequestHandlerOptions) {
44+
if (options.useAppDir === true) {
45+
return AppRouteHandler(options);
46+
} else {
47+
return PagesRouteHandler(options);
48+
}
49+
}
50+
51+
// for backward compatibility
52+
export { PagesRouteRequestHandlerOptions as RequestHandlerOptions };

packages/server/src/next/request-handler.ts renamed to packages/server/src/next/pages-route-handler.ts

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,29 +3,18 @@
33
import { DbClientContract } from '@zenstackhq/runtime';
44
import { ModelZodSchema, getModelZodSchemas } from '@zenstackhq/runtime/zod';
55
import { NextApiRequest, NextApiResponse } from 'next';
6+
import { PagesRouteRequestHandlerOptions } from '.';
67
import RPCAPIHandler from '../api/rpc';
7-
import { AdapterBaseOptions } from '../types';
88
import { buildUrlQuery, marshalToObject, unmarshalFromObject } from '../utils';
99

1010
/**
11-
* Options for initializing a Next.js API endpoint request handler.
12-
* @see requestHandler
13-
*/
14-
export interface RequestHandlerOptions extends AdapterBaseOptions {
15-
/**
16-
* Callback method for getting a Prisma instance for the given request/response pair.
17-
*/
18-
getPrisma: (req: NextApiRequest, res: NextApiResponse) => Promise<unknown> | unknown;
19-
}
20-
21-
/**
22-
* Creates a Next.js API endpoint request handler which encapsulates Prisma CRUD operations.
11+
* Creates a Next.js API endpoint (traditional "pages" route) request handler which encapsulates Prisma CRUD operations.
2312
*
2413
* @param options Options for initialization
2514
* @returns An API endpoint request handler
2615
*/
2716
export default function factory(
28-
options: RequestHandlerOptions
17+
options: PagesRouteRequestHandlerOptions
2918
): (req: NextApiRequest, res: NextApiResponse) => Promise<void> {
3019
let zodSchemas: ModelZodSchema | undefined;
3120
if (typeof options.zodSchemas === 'object') {
@@ -54,6 +43,10 @@ export default function factory(
5443
return;
5544
}
5645

46+
if (!req.query.path) {
47+
res.status(400).json(marshalToObject({ message: 'missing path parameter' }, useSuperJson));
48+
return;
49+
}
5750
const path = (req.query.path as string[]).join('/');
5851

5952
try {

0 commit comments

Comments
 (0)