feat(api): add bookingUrl field to event types API v2 response (#26195)
* feat(api): add bookingUrl field to event types API v2 response
Add a new bookingUrl field to EventTypeOutput_2024_06_14 that contains
the full, correct booking URL for each event type. This fixes the issue
where companion app and Chrome extension were hardcoding cal.com URLs
instead of using organization subdomains.
Changes:
- Add bookingUrl field to EventTypeOutput_2024_06_14 type definition
- Update repository to include organization data when fetching users
- Add buildBookingUrl method to compute URL using getOrgFullOrigin
- Include bookingUrl in getResponseEventType return object
The booking URL is computed using the first user's organization slug
(if any) to generate the correct subdomain URL (e.g., i.cal.com/keith/30min
for organization users instead of cal.com/keith/30min).
* fix: include organization data in getEventTypeByIdWithHosts
Update getEventTypeByIdWithHosts to use usersInclude to fetch
organization slug data, ensuring bookingUrl is computed correctly
for the GET /v2/event-types/{eventTypeId} endpoint.
* test: add unit tests and E2E assertions for bookingUrl field
* fix(api-v2): correct bookingUrl format for org users and fix double slashes
Fix issues with bookingUrl field in event types API v2 response:
- Remove trailing slashes from base URLs to prevent double slashes
- Use profile.username instead of user.username for organization users
(profile contains clean username without org suffix)
- Include user profiles in repository queries to access profile data
- Create local org-domains utility to replace @calcom/features dependency
which isn't available in API v2 runtime
Changes:
- Add apps/api/v2/src/lib/org-domains.ts with getOrgFullOrigin function
adapted from @calcom/features/ee/organizations/lib/orgDomains
- Update event-types.repository.ts to include profiles in usersInclude
- Update buildBookingUrl() to prioritize profile data when available
- Update unit tests to cover organization user profile scenarios
Fixes:
- Double slashes in URLs (e.g., http://localhost:3000//user/slug)
- Incorrect username format for org users (e.g., owner1-acme instead
of owner1)
- Missing organization subdomain in booking URLs (e.g., should be
http://acme.localhost:3000/owner1/30min not
http://localhost:3000/owner1-acme/30min)
* deslop ai code
* handle empty username in both code and write test for it, use select instead of include
* feat(api-v2): reuse core org domain logic for event type bookingUrl
Export getOrgFullOrigin and subdomainSuffix from @calcom/platform-libraries/organizations
to reuse existing core logic instead of duplicating it in API v2. This ensures consistency
across the codebase and reduces maintenance burden.
- Export getOrgFullOrigin and subdomainSuffix from platform-libraries/organizations
- Update output-event-types.service to import from @calcom/platform-libraries/organizations
- Remove duplicate org-domains.ts file from API v2
- Update test mocks to use platform-libraries import path
- buildBookingUrl method now uses core getOrgFullOrigin function
This addresses feedback to reuse core code rather than introducing duplicate logic.
* test(api-v2): exclude bookingUrl from output comparison in e2e test
* refactor(api-v2): reuse core logic for event type bookingUrl
Replace duplicated username/org extraction logic in buildBookingUrl with
core functions. Use getBookerBaseUrlSync from @calcom/platform-libraries/organizations
for base URL generation, and add enrichUserWithProfile method that follows
the same pattern as core's UserRepository.enrichUsersWithTheirProfiles.
The enrichment logic is applied synchronously since profiles are already
fetched by the repository, avoiding the need to make the service async.
This ensures consistency with core patterns while maintaining the existing
synchronous API contract.
- Replace getOrgFullOrigin with getBookerBaseUrlSync
- Add enrichUserWithProfile method following core enrichment pattern
- Simplify buildBookingUrl to use enriched user data
- Update tests to mock getBookerBaseUrlSync and verify enrichment flow
* trailing slash cubic comment
* address review comments
* address review comments 2
* better code
* Updated the test to give the user an organization
* address review comments again
* Revert "address review comments again"
This reverts commit 622ea3fc8dcd7f5c3113614afcfc86beadecca0e.
* allow both the full UserWithProfile and the partial EventTypeUser (from select queries) to use the same getUserMainProfile() method, eliminating code duplication without changing the performance-optimized repository queries.
* reveiw points
This commit is contained in:
@@ -280,7 +280,7 @@ export class BookingsController_2024_08_13 {
|
||||
const { bookings, pagination } = await this.bookingsService.getBookings(queryParams, {
|
||||
email: user.email,
|
||||
id: user.id,
|
||||
orgId: profile?.organizationId,
|
||||
orgId: profile?.organizationId ?? undefined,
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
+12
-2
@@ -635,6 +635,7 @@ describe("Event types Endpoints", () => {
|
||||
|
||||
expect(createdEventType.bookingFields).toEqual(expectedBookingFields);
|
||||
expect(createdEventType.bookingRequiresAuthentication).toEqual(true);
|
||||
expect(createdEventType.bookingUrl).toContain(`/${user.username}/${body.slug}`);
|
||||
expect(createdEventType.disableCancelling).toEqual({ disabled: true });
|
||||
expect(createdEventType.disableRescheduling).toEqual({ disabled: true });
|
||||
expect(createdEventType.calVideoSettings?.sendTranscriptionEmails).toEqual(true);
|
||||
@@ -731,6 +732,7 @@ describe("Event types Endpoints", () => {
|
||||
);
|
||||
expect(fetchedEventType?.color).toEqual(eventType.color);
|
||||
expect(fetchedEventType?.hidden).toEqual(false);
|
||||
expect(fetchedEventType?.bookingUrl).toContain(`/${user.username}/${eventType.slug}`);
|
||||
expect(fetchedEventType?.disableCancelling).toEqual(eventType.disableCancelling);
|
||||
expect(fetchedEventType?.disableRescheduling).toEqual(eventType.disableRescheduling);
|
||||
expect(fetchedEventType?.calVideoSettings?.sendTranscriptionEmails).toEqual(
|
||||
@@ -1551,6 +1553,7 @@ describe("Event types Endpoints", () => {
|
||||
eventType.lockTimeZoneToggleOnBookingPage
|
||||
);
|
||||
expect(fetchedEventType.color).toEqual(eventType.color);
|
||||
expect(fetchedEventType.bookingUrl).toContain(`/${user.username}/${eventType.slug}`);
|
||||
});
|
||||
|
||||
it("system admin can access another user's event type by id", async () => {
|
||||
@@ -2677,12 +2680,19 @@ describe("Event types Endpoints", () => {
|
||||
expect(createdEventType.title).toEqual(body.title);
|
||||
secondCreatedEventType = responseBody.data;
|
||||
|
||||
const { id, title, slug, ...restFirst } = firstCreatedEventType;
|
||||
const { id: id2, title: title2, slug: slug2, ...restSecond } = secondCreatedEventType;
|
||||
const { id, title, slug, bookingUrl, ...restFirst } = firstCreatedEventType;
|
||||
const {
|
||||
id: id2,
|
||||
title: title2,
|
||||
slug: slug2,
|
||||
bookingUrl: bookingUrl2,
|
||||
...restSecond
|
||||
} = secondCreatedEventType;
|
||||
expect(restFirst).toEqual(restSecond);
|
||||
expect(id2).not.toEqual(id);
|
||||
expect(title2).not.toEqual(title);
|
||||
expect(slug2).not.toEqual(slug);
|
||||
expect(bookingUrl2).not.toEqual(bookingUrl);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import type { SortOrderType } from "@calcom/platform-types";
|
||||
import type { Prisma } from "@calcom/prisma/client";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { InputEventTransformed_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/transformed";
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import type { SortOrderType } from "@calcom/platform-types";
|
||||
import type { Prisma } from "@calcom/prisma/client";
|
||||
|
||||
@Injectable()
|
||||
export class EventTypesRepository_2024_06_14 {
|
||||
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
|
||||
constructor(
|
||||
private readonly dbRead: PrismaReadService,
|
||||
private readonly dbWrite: PrismaWriteService
|
||||
) {}
|
||||
|
||||
async createUserEventType(
|
||||
userId: number,
|
||||
@@ -58,13 +60,58 @@ export class EventTypesRepository_2024_06_14 {
|
||||
});
|
||||
}
|
||||
|
||||
private readonly usersInclude = {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
username: true,
|
||||
avatarUrl: true,
|
||||
brandColor: true,
|
||||
darkBrandColor: true,
|
||||
weekStart: true,
|
||||
metadata: true,
|
||||
organizationId: true,
|
||||
organization: {
|
||||
select: { slug: true },
|
||||
},
|
||||
movedToProfile: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
organizationId: true,
|
||||
organization: {
|
||||
select: {
|
||||
id: true,
|
||||
slug: true,
|
||||
isPlatform: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
profiles: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
organizationId: true,
|
||||
organization: {
|
||||
select: {
|
||||
id: true,
|
||||
slug: true,
|
||||
isPlatform: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
async getUserEventType(userId: number, eventTypeId: number) {
|
||||
return this.dbRead.prisma.eventType.findFirst({
|
||||
where: {
|
||||
id: eventTypeId,
|
||||
userId,
|
||||
},
|
||||
include: { users: true, schedule: true, destinationCalendar: true },
|
||||
include: { users: this.usersInclude, schedule: true, destinationCalendar: true },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -74,7 +121,7 @@ export class EventTypesRepository_2024_06_14 {
|
||||
userId,
|
||||
},
|
||||
...(sortCreatedAt && { orderBy: { id: sortCreatedAt } }),
|
||||
include: { users: true, schedule: true, destinationCalendar: true },
|
||||
include: { users: this.usersInclude, schedule: true, destinationCalendar: true },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -85,14 +132,19 @@ export class EventTypesRepository_2024_06_14 {
|
||||
hidden: false,
|
||||
},
|
||||
...(sortCreatedAt && { orderBy: { id: sortCreatedAt } }),
|
||||
include: { users: true, schedule: true, destinationCalendar: true },
|
||||
include: { users: this.usersInclude, schedule: true, destinationCalendar: true },
|
||||
});
|
||||
}
|
||||
|
||||
async getEventTypeById(eventTypeId: number) {
|
||||
return this.dbRead.prisma.eventType.findUnique({
|
||||
where: { id: eventTypeId },
|
||||
include: { users: true, schedule: true, destinationCalendar: true, calVideoSettings: true },
|
||||
include: {
|
||||
users: this.usersInclude,
|
||||
schedule: true,
|
||||
destinationCalendar: true,
|
||||
calVideoSettings: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -100,7 +152,7 @@ export class EventTypesRepository_2024_06_14 {
|
||||
return this.dbRead.prisma.eventType.findUnique({
|
||||
where: { id: eventTypeId },
|
||||
include: {
|
||||
users: true,
|
||||
users: this.usersInclude,
|
||||
schedule: true,
|
||||
destinationCalendar: true,
|
||||
calVideoSettings: true,
|
||||
@@ -140,7 +192,7 @@ export class EventTypesRepository_2024_06_14 {
|
||||
slug: slug,
|
||||
},
|
||||
},
|
||||
include: { users: true, schedule: true, destinationCalendar: true },
|
||||
include: { users: this.usersInclude, schedule: true, destinationCalendar: true },
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+229
@@ -0,0 +1,229 @@
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
|
||||
import { OutputEventTypesService_2024_06_14 } from "./output-event-types.service";
|
||||
import { UsersService } from "@/modules/users/services/users.service";
|
||||
import { UsersRepository } from "@/modules/users/users.repository";
|
||||
|
||||
jest.mock("@calcom/platform-libraries/organizations", () => ({
|
||||
getBookerBaseUrlSync: jest.fn((slug: string | null) => {
|
||||
if (!slug) return "https://cal.com";
|
||||
return `https://${slug}.cal.com`;
|
||||
}),
|
||||
}));
|
||||
|
||||
describe("OutputEventTypesService_2024_06_14", () => {
|
||||
let service: OutputEventTypesService_2024_06_14;
|
||||
let configService: jest.Mocked<ConfigService>;
|
||||
let usersService: UsersService;
|
||||
|
||||
beforeEach(() => {
|
||||
configService = {
|
||||
get: jest.fn((key: string, defaultValue?: string) => {
|
||||
if (key === "app.baseUrl") return "https://cal.com";
|
||||
return defaultValue;
|
||||
}),
|
||||
} as any;
|
||||
|
||||
const usersRepository = {} as UsersRepository;
|
||||
usersService = new UsersService(usersRepository);
|
||||
|
||||
service = new OutputEventTypesService_2024_06_14(configService, usersService);
|
||||
});
|
||||
|
||||
describe("buildBookingUrl", () => {
|
||||
it("should return correct URL for user without organization", () => {
|
||||
const user = {
|
||||
id: 1,
|
||||
name: "John Doe",
|
||||
username: "john-doe",
|
||||
avatarUrl: null,
|
||||
brandColor: null,
|
||||
darkBrandColor: null,
|
||||
weekStart: "Monday",
|
||||
metadata: {},
|
||||
organizationId: null,
|
||||
organization: null,
|
||||
movedToProfile: null,
|
||||
profiles: [],
|
||||
};
|
||||
const slug = "30min";
|
||||
|
||||
const result = service.buildBookingUrl(user, slug);
|
||||
|
||||
expect(result).toBe("https://cal.com/john-doe/30min");
|
||||
});
|
||||
|
||||
it("should use profile username for org users", () => {
|
||||
const user = {
|
||||
id: 1,
|
||||
name: "Owner",
|
||||
username: "owner1-acme",
|
||||
avatarUrl: null,
|
||||
brandColor: null,
|
||||
darkBrandColor: null,
|
||||
weekStart: "Monday",
|
||||
metadata: {},
|
||||
organizationId: 1,
|
||||
organization: { slug: "acme" },
|
||||
movedToProfile: null,
|
||||
profiles: [
|
||||
{
|
||||
id: 100,
|
||||
username: "owner1",
|
||||
organizationId: 1,
|
||||
organization: { id: 1, slug: "acme", isPlatform: false },
|
||||
},
|
||||
],
|
||||
};
|
||||
const slug = "30min";
|
||||
|
||||
const result = service.buildBookingUrl(user, slug);
|
||||
|
||||
expect(result).toBe("https://acme.cal.com/owner1/30min");
|
||||
});
|
||||
|
||||
it("should fall back to user username when profile has no username", () => {
|
||||
const user = {
|
||||
id: 1,
|
||||
name: "Keith",
|
||||
username: "keith",
|
||||
avatarUrl: null,
|
||||
brandColor: null,
|
||||
darkBrandColor: null,
|
||||
weekStart: "Monday",
|
||||
metadata: {},
|
||||
organizationId: 2,
|
||||
organization: { slug: "i" },
|
||||
movedToProfile: null,
|
||||
profiles: [
|
||||
{
|
||||
id: 200,
|
||||
username: null,
|
||||
organizationId: 2,
|
||||
organization: { id: 2, slug: "i", isPlatform: false },
|
||||
},
|
||||
],
|
||||
};
|
||||
const slug = "30min";
|
||||
|
||||
const result = service.buildBookingUrl(user, slug);
|
||||
|
||||
expect(result).toBe("https://i.cal.com/keith/30min");
|
||||
});
|
||||
|
||||
it("should return empty string when user is undefined", () => {
|
||||
const user = undefined;
|
||||
const slug = "30min";
|
||||
|
||||
const result = service.buildBookingUrl(user, slug);
|
||||
|
||||
expect(result).toBe("");
|
||||
});
|
||||
|
||||
it("should handle organization with null slug", () => {
|
||||
const user = {
|
||||
id: 1,
|
||||
name: "User",
|
||||
username: "user",
|
||||
avatarUrl: null,
|
||||
brandColor: null,
|
||||
darkBrandColor: null,
|
||||
weekStart: "Monday",
|
||||
metadata: {},
|
||||
organizationId: 3,
|
||||
organization: { slug: null },
|
||||
movedToProfile: null,
|
||||
profiles: [
|
||||
{
|
||||
id: 300,
|
||||
username: "user",
|
||||
organizationId: 3,
|
||||
organization: { id: 3, slug: null, isPlatform: false },
|
||||
},
|
||||
],
|
||||
};
|
||||
const slug = "consultation";
|
||||
|
||||
const result = service.buildBookingUrl(user, slug);
|
||||
|
||||
expect(result).toBe("https://cal.com/user/consultation");
|
||||
});
|
||||
|
||||
it("should handle base URL with trailing slash", () => {
|
||||
const { getBookerBaseUrlSync } = require("@calcom/platform-libraries/organizations");
|
||||
getBookerBaseUrlSync.mockReturnValueOnce("https://acme.cal.com/");
|
||||
|
||||
const user = {
|
||||
id: 1,
|
||||
name: "John",
|
||||
username: "john-acme",
|
||||
avatarUrl: null,
|
||||
brandColor: null,
|
||||
darkBrandColor: null,
|
||||
weekStart: "Monday",
|
||||
metadata: {},
|
||||
organizationId: 1,
|
||||
organization: { slug: "acme" },
|
||||
movedToProfile: null,
|
||||
profiles: [
|
||||
{
|
||||
id: 400,
|
||||
username: "john",
|
||||
organizationId: 1,
|
||||
organization: { id: 1, slug: "acme", isPlatform: false },
|
||||
},
|
||||
],
|
||||
};
|
||||
const slug = "30min";
|
||||
|
||||
const result = service.buildBookingUrl(user, slug);
|
||||
|
||||
expect(result).toBe("https://acme.cal.com/john/30min");
|
||||
});
|
||||
|
||||
it("should return empty string when username is empty", () => {
|
||||
const user = {
|
||||
id: 1,
|
||||
name: "User",
|
||||
username: "",
|
||||
avatarUrl: null,
|
||||
brandColor: null,
|
||||
darkBrandColor: null,
|
||||
weekStart: "Monday",
|
||||
metadata: {},
|
||||
organizationId: null,
|
||||
organization: null,
|
||||
movedToProfile: null,
|
||||
profiles: [],
|
||||
};
|
||||
const slug = "30min";
|
||||
|
||||
const result = service.buildBookingUrl(user, slug);
|
||||
|
||||
expect(result).toBe("");
|
||||
});
|
||||
|
||||
it("should return empty string when username is null", () => {
|
||||
const user = {
|
||||
id: 1,
|
||||
name: "User",
|
||||
username: null,
|
||||
avatarUrl: null,
|
||||
brandColor: null,
|
||||
darkBrandColor: null,
|
||||
weekStart: "Monday",
|
||||
metadata: {},
|
||||
organizationId: null,
|
||||
organization: null,
|
||||
movedToProfile: null,
|
||||
profiles: [],
|
||||
};
|
||||
const slug = "30min";
|
||||
|
||||
const result = service.buildBookingUrl(user, slug);
|
||||
|
||||
expect(result).toBe("");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+78
-27
@@ -1,41 +1,65 @@
|
||||
import {
|
||||
transformLocationsInternalToApi,
|
||||
transformBookingFieldsInternalToApi,
|
||||
InternalLocationSchema,
|
||||
SystemField,
|
||||
CustomField,
|
||||
transformIntervalLimitsInternalToApi,
|
||||
transformFutureBookingLimitsInternalToApi,
|
||||
transformRecurrenceInternalToApi,
|
||||
transformBookerLayoutsInternalToApi,
|
||||
transformRequiresConfirmationInternalToApi,
|
||||
transformEventTypeColorsInternalToApi,
|
||||
transformSeatsInternalToApi,
|
||||
InternalLocation,
|
||||
BookingFieldSchema,
|
||||
} from "@/ee/event-types/event-types_2024_06_14/transformers";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import {
|
||||
userMetadata,
|
||||
getBookingFieldsWithSystemFields,
|
||||
parseBookingLimit,
|
||||
parseRecurringEvent,
|
||||
getBookingFieldsWithSystemFields,
|
||||
userMetadata,
|
||||
} from "@calcom/platform-libraries";
|
||||
import { EventTypeMetaDataSchema, parseEventTypeColor } from "@calcom/platform-libraries/event-types";
|
||||
import { getBookerBaseUrlSync } from "@calcom/platform-libraries/organizations";
|
||||
import type {
|
||||
TransformFutureBookingsLimitSchema_2024_06_14,
|
||||
BookerLayoutsTransformedSchema,
|
||||
NoticeThresholdTransformedSchema,
|
||||
EventTypeOutput_2024_06_14,
|
||||
OutputUnknownLocation_2024_06_14,
|
||||
OutputUnknownBookingField_2024_06_14,
|
||||
NoticeThresholdTransformedSchema,
|
||||
OutputBookingField_2024_06_14,
|
||||
OutputUnknownBookingField_2024_06_14,
|
||||
OutputUnknownLocation_2024_06_14,
|
||||
TransformFutureBookingsLimitSchema_2024_06_14,
|
||||
} from "@calcom/platform-types";
|
||||
import type { EventType, User, Schedule, DestinationCalendar, CalVideoSettings } from "@calcom/prisma/client";
|
||||
import type {
|
||||
CalVideoSettings,
|
||||
DestinationCalendar,
|
||||
EventType,
|
||||
Prisma,
|
||||
Schedule,
|
||||
Team,
|
||||
} from "@calcom/prisma/client";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import {
|
||||
BookingFieldSchema,
|
||||
CustomField,
|
||||
InternalLocation,
|
||||
InternalLocationSchema,
|
||||
SystemField,
|
||||
transformBookerLayoutsInternalToApi,
|
||||
transformBookingFieldsInternalToApi,
|
||||
transformEventTypeColorsInternalToApi,
|
||||
transformFutureBookingLimitsInternalToApi,
|
||||
transformIntervalLimitsInternalToApi,
|
||||
transformLocationsInternalToApi,
|
||||
transformRecurrenceInternalToApi,
|
||||
transformRequiresConfirmationInternalToApi,
|
||||
transformSeatsInternalToApi,
|
||||
} from "@/ee/event-types/event-types_2024_06_14/transformers";
|
||||
import { ProfileMinimal, UsersService } from "@/modules/users/services/users.service";
|
||||
|
||||
type EventTypeUser = {
|
||||
id: number;
|
||||
name: string | null;
|
||||
username: string | null;
|
||||
avatarUrl: string | null;
|
||||
brandColor: string | null;
|
||||
darkBrandColor: string | null;
|
||||
weekStart: string;
|
||||
metadata: Prisma.JsonValue;
|
||||
organizationId: number | null;
|
||||
organization?: { slug: string | null } | null;
|
||||
movedToProfile?: ProfileMinimal | null;
|
||||
profiles?: ProfileMinimal[];
|
||||
};
|
||||
|
||||
type EventTypeRelations = {
|
||||
users: User[];
|
||||
users: EventTypeUser[];
|
||||
schedule: Schedule | null;
|
||||
destinationCalendar?: DestinationCalendar | null;
|
||||
calVideoSettings?: CalVideoSettings | null;
|
||||
@@ -106,6 +130,11 @@ type Input = Pick<
|
||||
|
||||
@Injectable()
|
||||
export class OutputEventTypesService_2024_06_14 {
|
||||
constructor(
|
||||
private readonly configService: ConfigService,
|
||||
private readonly usersService: UsersService
|
||||
) {}
|
||||
|
||||
getResponseEventType(
|
||||
ownerId: number,
|
||||
databaseEventType: Input,
|
||||
@@ -184,6 +213,7 @@ export class OutputEventTypesService_2024_06_14 {
|
||||
} as TransformFutureBookingsLimitSchema_2024_06_14);
|
||||
const destinationCalendar = this.transformDestinationCalendar(databaseEventType.destinationCalendar);
|
||||
const bookerActiveBookingsLimit = this.transformBookerActiveBookingsLimit(databaseEventType);
|
||||
const bookingUrl = this.buildBookingUrl(databaseEventType.users[0], slug);
|
||||
const disableReschedulingOutput = this.transformDisableRescheduling(
|
||||
disableRescheduling,
|
||||
minimumRescheduleNotice
|
||||
@@ -239,6 +269,7 @@ export class OutputEventTypesService_2024_06_14 {
|
||||
hidden,
|
||||
bookingRequiresAuthentication,
|
||||
bookerActiveBookingsLimit,
|
||||
bookingUrl,
|
||||
disableCancelling: disableCancellingOutput,
|
||||
disableRescheduling: disableReschedulingOutput,
|
||||
interfaceLanguage,
|
||||
@@ -342,7 +373,7 @@ export class OutputEventTypesService_2024_06_14 {
|
||||
return EventTypeMetaDataSchema.parse(metadata);
|
||||
}
|
||||
|
||||
transformUsers(users: User[]) {
|
||||
transformUsers(users: EventTypeUser[]) {
|
||||
return users.map((user) => {
|
||||
const metadata = user.metadata ? userMetadata.parse(user.metadata) : {};
|
||||
return {
|
||||
@@ -405,6 +436,26 @@ export class OutputEventTypesService_2024_06_14 {
|
||||
});
|
||||
}
|
||||
|
||||
buildBookingUrl(user: EventTypeUser | undefined, slug: string): string {
|
||||
if (!user) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const profile = this.usersService.getUserMainProfile(user);
|
||||
const username = profile?.username ?? user.username;
|
||||
|
||||
if (!username) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const orgSlug = profile?.organization?.slug ?? null;
|
||||
const webAppUrl = this.configService.get<string>("app.baseUrl", "https://app.cal.com");
|
||||
const baseUrl = orgSlug ? getBookerBaseUrlSync(orgSlug) : webAppUrl;
|
||||
const normalizedBaseUrl = baseUrl.replace(/\/$/, "");
|
||||
|
||||
return `${normalizedBaseUrl}/${username}/${slug}`;
|
||||
}
|
||||
|
||||
getResponseEventTypesWithoutHiddenFields(
|
||||
eventTypes: EventTypeOutput_2024_06_14[]
|
||||
): EventTypeOutput_2024_06_14[] {
|
||||
|
||||
@@ -3,6 +3,23 @@ import { Injectable } from "@nestjs/common";
|
||||
|
||||
import type { User } from "@calcom/prisma/client";
|
||||
|
||||
export type ProfileMinimal = {
|
||||
id: number;
|
||||
username?: string | null;
|
||||
organizationId: number | null;
|
||||
organization: {
|
||||
id: number;
|
||||
slug?: string | null;
|
||||
isPlatform: boolean;
|
||||
} | null;
|
||||
};
|
||||
|
||||
export type UserWithProfileMinimal = {
|
||||
organizationId?: number | null;
|
||||
movedToProfile?: ProfileMinimal | null;
|
||||
profiles?: ProfileMinimal[];
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class UsersService {
|
||||
constructor(private readonly usersRepository: UsersRepository) {}
|
||||
@@ -22,7 +39,7 @@ export class UsersService {
|
||||
return users;
|
||||
}
|
||||
|
||||
getUserMainProfile(user: UserWithProfile) {
|
||||
getUserMainProfile(user: UserWithProfileMinimal): ProfileMinimal | undefined {
|
||||
return (
|
||||
user?.movedToProfile ||
|
||||
user.profiles?.find((p) => p.organizationId === user.organizationId) ||
|
||||
@@ -30,8 +47,8 @@ export class UsersService {
|
||||
);
|
||||
}
|
||||
|
||||
getUserMainOrgId(user: UserWithProfile) {
|
||||
return this.getUserMainProfile(user)?.organizationId ?? user.organizationId;
|
||||
getUserMainOrgId(user: UserWithProfileMinimal): number | null {
|
||||
return this.getUserMainProfile(user)?.organizationId ?? user.organizationId ?? null;
|
||||
}
|
||||
|
||||
getUserProfileByOrgId(user: UserWithProfile, organizationId: number) {
|
||||
|
||||
@@ -21003,6 +21003,12 @@
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"bookingUrl": {
|
||||
"type": "string",
|
||||
"description": "Full URL to the booking page for this event type",
|
||||
"example": "https://cal.com/john-doe/30min",
|
||||
"format": "uri"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -21026,7 +21032,8 @@
|
||||
"hidden",
|
||||
"bookingRequiresAuthentication",
|
||||
"ownerId",
|
||||
"users"
|
||||
"users",
|
||||
"bookingUrl"
|
||||
]
|
||||
},
|
||||
"CreateEventTypeOutput_2024_06_14": {
|
||||
|
||||
@@ -2,3 +2,6 @@ export { getOrganizationRepository } from "@calcom/features/ee/organizations/di/
|
||||
export { OrganizationRepository } from "@calcom/features/ee/organizations/repositories/OrganizationRepository";
|
||||
export { OrganizationMembershipService } from "@calcom/features/ee/organizations/lib/service/OrganizationMembershipService";
|
||||
export type { IOrganizationRepository } from "@calcom/features/ee/organizations/lib/repository/IOrganizationRepository";
|
||||
|
||||
export { getOrgFullOrigin, subdomainSuffix } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
export { getBookerBaseUrlSync } from "@calcom/features/ee/organizations/lib/getBookerBaseUrlSync";
|
||||
|
||||
+8
@@ -546,6 +546,14 @@ export class EventTypeOutput_2024_06_14 extends BaseEventTypeOutput_2024_06_14 {
|
||||
@IsArray()
|
||||
@DocsProperty()
|
||||
users!: User_2024_06_14[];
|
||||
|
||||
@IsString()
|
||||
@DocsProperty({
|
||||
description: "Full URL to the booking page for this event type",
|
||||
example: "https://cal.com/john-doe/30min",
|
||||
format: "uri",
|
||||
})
|
||||
bookingUrl!: string;
|
||||
}
|
||||
|
||||
export class TeamEventTypeOutput_2024_06_14 extends BaseEventTypeOutput_2024_06_14 {
|
||||
|
||||
Reference in New Issue
Block a user