api architecture

This commit is contained in:
Timur Ercan
2023-01-09 17:10:36 +01:00
parent 579e1333b3
commit 79670b4ab4
10 changed files with 171 additions and 90 deletions

View File

@ -7,7 +7,10 @@ const nextConfig = {
distDir: "build",
};
const withTM = require("next-transpile-modules")(["@documenso/prisma"]);
const withTM = require("next-transpile-modules")([
"@documenso/prisma",
"@documenso/lib",
]);
const plugins = [];
plugins.push(withTM);

View File

@ -1,49 +1 @@
import PrismaClient from "@documenso/prisma";
import type { NextApiRequest, NextApiResponse } from "next";
export default async function userHandler(
req: NextApiRequest,
res: NextApiResponse
) {
const { method, body } = req;
const prisma = new PrismaClient();
// Check Session
switch (method) {
case "POST":
if (!body.userId) {
res.status(400).end("Owner ID cannot be empty.");
}
try {
const newDocument: any = await prisma.document
.create({
data: { userId: body.userId, document: body.document },
})
.then(async () => {
await prisma.$disconnect();
res.status(200).send(newDocument);
});
} catch (error) {
await prisma.$disconnect();
res.status(500).end("An error has occured.");
}
break;
case "GET":
// GET all docs for user in session
let documents = await prisma.document.findMany({
where: {
userId: body.userId,
},
});
res.status(200).send(documents);
break;
default:
res.setHeader("Allow", ["GET", "POST"]);
res.status(405).end(`Method ${method} Not Allowed`);
}
}
export {};

View File

@ -1,12 +1,19 @@
import type { NextApiRequest, NextApiResponse } from "next";
type Data = {
import { defaultHandler, defaultResponder } from "@documenso/lib/server";
import prisma from "@documenso/prisma";
type responseData = {
status: string;
};
export default function handler(
req: NextApiRequest,
res: NextApiResponse<Data>
) {
res.status(200).json({ status: "Api up and running :)" });
// Return a healthy 200 status code for uptime monitoring and render.com zero-downtime-deploy
async function getHandler(req: NextApiRequest, res: NextApiResponse) {
// A generic database access to make sure the service is healthy.
const users = await prisma.user.findFirst();
res.status(200).json({ message: "Api up and running :)" });
}
export default defaultHandler({
GET: Promise.resolve({ default: defaultResponder(getHandler) }),
});

View File

@ -1,42 +1,31 @@
// POST to create
import PrismaClient from "@documenso/prisma";
import User from "@documenso/prisma";
import {
defaultHandler,
defaultResponder,
HttpError,
} from "@documenso/lib/server";
import prisma from "@documenso/prisma";
import type { NextApiRequest, NextApiResponse } from "next";
import { json } from "stream/consumers";
export default async function userHandler(
req: NextApiRequest,
res: NextApiResponse<User>
) {
async function postHandler(req: NextApiRequest, res: NextApiResponse) {
const { method, body } = req;
const prisma = new PrismaClient();
switch (method) {
case "POST":
if (!body.email) {
res.status(400).end("Email cannot be empty.");
}
try {
let newUser: any;
newUser = await prisma.user
.create({
data: { email: body.email },
})
.then(async () => {
await prisma.$disconnect();
res.status(200).send(newUser);
});
} catch (error) {
await prisma.$disconnect();
res.status(500).end("An error has occured. Error: " + error);
}
break;
default:
res.setHeader("Allow", ["POST"]);
res.status(405).end(`Method ${method} Not Allowed`);
if (!body.email) {
return res.status(400).json({ message: "Email cannot be empty." });
}
let newUser: any;
newUser = await prisma.user
.create({
data: { email: body.email },
})
.then(async () => {
return res.status(201).send(newUser);
});
}
export default defaultHandler({
POST: Promise.resolve({ default: defaultResponder(postHandler) }),
});

View File

@ -0,0 +1,24 @@
import type { NextApiHandler, NextApiRequest, NextApiResponse } from "next";
type Handlers = {
[method in "GET" | "POST" | "PATCH" | "PUT" | "DELETE"]?: Promise<{ default: NextApiHandler }>;
};
/** Allows us to split big API handlers by method */
export const defaultHandler = (handlers: Handlers) => async (req: NextApiRequest, res: NextApiResponse) => {
const handler = (await handlers[req.method as keyof typeof handlers])?.default;
// auto catch unsupported methods.
if (!handler) {
return res
.status(405)
.json({ message: `Method Not Allowed (Allow: ${Object.keys(handlers).join(",")})` });
}
try {
await handler(req, res);
return;
} catch (error) {
console.error(error);
return res.status(500).json({ message: "Something went wrong" });
}
};

View File

@ -0,0 +1,20 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { getServerErrorFromUnknown } from "@documenso/lib/server";
type Handle<T> = (req: NextApiRequest, res: NextApiResponse) => Promise<T>;
/** Allows us to get type inference from API handler responses */
export function defaultResponder<T>(f: Handle<T>) {
return async (req: NextApiRequest, res: NextApiResponse) => {
try {
const result = await f(req, res);
if (result) res.json(result);
} catch (err) {
console.error(err);
const error = getServerErrorFromUnknown(err);
res.statusCode = error.statusCode;
res.json({ message: error.message });
}
};
}

View File

@ -0,0 +1,43 @@
import {
PrismaClientKnownRequestError,
NotFoundError,
} from "@prisma/client/runtime";
import { HttpError } from "@documenso/lib/server";
export function getServerErrorFromUnknown(cause: unknown): HttpError {
// Error was manually thrown and does not need to be parsed.
if (cause instanceof HttpError) {
return cause;
}
if (cause instanceof SyntaxError) {
return new HttpError({
statusCode: 500,
message: "Unexpected error, please reach out for our customer support.",
});
}
if (cause instanceof PrismaClientKnownRequestError) {
return new HttpError({ statusCode: 400, message: cause.message, cause });
}
if (cause instanceof NotFoundError) {
return new HttpError({ statusCode: 404, message: cause.message, cause });
}
if (cause instanceof Error) {
return new HttpError({ statusCode: 500, message: cause.message, cause });
}
if (typeof cause === "string") {
// @ts-expect-error https://github.com/tc39/proposal-error-cause
return new Error(cause, { cause });
}
// Catch-All if none of the above triggered and something (even more) unexpected happend
return new HttpError({
statusCode: 500,
message: `Unhandled error of type '${typeof cause}'. Please reach out for our customer support.`,
});
}

View File

@ -0,0 +1,33 @@
export class HttpError<TCode extends number = number> extends Error {
public readonly cause?: Error;
public readonly statusCode: TCode;
public readonly message: string;
public readonly url: string | undefined;
public readonly method: string | undefined;
constructor(opts: { url?: string; method?: string; message?: string; statusCode: TCode; cause?: Error }) {
super(opts.message ?? `HTTP Error ${opts.statusCode} `);
Object.setPrototypeOf(this, HttpError.prototype);
this.name = HttpError.prototype.constructor.name;
this.cause = opts.cause;
this.statusCode = opts.statusCode;
this.url = opts.url;
this.method = opts.method;
this.message = opts.message ?? `HTTP Error ${opts.statusCode}`;
if (opts.cause instanceof Error && opts.cause.stack) {
this.stack = opts.cause.stack;
}
}
public static fromRequest(request: Request, response: Response) {
return new HttpError({
message: response.statusText,
url: response.url,
method: request.method,
statusCode: response.status,
});
}
}

View File

@ -0,0 +1,4 @@
export { defaultHandler } from "./defaultHandler";
export { defaultResponder } from "./defaultResponder";
export { HttpError } from "./http-error";
export { getServerErrorFromUnknown } from "./getServerErrorFromUnknown";

View File

@ -1,3 +1,9 @@
import { PrismaClient } from "@prisma/client";
export default PrismaClient;
declare global {
var prismaClientSingleton: PrismaClient | undefined;
}
export const prisma = globalThis.prismaClientSingleton || new PrismaClient();
export default prisma;