From e6daed762bcce2166b373bc59d907c7307061918 Mon Sep 17 00:00:00 2001 From: Lauris Skraucis Date: Fri, 21 Feb 2025 09:08:00 +0100 Subject: [PATCH] feat: v2 routing forms responses endpoints (#19319) * wip: routing forms responses * refactor structure * Revert "refactor structure" This reverts commit b641c06592cd9314008e81c4584039bd42393ac7. * routing forms * remove unused file --- .../is-routing-form-in-team.guard.ts | 40 ++++ .../organizations/organizations.module.ts | 2 + ...ing-forms-responses.controller.e2e-spec.ts | 197 ++++++++++++++++++ ...eams-routing-forms-responses.controller.ts | 52 +++++ ...ns-teams-routing-forms-responses.module.ts | 17 ++ .../get-routing-form-responses.output.ts | 17 ++ .../routing-forms-responses.module.ts | 21 ++ .../routing-forms-responses.repository.ts | 18 ++ .../routing-forms-responses-output.service.ts | 31 +++ .../routing-forms-responses.service.ts | 17 ++ .../routing-forms/routing-forms.module.ts | 10 + .../routing-forms/routing-forms.repository.ts | 16 ++ apps/api/v2/swagger/documentation.json | 92 ++++++++ .../routing-forms.repository.fixture.ts | 26 +++ docs/api-reference/v2/openapi.json | 101 +++++++++ packages/platform/types/index.ts | 1 + .../platform/types/routing-forms/index.ts | 1 + .../types/routing-forms/responses/index.ts | 1 + .../responses/routing-form-response.output.ts | 54 +++++ 19 files changed, 714 insertions(+) create mode 100644 apps/api/v2/src/modules/auth/guards/routing-forms/is-routing-form-in-team.guard.ts create mode 100644 apps/api/v2/src/modules/organizations/teams/routing-forms/controllers/organizations-teams-routing-forms-responses.controller.e2e-spec.ts create mode 100644 apps/api/v2/src/modules/organizations/teams/routing-forms/controllers/organizations-teams-routing-forms-responses.controller.ts create mode 100644 apps/api/v2/src/modules/organizations/teams/routing-forms/organizations-teams-routing-forms-responses.module.ts create mode 100644 apps/api/v2/src/modules/organizations/teams/routing-forms/outputs/get-routing-form-responses.output.ts create mode 100644 apps/api/v2/src/modules/routing-forms-responses/routing-forms-responses.module.ts create mode 100644 apps/api/v2/src/modules/routing-forms-responses/routing-forms-responses.repository.ts create mode 100644 apps/api/v2/src/modules/routing-forms-responses/services/routing-forms-responses-output.service.ts create mode 100644 apps/api/v2/src/modules/routing-forms-responses/services/routing-forms-responses.service.ts create mode 100644 apps/api/v2/src/modules/routing-forms/routing-forms.module.ts create mode 100644 apps/api/v2/src/modules/routing-forms/routing-forms.repository.ts create mode 100644 apps/api/v2/test/fixtures/repository/routing-forms.repository.fixture.ts create mode 100644 packages/platform/types/routing-forms/index.ts create mode 100644 packages/platform/types/routing-forms/responses/index.ts create mode 100644 packages/platform/types/routing-forms/responses/routing-form-response.output.ts diff --git a/apps/api/v2/src/modules/auth/guards/routing-forms/is-routing-form-in-team.guard.ts b/apps/api/v2/src/modules/auth/guards/routing-forms/is-routing-form-in-team.guard.ts new file mode 100644 index 0000000000..5dc2ab93e6 --- /dev/null +++ b/apps/api/v2/src/modules/auth/guards/routing-forms/is-routing-form-in-team.guard.ts @@ -0,0 +1,40 @@ +import { RoutingFormsRepository } from "@/modules/routing-forms/routing-forms.repository"; +import { + Injectable, + CanActivate, + ExecutionContext, + ForbiddenException, + NotFoundException, +} from "@nestjs/common"; +import { Request } from "express"; + +import { Team } from "@calcom/prisma/client"; + +@Injectable() +export class IsRoutingFormInTeam implements CanActivate { + constructor(private routingFormsRepository: RoutingFormsRepository) {} + + async canActivate(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest(); + const teamId: string = request.params.teamId; + const routingFormId: string = request.params.routingFormId; + + if (!routingFormId) { + throw new ForbiddenException("No routing form id found in request params."); + } + + if (!teamId) { + throw new ForbiddenException("No team id found in request params."); + } + + const routingForm = await this.routingFormsRepository.getTeamRoutingForm(Number(teamId), routingFormId); + + if (!routingForm) { + throw new NotFoundException( + `Team with id=(${teamId}) routing form with id=(${routingFormId}) not found.` + ); + } + + return true; + } +} diff --git a/apps/api/v2/src/modules/organizations/organizations.module.ts b/apps/api/v2/src/modules/organizations/organizations.module.ts index 4991f1e9ac..c5a87ebae2 100644 --- a/apps/api/v2/src/modules/organizations/organizations.module.ts +++ b/apps/api/v2/src/modules/organizations/organizations.module.ts @@ -41,6 +41,7 @@ import { OrganizationsTeamsService } from "@/modules/organizations/services/orga import { OrganizationsUsersService } from "@/modules/organizations/services/organizations-users-service"; import { OrganizationsWebhooksService } from "@/modules/organizations/services/organizations-webhooks.service"; import { OrganizationsService } from "@/modules/organizations/services/organizations.service"; +import { OrganizationsTeamsRoutingFormsModule } from "@/modules/organizations/teams/routing-forms/organizations-teams-routing-forms-responses.module"; import { PrismaModule } from "@/modules/prisma/prisma.module"; import { RedisModule } from "@/modules/redis/redis.module"; import { StripeModule } from "@/modules/stripe/stripe.module"; @@ -62,6 +63,7 @@ import { Module } from "@nestjs/common"; EventTypesModule_2024_06_14, TeamsEventTypesModule, TeamsModule, + OrganizationsTeamsRoutingFormsModule, ], providers: [ OrganizationsRepository, diff --git a/apps/api/v2/src/modules/organizations/teams/routing-forms/controllers/organizations-teams-routing-forms-responses.controller.e2e-spec.ts b/apps/api/v2/src/modules/organizations/teams/routing-forms/controllers/organizations-teams-routing-forms-responses.controller.e2e-spec.ts new file mode 100644 index 0000000000..ddc80bf934 --- /dev/null +++ b/apps/api/v2/src/modules/organizations/teams/routing-forms/controllers/organizations-teams-routing-forms-responses.controller.e2e-spec.ts @@ -0,0 +1,197 @@ +import { bootstrap } from "@/app"; +import { AppModule } from "@/app.module"; +import { GetRoutingFormResponsesOutput } from "@/modules/organizations/teams/routing-forms/outputs/get-routing-form-responses.output"; +import { PrismaModule } from "@/modules/prisma/prisma.module"; +import { TokensModule } from "@/modules/tokens/tokens.module"; +import { UsersModule } from "@/modules/users/users.module"; +import { INestApplication } from "@nestjs/common"; +import { NestExpressApplication } from "@nestjs/platform-express"; +import { Test } from "@nestjs/testing"; +import { User } from "@prisma/client"; +import * as request from "supertest"; +import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repository.fixture"; +import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture"; +import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture"; +import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture"; +import { RoutingFormsRepositoryFixture } from "test/fixtures/repository/routing-forms.repository.fixture"; +import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture"; +import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture"; +import { randomString } from "test/utils/randomString"; + +import { SUCCESS_STATUS } from "@calcom/platform-constants"; +import { Team } from "@calcom/prisma/client"; + +describe("Organizations Teams Routing Forms Responses", () => { + let app: INestApplication; + + let userRepositoryFixture: UserRepositoryFixture; + let organizationsRepositoryFixture: OrganizationRepositoryFixture; + + let teamsRepositoryFixture: TeamRepositoryFixture; + let profileRepositoryFixture: ProfileRepositoryFixture; + let routingFormsRepositoryFixture: RoutingFormsRepositoryFixture; + let apiKeysRepositoryFixture: ApiKeysRepositoryFixture; + let membershipsRepositoryFixture: MembershipRepositoryFixture; + + let org: Team; + let orgTeam: Team; + + const authEmail = `organizations-teams-routing-forms-responses-user-${randomString()}@api.com`; + let user: User; + let apiKeyString: string; + + let routingFormId: string; + const routingFormResponses = [ + { + id: 1, + formFillerId: "cm78tvkvd0001kh8jq0tu5iq9", + response: { + "participant-field": { + label: "participant", + value: "mamut", + }, + }, + createdAt: new Date("2025-02-17T09:03:18.121Z"), + }, + ]; + + beforeAll(async () => { + const moduleRef = await Test.createTestingModule({ + imports: [AppModule, PrismaModule, UsersModule, TokensModule], + }).compile(); + + userRepositoryFixture = new UserRepositoryFixture(moduleRef); + organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef); + teamsRepositoryFixture = new TeamRepositoryFixture(moduleRef); + profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef); + routingFormsRepositoryFixture = new RoutingFormsRepositoryFixture(moduleRef); + apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef); + membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef); + + org = await organizationsRepositoryFixture.create({ + name: `organizations-teams-routing-forms-responses-organization-${randomString()}`, + isOrganization: true, + }); + + user = await userRepositoryFixture.create({ + email: authEmail, + username: authEmail, + }); + + const { keyString } = await apiKeysRepositoryFixture.createApiKey(user.id, null); + apiKeyString = keyString; + + orgTeam = await teamsRepositoryFixture.create({ + name: `organizations-teams-routing-forms-responses-team-${randomString()}`, + isOrganization: false, + parent: { connect: { id: org.id } }, + }); + + await membershipsRepositoryFixture.create({ + role: "ADMIN", + user: { connect: { id: user.id } }, + team: { connect: { id: org.id } }, + }); + + await membershipsRepositoryFixture.create({ + role: "ADMIN", + user: { connect: { id: user.id } }, + team: { connect: { id: orgTeam.id } }, + }); + + await profileRepositoryFixture.create({ + uid: `usr-${user.id}`, + username: authEmail, + organization: { + connect: { + id: org.id, + }, + }, + user: { + connect: { + id: user.id, + }, + }, + }); + + const routingForm = await routingFormsRepositoryFixture.create({ + name: "Test Routing Form", + description: null, + position: 0, + disabled: false, + fields: JSON.stringify([ + { + type: "text", + label: "participant", + required: true, + }, + ]), + routes: JSON.stringify([ + { + action: { type: "customPageMessage", value: "Thank you for your response" }, + }, + ]), + user: { + connect: { + id: user.id, + }, + }, + team: { + connect: { + id: orgTeam.id, + }, + }, + responses: { + create: routingFormResponses, + }, + }); + routingFormId = routingForm.id; + + app = moduleRef.createNestApplication(); + bootstrap(app as NestExpressApplication); + + await app.init(); + }); + + it("should not get routing form responses for non existing org", async () => { + return request(app.getHttpServer()) + .get(`/v2/organizations/99999/teams/${orgTeam.id}/routing-forms/${routingFormId}/responses`) + .expect(401); + }); + + it("should not get routing form responses for non existing team", async () => { + return request(app.getHttpServer()) + .get(`/v2/organizations/${org.id}/teams/99999/routing-forms/${routingFormId}/responses`) + .expect(401); + }); + + it("should not get routing form responses for non existing routing form", async () => { + return request(app.getHttpServer()) + .get(`/v2/organizations/${org.id}/teams/${orgTeam.id}/routing-forms/99999/responses`) + .expect(401); + }); + + it("should get routing form responses", async () => { + return request(app.getHttpServer()) + .get(`/v2/organizations/${org.id}/teams/${orgTeam.id}/routing-forms/${routingFormId}/responses`) + .set({ Authorization: `Bearer cal_test_${apiKeyString}` }) + .expect(200) + .then((response) => { + const responseBody: GetRoutingFormResponsesOutput = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + const responseData = responseBody.data; + expect(responseData).toBeDefined(); + expect(responseData.length).toEqual(1); + expect(responseData[0].id).toEqual(routingFormResponses[0].id); + expect(responseData[0].response).toEqual(routingFormResponses[0].response); + expect(responseData[0].formFillerId).toEqual(routingFormResponses[0].formFillerId); + expect(responseData[0].createdAt).toEqual(routingFormResponses[0].createdAt.toISOString()); + }); + }); + + afterAll(async () => { + await userRepositoryFixture.deleteByEmail(user.email); + await organizationsRepositoryFixture.delete(org.id); + await app.close(); + }); +}); diff --git a/apps/api/v2/src/modules/organizations/teams/routing-forms/controllers/organizations-teams-routing-forms-responses.controller.ts b/apps/api/v2/src/modules/organizations/teams/routing-forms/controllers/organizations-teams-routing-forms-responses.controller.ts new file mode 100644 index 0000000000..cff980b761 --- /dev/null +++ b/apps/api/v2/src/modules/organizations/teams/routing-forms/controllers/organizations-teams-routing-forms-responses.controller.ts @@ -0,0 +1,52 @@ +import { API_VERSIONS_VALUES } from "@/lib/api-versions"; +import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator"; +import { Roles } from "@/modules/auth/decorators/roles/roles.decorator"; +import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; +import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard"; +import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard"; +import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard"; +import { IsRoutingFormInTeam } from "@/modules/auth/guards/routing-forms/is-routing-form-in-team.guard"; +import { IsTeamInOrg } from "@/modules/auth/guards/teams/is-team-in-org.guard"; +import { Controller, Get, Param, UseGuards } from "@nestjs/common"; +import { ApiOperation, ApiTags } from "@nestjs/swagger"; +import { plainToClass } from "class-transformer"; + +import { SUCCESS_STATUS } from "@calcom/platform-constants"; +import { RoutingFormResponseOutput } from "@calcom/platform-types"; + +import { RoutingFormsResponsesService } from "../../../../routing-forms-responses/services/routing-forms-responses.service"; +import { GetRoutingFormResponsesOutput } from "../outputs/get-routing-form-responses.output"; + +@Controller({ + path: "/v2/organizations/:orgId/teams/:teamId/routing-forms/:routingFormId/responses", + version: API_VERSIONS_VALUES, +}) +@ApiTags("Orgs / Teams / Routing forms / Responses") +@UseGuards( + ApiAuthGuard, + IsOrgGuard, + IsTeamInOrg, + IsRoutingFormInTeam, + PlatformPlanGuard, + IsAdminAPIEnabledGuard +) +export class OrganizationsTeamsRoutingFormsResponsesController { + constructor(private readonly routingFormsResponsesService: RoutingFormsResponsesService) {} + + @Get() + @ApiOperation({ summary: "Get routing form responses" }) + @Roles("ORG_ADMIN") + @PlatformPlan("ESSENTIALS") + async getRoutingFormResponses( + @Param("routingFormId") routingFormId: string + ): Promise { + const routingFormResponses = await this.routingFormsResponsesService.getRoutingFormResponses( + routingFormId + ); + + return { + status: SUCCESS_STATUS, + data: routingFormResponses, + }; + } +} diff --git a/apps/api/v2/src/modules/organizations/teams/routing-forms/organizations-teams-routing-forms-responses.module.ts b/apps/api/v2/src/modules/organizations/teams/routing-forms/organizations-teams-routing-forms-responses.module.ts new file mode 100644 index 0000000000..37ddd54928 --- /dev/null +++ b/apps/api/v2/src/modules/organizations/teams/routing-forms/organizations-teams-routing-forms-responses.module.ts @@ -0,0 +1,17 @@ +import { OrganizationsRepository } from "@/modules/organizations/organizations.repository"; +import { OrganizationsTeamsRepository } from "@/modules/organizations/repositories/organizations-teams.repository"; +import { PrismaModule } from "@/modules/prisma/prisma.module"; +import { RedisModule } from "@/modules/redis/redis.module"; +import { RoutingFormsResponsesModule } from "@/modules/routing-forms-responses/routing-forms-responses.module"; +import { RoutingFormsModule } from "@/modules/routing-forms/routing-forms.module"; +import { StripeModule } from "@/modules/stripe/stripe.module"; +import { Module } from "@nestjs/common"; + +import { OrganizationsTeamsRoutingFormsResponsesController } from "./controllers/organizations-teams-routing-forms-responses.controller"; + +@Module({ + imports: [PrismaModule, StripeModule, RedisModule, RoutingFormsResponsesModule, RoutingFormsModule], + providers: [OrganizationsRepository, OrganizationsTeamsRepository], + controllers: [OrganizationsTeamsRoutingFormsResponsesController], +}) +export class OrganizationsTeamsRoutingFormsModule {} diff --git a/apps/api/v2/src/modules/organizations/teams/routing-forms/outputs/get-routing-form-responses.output.ts b/apps/api/v2/src/modules/organizations/teams/routing-forms/outputs/get-routing-form-responses.output.ts new file mode 100644 index 0000000000..c32ebd9d0b --- /dev/null +++ b/apps/api/v2/src/modules/organizations/teams/routing-forms/outputs/get-routing-form-responses.output.ts @@ -0,0 +1,17 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { Expose, Type } from "class-transformer"; +import { IsEnum } from "class-validator"; + +import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants"; +import { RoutingFormResponseOutput } from "@calcom/platform-types"; + +export class GetRoutingFormResponsesOutput { + @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) + @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) + status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; + + @ApiProperty({ type: [RoutingFormResponseOutput] }) + @Expose() + @Type(() => RoutingFormResponseOutput) + data!: RoutingFormResponseOutput[]; +} diff --git a/apps/api/v2/src/modules/routing-forms-responses/routing-forms-responses.module.ts b/apps/api/v2/src/modules/routing-forms-responses/routing-forms-responses.module.ts new file mode 100644 index 0000000000..093cdada29 --- /dev/null +++ b/apps/api/v2/src/modules/routing-forms-responses/routing-forms-responses.module.ts @@ -0,0 +1,21 @@ +import { OrganizationsRepository } from "@/modules/organizations/organizations.repository"; +import { PrismaModule } from "@/modules/prisma/prisma.module"; +import { RoutingFormsResponsesRepository } from "@/modules/routing-forms-responses/routing-forms-responses.repository"; +import { RoutingFormsResponsesOutputService } from "@/modules/routing-forms-responses/services/routing-forms-responses-output.service"; +import { RoutingFormsResponsesService } from "@/modules/routing-forms-responses/services/routing-forms-responses.service"; +import { Module } from "@nestjs/common"; + +@Module({ + imports: [PrismaModule], + providers: [ + RoutingFormsResponsesService, + RoutingFormsResponsesRepository, + RoutingFormsResponsesOutputService, + ], + exports: [ + RoutingFormsResponsesService, + RoutingFormsResponsesRepository, + RoutingFormsResponsesOutputService, + ], +}) +export class RoutingFormsResponsesModule {} diff --git a/apps/api/v2/src/modules/routing-forms-responses/routing-forms-responses.repository.ts b/apps/api/v2/src/modules/routing-forms-responses/routing-forms-responses.repository.ts new file mode 100644 index 0000000000..b079f43aa3 --- /dev/null +++ b/apps/api/v2/src/modules/routing-forms-responses/routing-forms-responses.repository.ts @@ -0,0 +1,18 @@ +import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; +import { Injectable } from "@nestjs/common"; + +@Injectable() +export class RoutingFormsResponsesRepository { + constructor(private readonly dbRead: PrismaReadService) {} + + async getRoutingFormResponses(routingFormId: string) { + return this.dbRead.prisma.app_RoutingForms_FormResponse.findMany({ + where: { + formId: routingFormId, + }, + orderBy: { + createdAt: "desc", + }, + }); + } +} diff --git a/apps/api/v2/src/modules/routing-forms-responses/services/routing-forms-responses-output.service.ts b/apps/api/v2/src/modules/routing-forms-responses/services/routing-forms-responses-output.service.ts new file mode 100644 index 0000000000..38c787ac36 --- /dev/null +++ b/apps/api/v2/src/modules/routing-forms-responses/services/routing-forms-responses-output.service.ts @@ -0,0 +1,31 @@ +import { Injectable } from "@nestjs/common"; +import { plainToClass } from "class-transformer"; + +import { RoutingFormResponseOutput, RoutingFormResponseResponseOutput } from "@calcom/platform-types"; +import { App_RoutingForms_FormResponse } from "@calcom/prisma/client"; + +@Injectable() +export class RoutingFormsResponsesOutputService { + getRoutingFormResponses( + dbRoutingFormResponses: App_RoutingForms_FormResponse[] + ): RoutingFormResponseOutput[] { + return dbRoutingFormResponses.map((response) => { + const parsed = plainToClass(RoutingFormResponseOutput, response, { strategy: "excludeAll" }); + + // note(Lauris): I don't know why plainToClass(RoutingFormResponseOutput) + // erases nested "response" object so parsing and attaching it manually + const parsedResponse: Record = {}; + const responseData = response.response || {}; + for (const [key, value] of Object.entries(responseData)) { + parsedResponse[key] = plainToClass(RoutingFormResponseResponseOutput, value, { + strategy: "excludeAll", + }); + } + + return { + ...parsed, + response: parsedResponse, + }; + }); + } +} diff --git a/apps/api/v2/src/modules/routing-forms-responses/services/routing-forms-responses.service.ts b/apps/api/v2/src/modules/routing-forms-responses/services/routing-forms-responses.service.ts new file mode 100644 index 0000000000..c9c76f6603 --- /dev/null +++ b/apps/api/v2/src/modules/routing-forms-responses/services/routing-forms-responses.service.ts @@ -0,0 +1,17 @@ +import { RoutingFormsResponsesOutputService } from "@/modules/routing-forms-responses/services/routing-forms-responses-output.service"; +import { Injectable } from "@nestjs/common"; + +import { RoutingFormsResponsesRepository } from "../routing-forms-responses.repository"; + +@Injectable() +export class RoutingFormsResponsesService { + constructor( + private readonly routingFormsRepository: RoutingFormsResponsesRepository, + private readonly routingFormsResponsesOutputService: RoutingFormsResponsesOutputService + ) {} + + async getRoutingFormResponses(routingFormId: string) { + const responses = await this.routingFormsRepository.getRoutingFormResponses(routingFormId); + return this.routingFormsResponsesOutputService.getRoutingFormResponses(responses); + } +} diff --git a/apps/api/v2/src/modules/routing-forms/routing-forms.module.ts b/apps/api/v2/src/modules/routing-forms/routing-forms.module.ts new file mode 100644 index 0000000000..579169067a --- /dev/null +++ b/apps/api/v2/src/modules/routing-forms/routing-forms.module.ts @@ -0,0 +1,10 @@ +import { PrismaModule } from "@/modules/prisma/prisma.module"; +import { RoutingFormsRepository } from "@/modules/routing-forms/routing-forms.repository"; +import { Module } from "@nestjs/common"; + +@Module({ + imports: [PrismaModule], + providers: [RoutingFormsRepository], + exports: [RoutingFormsRepository], +}) +export class RoutingFormsModule {} diff --git a/apps/api/v2/src/modules/routing-forms/routing-forms.repository.ts b/apps/api/v2/src/modules/routing-forms/routing-forms.repository.ts new file mode 100644 index 0000000000..f648acff47 --- /dev/null +++ b/apps/api/v2/src/modules/routing-forms/routing-forms.repository.ts @@ -0,0 +1,16 @@ +import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; +import { Injectable } from "@nestjs/common"; + +@Injectable() +export class RoutingFormsRepository { + constructor(private readonly dbRead: PrismaReadService) {} + + async getTeamRoutingForm(teamId: number, routingFormId: string) { + return this.dbRead.prisma.app_RoutingForms_Form.findFirst({ + where: { + id: routingFormId, + teamId, + }, + }); + } +} diff --git a/apps/api/v2/swagger/documentation.json b/apps/api/v2/swagger/documentation.json index 2c02d89698..639e220a65 100644 --- a/apps/api/v2/swagger/documentation.json +++ b/apps/api/v2/swagger/documentation.json @@ -2557,6 +2557,37 @@ ] } }, + "/v2/organizations/{orgId}/teams/{teamId}/routing-forms/{routing}-formId/responses": { + "get": { + "operationId": "OrganizationsTeamsRoutingFormsResponsesController_getRoutingFormResponses", + "summary": "Get routing form responses", + "parameters": [ + { + "name": "routing-formId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetRoutingFormResponsesOutput" + } + } + } + } + }, + "tags": [ + "Orgs / Teams / Routing forms / Responses" + ] + } + }, "/v2/organizations/{orgId}/teams/{teamId}/users/{userId}/schedules": { "get": { "operationId": "OrganizationsTeamsSchedulesController_getUserSchedules", @@ -14323,6 +14354,67 @@ "data" ] }, + "RoutingFormResponseOutput": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "formId": { + "type": "string" + }, + "formFillerId": { + "type": "string" + }, + "routedToBookingUid": { + "type": "string" + }, + "response": { + "type": "object", + "example": { + "f00b26df-f54b-4985-8d98-17c5482c6a24": { + "label": "participant", + "value": "mamut" + } + } + }, + "createdAt": { + "format": "date-time", + "type": "string" + } + }, + "required": [ + "id", + "formId", + "formFillerId", + "routedToBookingUid", + "response", + "createdAt" + ] + }, + "GetRoutingFormResponsesOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoutingFormResponseOutput" + } + } + }, + "required": [ + "status", + "data" + ] + }, "ProviderVerifyClientData": { "type": "object", "properties": { diff --git a/apps/api/v2/test/fixtures/repository/routing-forms.repository.fixture.ts b/apps/api/v2/test/fixtures/repository/routing-forms.repository.fixture.ts new file mode 100644 index 0000000000..2c5da5584f --- /dev/null +++ b/apps/api/v2/test/fixtures/repository/routing-forms.repository.fixture.ts @@ -0,0 +1,26 @@ +import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; +import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; +import { TestingModule } from "@nestjs/testing"; +import { Prisma, App_RoutingForms_Form } from "@prisma/client"; + +export class RoutingFormsRepositoryFixture { + private prismaReadClient: PrismaReadService["prisma"]; + private prismaWriteClient: PrismaWriteService["prisma"]; + + constructor(private readonly module: TestingModule) { + this.prismaReadClient = module.get(PrismaReadService).prisma; + this.prismaWriteClient = module.get(PrismaWriteService).prisma; + } + + async get(routingFormId: App_RoutingForms_Form["id"]) { + return this.prismaReadClient.app_RoutingForms_Form.findUnique({ where: { id: routingFormId } }); + } + + async create(data: Prisma.App_RoutingForms_FormCreateInput) { + return this.prismaWriteClient.app_RoutingForms_Form.create({ data }); + } + + async delete(routingFormId: App_RoutingForms_Form["id"]) { + return this.prismaWriteClient.app_RoutingForms_Form.delete({ where: { id: routingFormId } }); + } +} diff --git a/docs/api-reference/v2/openapi.json b/docs/api-reference/v2/openapi.json index d8914e06c7..a7ce9b5bf8 100644 --- a/docs/api-reference/v2/openapi.json +++ b/docs/api-reference/v2/openapi.json @@ -2422,6 +2422,35 @@ "tags": ["Orgs / Teams / Memberships"] } }, + "/v2/organizations/{orgId}/teams/{teamId}/routing-forms/{routing}-formId/responses": { + "get": { + "operationId": "OrganizationsTeamsRoutingFormsResponsesController_getRoutingFormResponses", + "summary": "Get routing form responses", + "parameters": [ + { + "name": "routing-formId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetRoutingFormResponsesOutput" + } + } + } + } + }, + "tags": ["Orgs / Teams / Routing forms / Responses"] + } + }, "/v2/organizations/{orgId}/teams/{teamId}/users/{userId}/schedules": { "get": { "operationId": "OrganizationsTeamsSchedulesController_getUserSchedules", @@ -12917,6 +12946,54 @@ }, "required": ["status", "data"] }, + "RoutingFormResponseOutput": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "formId": { + "type": "string" + }, + "formFillerId": { + "type": "string" + }, + "routedToBookingUid": { + "type": "string" + }, + "response": { + "type": "object", + "example": { + "f00b26df-f54b-4985-8d98-17c5482c6a24": { + "label": "participant", + "value": "mamut" + } + } + }, + "createdAt": { + "format": "date-time", + "type": "string" + } + }, + "required": ["id", "formId", "formFillerId", "routedToBookingUid", "response", "createdAt"] + }, + "GetRoutingFormResponsesOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": ["success", "error"] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoutingFormResponseOutput" + } + } + }, + "required": ["status", "data"] + }, "ProviderVerifyClientData": { "type": "object", "properties": { @@ -13856,6 +13933,10 @@ "key": "value" } }, + "rating": { + "type": "number", + "example": 4 + }, "attendees": { "type": "array", "items": { @@ -13996,6 +14077,10 @@ "key": "value" } }, + "rating": { + "type": "number", + "example": 4 + }, "attendees": { "type": "array", "items": { @@ -14232,6 +14317,10 @@ "key": "value" } }, + "rating": { + "type": "number", + "example": 4 + }, "seatUid": { "type": "string", "example": "3be561a9-31f1-4b8e-aefc-9d9a085f0dd1" @@ -14362,6 +14451,10 @@ "key": "value" } }, + "rating": { + "type": "number", + "example": 4 + }, "seatUid": { "type": "string", "example": "3be561a9-31f1-4b8e-aefc-9d9a085f0dd1" @@ -14531,6 +14624,10 @@ "key": "value" } }, + "rating": { + "type": "number", + "example": 4 + }, "attendees": { "type": "array", "items": { @@ -14656,6 +14753,10 @@ "key": "value" } }, + "rating": { + "type": "number", + "example": 4 + }, "attendees": { "type": "array", "items": { diff --git a/packages/platform/types/index.ts b/packages/platform/types/index.ts index 9b23abb356..aa7b0290eb 100644 --- a/packages/platform/types/index.ts +++ b/packages/platform/types/index.ts @@ -10,3 +10,4 @@ export * from "./event-types"; export * from "./organizations"; export * from "./teams"; export * from "./embed"; +export * from "./routing-forms"; diff --git a/packages/platform/types/routing-forms/index.ts b/packages/platform/types/routing-forms/index.ts new file mode 100644 index 0000000000..8818ea007e --- /dev/null +++ b/packages/platform/types/routing-forms/index.ts @@ -0,0 +1 @@ +export * from "./responses"; diff --git a/packages/platform/types/routing-forms/responses/index.ts b/packages/platform/types/routing-forms/responses/index.ts new file mode 100644 index 0000000000..93ffea5be0 --- /dev/null +++ b/packages/platform/types/routing-forms/responses/index.ts @@ -0,0 +1 @@ +export * from "./routing-form-response.output"; diff --git a/packages/platform/types/routing-forms/responses/routing-form-response.output.ts b/packages/platform/types/routing-forms/responses/routing-form-response.output.ts new file mode 100644 index 0000000000..8434414e7a --- /dev/null +++ b/packages/platform/types/routing-forms/responses/routing-form-response.output.ts @@ -0,0 +1,54 @@ +import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; +import { Expose } from "class-transformer"; +import { IsDate, IsInt, IsString, IsObject } from "class-validator"; + +export class RoutingFormResponseResponseOutput { + @ApiProperty() + @IsString() + @Expose() + label!: string; + + @ApiPropertyOptional() + @IsString() + @Expose() + identifier?: string; + + @ApiProperty() + @IsString() + @Expose() + value!: string | number | string[]; +} + +export class RoutingFormResponseOutput { + @ApiProperty() + @IsInt() + @Expose() + id!: string; + + @ApiProperty() + @IsInt() + @Expose() + formId!: string; + + @ApiProperty() + @IsString() + @Expose() + formFillerId!: string; + + @ApiProperty() + @Expose() + @IsString() + routedToBookingUid!: string; + + @ApiProperty({ + example: { "f00b26df-f54b-4985-8d98-17c5482c6a24": { label: "participant", value: "mamut" } }, + }) + @IsObject() + @Expose() + response!: Record; + + @ApiProperty() + @Expose() + @IsDate() + createdAt!: Date; +}