api architecture
This commit is contained in:
@@ -7,7 +7,10 @@ const nextConfig = {
|
|||||||
distDir: "build",
|
distDir: "build",
|
||||||
};
|
};
|
||||||
|
|
||||||
const withTM = require("next-transpile-modules")(["@documenso/prisma"]);
|
const withTM = require("next-transpile-modules")([
|
||||||
|
"@documenso/prisma",
|
||||||
|
"@documenso/lib",
|
||||||
|
]);
|
||||||
const plugins = [];
|
const plugins = [];
|
||||||
plugins.push(withTM);
|
plugins.push(withTM);
|
||||||
|
|
||||||
|
|||||||
@@ -1,49 +1 @@
|
|||||||
import PrismaClient from "@documenso/prisma";
|
export {};
|
||||||
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`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,12 +1,19 @@
|
|||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
|
||||||
type Data = {
|
import { defaultHandler, defaultResponder } from "@documenso/lib/server";
|
||||||
|
import prisma from "@documenso/prisma";
|
||||||
|
|
||||||
|
type responseData = {
|
||||||
status: string;
|
status: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function handler(
|
// Return a healthy 200 status code for uptime monitoring and render.com zero-downtime-deploy
|
||||||
req: NextApiRequest,
|
async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
res: NextApiResponse<Data>
|
// A generic database access to make sure the service is healthy.
|
||||||
) {
|
const users = await prisma.user.findFirst();
|
||||||
res.status(200).json({ status: "Api up and running :)" });
|
res.status(200).json({ message: "Api up and running :)" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default defaultHandler({
|
||||||
|
GET: Promise.resolve({ default: defaultResponder(getHandler) }),
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,42 +1,31 @@
|
|||||||
// POST to create
|
// POST to create
|
||||||
import PrismaClient from "@documenso/prisma";
|
import {
|
||||||
import User from "@documenso/prisma";
|
defaultHandler,
|
||||||
|
defaultResponder,
|
||||||
|
HttpError,
|
||||||
|
} from "@documenso/lib/server";
|
||||||
|
import prisma from "@documenso/prisma";
|
||||||
|
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import { json } from "stream/consumers";
|
import { json } from "stream/consumers";
|
||||||
|
|
||||||
export default async function userHandler(
|
async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
req: NextApiRequest,
|
|
||||||
res: NextApiResponse<User>
|
|
||||||
) {
|
|
||||||
const { method, body } = req;
|
const { method, body } = req;
|
||||||
const prisma = new PrismaClient();
|
|
||||||
|
|
||||||
switch (method) {
|
|
||||||
case "POST":
|
|
||||||
if (!body.email) {
|
if (!body.email) {
|
||||||
res.status(400).end("Email cannot be empty.");
|
return res.status(400).json({ message: "Email cannot be empty." });
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
let newUser: any;
|
let newUser: any;
|
||||||
newUser = await prisma.user
|
newUser = await prisma.user
|
||||||
.create({
|
.create({
|
||||||
data: { email: body.email },
|
data: { email: body.email },
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
await prisma.$disconnect();
|
return res.status(201).send(newUser);
|
||||||
res.status(200).send(newUser);
|
|
||||||
});
|
});
|
||||||
} catch (error) {
|
|
||||||
await prisma.$disconnect();
|
|
||||||
res.status(500).end("An error has occured. Error: " + error);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
export default defaultHandler({
|
||||||
|
POST: Promise.resolve({ default: defaultResponder(postHandler) }),
|
||||||
default:
|
});
|
||||||
res.setHeader("Allow", ["POST"]);
|
|
||||||
res.status(405).end(`Method ${method} Not Allowed`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
24
packages/lib/server/defaultHandler.ts
Normal file
24
packages/lib/server/defaultHandler.ts
Normal 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" });
|
||||||
|
}
|
||||||
|
};
|
||||||
20
packages/lib/server/defaultResponder.ts
Normal file
20
packages/lib/server/defaultResponder.ts
Normal 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 });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
43
packages/lib/server/getServerErrorFromUnknown.ts
Normal file
43
packages/lib/server/getServerErrorFromUnknown.ts
Normal 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.`,
|
||||||
|
});
|
||||||
|
}
|
||||||
33
packages/lib/server/http-error.ts
Normal file
33
packages/lib/server/http-error.ts
Normal 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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
4
packages/lib/server/index.ts
Normal file
4
packages/lib/server/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export { defaultHandler } from "./defaultHandler";
|
||||||
|
export { defaultResponder } from "./defaultResponder";
|
||||||
|
export { HttpError } from "./http-error";
|
||||||
|
export { getServerErrorFromUnknown } from "./getServerErrorFromUnknown";
|
||||||
@@ -1,3 +1,9 @@
|
|||||||
import { PrismaClient } from "@prisma/client";
|
import { PrismaClient } from "@prisma/client";
|
||||||
|
|
||||||
export default PrismaClient;
|
declare global {
|
||||||
|
var prismaClientSingleton: PrismaClient | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const prisma = globalThis.prismaClientSingleton || new PrismaClient();
|
||||||
|
|
||||||
|
export default prisma;
|
||||||
|
|||||||
Reference in New Issue
Block a user