feat: v2 event-types POST, PATCH, DELETE endpoints (#14237)
* create event type: use createEventType from trpc handler * create event type: simplify handler input and create e2e test * update event type + test * delete end point * check if owner owns event type * fix: starting v2 * fixes * remove unused enum * fix TS issue for update input * docs refresh * docs * docs * simplify event types docs * docs refactor * fix: check if user already has event type with existing slug * refactor update into a service * fix tests * remove unused import * update create event link * refactor: create event handler process passed locations * move input out of folder * reuse eventtype location for inputs * docs: example POST event-type return --------- Co-authored-by: Rajiv Sahal <sahalrajiv-extc@atharvacoe.ac.in> Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
This commit is contained in:
co-authored by
Rajiv Sahal
Morgan
parent
2cbf9e1ac8
commit
3b263e9282
@@ -7,7 +7,7 @@
|
||||
"plugins": [
|
||||
{
|
||||
"name": "@nestjs/swagger",
|
||||
"options": { "dtoFileNameSuffix": ".input.ts", "classValidatorShim": true }
|
||||
"options": { "dtoFileNameSuffix": [".input.ts", ".output.ts", ".dto.ts"], "classValidatorShim": true }
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { EventTypesModule } from "@/ee/event-types/event-types.module";
|
||||
import { CreateEventTypeInput } from "@/ee/event-types/inputs/create-event-type.input";
|
||||
import { UpdateEventTypeInput } from "@/ee/event-types/inputs/update-event-type.input";
|
||||
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
|
||||
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
|
||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||
@@ -61,7 +63,7 @@ describe("Event types Endpoints", () => {
|
||||
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
||||
|
||||
const userEmail = "event-types-test-e2e@api.com";
|
||||
const name = "bob";
|
||||
const name = "bob the builder";
|
||||
const username = name;
|
||||
let eventType: EventType;
|
||||
let user: User;
|
||||
@@ -86,6 +88,7 @@ describe("Event types Endpoints", () => {
|
||||
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
||||
|
||||
organization = await teamRepositoryFixture.create({ name: "organization" });
|
||||
oAuthClient = await createOAuthClient(organization.id);
|
||||
@@ -95,17 +98,6 @@ describe("Event types Endpoints", () => {
|
||||
username,
|
||||
});
|
||||
|
||||
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
||||
|
||||
eventType = await eventTypesRepositoryFixture.create(
|
||||
{
|
||||
length: 60,
|
||||
title: "peer coding session",
|
||||
slug: "peer-coding",
|
||||
},
|
||||
user.id
|
||||
);
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
@@ -129,6 +121,50 @@ describe("Event types Endpoints", () => {
|
||||
expect(user).toBeDefined();
|
||||
});
|
||||
|
||||
it("should create an event type", async () => {
|
||||
const body: CreateEventTypeInput = {
|
||||
title: "Test Event Type",
|
||||
slug: "test-event-type",
|
||||
description: "A description of the test event type.",
|
||||
length: 60,
|
||||
hidden: false,
|
||||
locations: [
|
||||
{
|
||||
type: "Online",
|
||||
link: "https://example.com/meet",
|
||||
displayLocationPublicly: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post("/api/v2/event-types")
|
||||
.send(body)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: ApiSuccessResponse<EventType> = response.body;
|
||||
expect(responseBody.data).toHaveProperty("id");
|
||||
expect(responseBody.data.title).toEqual(body.title);
|
||||
eventType = responseBody.data;
|
||||
});
|
||||
});
|
||||
|
||||
it("should update event type", async () => {
|
||||
const newTitle = "Updated title";
|
||||
|
||||
const body: UpdateEventTypeInput = {
|
||||
title: newTitle,
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.patch(`/api/v2/event-types/${eventType.id}`)
|
||||
.send(body)
|
||||
.expect(200)
|
||||
.then(async () => {
|
||||
eventType.title = newTitle;
|
||||
});
|
||||
});
|
||||
|
||||
it(`/GET/:id`, async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(`/api/v2/event-types/${eventType.id}`)
|
||||
@@ -154,10 +190,8 @@ describe("Event types Endpoints", () => {
|
||||
.expect(200);
|
||||
|
||||
const responseBody: ApiSuccessResponse<EventTypesByViewer> = response.body;
|
||||
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
console.log("asap responseBody.data", responseBody.data);
|
||||
expect(responseBody.data.eventTypeGroups).toBeDefined();
|
||||
expect(responseBody.data.eventTypeGroups).toBeDefined();
|
||||
expect(responseBody.data.eventTypeGroups[0]).toBeDefined();
|
||||
@@ -178,7 +212,6 @@ describe("Event types Endpoints", () => {
|
||||
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
console.log("asap responseBody.data", responseBody.data);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data.length).toEqual(1);
|
||||
expect(responseBody.data[0].id).toEqual(eventType.id);
|
||||
@@ -192,10 +225,18 @@ describe("Event types Endpoints", () => {
|
||||
.expect(404);
|
||||
});
|
||||
|
||||
it("should delete schedule", async () => {
|
||||
return request(app.getHttpServer()).delete(`/api/v2/event-types/${eventType.id}`).expect(200);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await oauthClientRepositoryFixture.delete(oAuthClient.id);
|
||||
await teamRepositoryFixture.delete(organization.id);
|
||||
await eventTypesRepositoryFixture.delete(eventType.id);
|
||||
try {
|
||||
await eventTypesRepositoryFixture.delete(eventType.id);
|
||||
} catch (e) {
|
||||
// Event type might have been deleted by the test
|
||||
}
|
||||
try {
|
||||
await userRepositoryFixture.delete(user.id);
|
||||
} catch (e) {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { CreateEventTypeInput } from "@/ee/event-types/inputs/create-event-type.input";
|
||||
import { UpdateEventTypeInput } from "@/ee/event-types/inputs/update-event-type.input";
|
||||
import { CreateEventTypeOutput } from "@/ee/event-types/outputs/create-event-type.output";
|
||||
import { EventTypesService } from "@/ee/event-types/services/event-types.service";
|
||||
import { ForAtom } from "@/lib/atoms/decorators/for-atom.decorator";
|
||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
@@ -6,12 +8,29 @@ import { Permissions } from "@/modules/auth/decorators/permissions/permissions.d
|
||||
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import { Controller, UseGuards, Get, Param, Post, Body, NotFoundException } from "@nestjs/common";
|
||||
import {
|
||||
Controller,
|
||||
UseGuards,
|
||||
Get,
|
||||
Param,
|
||||
Post,
|
||||
Body,
|
||||
NotFoundException,
|
||||
Patch,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
Delete,
|
||||
} from "@nestjs/common";
|
||||
import { ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
import { EventType } from "@prisma/client";
|
||||
|
||||
import { EventTypesByViewer } from "@calcom/lib";
|
||||
import { EVENT_TYPE_READ, EVENT_TYPE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import type { EventType as AtomEventType, EventTypesPublic } from "@calcom/platform-libraries";
|
||||
import type {
|
||||
EventType as AtomEventType,
|
||||
EventTypesPublic,
|
||||
UpdateEventTypeReturn,
|
||||
} from "@calcom/platform-libraries";
|
||||
import { getEventTypesByViewer } from "@calcom/platform-libraries";
|
||||
import { ApiResponse, ApiSuccessResponse } from "@calcom/platform-types";
|
||||
|
||||
@@ -20,6 +39,7 @@ import { ApiResponse, ApiSuccessResponse } from "@calcom/platform-types";
|
||||
version: "2",
|
||||
})
|
||||
@UseGuards(PermissionsGuard)
|
||||
@DocsTags("Event types")
|
||||
export class EventTypesController {
|
||||
constructor(private readonly eventTypesService: EventTypesService) {}
|
||||
|
||||
@@ -29,8 +49,8 @@ export class EventTypesController {
|
||||
async createEventType(
|
||||
@Body() body: CreateEventTypeInput,
|
||||
@GetUser() user: UserWithProfile
|
||||
): Promise<ApiResponse<EventType>> {
|
||||
const eventType = await this.eventTypesService.createUserEventType(user.id, body);
|
||||
): Promise<CreateEventTypeOutput> {
|
||||
const eventType = await this.eventTypesService.createUserEventType(user, body);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
@@ -89,4 +109,36 @@ export class EventTypesController {
|
||||
data: eventTypes,
|
||||
};
|
||||
}
|
||||
|
||||
@Patch("/:eventTypeId")
|
||||
@Permissions([EVENT_TYPE_WRITE])
|
||||
@UseGuards(AccessTokenGuard)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async updateEventType(
|
||||
@Param("eventTypeId") eventTypeId: number,
|
||||
@Body() body: UpdateEventTypeInput,
|
||||
@GetUser() user: UserWithProfile
|
||||
): Promise<ApiResponse<UpdateEventTypeReturn["eventType"]>> {
|
||||
const eventType = await this.eventTypesService.updateEventType(eventTypeId, body, user);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: eventType,
|
||||
};
|
||||
}
|
||||
|
||||
@Delete("/:eventTypeId")
|
||||
@Permissions([EVENT_TYPE_WRITE])
|
||||
@UseGuards(AccessTokenGuard)
|
||||
async deleteEventType(
|
||||
@Param("eventTypeId") eventTypeId: number,
|
||||
@GetUser("id") userId: number
|
||||
): Promise<ApiResponse<EventType>> {
|
||||
const eventType = await this.eventTypesService.deleteEventType(eventTypeId, userId);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: eventType,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,13 @@ import { EventTypesRepository } from "@/ee/event-types/event-types.repository";
|
||||
import { EventTypesService } from "@/ee/event-types/services/event-types.service";
|
||||
import { MembershipsModule } from "@/modules/memberships/memberships.module";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { SelectedCalendarsModule } from "@/modules/selected-calendars/selected-calendars.module";
|
||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||
import { UsersModule } from "@/modules/users/users.module";
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule, MembershipsModule, TokensModule, UsersModule],
|
||||
imports: [PrismaModule, MembershipsModule, TokensModule, UsersModule, SelectedCalendarsModule],
|
||||
providers: [EventTypesRepository, EventTypesService],
|
||||
controllers: [EventTypesController],
|
||||
exports: [EventTypesService, EventTypesRepository],
|
||||
|
||||
@@ -10,7 +10,10 @@ import { getEventTypeById } from "@calcom/platform-libraries";
|
||||
export class EventTypesRepository {
|
||||
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
|
||||
|
||||
async createUserEventType(userId: number, body: CreateEventTypeInput) {
|
||||
async createUserEventType(
|
||||
userId: number,
|
||||
body: Pick<CreateEventTypeInput, "title" | "slug" | "length" | "hidden">
|
||||
) {
|
||||
return this.dbWrite.prisma.eventType.create({
|
||||
data: {
|
||||
...body,
|
||||
@@ -59,4 +62,19 @@ export class EventTypesRepository {
|
||||
async getEventTypeById(eventTypeId: number) {
|
||||
return this.dbRead.prisma.eventType.findUnique({ where: { id: eventTypeId } });
|
||||
}
|
||||
|
||||
async getUserEventTypeBySlug(userId: number, slug: string) {
|
||||
return this.dbRead.prisma.eventType.findUnique({
|
||||
where: {
|
||||
userId_slug: {
|
||||
userId: userId,
|
||||
slug: slug,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async deleteEventType(eventTypeId: number) {
|
||||
return this.dbWrite.prisma.eventType.delete({ where: { id: eventTypeId } });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,53 @@
|
||||
import { IsNumber, IsString, Min } from "class-validator";
|
||||
import { EventTypeLocation } from "@/ee/event-types/inputs/event-type-location.input";
|
||||
import { ApiProperty as DocsProperty, ApiHideProperty } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsString, IsNumber, IsBoolean, IsOptional, ValidateNested, Min, IsArray } from "class-validator";
|
||||
|
||||
export const CREATE_EVENT_LENGTH_EXAMPLE = 60;
|
||||
export const CREATE_EVENT_SLUG_EXAMPLE = "cooking-class";
|
||||
export const CREATE_EVENT_TITLE_EXAMPLE = "Learn the secrets of masterchief!";
|
||||
export const CREATE_EVENT_DESCRIPTION_EXAMPLE =
|
||||
"Discover the culinary wonders of the Argentina by making the best flan ever!";
|
||||
|
||||
// note(Lauris): We will gradually expose more properties if any customer needs them.
|
||||
// Just uncomment any below when requested.
|
||||
export class CreateEventTypeInput {
|
||||
@IsNumber()
|
||||
@Min(1)
|
||||
@DocsProperty({ example: CREATE_EVENT_LENGTH_EXAMPLE })
|
||||
length!: number;
|
||||
|
||||
@IsString()
|
||||
@DocsProperty({ example: CREATE_EVENT_SLUG_EXAMPLE })
|
||||
slug!: string;
|
||||
|
||||
@IsString()
|
||||
@DocsProperty({ example: CREATE_EVENT_TITLE_EXAMPLE })
|
||||
title!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@DocsProperty({ example: CREATE_EVENT_DESCRIPTION_EXAMPLE })
|
||||
description?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiHideProperty()
|
||||
hidden?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => EventTypeLocation)
|
||||
@IsArray()
|
||||
locations?: EventTypeLocation[];
|
||||
|
||||
// @ApiHideProperty()
|
||||
// @IsOptional()
|
||||
// @IsNumber()
|
||||
// teamId?: number;
|
||||
|
||||
// @ApiHideProperty()
|
||||
// @IsOptional()
|
||||
// @IsEnum(SchedulingType)
|
||||
// schedulingType?: SchedulingType; -> import { SchedulingType } from "@/ee/event-types/inputs/enums/scheduling-type";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
export enum Editable {
|
||||
system = "system",
|
||||
systemButOptional = "system-but-optional",
|
||||
systemButHidden = "system-but-hidden",
|
||||
user = "user",
|
||||
userReadonly = "user-readonly",
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
export enum BaseField {
|
||||
number = "number",
|
||||
boolean = "boolean",
|
||||
address = "address",
|
||||
name = "name",
|
||||
text = "text",
|
||||
textarea = "textarea",
|
||||
email = "email",
|
||||
phone = "phone",
|
||||
multiemail = "multiemail",
|
||||
select = "select",
|
||||
multiselect = "multiselect",
|
||||
checkbox = "checkbox",
|
||||
radio = "radio",
|
||||
radioInput = "radioInput",
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export enum Frequency {
|
||||
YEARLY = 0,
|
||||
MONTHLY = 1,
|
||||
WEEKLY = 2,
|
||||
DAILY = 3,
|
||||
HOURLY = 4,
|
||||
MINUTELY = 5,
|
||||
SECONDLY = 6,
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export enum PeriodType {
|
||||
UNLIMITED = "UNLIMITED",
|
||||
ROLLING = "ROLLING",
|
||||
RANGE = "RANGE",
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export enum SchedulingType {
|
||||
ROUND_ROBIN = "ROUND_ROBIN",
|
||||
COLLECTIVE = "COLLECTIVE",
|
||||
MANAGED = "MANAGED",
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { ApiProperty as DocsProperty, ApiHideProperty } from "@nestjs/swagger";
|
||||
import { IsString, IsNumber, IsBoolean, IsOptional, IsUrl } from "class-validator";
|
||||
|
||||
// note(Lauris): We will gradually expose more properties if any customer needs them.
|
||||
// Just uncomment any below when requested.
|
||||
|
||||
export class EventTypeLocation {
|
||||
@IsString()
|
||||
@DocsProperty({ example: "link" })
|
||||
type!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiHideProperty()
|
||||
address?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsUrl()
|
||||
@DocsProperty({ example: "https://masterchief.com/argentina/flan/video/9129412" })
|
||||
link?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiHideProperty()
|
||||
displayLocationPublicly?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiHideProperty()
|
||||
hostPhoneNumber?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@ApiHideProperty()
|
||||
credentialId?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiHideProperty()
|
||||
teamName?: string;
|
||||
}
|
||||
@@ -0,0 +1,412 @@
|
||||
import { Editable } from "@/ee/event-types/inputs/enums/editable";
|
||||
import { BaseField } from "@/ee/event-types/inputs/enums/field-type";
|
||||
import { Frequency } from "@/ee/event-types/inputs/enums/frequency";
|
||||
import { EventTypeLocation } from "@/ee/event-types/inputs/event-type-location.input";
|
||||
import { Type } from "class-transformer";
|
||||
import {
|
||||
IsString,
|
||||
IsBoolean,
|
||||
IsOptional,
|
||||
ValidateNested,
|
||||
Min,
|
||||
IsInt,
|
||||
IsEnum,
|
||||
IsArray,
|
||||
IsDate,
|
||||
IsNumber,
|
||||
} from "class-validator";
|
||||
|
||||
// note(Lauris): We will gradually expose more properties if any customer needs them.
|
||||
// Just uncomment any below when requested. Go to bottom of file to see UpdateEventTypeInput.
|
||||
|
||||
class Option {
|
||||
@IsString()
|
||||
value!: string;
|
||||
|
||||
@IsString()
|
||||
label!: string;
|
||||
}
|
||||
|
||||
class Source {
|
||||
@IsString()
|
||||
id!: string;
|
||||
|
||||
@IsString()
|
||||
type!: string;
|
||||
|
||||
@IsString()
|
||||
label!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
editUrl?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
fieldRequired?: boolean;
|
||||
}
|
||||
|
||||
class View {
|
||||
@IsString()
|
||||
id!: string;
|
||||
|
||||
@IsString()
|
||||
label!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
description?: string;
|
||||
}
|
||||
|
||||
class OptionsInput {
|
||||
@IsString()
|
||||
type!: "address" | "text" | "phone";
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
required?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
class VariantField {
|
||||
@IsString()
|
||||
type!: BaseField;
|
||||
|
||||
@IsString()
|
||||
name!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
label?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
labelAsSafeHtml?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
placeholder?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
class Variant {
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => VariantField)
|
||||
fields!: VariantField[];
|
||||
}
|
||||
|
||||
class VariantsConfig {
|
||||
variants!: Record<string, Variant>;
|
||||
}
|
||||
|
||||
export class BookingField {
|
||||
@IsEnum(BaseField)
|
||||
type!: BaseField;
|
||||
|
||||
@IsString()
|
||||
name!: string;
|
||||
|
||||
@IsOptional()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => Option)
|
||||
options?: Option[];
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
label?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
labelAsSafeHtml?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
defaultLabel?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
placeholder?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
required?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
getOptionsAt?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => OptionsInput)
|
||||
optionsInputs?: Record<string, OptionsInput>;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
variant?: string;
|
||||
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => VariantsConfig)
|
||||
variantsConfig?: VariantsConfig;
|
||||
|
||||
@IsOptional()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => View)
|
||||
views?: View[];
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
hideWhenJustOneOption?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
hidden?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(Editable)
|
||||
editable?: Editable;
|
||||
|
||||
@IsOptional()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => Source)
|
||||
sources?: Source[];
|
||||
}
|
||||
|
||||
export class RecurringEvent {
|
||||
@IsDate()
|
||||
@IsOptional()
|
||||
dtstart?: Date;
|
||||
|
||||
@IsInt()
|
||||
interval!: number;
|
||||
|
||||
@IsInt()
|
||||
count!: number;
|
||||
|
||||
@IsEnum(Frequency)
|
||||
freq!: Frequency;
|
||||
|
||||
@IsDate()
|
||||
@IsOptional()
|
||||
until?: Date;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
tzid?: string;
|
||||
}
|
||||
|
||||
export class IntervalLimits {
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
PER_DAY?: number;
|
||||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
PER_WEEK?: number;
|
||||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
PER_MONTH?: number;
|
||||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
PER_YEAR?: number;
|
||||
}
|
||||
|
||||
export class UpdateEventTypeInput {
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@IsOptional()
|
||||
length?: number;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
slug?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
title?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
description?: string;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
hidden?: boolean;
|
||||
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => EventTypeLocation)
|
||||
@IsOptional()
|
||||
locations?: EventTypeLocation[];
|
||||
|
||||
// @IsInt()
|
||||
// @IsOptional()
|
||||
// position?: number;
|
||||
|
||||
// @IsInt()
|
||||
// @IsOptional()
|
||||
// offsetStart?: number;
|
||||
|
||||
// @IsInt()
|
||||
// @IsOptional()
|
||||
// userId?: number;
|
||||
|
||||
// @IsInt()
|
||||
// @IsOptional()
|
||||
// profileId?: number;
|
||||
|
||||
// @IsInt()
|
||||
// @IsOptional()
|
||||
// teamId?: number;
|
||||
|
||||
// @IsString()
|
||||
// @IsOptional()
|
||||
// eventName?: string;
|
||||
|
||||
// @IsInt()
|
||||
// @IsOptional()
|
||||
// parentId?: number;
|
||||
|
||||
// @IsOptional()
|
||||
// @IsArray()
|
||||
// @ValidateNested({ each: true })
|
||||
// @Type(() => BookingField)
|
||||
// bookingFields?: BookingField[];
|
||||
|
||||
// @IsString()
|
||||
// @IsOptional()
|
||||
// timeZone?: string;
|
||||
|
||||
// @IsEnum(PeriodType)
|
||||
// @IsOptional()
|
||||
// periodType?: PeriodType; -> import { PeriodType } from "@/ee/event-types/inputs/enums/period-type";
|
||||
|
||||
// @IsDate()
|
||||
// @IsOptional()
|
||||
// periodStartDate?: Date;
|
||||
|
||||
// @IsDate()
|
||||
// @IsOptional()
|
||||
// periodEndDate?: Date;
|
||||
|
||||
// @IsInt()
|
||||
// @IsOptional()
|
||||
// periodDays?: number;
|
||||
|
||||
// @IsBoolean()
|
||||
// @IsOptional()
|
||||
// periodCountCalendarDays?: boolean;
|
||||
|
||||
// @IsBoolean()
|
||||
// @IsOptional()
|
||||
// lockTimeZoneToggleOnBookingPage?: boolean;
|
||||
|
||||
// @IsBoolean()
|
||||
// @IsOptional()
|
||||
// requiresConfirmation?: boolean;
|
||||
|
||||
// @IsBoolean()
|
||||
// @IsOptional()
|
||||
// requiresBookerEmailVerification?: boolean;
|
||||
|
||||
// @ValidateNested()
|
||||
// @Type(() => RecurringEvent)
|
||||
// @IsOptional()
|
||||
// recurringEvent?: RecurringEvent;
|
||||
|
||||
// @IsBoolean()
|
||||
// @IsOptional()
|
||||
// disableGuests?: boolean;
|
||||
|
||||
// @IsBoolean()
|
||||
// @IsOptional()
|
||||
// hideCalendarNotes?: boolean;
|
||||
|
||||
// @IsInt()
|
||||
// @Min(0)
|
||||
// @IsOptional()
|
||||
// minimumBookingNotice?: number;
|
||||
|
||||
// @IsInt()
|
||||
// @IsOptional()
|
||||
// beforeEventBuffer?: number;
|
||||
|
||||
// @IsInt()
|
||||
// @IsOptional()
|
||||
// afterEventBuffer?: number;
|
||||
|
||||
// @IsInt()
|
||||
// @IsOptional()
|
||||
// seatsPerTimeSlot?: number;
|
||||
|
||||
// @IsBoolean()
|
||||
// @IsOptional()
|
||||
// onlyShowFirstAvailableSlot?: boolean;
|
||||
|
||||
// @IsBoolean()
|
||||
// @IsOptional()
|
||||
// seatsShowAttendees?: boolean;
|
||||
|
||||
// @IsBoolean()
|
||||
// @IsOptional()
|
||||
// seatsShowAvailabilityCount?: boolean;
|
||||
|
||||
// @IsEnum(SchedulingType)
|
||||
// @IsOptional()
|
||||
// schedulingType?: SchedulingType; -> import { SchedulingType } from "@/ee/event-types/inputs/enums/scheduling-type";
|
||||
|
||||
// @IsInt()
|
||||
// @IsOptional()
|
||||
// scheduleId?: number;
|
||||
|
||||
// @IsInt()
|
||||
// @IsOptional()
|
||||
// price?: number;
|
||||
|
||||
// @IsString()
|
||||
// @IsOptional()
|
||||
// currency?: string;
|
||||
|
||||
// @IsInt()
|
||||
// @IsOptional()
|
||||
// slotInterval?: number;
|
||||
|
||||
// @IsString()
|
||||
// @IsOptional()
|
||||
// @IsUrl()
|
||||
// successRedirectUrl?: string;
|
||||
|
||||
// @ValidateNested()
|
||||
// @Type(() => IntervalLimits)
|
||||
// @IsOptional()
|
||||
// bookingLimits?: IntervalLimits;
|
||||
|
||||
// @ValidateNested()
|
||||
// @Type(() => IntervalLimits)
|
||||
// @IsOptional()
|
||||
// durationLimits?: IntervalLimits;
|
||||
|
||||
// @IsBoolean()
|
||||
// @IsOptional()
|
||||
// isInstantEvent?: boolean;
|
||||
|
||||
// @IsBoolean()
|
||||
// @IsOptional()
|
||||
// assignAllTeamMembers?: boolean;
|
||||
|
||||
// @IsBoolean()
|
||||
// @IsOptional()
|
||||
// useEventTypeDestinationCalendarEmail?: boolean;
|
||||
|
||||
// @IsInt()
|
||||
// @IsOptional()
|
||||
// secondaryEmailId?: number;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { EventTypeOutput } from "@/ee/event-types/outputs/event-type.output";
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsEnum, IsNotEmptyObject, ValidateNested } from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
export class CreateEventTypeOutput {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@ApiProperty({
|
||||
type: EventTypeOutput,
|
||||
})
|
||||
@IsNotEmptyObject()
|
||||
@ValidateNested()
|
||||
@Type(() => EventTypeOutput)
|
||||
data!: EventTypeOutput;
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
import {
|
||||
CREATE_EVENT_DESCRIPTION_EXAMPLE,
|
||||
CREATE_EVENT_LENGTH_EXAMPLE,
|
||||
CREATE_EVENT_SLUG_EXAMPLE,
|
||||
CREATE_EVENT_TITLE_EXAMPLE,
|
||||
} from "@/ee/event-types/inputs/create-event-type.input";
|
||||
import { PeriodType } from "@/ee/event-types/inputs/enums/period-type";
|
||||
import { SchedulingType } from "@/ee/event-types/inputs/enums/scheduling-type";
|
||||
import { EventTypeLocation } from "@/ee/event-types/inputs/event-type-location.input";
|
||||
import {
|
||||
BookingField,
|
||||
IntervalLimits,
|
||||
RecurringEvent,
|
||||
} from "@/ee/event-types/inputs/update-event-type.input";
|
||||
import { ApiProperty as DocsProperty, ApiHideProperty } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import {
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
IsDate,
|
||||
IsEnum,
|
||||
IsInt,
|
||||
IsJSON,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsString,
|
||||
ValidateNested,
|
||||
} from "class-validator";
|
||||
|
||||
export class EventTypeOutput {
|
||||
@IsInt()
|
||||
@DocsProperty({ example: 1 })
|
||||
id!: number;
|
||||
|
||||
@IsInt()
|
||||
@DocsProperty({ example: CREATE_EVENT_LENGTH_EXAMPLE })
|
||||
length!: number;
|
||||
|
||||
@IsString()
|
||||
@DocsProperty({ example: CREATE_EVENT_SLUG_EXAMPLE })
|
||||
slug!: string;
|
||||
|
||||
@IsString()
|
||||
@DocsProperty({ example: CREATE_EVENT_TITLE_EXAMPLE })
|
||||
title!: string;
|
||||
|
||||
@IsString()
|
||||
@DocsProperty({ example: CREATE_EVENT_DESCRIPTION_EXAMPLE })
|
||||
description!: string | null;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiHideProperty()
|
||||
hidden!: boolean;
|
||||
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => EventTypeLocation)
|
||||
@IsArray()
|
||||
locations!: EventTypeLocation | null;
|
||||
|
||||
@IsInt()
|
||||
@ApiHideProperty()
|
||||
position!: number;
|
||||
|
||||
@IsInt()
|
||||
@ApiHideProperty()
|
||||
offsetStart!: number;
|
||||
|
||||
@IsInt()
|
||||
@ApiHideProperty()
|
||||
userId!: number | null;
|
||||
|
||||
@IsInt()
|
||||
@ApiHideProperty()
|
||||
profileId!: number | null;
|
||||
|
||||
@IsInt()
|
||||
@ApiHideProperty()
|
||||
teamId!: number | null;
|
||||
|
||||
@IsString()
|
||||
@ApiHideProperty()
|
||||
eventName!: string | null;
|
||||
|
||||
@IsInt()
|
||||
@ApiHideProperty()
|
||||
parentId!: number | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => BookingField)
|
||||
@ApiHideProperty()
|
||||
bookingFields!: BookingField[] | null;
|
||||
|
||||
@IsString()
|
||||
@ApiHideProperty()
|
||||
timeZone!: string | null;
|
||||
|
||||
@IsEnum(PeriodType)
|
||||
@ApiHideProperty()
|
||||
periodType!: PeriodType | null;
|
||||
|
||||
@IsDate()
|
||||
@ApiHideProperty()
|
||||
periodStartDate!: Date | null;
|
||||
|
||||
@IsDate()
|
||||
@ApiHideProperty()
|
||||
periodEndDate!: Date | null;
|
||||
|
||||
@IsInt()
|
||||
@ApiHideProperty()
|
||||
periodDays!: number | null;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiHideProperty()
|
||||
periodCountCalendarDays!: boolean | null;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiHideProperty()
|
||||
lockTimeZoneToggleOnBookingPage!: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiHideProperty()
|
||||
requiresConfirmation!: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiHideProperty()
|
||||
requiresBookerEmailVerification!: boolean;
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => RecurringEvent)
|
||||
@IsOptional()
|
||||
@ApiHideProperty()
|
||||
recurringEvent!: RecurringEvent | null;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiHideProperty()
|
||||
disableGuests!: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiHideProperty()
|
||||
hideCalendarNotes!: boolean;
|
||||
|
||||
@IsInt()
|
||||
@ApiHideProperty()
|
||||
minimumBookingNotice!: number;
|
||||
|
||||
@IsInt()
|
||||
@ApiHideProperty()
|
||||
beforeEventBuffer!: number;
|
||||
|
||||
@IsInt()
|
||||
@ApiHideProperty()
|
||||
afterEventBuffer!: number;
|
||||
|
||||
@IsInt()
|
||||
@ApiHideProperty()
|
||||
seatsPerTimeSlot!: number | null;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiHideProperty()
|
||||
onlyShowFirstAvailableSlot!: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiHideProperty()
|
||||
seatsShowAttendees!: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiHideProperty()
|
||||
seatsShowAvailabilityCount!: boolean;
|
||||
|
||||
@IsEnum(SchedulingType)
|
||||
@ApiHideProperty()
|
||||
schedulingType!: SchedulingType | null;
|
||||
|
||||
@IsInt()
|
||||
@ApiHideProperty()
|
||||
scheduleId!: number | null;
|
||||
|
||||
@IsNumber()
|
||||
@ApiHideProperty()
|
||||
price!: number;
|
||||
|
||||
@IsString()
|
||||
@ApiHideProperty()
|
||||
currency!: string;
|
||||
|
||||
@IsInt()
|
||||
@ApiHideProperty()
|
||||
slotInterval!: number | null;
|
||||
|
||||
@IsJSON()
|
||||
@ApiHideProperty()
|
||||
metadata!: Record<string, any> | null;
|
||||
|
||||
@IsString()
|
||||
@ApiHideProperty()
|
||||
successRedirectUrl!: string | null;
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => IntervalLimits)
|
||||
@IsOptional()
|
||||
@ApiHideProperty()
|
||||
bookingLimits!: IntervalLimits;
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => IntervalLimits)
|
||||
@ApiHideProperty()
|
||||
durationLimits!: IntervalLimits;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiHideProperty()
|
||||
isInstantEvent!: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiHideProperty()
|
||||
assignAllTeamMembers!: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiHideProperty()
|
||||
useEventTypeDestinationCalendarEmail!: boolean;
|
||||
|
||||
@IsInt()
|
||||
@ApiHideProperty()
|
||||
secondaryEmailId!: number | null;
|
||||
}
|
||||
@@ -1,26 +1,73 @@
|
||||
import { DEFAULT_EVENT_TYPES } from "@/ee/event-types/constants/constants";
|
||||
import { EventTypesRepository } from "@/ee/event-types/event-types.repository";
|
||||
import { CreateEventTypeInput } from "@/ee/event-types/inputs/create-event-type.input";
|
||||
import { UpdateEventTypeInput } from "@/ee/event-types/inputs/update-event-type.input";
|
||||
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
|
||||
import { UserWithProfile, UsersRepository } from "@/modules/users/users.repository";
|
||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from "@nestjs/common";
|
||||
|
||||
import { createEventType, updateEventType } from "@calcom/platform-libraries";
|
||||
import { getEventTypesPublic, EventTypesPublic } from "@calcom/platform-libraries";
|
||||
import { EventType } from "@calcom/prisma/client";
|
||||
|
||||
@Injectable()
|
||||
export class EventTypesService {
|
||||
constructor(
|
||||
private readonly eventTypesRepository: EventTypesRepository,
|
||||
private readonly membershipsRepository: MembershipsRepository,
|
||||
private readonly usersRepository: UsersRepository
|
||||
private readonly usersRepository: UsersRepository,
|
||||
private readonly selectedCalendarsRepository: SelectedCalendarsRepository,
|
||||
private readonly dbWrite: PrismaWriteService
|
||||
) {}
|
||||
|
||||
async createUserEventType(userId: number, body: CreateEventTypeInput) {
|
||||
return this.eventTypesRepository.createUserEventType(userId, body);
|
||||
async createUserEventType(user: UserWithProfile, body: CreateEventTypeInput) {
|
||||
await this.checkCanCreateEventType(user.id, body);
|
||||
const eventTypeUser = await this.getUserToCreateEvent(user);
|
||||
const { eventType } = await createEventType({
|
||||
input: body,
|
||||
ctx: {
|
||||
user: eventTypeUser,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
prisma: this.dbWrite.prisma,
|
||||
},
|
||||
});
|
||||
return eventType;
|
||||
}
|
||||
|
||||
async checkCanCreateEventType(userId: number, body: CreateEventTypeInput) {
|
||||
const existsWithSlug = await this.eventTypesRepository.getUserEventTypeBySlug(userId, body.slug);
|
||||
if (existsWithSlug) {
|
||||
throw new BadRequestException("User already has an event type with this slug.");
|
||||
}
|
||||
}
|
||||
|
||||
async getUserToCreateEvent(user: UserWithProfile) {
|
||||
const organizationId = user.movedToProfile?.organizationId || user.organizationId;
|
||||
const isOrgAdmin = organizationId
|
||||
? await this.membershipsRepository.isUserOrganizationAdmin(user.id, organizationId)
|
||||
: false;
|
||||
const profileId = user.movedToProfile?.id || null;
|
||||
return {
|
||||
id: user.id,
|
||||
organizationId: user.organizationId,
|
||||
organization: { isOrgAdmin },
|
||||
profile: { id: profileId },
|
||||
metadata: user.metadata,
|
||||
};
|
||||
}
|
||||
|
||||
async getUserEventType(userId: number, eventTypeId: number) {
|
||||
return this.eventTypesRepository.getUserEventType(userId, eventTypeId);
|
||||
const eventType = await this.eventTypesRepository.getUserEventType(userId, eventTypeId);
|
||||
|
||||
if (!eventType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
this.checkUserOwnsEventType(userId, eventType);
|
||||
return eventType;
|
||||
}
|
||||
|
||||
async getUserEventTypeForAtom(user: UserWithProfile, eventTypeId: number) {
|
||||
@@ -30,7 +77,18 @@ export class EventTypesService {
|
||||
? await this.membershipsRepository.isUserOrganizationAdmin(user.id, organizationId)
|
||||
: false;
|
||||
|
||||
return this.eventTypesRepository.getUserEventTypeForAtom(user, isUserOrganizationAdmin, eventTypeId);
|
||||
const eventType = await this.eventTypesRepository.getUserEventTypeForAtom(
|
||||
user,
|
||||
isUserOrganizationAdmin,
|
||||
eventTypeId
|
||||
);
|
||||
|
||||
if (!eventType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
this.checkUserOwnsEventType(user.id, eventType.eventType);
|
||||
return eventType;
|
||||
}
|
||||
|
||||
async getEventTypesPublicByUsername(username: string): Promise<EventTypesPublic> {
|
||||
@@ -58,4 +116,51 @@ export class EventTypesService {
|
||||
|
||||
return defaultEventTypes;
|
||||
}
|
||||
|
||||
async updateEventType(eventTypeId: number, body: UpdateEventTypeInput, user: UserWithProfile) {
|
||||
this.checkCanUpdateEventType(user.id, eventTypeId);
|
||||
const eventTypeUser = await this.getUserToUpdateEvent(user);
|
||||
const { eventType } = await updateEventType({
|
||||
input: { id: eventTypeId, ...body },
|
||||
ctx: {
|
||||
user: eventTypeUser,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
prisma: this.dbWrite.prisma,
|
||||
},
|
||||
});
|
||||
|
||||
return eventType;
|
||||
}
|
||||
|
||||
async checkCanUpdateEventType(userId: number, eventTypeId: number) {
|
||||
const existingEventType = await this.getUserEventType(userId, eventTypeId);
|
||||
if (!existingEventType) {
|
||||
throw new NotFoundException(`Event type with id ${eventTypeId} not found`);
|
||||
}
|
||||
this.checkUserOwnsEventType(userId, existingEventType);
|
||||
}
|
||||
|
||||
async getUserToUpdateEvent(user: UserWithProfile) {
|
||||
const profileId = user.movedToProfile?.id || null;
|
||||
const selectedCalendars = await this.selectedCalendarsRepository.getUserSelectedCalendars(user.id);
|
||||
return { ...user, profile: { id: profileId }, selectedCalendars };
|
||||
}
|
||||
|
||||
async deleteEventType(eventTypeId: number, userId: number) {
|
||||
const existingEventType = await this.eventTypesRepository.getEventTypeById(eventTypeId);
|
||||
if (!existingEventType) {
|
||||
throw new NotFoundException(`Event type with ID=${eventTypeId} does not exist.`);
|
||||
}
|
||||
|
||||
this.checkUserOwnsEventType(userId, existingEventType);
|
||||
|
||||
return this.eventTypesRepository.deleteEventType(eventTypeId);
|
||||
}
|
||||
|
||||
checkUserOwnsEventType(userId: number, eventType: Pick<EventType, "id" | "userId">) {
|
||||
if (userId !== eventType.userId) {
|
||||
throw new ForbiddenException(`User with ID=${userId} does not own event type with ID=${eventType.id}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
@Module({
|
||||
imports: [],
|
||||
imports: [PrismaModule],
|
||||
providers: [SelectedCalendarsRepository],
|
||||
exports: [SelectedCalendarsRepository],
|
||||
})
|
||||
export class CredentialsModule {}
|
||||
export class SelectedCalendarsModule {}
|
||||
|
||||
@@ -16,4 +16,12 @@ export class SelectedCalendarsRepository {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getUserSelectedCalendars(userId: number) {
|
||||
return this.dbRead.prisma.selectedCalendar.findMany({
|
||||
where: {
|
||||
userId,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -533,12 +533,15 @@
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
"$ref": "#/components/schemas/CreateEventTypeOutput"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Event types"
|
||||
]
|
||||
},
|
||||
"get": {
|
||||
"operationId": "EventTypesController_getEventTypes",
|
||||
@@ -554,7 +557,10 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Event types"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/event-types/{eventTypeId}": {
|
||||
@@ -581,7 +587,76 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Event types"
|
||||
]
|
||||
},
|
||||
"patch": {
|
||||
"operationId": "EventTypesController_updateEventType",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "eventTypeId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UpdateEventTypeInput"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Event types"
|
||||
]
|
||||
},
|
||||
"delete": {
|
||||
"operationId": "EventTypesController_deleteEventType",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "eventTypeId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Event types"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/event-types/{username}/public": {
|
||||
@@ -608,7 +683,10 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Event types"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/ee/gcal/oauth/auth-url": {
|
||||
@@ -1324,11 +1402,11 @@
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "success",
|
||||
"enum": [
|
||||
"success",
|
||||
"error"
|
||||
]
|
||||
],
|
||||
"example": "success"
|
||||
},
|
||||
"data": {
|
||||
"example": {
|
||||
@@ -1367,7 +1445,8 @@
|
||||
"example": 3
|
||||
},
|
||||
"logo": {
|
||||
"type": "object",
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"example": "https://example.com/logo.png"
|
||||
},
|
||||
"redirectUris": {
|
||||
@@ -1530,7 +1609,118 @@
|
||||
"refreshToken"
|
||||
]
|
||||
},
|
||||
"EventTypeLocation": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"example": "link"
|
||||
},
|
||||
"link": {
|
||||
"type": "string",
|
||||
"example": "https://masterchief.com/argentina/flan/video/9129412"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
]
|
||||
},
|
||||
"CreateEventTypeInput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"length": {
|
||||
"type": "number",
|
||||
"minimum": 1,
|
||||
"example": 60
|
||||
},
|
||||
"slug": {
|
||||
"type": "string",
|
||||
"example": "cooking-class"
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"example": "Learn the secrets of masterchief!"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"example": "Discover the culinary wonders of the Argentina by making the best flan ever!"
|
||||
},
|
||||
"locations": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/EventTypeLocation"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"length",
|
||||
"slug",
|
||||
"title"
|
||||
]
|
||||
},
|
||||
"EventTypeOutput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number",
|
||||
"example": 1
|
||||
},
|
||||
"length": {
|
||||
"type": "number",
|
||||
"example": 60
|
||||
},
|
||||
"slug": {
|
||||
"type": "string",
|
||||
"example": "cooking-class"
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"example": "Learn the secrets of masterchief!"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"example": "Discover the culinary wonders of the Argentina by making the best flan ever!"
|
||||
},
|
||||
"locations": {
|
||||
"nullable": true,
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/EventTypeLocation"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"length",
|
||||
"slug",
|
||||
"title",
|
||||
"description",
|
||||
"locations"
|
||||
]
|
||||
},
|
||||
"CreateEventTypeOutput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "success",
|
||||
"enum": [
|
||||
"success",
|
||||
"error"
|
||||
]
|
||||
},
|
||||
"data": {
|
||||
"$ref": "#/components/schemas/EventTypeOutput"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status",
|
||||
"data"
|
||||
]
|
||||
},
|
||||
"UpdateEventTypeInput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"length": {
|
||||
@@ -1542,13 +1732,20 @@
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"hidden": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"locations": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/EventTypeLocation"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"length",
|
||||
"slug",
|
||||
"title"
|
||||
]
|
||||
}
|
||||
},
|
||||
"CreateAvailabilityInput": {
|
||||
"type": "object",
|
||||
|
||||
@@ -21,7 +21,7 @@ export class EventTypesRepositoryFixture {
|
||||
});
|
||||
}
|
||||
|
||||
async create(data: CreateEventTypeInput, userId: number) {
|
||||
async create(data: Pick<CreateEventTypeInput, "title" | "slug" | "length" | "hidden">, userId: number) {
|
||||
return this.prismaWriteClient.eventType.create({
|
||||
data: {
|
||||
...data,
|
||||
|
||||
@@ -2,7 +2,10 @@ import { prisma } from "@calcom/prisma";
|
||||
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
|
||||
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
||||
|
||||
export async function getUsersCredentials(user: NonNullable<TrpcSessionUser>) {
|
||||
type SessionUser = NonNullable<TrpcSessionUser>;
|
||||
type User = { id: SessionUser["id"] };
|
||||
|
||||
export async function getUsersCredentials(user: User) {
|
||||
const credentials = await prisma.credential.findMany({
|
||||
where: {
|
||||
userId: user.id,
|
||||
|
||||
@@ -19,10 +19,15 @@ export type UpdateScheduleOutputType = Awaited<
|
||||
export { getEventTypeById } from "@calcom/lib/event-types/getEventTypeById";
|
||||
export { getEventTypesByViewer } from "@calcom/lib/event-types/getEventTypesByViewer";
|
||||
export { getEventTypesPublic } from "@calcom/lib/event-types/getEventTypesPublic";
|
||||
export { createHandler as createEventType } from "@calcom/trpc/server/routers/viewer/eventTypes/create.handler";
|
||||
export { updateHandler as updateEventType } from "@calcom/trpc/server/routers/viewer/eventTypes/update.handler";
|
||||
|
||||
export { SchedulingType, PeriodType } from "@calcom/prisma/enums";
|
||||
|
||||
export type { EventType } from "@calcom/lib/event-types/getEventTypeById";
|
||||
export type { EventTypesByViewer } from "@calcom/lib/event-types/getEventTypesByViewer";
|
||||
export type { EventTypesPublic } from "@calcom/lib/event-types/getEventTypesPublic";
|
||||
export type { UpdateEventTypeReturn } from "@calcom/trpc/server/routers/viewer/eventTypes/update.handler";
|
||||
|
||||
export type PublicEventType = Awaited<ReturnType<typeof getPublicEvent>>;
|
||||
export { getPublicEvent };
|
||||
|
||||
@@ -318,6 +318,11 @@ export const createdEventSchema = z
|
||||
})
|
||||
.passthrough();
|
||||
|
||||
const schemaDefaultConferencingApp = z.object({
|
||||
appSlug: z.string().default("daily-video").optional(),
|
||||
appLink: z.string().optional(),
|
||||
});
|
||||
|
||||
export const userMetadata = z
|
||||
.object({
|
||||
proPaidForByTeamId: z.number().optional(),
|
||||
@@ -325,12 +330,7 @@ export const userMetadata = z
|
||||
vitalSettings: vitalSettingsUpdateSchema.optional(),
|
||||
isPremium: z.boolean().optional(),
|
||||
sessionTimeout: z.number().optional(), // Minutes
|
||||
defaultConferencingApp: z
|
||||
.object({
|
||||
appSlug: z.string().default("daily-video").optional(),
|
||||
appLink: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
defaultConferencingApp: schemaDefaultConferencingApp.optional(),
|
||||
defaultBookerLayouts: bookerLayouts.optional(),
|
||||
emailChangeWaitingForVerification: z
|
||||
.string()
|
||||
@@ -347,6 +347,8 @@ export const userMetadata = z
|
||||
})
|
||||
.nullable();
|
||||
|
||||
export type DefaultConferencingApp = z.infer<typeof schemaDefaultConferencingApp>;
|
||||
|
||||
export const orgSettingsSchema = z
|
||||
.object({
|
||||
isOrganizationVerified: z.boolean().optional(),
|
||||
|
||||
@@ -28,3 +28,5 @@ export const createEventTypeInput = z.object({
|
||||
description: z.string(),
|
||||
length: z.number(),
|
||||
}).strict();
|
||||
|
||||
export type EventTypeLocation = (z.infer<typeof imports.eventTypeLocations>)[number];
|
||||
@@ -7,9 +7,15 @@ import { TRPCError } from "@trpc/server";
|
||||
|
||||
import type { TSetDestinationCalendarInputSchema } from "./setDestinationCalendar.schema";
|
||||
|
||||
type SessionUser = NonNullable<TrpcSessionUser>;
|
||||
type User = {
|
||||
id: SessionUser["id"];
|
||||
selectedCalendars: SessionUser["selectedCalendars"];
|
||||
};
|
||||
|
||||
type SetDestinationCalendarOptions = {
|
||||
ctx: {
|
||||
user: NonNullable<TrpcSessionUser>;
|
||||
user: User;
|
||||
};
|
||||
input: TSetDestinationCalendarInputSchema;
|
||||
};
|
||||
|
||||
@@ -9,49 +9,43 @@ import { EventTypeRepository } from "@calcom/lib/server/repository/eventType";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
import { SchedulingType } from "@calcom/prisma/enums";
|
||||
import { userMetadata as userMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
import type { EventTypeLocation } from "@calcom/prisma/zod/custom/eventtype";
|
||||
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
import type { TrpcSessionUser } from "../../../trpc";
|
||||
import type { TCreateInputSchema } from "./create.schema";
|
||||
|
||||
type SessionUser = NonNullable<TrpcSessionUser>;
|
||||
type User = {
|
||||
id: SessionUser["id"];
|
||||
organizationId: SessionUser["organizationId"];
|
||||
organization: {
|
||||
isOrgAdmin: SessionUser["organization"]["isOrgAdmin"];
|
||||
};
|
||||
profile: {
|
||||
id: SessionUser["id"] | null;
|
||||
};
|
||||
metadata: SessionUser["metadata"];
|
||||
};
|
||||
|
||||
type CreateOptions = {
|
||||
ctx: {
|
||||
user: NonNullable<TrpcSessionUser>;
|
||||
user: User;
|
||||
prisma: PrismaClient;
|
||||
};
|
||||
input: TCreateInputSchema;
|
||||
};
|
||||
|
||||
export const createHandler = async ({ ctx, input }: CreateOptions) => {
|
||||
const { schedulingType, teamId, metadata, ...rest } = input;
|
||||
const { schedulingType, teamId, metadata, locations: inputLocations, ...rest } = input;
|
||||
|
||||
const userId = ctx.user.id;
|
||||
const isManagedEventType = schedulingType === SchedulingType.MANAGED;
|
||||
const isOrgAdmin = !!ctx.user?.organization?.isOrgAdmin;
|
||||
// Get Users default conferencing app
|
||||
|
||||
const defaultConferencingData = userMetadataSchema.parse(ctx.user.metadata)?.defaultConferencingApp;
|
||||
const appKeys = await getAppKeysFromSlug("daily-video");
|
||||
|
||||
let locations: { type: string; link?: string }[] = [];
|
||||
|
||||
// If no locations are passed in and the user has a daily api key then default to daily
|
||||
if (
|
||||
(typeof rest?.locations === "undefined" || rest.locations?.length === 0) &&
|
||||
typeof appKeys.api_key === "string"
|
||||
) {
|
||||
locations = [{ type: DailyLocationType }];
|
||||
}
|
||||
|
||||
if (defaultConferencingData && defaultConferencingData.appSlug !== "daily-video") {
|
||||
const credentials = await getUsersCredentials(ctx.user);
|
||||
const foundApp = getApps(credentials, true).filter(
|
||||
(app) => app.slug === defaultConferencingData.appSlug
|
||||
)[0]; // There is only one possible install here so index [0] is the one we are looking for ;
|
||||
const locationType = foundApp?.locationOption?.value ?? DailyLocationType; // Default to Daily if no location type is found
|
||||
locations = [{ type: locationType, link: defaultConferencingData.appLink }];
|
||||
}
|
||||
const locations: EventTypeLocation[] =
|
||||
inputLocations && inputLocations.length !== 0 ? inputLocations : await getDefaultLocations(ctx.user);
|
||||
|
||||
const data: Prisma.EventTypeCreateInput = {
|
||||
...rest,
|
||||
@@ -122,3 +116,23 @@ export const createHandler = async ({ ctx, input }: CreateOptions) => {
|
||||
throw new TRPCError({ code: "BAD_REQUEST" });
|
||||
}
|
||||
};
|
||||
|
||||
async function getDefaultLocations(user: User): Promise<EventTypeLocation[]> {
|
||||
const defaultConferencingData = userMetadataSchema.parse(user.metadata)?.defaultConferencingApp;
|
||||
const appKeys = await getAppKeysFromSlug("daily-video");
|
||||
|
||||
if (typeof appKeys.api_key === "string") {
|
||||
return [{ type: DailyLocationType }];
|
||||
}
|
||||
|
||||
if (defaultConferencingData && defaultConferencingData.appSlug !== "daily-video") {
|
||||
const credentials = await getUsersCredentials(user);
|
||||
const foundApp = getApps(credentials, true).filter(
|
||||
(app) => app.slug === defaultConferencingData.appSlug
|
||||
)[0]; // There is only one possible install here so index [0] is the one we are looking for ;
|
||||
const locationType = foundApp?.locationOption?.value ?? DailyLocationType; // Default to Daily if no location type is found
|
||||
return [{ type: locationType, link: defaultConferencingData.appLink }];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -18,15 +18,27 @@ import { setDestinationCalendarHandler } from "../../loggedInViewer/setDestinati
|
||||
import type { TUpdateInputSchema } from "./update.schema";
|
||||
import { ensureUniqueBookingFields, handleCustomInputs, handlePeriodType } from "./util";
|
||||
|
||||
type SessionUser = NonNullable<TrpcSessionUser>;
|
||||
type User = {
|
||||
id: SessionUser["id"];
|
||||
username: SessionUser["username"];
|
||||
profile: {
|
||||
id: SessionUser["profile"]["id"] | null;
|
||||
};
|
||||
selectedCalendars: SessionUser["selectedCalendars"];
|
||||
};
|
||||
|
||||
type UpdateOptions = {
|
||||
ctx: {
|
||||
user: NonNullable<TrpcSessionUser>;
|
||||
user: User;
|
||||
res?: NextApiResponse | GetServerSidePropsContext["res"];
|
||||
prisma: PrismaClient;
|
||||
};
|
||||
input: TUpdateInputSchema;
|
||||
};
|
||||
|
||||
export type UpdateEventTypeReturn = Awaited<ReturnType<typeof updateHandler>>;
|
||||
|
||||
export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
|
||||
const {
|
||||
schedule,
|
||||
|
||||
Reference in New Issue
Block a user