Skip to content

Commit

Permalink
feat: make nextjs adapter support next 13 app dir (zenstackhq#483)
Browse files Browse the repository at this point in the history
  • Loading branch information
ymc9 authored Jun 12, 2023
1 parent 8693852 commit a078b23
Show file tree
Hide file tree
Showing 6 changed files with 339 additions and 320 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:

strategy:
matrix:
node-version: [16.x]
node-version: [18.x]

steps:
- uses: actions/checkout@v3
Expand Down
2 changes: 1 addition & 1 deletion packages/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
"fastify-plugin": "^4.5.0",
"isomorphic-fetch": "^3.0.0",
"jest": "^29.5.0",
"next": "^12.3.1",
"next": "^13.4.5",
"rimraf": "^3.0.2",
"supertest": "^6.3.3",
"ts-jest": "^29.0.5",
Expand Down
85 changes: 85 additions & 0 deletions packages/server/src/next/app-route-handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */

import { DbClientContract } from '@zenstackhq/runtime';
import { ModelZodSchema, getModelZodSchemas } from '@zenstackhq/runtime/zod';
import { NextRequest, NextResponse } from 'next/server';
import { AppRouteRequestHandlerOptions } from '.';
import RPCAPIHandler from '../api/rpc';
import { buildUrlQuery, marshalToObject, unmarshalFromObject } from '../utils';

type Context = { params: { path: string[] } };

/**
* Creates a Next.js 13 "app dir" API route request handler which encapsulates Prisma CRUD operations.
*
* @param options Options for initialization
* @returns An API route request handler
*/
export default function factory(
options: AppRouteRequestHandlerOptions
): (req: NextRequest, context: Context) => Promise<NextResponse> {
let zodSchemas: ModelZodSchema | undefined;
if (typeof options.zodSchemas === 'object') {
zodSchemas = options.zodSchemas;
} else if (options.zodSchemas === true) {
zodSchemas = getModelZodSchemas();
}

const requestHandler = options.handler || RPCAPIHandler();
const useSuperJson = options.useSuperJson === true;

return async (req: NextRequest, context: Context) => {
const prisma = (await options.getPrisma(req)) as DbClientContract;
if (!prisma) {
return NextResponse.json(
marshalToObject({ message: 'unable to get prisma from request context' }, useSuperJson),
{ status: 500 }
);
}

const url = new URL(req.url);
let query: Record<string, string | string[]> = Object.fromEntries(url.searchParams);
try {
query = buildUrlQuery(query, useSuperJson);
} catch {
return NextResponse.json(marshalToObject({ message: 'invalid query parameters' }, useSuperJson), {
status: 400,
});
}

if (!context.params.path) {
return NextResponse.json(marshalToObject({ message: 'missing path parameter' }, useSuperJson), {
status: 400,
});
}
const path = context.params.path.join('/');

let requestBody: unknown;
if (req.body) {
try {
requestBody = await req.json();
} catch {
// noop
}
}

try {
const r = await requestHandler({
method: req.method!,
path,
query,
requestBody: unmarshalFromObject(requestBody, useSuperJson),
prisma,
modelMeta: options.modelMeta,
zodSchemas,
logger: options.logger,
});
return NextResponse.json(marshalToObject(r.body, useSuperJson), { status: r.status });
} catch (err) {
return NextResponse.json(
marshalToObject({ message: `An unhandled error occurred: ${err}` }, useSuperJson),
{ status: 500 }
);
}
};
}
54 changes: 52 additions & 2 deletions packages/server/src/next/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,52 @@
export { default as NextRequestHandler } from './request-handler';
export * from './request-handler';
import { NextApiRequest, NextApiResponse } from 'next';
import type { NextRequest } from 'next/server';
import type { AdapterBaseOptions } from '../types';
import { default as AppRouteHandler } from './app-route-handler';
import { default as PagesRouteHandler } from './pages-route-handler';

/**
* Options for initializing a Next.js API endpoint request handler.
*/
export interface PagesRouteRequestHandlerOptions extends AdapterBaseOptions {
/**
* Callback method for getting a Prisma instance for the given request/response pair.
*/
getPrisma: (req: NextApiRequest, res: NextApiResponse) => Promise<unknown> | unknown;

/**
* Use Next.js 13 app dir or not
*/
useAppDir?: false | undefined;
}

/**
* Options for initializing a Next.js 13 app dir API route handler.
*/
export interface AppRouteRequestHandlerOptions extends AdapterBaseOptions {
/**
* Callback method for getting a Prisma instance for the given request.
*/
getPrisma: (req: NextRequest) => Promise<unknown> | unknown;

/**
* Use Next.js 13 app dir or not
*/
useAppDir: true;
}

/**
* Creates a Next.js API route handler.
* @see https://zenstack.dev/docs/reference/server-adapters/next
*/
export function NextRequestHandler(options: PagesRouteRequestHandlerOptions): ReturnType<typeof PagesRouteHandler>;
export function NextRequestHandler(options: AppRouteRequestHandlerOptions): ReturnType<typeof AppRouteHandler>;
export function NextRequestHandler(options: PagesRouteRequestHandlerOptions | AppRouteRequestHandlerOptions) {
if (options.useAppDir === true) {
return AppRouteHandler(options);
} else {
return PagesRouteHandler(options);
}
}

// for backward compatibility
export { PagesRouteRequestHandlerOptions as RequestHandlerOptions };
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,18 @@
import { DbClientContract } from '@zenstackhq/runtime';
import { ModelZodSchema, getModelZodSchemas } from '@zenstackhq/runtime/zod';
import { NextApiRequest, NextApiResponse } from 'next';
import { PagesRouteRequestHandlerOptions } from '.';
import RPCAPIHandler from '../api/rpc';
import { AdapterBaseOptions } from '../types';
import { buildUrlQuery, marshalToObject, unmarshalFromObject } from '../utils';

/**
* Options for initializing a Next.js API endpoint request handler.
* @see requestHandler
*/
export interface RequestHandlerOptions extends AdapterBaseOptions {
/**
* Callback method for getting a Prisma instance for the given request/response pair.
*/
getPrisma: (req: NextApiRequest, res: NextApiResponse) => Promise<unknown> | unknown;
}

/**
* Creates a Next.js API endpoint request handler which encapsulates Prisma CRUD operations.
* Creates a Next.js API endpoint (traditional "pages" route) request handler which encapsulates Prisma CRUD operations.
*
* @param options Options for initialization
* @returns An API endpoint request handler
*/
export default function factory(
options: RequestHandlerOptions
options: PagesRouteRequestHandlerOptions
): (req: NextApiRequest, res: NextApiResponse) => Promise<void> {
let zodSchemas: ModelZodSchema | undefined;
if (typeof options.zodSchemas === 'object') {
Expand Down Expand Up @@ -54,6 +43,10 @@ export default function factory(
return;
}

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

try {
Expand Down
Loading

0 comments on commit a078b23

Please sign in to comment.