Compare commits
11 Commits
main
...
get-schedu
Author | SHA1 | Date |
---|---|---|
supalarry | 0a911542e9 | |
supalarry | 5f8fcdf100 | |
supalarry | 7afd7605f7 | |
supalarry | 6299f6f67b | |
supalarry | cd63eacff8 | |
supalarry | d84f54f28a | |
supalarry | b86fb6e075 | |
supalarry | e5be585c03 | |
supalarry | 6ebd64bd03 | |
supalarry | 0e2429e679 | |
supalarry | 2ced3c9aca |
|
@ -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\"",
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
# Generated dynamically based on Prisma schema
|
||||
types.ts
|
|
@ -0,0 +1 @@
|
|||
# kysely
|
|
@ -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;
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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("->"))}`;
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue