From a8d95d54e73656198b6791ae82a58ab2c8accdaf Mon Sep 17 00:00:00 2001 From: Lauris Skraucis Date: Tue, 26 Mar 2024 14:58:54 +0100 Subject: [PATCH] feat: v1 api connected-calendars endpoint (#14176) * feat: v1 api connected-calendars endpoint * refactor: flatten return data * refactor: shorten code * refactor: document userId for admins --- .../v1/lib/validations/connected-calendar.ts | 18 +++ .../v1/pages/api/connected-calendars/_get.ts | 146 ++++++++++++++++++ .../v1/pages/api/connected-calendars/index.ts | 9 ++ .../lib/getConnectedDestinationCalendars.ts | 2 +- 4 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 apps/api/v1/lib/validations/connected-calendar.ts create mode 100644 apps/api/v1/pages/api/connected-calendars/_get.ts create mode 100644 apps/api/v1/pages/api/connected-calendars/index.ts diff --git a/apps/api/v1/lib/validations/connected-calendar.ts b/apps/api/v1/lib/validations/connected-calendar.ts new file mode 100644 index 0000000000..470aea1bd3 --- /dev/null +++ b/apps/api/v1/lib/validations/connected-calendar.ts @@ -0,0 +1,18 @@ +import { z } from "zod"; + +const CalendarSchema = z.object({ + externalId: z.string(), + name: z.string(), + primary: z.boolean(), + readOnly: z.boolean(), +}); + +const IntegrationSchema = z.object({ + name: z.string(), + appId: z.string(), + userId: z.number(), + integration: z.string(), + calendars: z.array(CalendarSchema), +}); + +export const schemaConnectedCalendarsReadPublic = z.array(IntegrationSchema); diff --git a/apps/api/v1/pages/api/connected-calendars/_get.ts b/apps/api/v1/pages/api/connected-calendars/_get.ts new file mode 100644 index 0000000000..47085c33ac --- /dev/null +++ b/apps/api/v1/pages/api/connected-calendars/_get.ts @@ -0,0 +1,146 @@ +import type { NextApiRequest } from "next"; + +import type { UserWithCalendars } from "@calcom/lib/getConnectedDestinationCalendars"; +import { getConnectedDestinationCalendars } from "@calcom/lib/getConnectedDestinationCalendars"; +import { HttpError } from "@calcom/lib/http-error"; +import { defaultResponder } from "@calcom/lib/server"; +import prisma from "@calcom/prisma"; + +import { extractUserIdsFromQuery } from "~/lib/utils/extractUserIdsFromQuery"; +import { schemaConnectedCalendarsReadPublic } from "~/lib/validations/connected-calendar"; + +/** + * @swagger + * /connected-calendars: + * get: + * parameters: + * - in: query + * name: apiKey + * required: true + * schema: + * type: string + * description: Your API key + * - in: query + * name: userId + * required: false + * schema: + * type: number + * description: Admins can fetch connected calendars for other user e.g. &userId=1 or multiple users e.g. &userId=1&userId=2 + * summary: Fetch connected calendars + * tags: + * - connected-calendars + * responses: + * 200: + * description: OK + * content: + * application/json: + * schema: + * type: array + * items: + * type: object + * properties: + * name: + * type: string + * appId: + * type: string + * userId: + * type: number + * integration: + * type: string + * calendars: + * type: array + * items: + * type: object + * properties: + * externalId: + * type: string + * name: + * type: string + * primary: + * type: boolean + * readOnly: + * type: boolean + * examples: + * connectedCalendarExample: + * value: [ + * { + * "name": "Google Calendar", + * "appId": "google-calendar", + * "userId": 10, + * "integration": "google_calendar", + * "calendars": [ + * { + * "externalId": "alice@gmail.com", + * "name": "alice@gmail.com", + * "primary": true, + * "readOnly": false + * }, + * { + * "externalId": "addressbook#contacts@group.v.calendar.google.com", + * "name": "birthdays", + * "primary": false, + * "readOnly": true + * }, + * { + * "externalId": "en.latvian#holiday@group.v.calendar.google.com", + * "name": "Holidays in Narnia", + * "primary": false, + * "readOnly": true + * } + * ] + * } + * ] + * 401: + * description: Authorization information is missing or invalid. + * 403: + * description: Non admin user trying to fetch other user's connected calendars. + */ + +async function getHandler(req: NextApiRequest) { + const { userId, isAdmin } = req; + + if (!isAdmin && req.query.userId) throw new HttpError({ statusCode: 403, message: "ADMIN required" }); + + const userIds = req.query.userId ? extractUserIdsFromQuery(req) : [userId]; + + const usersWithCalendars = await prisma.user.findMany({ + where: { id: { in: userIds } }, + include: { + selectedCalendars: true, + destinationCalendar: true, + }, + }); + + return await getConnectedCalendars(usersWithCalendars); +} + +async function getConnectedCalendars(users: UserWithCalendars[]) { + const connectedDestinationCalendarsPromises = users.map((user) => + getConnectedDestinationCalendars(user, false, prisma).then((connectedCalendarsResult) => + connectedCalendarsResult.connectedCalendars.map((calendar) => ({ + userId: user.id, + ...calendar, + })) + ) + ); + const connectedDestinationCalendars = await Promise.all(connectedDestinationCalendarsPromises); + + const flattenedCalendars = connectedDestinationCalendars.flat(); + + const mapped = flattenedCalendars.map((calendar) => ({ + name: calendar.integration.name, + appId: calendar.integration.slug, + userId: calendar.userId, + integration: calendar.integration.type, + calendars: (calendar.calendars ?? []).map((c) => ({ + externalId: c.externalId, + name: c.name, + primary: c.primary ?? false, + readOnly: c.readOnly, + })), + })); + + return schemaConnectedCalendarsReadPublic.parse(mapped); +} + +export default defaultResponder(getHandler); diff --git a/apps/api/v1/pages/api/connected-calendars/index.ts b/apps/api/v1/pages/api/connected-calendars/index.ts new file mode 100644 index 0000000000..c53e4b8ef3 --- /dev/null +++ b/apps/api/v1/pages/api/connected-calendars/index.ts @@ -0,0 +1,9 @@ +import { defaultHandler } from "@calcom/lib/server"; + +import { withMiddleware } from "~/lib/helpers/withMiddleware"; + +export default withMiddleware()( + defaultHandler({ + GET: import("./_get"), + }) +); diff --git a/packages/lib/getConnectedDestinationCalendars.ts b/packages/lib/getConnectedDestinationCalendars.ts index 0e818cdd8b..aaa05c1b36 100644 --- a/packages/lib/getConnectedDestinationCalendars.ts +++ b/packages/lib/getConnectedDestinationCalendars.ts @@ -4,7 +4,7 @@ import type { DestinationCalendar, SelectedCalendar, User } from "@calcom/prisma import { AppCategories } from "@calcom/prisma/enums"; import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential"; -type UserWithCalendars = Pick & { +export type UserWithCalendars = Pick & { selectedCalendars: Pick[]; destinationCalendar: DestinationCalendar | null; };