-
Notifications
You must be signed in to change notification settings - Fork 8.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: v2 routing forms responses endpoints (#19319)
* wip: routing forms responses * refactor structure * Revert "refactor structure" This reverts commit b641c06. * routing forms * remove unused file
- Loading branch information
Showing
19 changed files
with
714 additions
and
0 deletions.
There are no files selected for viewing
40 changes: 40 additions & 0 deletions
40
apps/api/v2/src/modules/auth/guards/routing-forms/is-routing-form-in-team.guard.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<boolean> { | ||
const request = context.switchToHttp().getRequest<Request & { team: Team }>(); | ||
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
197 changes: 197 additions & 0 deletions
197
...ting-forms/controllers/organizations-teams-routing-forms-responses.controller.e2e-spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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(); | ||
}); | ||
}); |
52 changes: 52 additions & 0 deletions
52
...teams/routing-forms/controllers/organizations-teams-routing-forms-responses.controller.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<GetRoutingFormResponsesOutput> { | ||
const routingFormResponses = await this.routingFormsResponsesService.getRoutingFormResponses( | ||
routingFormId | ||
); | ||
|
||
return { | ||
status: SUCCESS_STATUS, | ||
data: routingFormResponses, | ||
}; | ||
} | ||
} |
17 changes: 17 additions & 0 deletions
17
...s/organizations/teams/routing-forms/organizations-teams-routing-forms-responses.module.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 {} |
17 changes: 17 additions & 0 deletions
17
...rc/modules/organizations/teams/routing-forms/outputs/get-routing-form-responses.output.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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[]; | ||
} |
21 changes: 21 additions & 0 deletions
21
apps/api/v2/src/modules/routing-forms-responses/routing-forms-responses.module.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 {} |
18 changes: 18 additions & 0 deletions
18
apps/api/v2/src/modules/routing-forms-responses/routing-forms-responses.repository.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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", | ||
}, | ||
}); | ||
} | ||
} |
Oops, something went wrong.