feat: dynamic prisma almost working

This commit is contained in:
Agusti Fernandez Pardo
2022-06-08 18:52:25 +02:00
parent e407a16766
commit 180e925bed
12 changed files with 226 additions and 39 deletions
+10
View File
@@ -178,3 +178,13 @@ We make sure of this by not using next in dev, but next build && next start, if
See <https://github.com/vercel/next.js/blob/canary/packages/next/server/dev/hot-reloader.ts#L79>. Here in dev mode OPTIONS method is hardcoded to return only GET and OPTIONS as allowed method. Running in Production mode would cause this file to be not used. This is hot-reloading logic only.
To remove this limitation, we need to ensure that on local endpoints are requested by swagger at /api/v1 and not /v1
## Hosted api through cal.com
Go to console.com
Add a deployment or go to an existing one.
Activate API or Admin addon
Provide your DATABASE-URL
Store it safely, you'll get a customApiID, save it.
call api.cal.com?apiKey=your_cal_instance_apiKey&customApiId=cal_datasource_key
+1 -1
View File
@@ -1,6 +1,6 @@
import prisma from "@calcom/prisma";
afterEach((done) => {
prisma().$disconnect().then();
prisma.$disconnect().then();
done();
});
-20
View File
@@ -1,20 +0,0 @@
import { NextMiddleware } from "next-api-middleware";
import prisma from "@calcom/prisma";
// This replaces the prisma client for the cusotm one if the customApiId is valid
export const customApiEndpoints: NextMiddleware = async (req, res, next) => {
const {
query: { customApiId },
} = req;
// If no custom api Id is provided, return the regular cal.com prisma client.
if (!customApiId) {
req.prisma = prisma();
await next();
}
// Hardcoded new database, this should be a dynamic call to console prisma/api?
const newDatabseUrl = process.env.DATABASE_PROD_URL;
req.prisma = prisma(newDatabseUrl);
await next();
};
+45
View File
@@ -0,0 +1,45 @@
import { hash } from "bcryptjs";
import cache from "memory-cache";
import { NextMiddleware } from "next-api-middleware";
import { prismaAdmin } from "@calcom/console/modules/common/utils/prisma";
import { asStringOrUndefined, asStringOrNull } from "@calcom/lib/asStringOrNull";
import { PRISMA_CLIENT_CACHING_TIME } from "@calcom/lib/constants";
import { prisma, customPrisma } from "@calcom/prisma";
// This replaces the prisma client for the cusotm one if the customCredentialsId is valid
export const customPrismaClient: NextMiddleware = async (req, res, next) => {
const {
query: { customCredentialsId },
} = req;
// If no custom api Id is provided, attach to request the regular cal.com prisma client.
if (!customCredentialsId) {
req.prisma = prisma;
await next();
} else {
const id = asStringOrUndefined(customCredentialsId);
// If we have a customCredentialsId, we check if it is valid.
const dataCredentials = await prismaAdmin.dataCredentials.findUnique({
where: { id },
});
if (!dataCredentials) {
res.status(400).json({ error: "Invalid custom credentials id" });
return;
}
const credentials = dataCredentials?.credentials;
const hashedUrl = await hash(credentials, 12);
const cachedPrisma = cache.get(hashedUrl);
if (!cachedPrisma) {
cache.put(
hashedUrl,
customPrisma({ datasources: { db: { url: credentials } } }),
PRISMA_CLIENT_CACHING_TIME // Cache the prisma client for 24 hours
);
}
req.prisma = cachedPrisma;
}
await next();
};
+4 -2
View File
@@ -3,14 +3,16 @@ import { NextMiddleware } from "next-api-middleware";
import type { PrismaClient } from ".prisma/client";
/** @todo figure how to use the one from `@calcom/types`fi */
/** @todo figure how to use the one from `@calcom/types` */
/** @todo: remove once `@calcom/types` is updated with it.*/
declare module "next" {
export interface NextApiRequest extends IncomingMessage {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
body: any;
userId: number;
method: string;
prisma: PrismaClient;
body: any;
session: { user: { id: number } };
query: { [key: string]: string | string[] };
}
}
+6 -12
View File
@@ -3,18 +3,6 @@ import { NextMiddleware } from "next-api-middleware";
import { hashAPIKey } from "@calcom/ee/lib/api/apiKeys";
// // import prisma from "@calcom/prisma";
// /** @todo figure how to use the one from `@calcom/types`fi */
// /** @todo: remove once `@calcom/types` is updated with it.*/
// declare module "next" {
// export interface NextApiRequest extends IncomingMessage {
// userId: number;
// method: string;
// query: { [key: string]: string | string[] };
// }
// }
// Used to check if the apiKey is not expired, could be extracted if reused. but not for now.
export const dateNotInPast = function (date: Date) {
const now = new Date();
@@ -27,12 +15,18 @@ export const dateNotInPast = function (date: Date) {
export const verifyApiKey: NextMiddleware = async (req, res, next) => {
const { prisma } = req;
if (!req.query.apiKey) return res.status(401).json({ message: "No apiKey provided" });
console.log("req.query.apiKey", req.query.apiKey);
// We remove the prefix from the user provided api_key. If no env set default to "cal_"
const strippedApiKey = `${req.query.apiKey}`.replace(process.env.API_KEY_PREFIX || "cal_", "");
console.log("strippedApiKey", strippedApiKey);
// Hash the key again before matching against the database records.
const hashedKey = hashAPIKey(strippedApiKey);
console.log("hashedKey", hashedKey);
// Check if the hashed api key exists in database.
const apiKey = await prisma.apiKey.findUnique({ where: { hashedKey } });
// console.log("apiKey", apiKey);
// If we cannot find any api key. Throw a 401 Unauthorized.
if (!apiKey) return res.status(401).json({ error: "Your apiKey is not valid" });
if (apiKey.expiresAt && dateNotInPast(apiKey.expiresAt)) {
+4 -3
View File
@@ -2,7 +2,7 @@ import { label } from "next-api-middleware";
import { addRequestId } from "./addRequestid";
import { captureErrors } from "./captureErrors";
import { customApiEndpoints } from "./customApiEndpoints";
import { customPrismaClient } from "./customPrisma";
import { extendRequest } from "./extendRequest";
import {
HTTP_POST,
@@ -24,11 +24,12 @@ const withMiddleware = label(
HTTP_DELETE,
addRequestId,
verifyApiKey,
customApiEndpoints,
customPrismaClient,
extendRequest,
sentry: captureErrors,
},
["sentry", "customApiEndpoints", "verifyApiKey", "addRequestId", "extendRequest"] // <-- Provide a list of middleware to call automatically
// The order here, determines the order of execution, put customPrismaClient before verifyApiKey always.
["sentry", "customPrismaClient", "verifyApiKey", "addRequestId", "extendRequest"] // <-- Provide a list of middleware to call automatically
);
export { withMiddleware };
+1 -1
View File
@@ -3,6 +3,6 @@ import { UserPermissionRole } from "@prisma/client";
import prisma from "@calcom/prisma";
export const isAdminGuard = async (userId: number) => {
const user = await prisma().user.findUnique({ where: { id: userId } });
const user = await prisma.user.findUnique({ where: { id: userId } });
return user?.role === UserPermissionRole.ADMIN;
};
+6
View File
@@ -2,8 +2,14 @@
// This makes our @calcom/prisma package from the monorepo to be transpiled and usable by API
const withTM = require("next-transpile-modules")([
"@calcom/app-store",
"@calcom/console",
"@calcom/prisma",
"@calcom/types",
"@calcom/core",
"@calcom/stripe",
"@calcom/emails",
"@calcom/lib",
"@calcom/ui",
"@calcom/ee",
]);
+4
View File
@@ -20,13 +20,17 @@
},
"devDependencies": {
"@calcom/tsconfig": "*",
"@calcom/types": "*",
"babel-jest": "^28.1.0",
"jest": "^28.1.0",
"node-mocks-http": "^1.11.0"
},
"dependencies": {
"@calcom/console": "*",
"@calcom/prisma": "*",
"@sentry/nextjs": "^6.19.7",
"bcryptjs": "^2.4.3",
"memory-cache": "^0.2.0",
"modify-response-middleware": "^1.1.0",
"next": "^12.1.6",
"next-api-middleware": "^1.0.1",
+144
View File
@@ -0,0 +1,144 @@
// import availability from "@calcom/core/availability";
// export default availability;
import { Prisma } from "@prisma/client";
import dayjs from "dayjs";
import timezone from "dayjs/plugin/timezone";
import utc from "dayjs/plugin/utc";
import type { NextApiRequest, NextApiResponse } from "next";
// import prisma from "@calcom/prisma";
import { asNumberOrThrow, asStringOrNull } from "@calcom/lib/asStringOrNull";
import { getWorkingHours } from "@calcom/lib/availability";
import getBusyTimes from "@calcom/lib/getBusyTimes";
dayjs.extend(utc);
dayjs.extend(timezone);
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const { prisma } = req;
const id = asNumberOrThrow(req.query.id);
const dateFrom = dayjs(asStringOrNull(req.query.dateFrom));
const dateTo = dayjs(asStringOrNull(req.query.dateTo));
const eventTypeId = typeof req.query.eventTypeId === "string" ? parseInt(req.query.eventTypeId) : undefined;
if (!dateFrom.isValid() || !dateTo.isValid()) {
return res.status(400).json({ message: "Invalid time range given." });
}
const rawUser = await prisma.user.findUnique({
where: {
id,
},
select: {
credentials: true,
timeZone: true,
bufferTime: true,
availability: true,
id: true,
startTime: true,
endTime: true,
selectedCalendars: true,
schedules: {
select: {
availability: true,
timeZone: true,
id: true,
},
},
defaultScheduleId: true,
},
});
const getEventType = (id: number) =>
prisma.eventType.findUnique({
where: { id },
select: {
seatsPerTimeSlot: true,
timeZone: true,
schedule: {
select: {
availability: true,
timeZone: true,
},
},
availability: {
select: {
startTime: true,
endTime: true,
days: true,
},
},
},
});
type EventType = Prisma.PromiseReturnType<typeof getEventType>;
let eventType: EventType | null = null;
if (eventTypeId) eventType = await getEventType(eventTypeId);
if (!rawUser) throw new Error("No user found");
const { selectedCalendars, ...currentUser } = rawUser;
const busyTimes = await getBusyTimes({
credentials: currentUser.credentials,
startTime: dateFrom.format(),
endTime: dateTo.format(),
eventTypeId,
userId: currentUser.id,
selectedCalendars,
});
const bufferedBusyTimes = busyTimes.map((a) => ({
start: dayjs(a.start).subtract(currentUser.bufferTime, "minute"),
end: dayjs(a.end).add(currentUser.bufferTime, "minute"),
}));
const schedule = eventType?.schedule
? { ...eventType?.schedule }
: {
...currentUser.schedules.filter(
(schedule) => !currentUser.defaultScheduleId || schedule.id === currentUser.defaultScheduleId
)[0],
};
const timeZone = schedule.timeZone || eventType?.timeZone || currentUser.timeZone;
const workingHours = getWorkingHours(
{
timeZone,
},
schedule.availability ||
(eventType?.availability.length ? eventType.availability : currentUser.availability)
);
/* Current logic is if a booking is in a time slot mark it as busy, but seats can have more than one attendee so grab
current bookings with a seats event type and display them on the calendar, even if they are full */
let currentSeats;
if (eventType?.seatsPerTimeSlot) {
currentSeats = await prisma.booking.findMany({
where: {
eventTypeId: eventTypeId,
startTime: {
gte: dateFrom.format(),
lte: dateTo.format(),
},
},
select: {
uid: true,
startTime: true,
_count: {
select: {
attendees: true,
},
},
},
});
}
res.status(200).json({
busy: bufferedBusyTimes,
timeZone,
workingHours,
currentSeats,
});
}
+1
View File
@@ -1,6 +1,7 @@
{
"extends": "@calcom/tsconfig/nextjs.json",
"compilerOptions": {
"target": "es2017",
"strict": true,
"baseUrl": ".",
"paths": {