Skip to content

feat: copy nextjs adapter over to server package #420

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 1 commit into from
May 19, 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
1 change: 1 addition & 0 deletions packages/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"fastify-plugin": "^4.5.0",
"isomorphic-fetch": "^3.0.0",
"jest": "^29.5.0",
"next": "^12.3.1",
"rimraf": "^3.0.2",
"supertest": "^6.3.3",
"ts-jest": "^29.0.5",
Expand Down
2 changes: 2 additions & 0 deletions packages/server/src/next/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default as NextRequestHandler } from './request-handler';
export * from './request-handler';
177 changes: 177 additions & 0 deletions packages/server/src/next/request-handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import {
DbClientContract,
DbOperations,
isPrismaClientKnownRequestError,
isPrismaClientUnknownRequestError,
isPrismaClientValidationError,
} from '@zenstackhq/runtime';
import { NextApiRequest, NextApiResponse } from 'next';
import { logError } from '../api/utils';
import { AdapterBaseOptions } from '../types';
import { marshalToObject, unmarshalFromObject, unmarshalFromString } 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.
*
* @param options Options for initialization
* @returns An API endpoint request handler
*/
export default function requestHandler(
options: RequestHandlerOptions
): (req: NextApiRequest, res: NextApiResponse) => Promise<void> {
return async (req: NextApiRequest, res: NextApiResponse) => {
const prisma = await options.getPrisma(req, res);
if (!prisma) {
sendResponse(
res,
500,
{
error: 'unable to get prisma from request context',
},
options.useSuperJson === true
);
return;
}
return handleRequest(req, res, prisma as DbClientContract, options);
};
}

async function handleRequest(
req: NextApiRequest,
res: NextApiResponse,
prisma: DbClientContract,
options: RequestHandlerOptions
): Promise<void> {
const [model, op] = req.query.path as string[];

const dbOp = op as keyof DbOperations;
let args: unknown;
let resCode = 200;
const useSuperJson = options.useSuperJson === true;

switch (dbOp) {
case 'create':
case 'createMany':
case 'upsert':
if (req.method !== 'POST') {
sendResponse(res, 400, { error: 'invalid http method' }, useSuperJson);
return;
}
args = unmarshalFromObject(req.body, options.useSuperJson);
// TODO: upsert's status code should be conditional
resCode = 201;
break;

case 'findFirst':
case 'findUnique':
case 'findMany':
case 'aggregate':
case 'groupBy':
case 'count':
if (req.method !== 'GET') {
sendResponse(res, 400, { error: 'invalid http method' }, useSuperJson);
return;
}
args = req.query.q ? unmarshalFromString(req.query.q as string, options.useSuperJson) : {};
break;

case 'update':
case 'updateMany':
if (req.method !== 'PUT' && req.method !== 'PATCH') {
sendResponse(res, 400, { error: 'invalid http method' }, useSuperJson);
return;
}
args = unmarshalFromObject(req.body, options.useSuperJson);
break;

case 'delete':
case 'deleteMany':
if (req.method !== 'DELETE') {
sendResponse(res, 400, { error: 'invalid http method' }, useSuperJson);
return;
}
args = req.query.q ? unmarshalFromString(req.query.q as string, options.useSuperJson) : {};
break;

default:
sendResponse(res, 400, { error: `unknown method name: ${op}` }, useSuperJson);
return;
}

try {
if (!prisma[model]) {
sendResponse(res, 400, { error: `unknown model name: ${model}` }, useSuperJson);
return;
}
const result = await prisma[model][dbOp](args);
sendResponse(res, resCode, result, useSuperJson);
} catch (err) {
if (isPrismaClientKnownRequestError(err)) {
logError(options.logger, err.message, err.code);
if (err.code === 'P2004') {
// rejected by policy
sendResponse(
res,
403,
{
prisma: true,
rejectedByPolicy: true,
code: err.code,
message: err.message,
reason: err.meta?.reason,
},
useSuperJson
);
} else {
sendResponse(
res,
400,
{
prisma: true,
code: err.code,
message: err.message,
reason: err.meta?.reason,
},
useSuperJson
);
}
} else if (isPrismaClientUnknownRequestError(err) || isPrismaClientValidationError(err)) {
logError(options.logger, err.message);
sendResponse(
res,
400,
{
prisma: true,
message: err.message,
},
useSuperJson
);
} else {
const _err = err as Error;
logError(options.logger, _err.message + (_err.stack ? '\n' + _err.stack : ''));
sendResponse(
res,
500,
{
message: (err as Error).message,
},
useSuperJson
);
}
}
}

function sendResponse(res: NextApiResponse, status: number, data: unknown, useSuperJson: boolean): void {
res.status(status).send(marshalToObject(data, useSuperJson));
}
Loading