Skip to content

feat: make nextjs adapter support next 13 app dir #483

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jun 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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