Compare commits

...

11 Commits

Author SHA1 Message Date
supalarry 0a911542e9 kysely query 6 2023-10-30 11:25:25 +01:00
supalarry 5f8fcdf100 kysely query 5 2023-10-30 10:38:36 +01:00
supalarry 7afd7605f7 kysely query 4 2023-10-30 09:47:14 +01:00
supalarry 6299f6f67b kysely query 3 2023-10-30 09:20:16 +01:00
supalarry cd63eacff8 Revert "kysely query 3"
This reverts commit e5be585c03.
2023-10-26 12:32:35 +02:00
supalarry d84f54f28a fix: kysely query 2 2023-10-26 12:13:02 +02:00
supalarry b86fb6e075 fix: kysely query 2 2023-10-26 11:55:06 +02:00
supalarry e5be585c03 kysely query 3 2023-10-26 10:10:09 +02:00
supalarry 6ebd64bd03 kysely query 2 2023-10-25 20:00:13 +02:00
supalarry 0e2429e679 kysely query 1 2023-10-25 13:10:49 +02:00
supalarry 2ced3c9aca init kysely 2023-10-25 10:06:02 +02:00
10 changed files with 899 additions and 4107 deletions

View File

@ -37,7 +37,7 @@
"dev:console": "turbo run dev --scope=\"@calcom/web\" --scope=\"@calcom/console\"",
"dev:swagger": "turbo run dev --scope=\"@calcom/api\" --scope=\"@calcom/swagger\"",
"dev:website": "turbo run dev --scope=\"@calcom/web\" --scope=\"@calcom/website\"",
"dev": "turbo run dev --scope=\"@calcom/web\"",
"dev": "yarn workspace @calcom/prisma generate-schemas && turbo run dev --scope=\"@calcom/web\"",
"build-storybook": "turbo run build --scope=\"@calcom/storybook\"",
"dx": "turbo run dx",
"i-dev": "infisical run -- turbo run dev --scope=\"@calcom/web\"",

2
packages/kysely/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
# Generated dynamically based on Prisma schema
types.ts

View File

@ -0,0 +1 @@
# kysely

20
packages/kysely/index.ts Normal file
View File

@ -0,0 +1,20 @@
import { Kysely, PostgresDialect } from "kysely";
import { Pool } from "pg";
import type { DB } from "./types.ts";
const dialect = new PostgresDialect({
pool: new Pool({
database: "calendso",
host: "localhost",
user: "postgres",
password: "postgres",
port: 5450,
}),
});
const db = new Kysely<DB>({
dialect,
});
export default db;

View File

@ -0,0 +1,12 @@
{
"name": "@calcom/kysely",
"private": true,
"main": "index.ts",
"dependencies": {
"kysely": "^0.26.3",
"pg": "^8.11.3"
},
"devDependencies": {
"@types/pg": "^8.10.7"
}
}

View File

@ -0,0 +1,14 @@
import type { ExpressionBuilder, StringReference } from "kysely";
import { sql } from "kysely";
export function traverseJSON<DB, TB extends keyof DB>(
eb: ExpressionBuilder<DB, TB>,
column: StringReference<DB, TB>,
path: string | [string, ...string[]]
) {
if (!Array.isArray(path)) {
path = [path];
}
return sql`${sql.ref(column)}->${sql.raw(path.map((item) => `'${item}'`).join("->"))}`;
}

View File

@ -28,6 +28,7 @@
"@prisma/extension-accelerate": "^0.6.2",
"@prisma/generator-helper": "^5.4.2",
"prisma": "^5.4.2",
"prisma-kysely": "^1.7.1",
"ts-node": "^10.9.1",
"zod": "^3.22.2",
"zod-prisma": "^0.5.4"

View File

@ -22,6 +22,12 @@ generator enums {
provider = "ts-node --transpile-only ./enum-generator"
}
generator kysely {
provider = "prisma-kysely"
output = "../kysely"
fileName = "types.ts"
}
enum SchedulingType {
ROUND_ROBIN @map("roundRobin")
COLLECTIVE @map("collective")

View File

@ -1,3 +1,5 @@
import type { Expression, SqlBool } from "kysely";
import { jsonObjectFrom, jsonArrayFrom } from "kysely/helpers/postgres";
// eslint-disable-next-line no-restricted-imports
import { countBy } from "lodash";
import { v4 as uuid } from "uuid";
@ -9,11 +11,14 @@ import type { Dayjs } from "@calcom/dayjs";
import dayjs from "@calcom/dayjs";
import { getSlugOrRequestedSlug, orgDomainConfig } from "@calcom/ee/organizations/lib/orgDomains";
import { isEventTypeLoggingEnabled } from "@calcom/features/bookings/lib/isEventTypeLoggingEnabled";
import db from "@calcom/kysely";
import { traverseJSON } from "@calcom/kysely/utils/json/traverse";
import { getDefaultEvent } from "@calcom/lib/defaultEvents";
import isTimeOutOfBounds from "@calcom/lib/isOutOfBounds";
import logger from "@calcom/lib/logger";
import { performance } from "@calcom/lib/server/perfObserver";
import getSlots from "@calcom/lib/slots";
import slugify from "@calcom/lib/slugify";
import prisma, { availabilityUserSelect } from "@calcom/prisma";
import { SchedulingType } from "@calcom/prisma/enums";
import { BookingStatus } from "@calcom/prisma/enums";
@ -88,8 +93,9 @@ async function getEventTypeId({
}) {
if (!eventTypeSlug || !slug) return null;
let teamId;
let userId;
let teamId: number | undefined;
let userId: number | undefined;
if (isTeamEvent) {
teamId = await getTeamIdFromSlug(
slug,
@ -101,19 +107,23 @@ async function getEventTypeId({
organizationDetails ?? { currentOrgDomain: null, isValidOrgDomain: false }
);
}
const eventType = await prisma.eventType.findFirst({
where: {
slug: eventTypeSlug,
...(teamId ? { teamId } : {}),
...(userId ? { userId } : {}),
},
select: {
id: true,
},
});
const eventType = await db
.selectFrom("EventType")
.where((eb) =>
eb.and({
slug: eventTypeSlug,
...(teamId ? { teamId } : {}),
...(userId ? { userId } : {}),
})
)
.select("id")
.executeTakeFirst();
if (!eventType) {
throw new TRPCError({ code: "NOT_FOUND" });
}
return eventType?.id;
}
@ -136,75 +146,210 @@ export async function getEventType(
return null;
}
const eventType = await prisma.eventType.findUnique({
where: {
id: eventTypeId,
},
select: {
id: true,
slug: true,
minimumBookingNotice: true,
length: true,
offsetStart: true,
seatsPerTimeSlot: true,
timeZone: true,
slotInterval: true,
beforeEventBuffer: true,
afterEventBuffer: true,
bookingLimits: true,
durationLimits: true,
schedulingType: true,
periodType: true,
periodStartDate: true,
periodEndDate: true,
periodCountCalendarDays: true,
periodDays: true,
metadata: true,
schedule: {
select: {
availability: {
select: {
date: true,
startTime: true,
endTime: true,
days: true,
},
},
timeZone: true,
},
},
availability: {
select: {
date: true,
startTime: true,
endTime: true,
days: true,
},
},
hosts: {
select: {
isFixed: true,
user: {
select: {
credentials: { select: credentialForCalendarServiceSelect },
...availabilityUserSelect,
},
},
},
},
users: {
select: {
credentials: { select: credentialForCalendarServiceSelect },
...availabilityUserSelect,
},
},
},
});
const eventType = await db
.selectFrom("EventType")
.where("id", "=", eventTypeId)
.select((eb) => [
"EventType.id",
"EventType.slug",
"EventType.minimumBookingNotice",
"EventType.length",
"EventType.offsetStart",
"EventType.seatsPerTimeSlot",
"EventType.timeZone",
"EventType.slotInterval",
"EventType.beforeEventBuffer",
"EventType.afterEventBuffer",
"EventType.bookingLimits",
"EventType.durationLimits",
"EventType.schedulingType",
"EventType.periodType",
"EventType.periodStartDate",
"EventType.periodEndDate",
"EventType.periodCountCalendarDays",
"EventType.periodDays",
"EventType.metadata",
jsonObjectFrom(
eb
.selectFrom("Schedule")
.whereRef("Schedule.id", "=", "EventType.scheduleId")
.select((eb) => [
"Schedule.timeZone",
jsonArrayFrom(
eb
.selectFrom("Availability")
.whereRef("Availability.scheduleId", "=", "Schedule.id")
.select(["date", "startTime", "endTime", "days"])
).as("availability"),
])
).as("schedule"),
jsonArrayFrom(
eb
.selectFrom("Availability")
.whereRef("Availability.eventTypeId", "=", "EventType.id")
.select(["date", "startTime", "endTime", "days"])
).as("availability"),
jsonArrayFrom(
eb
.selectFrom("Host")
.leftJoin("users", "users.id", "Host.userId")
.leftJoin("Credential", "Credential.userId", "users.id")
.whereRef("Host.eventTypeId", "=", "EventType.id")
.select((eb) => [
"isFixed",
jsonObjectFrom(
eb
.selectFrom("users")
.whereRef("users.id", "=", "Host.userId")
.select((eb) => [
jsonArrayFrom(
eb
.selectFrom("Credential")
.whereRef("Credential.userId", "=", "users.id")
.select([
"id",
"appId",
"type",
"userId",
"teamId",
"key",
"invalid",
jsonObjectFrom(
eb
.selectFrom("users")
.whereRef("users.id", "=", "Credential.userId")
.select("email")
).as("user"),
])
).as("credentials"),
"id",
"timeZone",
"email",
"bufferTime",
"startTime",
"username",
"endTime",
"timeFormat",
"defaultScheduleId",
jsonArrayFrom(
eb
.selectFrom("Schedule")
.whereRef("Schedule.userId", "=", "users.id")
.select((eb) => [
"id",
"timeZone",
jsonArrayFrom(
eb
.selectFrom("Availability")
.select(["date", "startTime", "endTime", "days"])
.whereRef("Availability.scheduleId", "=", "Schedule.id")
).as("availability"),
])
).as("schedules"),
jsonArrayFrom(
eb.selectFrom("Availability").selectAll().whereRef("Availability.userId", "=", "users.id")
).as("availability"),
jsonArrayFrom(
eb
.selectFrom("SelectedCalendar")
.selectAll()
.whereRef("SelectedCalendar.userId", "=", "users.id")
).as("selectedCalendars"),
])
).as("user"),
])
).as("hosts"),
jsonArrayFrom(
eb
.selectFrom("_user_eventtype")
.leftJoin("users", "users.id", "_user_eventtype.B")
.whereRef("_user_eventtype.A", "=", "EventType.id")
.leftJoin("Credential", "Credential.userId", "users.id")
.select((eb) => [
jsonArrayFrom(
eb
.selectFrom("Credential")
.whereRef("Credential.userId", "=", "users.id")
.select([
"id",
"appId",
"type",
"userId",
"teamId",
"key",
"invalid",
jsonObjectFrom(
eb.selectFrom("users").whereRef("users.id", "=", "Credential.userId").select("email")
).as("user"),
])
).as("credentials"),
"users.id",
"users.timeZone",
"users.email",
"users.bufferTime",
"users.startTime",
"users.username",
"users.endTime",
"users.timeFormat",
"users.defaultScheduleId",
jsonArrayFrom(
eb
.selectFrom("Schedule")
.whereRef("Schedule.userId", "=", "users.id")
.select((eb) => [
"id",
"timeZone",
jsonArrayFrom(
eb
.selectFrom("Availability")
.select(["date", "startTime", "endTime", "days"])
.whereRef("Availability.scheduleId", "=", "Schedule.id")
).as("availability"),
])
).as("schedules"),
jsonArrayFrom(
eb.selectFrom("Availability").selectAll().whereRef("Availability.userId", "=", "users.id")
).as("availability"),
jsonArrayFrom(
eb
.selectFrom("SelectedCalendar")
.selectAll()
.whereRef("SelectedCalendar.userId", "=", "users.id")
).as("selectedCalendars"),
])
).as("users"),
])
.executeTakeFirst();
// note(Lauris): Availability startTime and endTime have DateTime in Prisma schema,
// but in DB only hh:mm:ss are stored e.g. "09:00:00". Prisma transforms data as follows:
if (!eventType) {
return null;
}
eventType.users.forEach((user) => {
user.schedules.forEach((schedule) => {
schedule.availability.forEach((availability) => {
availability.startTime = new Date(`1970-01-01T${availability.startTime}.000Z`);
availability.endTime = new Date(`1970-01-01T${availability.endTime}.000Z`);
});
});
});
eventType.hosts.forEach((host) => {
host.user?.schedules.forEach((schedule) => {
schedule.availability.forEach((availability) => {
availability.startTime = new Date(`1970-01-01T${availability.startTime}.000Z`);
availability.endTime = new Date(`1970-01-01T${availability.endTime}.000Z`);
});
});
});
eventType.schedule?.availability.forEach((entry) => {
entry.startTime = new Date(`1970-01-01T${entry.startTime}.000Z`);
entry.endTime = new Date(`1970-01-01T${entry.endTime}.000Z`);
});
return {
...eventType,
metadata: EventTypeMetaDataSchema.parse(eventType.metadata),
@ -458,25 +603,42 @@ export async function getAvailableSlots({ input, ctx }: GetScheduleOptions) {
let availableTimeSlots: typeof timeSlots = [];
// Load cached busy slots
const selectedSlots =
/* FIXME: For some reason this returns undefined while testing in Jest */
(await prisma.selectedSlots.findMany({
where: {
userId: { in: usersWithCredentials.map((user) => user.id) },
releaseAt: { gt: dayjs.utc().format() },
},
select: {
id: true,
slotUtcStartDate: true,
slotUtcEndDate: true,
userId: true,
isSeat: true,
eventTypeId: true,
},
})) || [];
await prisma.selectedSlots.deleteMany({
where: { eventTypeId: { equals: eventType.id }, id: { notIn: selectedSlots.map((item) => item.id) } },
});
(await db
.selectFrom("SelectedSlots")
.where((eb) =>
eb.and([
eb(
"userId",
"in",
usersWithCredentials.map((user) => user.id)
),
eb("releaseAt", ">", new Date(dayjs.utc().format())),
])
)
.select(["id", "slotUtcStartDate", "slotUtcEndDate", "userId", "isSeat", "eventTypeId"])
.execute()) || [];
await db
.deleteFrom("SelectedSlots")
.where((eb) => {
const and: Expression<SqlBool>[] = [];
and.push(eb("eventTypeId", "=", eventType.id));
if (selectedSlots.length) {
and.push(
eb(
"id",
"not in",
selectedSlots.map((item) => item.id)
)
);
}
return eb.and(and);
})
.execute();
availableTimeSlots = timeSlots;
@ -614,15 +776,30 @@ async function getUserIdFromUsername(
organizationDetails: { currentOrgDomain: string | null; isValidOrgDomain: boolean }
) {
const { currentOrgDomain, isValidOrgDomain } = organizationDetails;
const user = await prisma.user.findFirst({
where: {
username,
organization: isValidOrgDomain && currentOrgDomain ? getSlugOrRequestedSlug(currentOrgDomain) : null,
},
select: {
id: true,
},
});
const user = await db
.selectFrom("users")
.innerJoin("Team", "Team.id", "users.organizationId")
.where((eb) => {
const and: Expression<SqlBool>[] = [];
and.push(eb("username", "=", username));
if (isValidOrgDomain && currentOrgDomain) {
const slugifiedValue = slugify(currentOrgDomain);
and.push(
eb.or([
eb("Team.slug", "=", slugifiedValue),
eb(traverseJSON(eb, "metadata", "requestedSlug"), "=", slugifiedValue),
])
);
}
return eb.and(and);
})
.select("users.id")
.executeTakeFirst();
return user?.id;
}
@ -631,14 +808,26 @@ async function getTeamIdFromSlug(
organizationDetails: { currentOrgDomain: string | null; isValidOrgDomain: boolean }
) {
const { currentOrgDomain, isValidOrgDomain } = organizationDetails;
const team = await prisma.team.findFirst({
where: {
slug,
parent: isValidOrgDomain && currentOrgDomain ? getSlugOrRequestedSlug(currentOrgDomain) : null,
},
select: {
id: true,
},
});
const team = await db
.selectFrom("Team")
.innerJoin("Team as Parent", "Parent.id", "Team.parentId")
.where((eb) => {
const and: Expression<SqlBool>[] = [];
and.push(eb("Team.slug", "=", slug));
if (isValidOrgDomain && currentOrgDomain) {
const slugifiedValue = slugify(currentOrgDomain);
and.push(
eb.or([
eb("Parent.slug", "=", slugifiedValue),
eb(traverseJSON(eb, "Parent.metadata", "requestedSlug"), "=", slugifiedValue),
])
);
}
return eb.and(and);
})
.select("Team.id")
.executeTakeFirst();
return team?.id;
}

4535
yarn.lock

File diff suppressed because it is too large Load Diff