feat: organization schedules endpoints (#15584)

* feat: organization schedules endpoints

* refactor: add IsUserInOrg guard

* refactor: separate schedules controller folder

* tests

* chore: add roles guard and test it

* refactor: dont use Promise.all

* refactor: rely on profiles instead of user.organizationId

* feat: add pagination to getOrganizationSchedules

* refactor: default skip=0 and take=250

* chore: regenerate swagger doc
This commit is contained in:
Lauris Skraucis
2024-06-27 19:04:26 +00:00
committed by GitHub
parent dda4ce5338
commit 52c7a89785
10 changed files with 1080 additions and 276 deletions
@@ -17,6 +17,6 @@ import { Module } from "@nestjs/common";
OutputSchedulesService_2024_06_11,
],
controllers: [SchedulesController_2024_06_11],
exports: [SchedulesService_2024_06_11, SchedulesRepository_2024_06_11],
exports: [SchedulesService_2024_06_11, SchedulesRepository_2024_06_11, OutputSchedulesService_2024_06_11],
})
export class SchedulesModule_2024_06_11 {}
@@ -0,0 +1,155 @@
import { SchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/schedules.service";
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard";
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
import { IsUserInOrg } from "@/modules/auth/guards/users/is-user-in-org.guard";
import { OrganizationsSchedulesService } from "@/modules/organizations/services/organizations-schedules.service";
import {
Controller,
UseGuards,
Get,
Post,
Param,
ParseIntPipe,
Body,
Patch,
Delete,
HttpCode,
HttpStatus,
Query,
} from "@nestjs/common";
import { ApiTags as DocsTags } from "@nestjs/swagger";
import { Transform } from "class-transformer";
import { IsNumber, Min, Max, IsOptional } from "class-validator";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import {
CreateScheduleInput_2024_06_11,
CreateScheduleOutput_2024_06_11,
DeleteScheduleOutput_2024_06_11,
GetScheduleOutput_2024_06_11,
GetSchedulesOutput_2024_06_11,
UpdateScheduleInput_2024_06_11,
UpdateScheduleOutput_2024_06_11,
} from "@calcom/platform-types";
class SkipTakePagination {
@Transform(({ value }: { value: string }) => value && parseInt(value))
@IsNumber()
@Min(1)
@Max(250)
@IsOptional()
take?: number;
@Transform(({ value }: { value: string }) => value && parseInt(value))
@IsNumber()
@Min(0)
@IsOptional()
skip?: number;
}
@Controller({
path: "/v2/organizations/:orgId",
version: API_VERSIONS_VALUES,
})
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard)
@DocsTags("Organizations Schedules")
export class OrganizationsSchedulesController {
constructor(
private schedulesService: SchedulesService_2024_06_11,
private organizationScheduleService: OrganizationsSchedulesService
) {}
@Roles("ORG_ADMIN")
@Get("/schedules")
async getOrganizationSchedules(
@Param("orgId", ParseIntPipe) orgId: number,
@Query() queryParams: SkipTakePagination
): Promise<GetSchedulesOutput_2024_06_11> {
const { skip, take } = queryParams;
const schedules = await this.organizationScheduleService.getOrganizationSchedules(orgId, skip, take);
return {
status: SUCCESS_STATUS,
data: schedules,
};
}
@Roles("ORG_ADMIN")
@UseGuards(IsUserInOrg)
@Post("/users/:userId/schedules")
async createUserSchedule(
@Param("userId", ParseIntPipe) userId: number,
@Body() bodySchedule: CreateScheduleInput_2024_06_11
): Promise<CreateScheduleOutput_2024_06_11> {
const schedule = await this.schedulesService.createUserSchedule(userId, bodySchedule);
return {
status: SUCCESS_STATUS,
data: schedule,
};
}
@Roles("ORG_ADMIN")
@UseGuards(IsUserInOrg)
@Get("/users/:userId/schedules/:scheduleId")
async getUserSchedule(
@Param("userId", ParseIntPipe) userId: number,
@Param("scheduleId") scheduleId: number
): Promise<GetScheduleOutput_2024_06_11> {
const schedule = await this.schedulesService.getUserSchedule(userId, scheduleId);
return {
status: SUCCESS_STATUS,
data: schedule,
};
}
@Roles("ORG_ADMIN")
@UseGuards(IsUserInOrg)
@Get("/users/:userId/schedules")
async getUserSchedules(
@Param("userId", ParseIntPipe) userId: number
): Promise<GetSchedulesOutput_2024_06_11> {
const schedules = await this.schedulesService.getUserSchedules(userId);
return {
status: SUCCESS_STATUS,
data: schedules,
};
}
@Roles("ORG_ADMIN")
@UseGuards(IsUserInOrg)
@Patch("/users/:userId/schedules/:scheduleId")
async updateUserSchedule(
@Param("userId", ParseIntPipe) userId: number,
@Param("scheduleId", ParseIntPipe) scheduleId: number,
@Body() bodySchedule: UpdateScheduleInput_2024_06_11
): Promise<UpdateScheduleOutput_2024_06_11> {
const updatedSchedule = await this.schedulesService.updateUserSchedule(userId, scheduleId, bodySchedule);
return {
status: SUCCESS_STATUS,
data: updatedSchedule,
};
}
@Roles("ORG_ADMIN")
@UseGuards(IsUserInOrg)
@Delete("/users/:userId/schedules/:scheduleId")
@HttpCode(HttpStatus.OK)
async deleteUserSchedule(
@Param("userId", ParseIntPipe) userId: number,
@Param("scheduleId", ParseIntPipe) scheduleId: number
): Promise<DeleteScheduleOutput_2024_06_11> {
await this.schedulesService.deleteUserSchedule(userId, scheduleId);
return {
status: SUCCESS_STATUS,
};
}
}
@@ -0,0 +1,338 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
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 * as request from "supertest";
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { withApiAuth } from "test/utils/withApiAuth";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import {
CreateScheduleInput_2024_06_11,
CreateScheduleOutput_2024_06_11,
GetScheduleOutput_2024_06_11,
GetSchedulesOutput_2024_06_11,
ScheduleAvailabilityInput_2024_06_11,
ScheduleOutput_2024_06_11,
UpdateScheduleInput_2024_06_11,
UpdateScheduleOutput_2024_06_11,
} from "@calcom/platform-types";
import { User, Team, Membership, Profile } from "@calcom/prisma/client";
describe("Organizations Schedules Endpoints", () => {
describe("User lacks required role", () => {
let app: INestApplication;
let userRepositoryFixture: UserRepositoryFixture;
let organizationsRepositoryFixture: TeamRepositoryFixture;
let membershipFixtures: MembershipRepositoryFixture;
const userEmail = "mr-robot@schedules-api.com";
let user: User;
let org: Team;
let membership: Membership;
beforeAll(async () => {
const moduleRef = await withApiAuth(
userEmail,
Test.createTestingModule({
imports: [AppModule, PrismaModule, UsersModule, TokensModule],
})
).compile();
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
organizationsRepositoryFixture = new TeamRepositoryFixture(moduleRef);
membershipFixtures = new MembershipRepositoryFixture(moduleRef);
org = await organizationsRepositoryFixture.create({
name: "Ecorp",
isOrganization: true,
});
user = await userRepositoryFixture.create({
email: userEmail,
username: userEmail,
organization: { connect: { id: org.id } },
});
membership = await membershipFixtures.addUserToOrg(user, org, "MEMBER", true);
app = moduleRef.createNestApplication();
bootstrap(app as NestExpressApplication);
await app.init();
});
it("should be defined", () => {
expect(userRepositoryFixture).toBeDefined();
expect(organizationsRepositoryFixture).toBeDefined();
expect(user).toBeDefined();
expect(org).toBeDefined();
});
it("should not be able to create schedule for org user", async () => {
return request(app.getHttpServer())
.post(`/v2/organizations/${org.id}/users/${user.id}/schedules`)
.send({
name: "work",
timeZone: "Europe/Rome",
isDefault: true,
})
.expect(403);
});
it("should not be able to get org schedules", async () => {
return request(app.getHttpServer()).get(`/v2/organizations/${org.id}/schedules`).expect(403);
});
it("should mot be able to get user schedules", async () => {
return request(app.getHttpServer())
.get(`/v2/organizations/${org.id}/users/${user.id}/schedules/`)
.expect(403);
});
afterAll(async () => {
await membershipFixtures.delete(membership.id);
await userRepositoryFixture.deleteByEmail(user.email);
await organizationsRepositoryFixture.delete(org.id);
await app.close();
});
});
describe("User has required role", () => {
let app: INestApplication;
let userRepositoryFixture: UserRepositoryFixture;
let organizationsRepositoryFixture: TeamRepositoryFixture;
let membershipFixtures: MembershipRepositoryFixture;
let profileRepositoryFixture: ProfileRepositoryFixture;
const userEmail = "mr-robot@schedules-api.com";
const username = "mr-robot";
let user: User;
let org: Team;
let profile: Profile;
let membership: Membership;
let createdSchedule: ScheduleOutput_2024_06_11;
const createScheduleInput: CreateScheduleInput_2024_06_11 = {
name: "work",
timeZone: "Europe/Rome",
isDefault: true,
};
const defaultAvailability: ScheduleAvailabilityInput_2024_06_11[] = [
{
days: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"],
startTime: "09:00",
endTime: "17:00",
},
];
beforeAll(async () => {
const moduleRef = await withApiAuth(
userEmail,
Test.createTestingModule({
imports: [AppModule, PrismaModule, UsersModule, TokensModule],
})
).compile();
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
organizationsRepositoryFixture = new TeamRepositoryFixture(moduleRef);
membershipFixtures = new MembershipRepositoryFixture(moduleRef);
profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
org = await organizationsRepositoryFixture.create({
name: "Ecorp",
isOrganization: true,
});
user = await userRepositoryFixture.create({
email: userEmail,
username: userEmail,
organization: { connect: { id: org.id } },
});
profile = await profileRepositoryFixture.create({
uid: `usr-${user.id}`,
username: username,
organization: {
connect: {
id: org.id,
},
},
user: {
connect: {
id: user.id,
},
},
});
membership = await membershipFixtures.addUserToOrg(user, org, "ADMIN", true);
app = moduleRef.createNestApplication();
bootstrap(app as NestExpressApplication);
await app.init();
});
it("should be defined", () => {
expect(userRepositoryFixture).toBeDefined();
expect(organizationsRepositoryFixture).toBeDefined();
expect(user).toBeDefined();
expect(org).toBeDefined();
});
it("should create schedule for org user", async () => {
return request(app.getHttpServer())
.post(`/v2/organizations/${org.id}/users/${user.id}/schedules`)
.send(createScheduleInput)
.expect(201)
.then(async (response) => {
const responseBody: CreateScheduleOutput_2024_06_11 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
createdSchedule = response.body.data;
const expectedSchedule = {
...createScheduleInput,
availability: defaultAvailability,
overrides: [],
};
outputScheduleMatchesExpected(createdSchedule, expectedSchedule, 1);
const scheduleOwner = createdSchedule.ownerId
? await userRepositoryFixture.get(createdSchedule.ownerId)
: null;
expect(scheduleOwner?.defaultScheduleId).toEqual(createdSchedule.id);
});
});
function outputScheduleMatchesExpected(
outputSchedule: ScheduleOutput_2024_06_11 | null,
expected: CreateScheduleInput_2024_06_11 & {
availability: CreateScheduleInput_2024_06_11["availability"];
} & {
overrides: CreateScheduleInput_2024_06_11["overrides"];
},
expectedAvailabilityLength: number
) {
expect(outputSchedule).toBeTruthy();
expect(outputSchedule?.name).toEqual(expected.name);
expect(outputSchedule?.timeZone).toEqual(expected.timeZone);
expect(outputSchedule?.isDefault).toEqual(expected.isDefault);
expect(outputSchedule?.availability.length).toEqual(expectedAvailabilityLength);
const outputScheduleAvailability = outputSchedule?.availability[0];
expect(outputScheduleAvailability).toBeDefined();
expect(outputScheduleAvailability?.days).toEqual(expected.availability?.[0].days);
expect(outputScheduleAvailability?.startTime).toEqual(expected.availability?.[0].startTime);
expect(outputScheduleAvailability?.endTime).toEqual(expected.availability?.[0].endTime);
expect(JSON.stringify(outputSchedule?.overrides)).toEqual(JSON.stringify(expected.overrides));
}
it("should get org schedules", async () => {
return request(app.getHttpServer())
.get(`/v2/organizations/${org.id}/schedules`)
.expect(200)
.then(async (response) => {
const responseBody: GetSchedulesOutput_2024_06_11 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
const schedules = response.body.data;
expect(schedules.length).toEqual(1);
const expectedSchedule = {
...createScheduleInput,
availability: defaultAvailability,
overrides: [],
};
outputScheduleMatchesExpected(schedules[0], expectedSchedule, 1);
});
});
it("should get org user schedule", async () => {
return request(app.getHttpServer())
.get(`/v2/organizations/${org.id}/users/${user.id}/schedules/${createdSchedule.id}`)
.expect(200)
.then(async (response) => {
const responseBody: GetScheduleOutput_2024_06_11 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
const schedule = response.body.data;
const expectedSchedule = {
...createScheduleInput,
availability: defaultAvailability,
overrides: [],
};
outputScheduleMatchesExpected(schedule, expectedSchedule, 1);
});
});
it("should get user schedules", async () => {
return request(app.getHttpServer())
.get(`/v2/organizations/${org.id}/users/${user.id}/schedules/`)
.expect(200)
.then(async (response) => {
const responseBody: GetSchedulesOutput_2024_06_11 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
const schedules = response.body.data;
expect(schedules.length).toEqual(1);
const expectedSchedule = {
...createScheduleInput,
availability: defaultAvailability,
overrides: [],
};
outputScheduleMatchesExpected(schedules[0], expectedSchedule, 1);
});
});
it("should update user schedule name", async () => {
const newScheduleName = "updated-schedule-name";
const body: UpdateScheduleInput_2024_06_11 = {
name: newScheduleName,
};
return request(app.getHttpServer())
.patch(`/v2/organizations/${org.id}/users/${user.id}/schedules/${createdSchedule.id}`)
.send(body)
.expect(200)
.then((response: any) => {
const responseData: UpdateScheduleOutput_2024_06_11 = response.body;
expect(responseData.status).toEqual(SUCCESS_STATUS);
const responseSchedule = responseData.data;
const expectedSchedule = { ...createdSchedule, name: newScheduleName };
outputScheduleMatchesExpected(responseSchedule, expectedSchedule, 1);
createdSchedule = responseSchedule;
});
});
it("should delete user schedule", async () => {
return request(app.getHttpServer())
.delete(`/v2/organizations/${org.id}/users/${user.id}/schedules/${createdSchedule.id}`)
.expect(200);
});
afterAll(async () => {
await profileRepositoryFixture.delete(profile.id);
await membershipFixtures.delete(membership.id);
await userRepositoryFixture.deleteByEmail(user.email);
await organizationsRepositoryFixture.delete(org.id);
await app.close();
});
});
});
@@ -1,23 +1,30 @@
import { SchedulesModule_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.module";
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
import { OrganizationsTeamsController } from "@/modules/organizations/controllers/organizations-teams.controller";
import { OrganizationsSchedulesController } from "@/modules/organizations/controllers/schedules/organizations-schedules.controller";
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
import { OrganizationSchedulesRepository } from "@/modules/organizations/repositories/organizations-schedules.repository";
import { OrganizationsSchedulesService } from "@/modules/organizations/services/organizations-schedules.service";
import { OrganizationsTeamsRepository } from "@/modules/organizations/repositories/organizations-teams.repository";
import { OrganizationsTeamsService } from "@/modules/organizations/services/organizations-teams.service";
import { OrganizationsService } from "@/modules/organizations/services/organizations.service";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { StripeModule } from "@/modules/stripe/stripe.module";
import { UsersModule } from "@/modules/users/users.module";
import { Module } from "@nestjs/common";
@Module({
imports: [PrismaModule, StripeModule],
imports: [PrismaModule, StripeModule, SchedulesModule_2024_06_11, UsersModule],
providers: [
OrganizationsRepository,
OrganizationsTeamsRepository,
OrganizationsService,
OrganizationsTeamsService,
MembershipsRepository,
OrganizationsSchedulesService,
OrganizationSchedulesRepository,
],
exports: [OrganizationsService, OrganizationsRepository, OrganizationsTeamsRepository],
controllers: [OrganizationsTeamsController],
controllers: [OrganizationsTeamsController, OrganizationsSchedulesController],
})
export class OrganizationsModule {}
@@ -0,0 +1,23 @@
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { Injectable } from "@nestjs/common";
@Injectable()
export class OrganizationSchedulesRepository {
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
async getSchedulesByUserIds(userIds: number[], skip: number, take: number) {
return this.dbRead.prisma.schedule.findMany({
where: {
userId: {
in: userIds,
},
},
include: {
availability: true,
},
skip,
take,
});
}
}
@@ -0,0 +1,30 @@
import { OutputSchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/output-schedules.service";
import { OrganizationSchedulesRepository } from "@/modules/organizations/repositories/organizations-schedules.repository";
import { UsersRepository } from "@/modules/users/users.repository";
import { Injectable } from "@nestjs/common";
import { ScheduleOutput_2024_06_11 } from "@calcom/platform-types";
@Injectable()
export class OrganizationsSchedulesService {
constructor(
private readonly organizationSchedulesService: OrganizationSchedulesRepository,
private readonly outputSchedulesService: OutputSchedulesService_2024_06_11,
private readonly usersRepository: UsersRepository
) {}
async getOrganizationSchedules(organizationId: number, skip = 0, take = 250) {
const users = await this.usersRepository.getOrganizationUsers(organizationId);
const usersIds = users.map((user) => user.id);
const schedules = await this.organizationSchedulesService.getSchedulesByUserIds(usersIds, skip, take);
const responseSchedules: ScheduleOutput_2024_06_11[] = [];
for (const schedule of schedules) {
responseSchedules.push(await this.outputSchedulesService.getResponseSchedule(schedule));
}
return responseSchedules;
}
}
@@ -181,6 +181,18 @@ export class UsersRepository {
return user?.defaultScheduleId;
}
async getOrganizationUsers(organizationId: number) {
const profiles = await this.dbRead.prisma.profile.findMany({
where: {
organizationId,
},
include: {
user: true,
},
});
return profiles.map((profile) => profile.user);
}
}
function capitalizeTimezone(timezone: string) {
+485 -273
View File
@@ -977,29 +977,19 @@
]
}
},
"/v2/schedules": {
"post": {
"operationId": "SchedulesController_2024_06_11_createSchedule",
"parameters": [],
"responses": {
"201": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreateScheduleOutput_2024_06_11"
}
}
"/v2/organizations/{orgId}/schedules": {
"get": {
"operationId": "OrganizationsSchedulesController_getOrganizationSchedules",
"parameters": [
{
"name": "orgId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
}
},
"tags": [
"Schedules"
]
},
"get": {
"operationId": "SchedulesController_2024_06_11_getSchedules",
"parameters": [],
],
"responses": {
"200": {
"description": "",
@@ -1013,28 +1003,80 @@
}
},
"tags": [
"Schedules"
"Organizations Schedules"
]
}
},
"/v2/schedules/default": {
"get": {
"operationId": "SchedulesController_2024_06_11_getDefaultSchedule",
"parameters": [],
"/v2/organizations/{orgId}/users/{userId}/schedules": {
"post": {
"operationId": "OrganizationsSchedulesController_createUserSchedule",
"parameters": [
{
"name": "userId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
}
],
"responses": {
"200": {
"description": "Returns the default schedule"
"201": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreateScheduleOutput_2024_06_11"
}
}
}
}
},
"tags": [
"Schedules"
"Organizations Schedules"
]
},
"get": {
"operationId": "OrganizationsSchedulesController_getUserSchedules",
"parameters": [
{
"name": "userId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/GetSchedulesOutput_2024_06_11"
}
}
}
}
},
"tags": [
"Organizations Schedules"
]
}
},
"/v2/schedules/{scheduleId}": {
"/v2/organizations/{orgId}/users/{userId}/schedules/{scheduleId}": {
"get": {
"operationId": "SchedulesController_2024_06_11_getSchedule",
"operationId": "OrganizationsSchedulesController_getUserSchedule",
"parameters": [
{
"name": "userId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
},
{
"name": "scheduleId",
"required": true,
@@ -1057,18 +1099,26 @@
}
},
"tags": [
"Schedules"
"Organizations Schedules"
]
},
"patch": {
"operationId": "SchedulesController_2024_06_11_updateSchedule",
"operationId": "OrganizationsSchedulesController_updateUserSchedule",
"parameters": [
{
"name": "userId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
},
{
"name": "scheduleId",
"required": true,
"in": "path",
"schema": {
"type": "string"
"type": "number"
}
}
],
@@ -1085,12 +1135,20 @@
}
},
"tags": [
"Schedules"
"Organizations Schedules"
]
},
"delete": {
"operationId": "SchedulesController_2024_06_11_deleteSchedule",
"operationId": "OrganizationsSchedulesController_deleteUserSchedule",
"parameters": [
{
"name": "userId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
},
{
"name": "scheduleId",
"required": true,
@@ -1112,6 +1170,163 @@
}
}
},
"tags": [
"Organizations Schedules"
]
}
},
"/v2/schedules": {
"post": {
"operationId": "SchedulesController_2024_04_15_createSchedule",
"parameters": [],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreateScheduleInput_2024_04_15"
}
}
}
},
"responses": {
"201": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreateScheduleOutput_2024_04_15"
}
}
}
}
},
"tags": [
"Schedules"
]
},
"get": {
"operationId": "SchedulesController_2024_04_15_getSchedules",
"parameters": [],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/GetSchedulesOutput_2024_04_15"
}
}
}
}
},
"tags": [
"Schedules"
]
}
},
"/v2/schedules/default": {
"get": {
"operationId": "SchedulesController_2024_04_15_getDefaultSchedule",
"parameters": [],
"responses": {
"200": {
"description": "Returns the default schedule",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/GetDefaultScheduleOutput_2024_04_15"
}
}
}
}
},
"tags": [
"Schedules"
]
}
},
"/v2/schedules/{scheduleId}": {
"get": {
"operationId": "SchedulesController_2024_04_15_getSchedule",
"parameters": [
{
"name": "scheduleId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/GetScheduleOutput_2024_04_15"
}
}
}
}
},
"tags": [
"Schedules"
]
},
"patch": {
"operationId": "SchedulesController_2024_04_15_updateSchedule",
"parameters": [
{
"name": "scheduleId",
"required": true,
"in": "path",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UpdateScheduleOutput_2024_04_15"
}
}
}
}
},
"tags": [
"Schedules"
]
},
"delete": {
"operationId": "SchedulesController_2024_04_15_deleteSchedule",
"parameters": [
{
"name": "scheduleId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DeleteScheduleOutput_2024_04_15"
}
}
}
}
},
"tags": [
"Schedules"
]
@@ -3465,16 +3680,12 @@
"CreateOrgTeamDto": {
"type": "object",
"properties": {
"id": {
"type": "number"
},
"name": {
"type": "string",
"minimum": 1
"minLength": 1
},
"slug": {
"type": "string",
"minimum": 1
"type": "string"
},
"logoUrl": {
"type": "string"
@@ -3492,7 +3703,8 @@
"type": "string"
},
"hideBranding": {
"type": "boolean"
"type": "boolean",
"default": false
},
"isPrivate": {
"type": "boolean"
@@ -3531,6 +3743,236 @@
"name"
]
},
"ScheduleAvailabilityInput_2024_06_11": {
"type": "object",
"properties": {
"days": {
"example": [
"Monday",
"Tuesday"
],
"type": "array",
"items": {
"type": "object"
}
},
"startTime": {
"type": "string",
"pattern": "TIME_FORMAT_HH_MM",
"example": "09:00"
},
"endTime": {
"type": "string",
"pattern": "TIME_FORMAT_HH_MM",
"example": "10:00"
}
},
"required": [
"days",
"startTime",
"endTime"
]
},
"ScheduleOverrideInput_2024_06_11": {
"type": "object",
"properties": {
"date": {
"type": "string",
"example": "2024-05-20"
},
"startTime": {
"type": "string",
"pattern": "TIME_FORMAT_HH_MM",
"example": "12:00"
},
"endTime": {
"type": "string",
"pattern": "TIME_FORMAT_HH_MM",
"example": "13:00"
}
},
"required": [
"date",
"startTime",
"endTime"
]
},
"ScheduleOutput_2024_06_11": {
"type": "object",
"properties": {
"id": {
"type": "number",
"example": 254
},
"ownerId": {
"type": "number",
"example": 478
},
"name": {
"type": "string",
"example": "One-on-one coaching"
},
"timeZone": {
"type": "string",
"example": "Europe/Rome"
},
"availability": {
"example": [
{
"days": [
"Monday",
"Tuesday"
],
"startTime": "09:00",
"endTime": "10:00"
}
],
"type": "array",
"items": {
"$ref": "#/components/schemas/ScheduleAvailabilityInput_2024_06_11"
}
},
"isDefault": {
"type": "boolean",
"example": true
},
"overrides": {
"example": [
{
"date": "2024-05-20",
"startTime": "12:00",
"endTime": "13:00"
}
],
"type": "array",
"items": {
"$ref": "#/components/schemas/ScheduleOverrideInput_2024_06_11"
}
}
},
"required": [
"id",
"ownerId",
"name",
"timeZone",
"availability",
"isDefault",
"overrides"
]
},
"GetSchedulesOutput_2024_06_11": {
"type": "object",
"properties": {
"status": {
"type": "string",
"example": "success",
"enum": [
"success",
"error"
]
},
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/ScheduleOutput_2024_06_11"
}
},
"error": {
"type": "object"
}
},
"required": [
"status",
"data"
]
},
"CreateScheduleOutput_2024_06_11": {
"type": "object",
"properties": {
"status": {
"type": "string",
"example": "success",
"enum": [
"success",
"error"
]
},
"data": {
"$ref": "#/components/schemas/ScheduleOutput_2024_06_11"
}
},
"required": [
"status",
"data"
]
},
"GetScheduleOutput_2024_06_11": {
"type": "object",
"properties": {
"status": {
"type": "string",
"example": "success",
"enum": [
"success",
"error"
]
},
"data": {
"nullable": true,
"allOf": [
{
"$ref": "#/components/schemas/ScheduleOutput_2024_06_11"
}
]
},
"error": {
"type": "object"
}
},
"required": [
"status",
"data"
]
},
"UpdateScheduleOutput_2024_06_11": {
"type": "object",
"properties": {
"status": {
"type": "string",
"example": "success",
"enum": [
"success",
"error"
]
},
"data": {
"$ref": "#/components/schemas/ScheduleOutput_2024_06_11"
},
"error": {
"type": "object"
}
},
"required": [
"status",
"data"
]
},
"DeleteScheduleOutput_2024_06_11": {
"type": "object",
"properties": {
"status": {
"type": "string",
"example": "success",
"enum": [
"success",
"error"
]
}
},
"required": [
"status"
]
},
"CreateAvailabilityInput_2024_04_15": {
"type": "object",
"properties": {
@@ -4073,236 +4515,6 @@
"status"
]
},
"ScheduleAvailabilityInput_2024_06_11": {
"type": "object",
"properties": {
"days": {
"example": [
"Monday",
"Tuesday"
],
"type": "array",
"items": {
"type": "object"
}
},
"startTime": {
"type": "string",
"pattern": "TIME_FORMAT_HH_MM",
"example": "09:00"
},
"endTime": {
"type": "string",
"pattern": "TIME_FORMAT_HH_MM",
"example": "10:00"
}
},
"required": [
"days",
"startTime",
"endTime"
]
},
"ScheduleOverrideInput_2024_06_11": {
"type": "object",
"properties": {
"date": {
"type": "string",
"example": "2024-05-20"
},
"startTime": {
"type": "string",
"pattern": "TIME_FORMAT_HH_MM",
"example": "12:00"
},
"endTime": {
"type": "string",
"pattern": "TIME_FORMAT_HH_MM",
"example": "13:00"
}
},
"required": [
"date",
"startTime",
"endTime"
]
},
"ScheduleOutput_2024_06_11": {
"type": "object",
"properties": {
"id": {
"type": "number",
"example": 254
},
"ownerId": {
"type": "number",
"example": 478
},
"name": {
"type": "string",
"example": "One-on-one coaching"
},
"timeZone": {
"type": "string",
"example": "Europe/Rome"
},
"availability": {
"example": [
{
"days": [
"Monday",
"Tuesday"
],
"startTime": "09:00",
"endTime": "10:00"
}
],
"type": "array",
"items": {
"$ref": "#/components/schemas/ScheduleAvailabilityInput_2024_06_11"
}
},
"isDefault": {
"type": "boolean",
"example": true
},
"overrides": {
"example": [
{
"date": "2024-05-20",
"startTime": "12:00",
"endTime": "13:00"
}
],
"type": "array",
"items": {
"$ref": "#/components/schemas/ScheduleOverrideInput_2024_06_11"
}
}
},
"required": [
"id",
"ownerId",
"name",
"timeZone",
"availability",
"isDefault",
"overrides"
]
},
"CreateScheduleOutput_2024_06_11": {
"type": "object",
"properties": {
"status": {
"type": "string",
"example": "success",
"enum": [
"success",
"error"
]
},
"data": {
"$ref": "#/components/schemas/ScheduleOutput_2024_06_11"
}
},
"required": [
"status",
"data"
]
},
"GetScheduleOutput_2024_06_11": {
"type": "object",
"properties": {
"status": {
"type": "string",
"example": "success",
"enum": [
"success",
"error"
]
},
"data": {
"nullable": true,
"allOf": [
{
"$ref": "#/components/schemas/ScheduleOutput_2024_06_11"
}
]
},
"error": {
"type": "object"
}
},
"required": [
"status",
"data"
]
},
"GetSchedulesOutput_2024_06_11": {
"type": "object",
"properties": {
"status": {
"type": "string",
"example": "success",
"enum": [
"success",
"error"
]
},
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/ScheduleOutput_2024_06_11"
}
},
"error": {
"type": "object"
}
},
"required": [
"status",
"data"
]
},
"UpdateScheduleOutput_2024_06_11": {
"type": "object",
"properties": {
"status": {
"type": "string",
"example": "success",
"enum": [
"success",
"error"
]
},
"data": {
"$ref": "#/components/schemas/ScheduleOutput_2024_06_11"
},
"error": {
"type": "object"
}
},
"required": [
"status",
"data"
]
},
"DeleteScheduleOutput_2024_06_11": {
"type": "object",
"properties": {
"status": {
"type": "string",
"example": "success",
"enum": [
"success",
"error"
]
}
},
"required": [
"status"
]
},
"MeOutput": {
"type": "object",
"properties": {
@@ -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 } from "@prisma/client";
export class ProfileRepositoryFixture {
private primaReadClient: PrismaReadService["prisma"];
private prismaWriteClient: PrismaWriteService["prisma"];
constructor(private readonly module: TestingModule) {
this.primaReadClient = module.get(PrismaReadService).prisma;
this.prismaWriteClient = module.get(PrismaWriteService).prisma;
}
async get(profileId: number) {
return this.primaReadClient.profile.findFirst({ where: { id: profileId } });
}
async create(data: Prisma.ProfileCreateInput) {
return this.prismaWriteClient.profile.create({ data });
}
async delete(profileId: number) {
return this.prismaWriteClient.profile.delete({ where: { id: profileId } });
}
}
+1
View File
@@ -301,6 +301,7 @@ model User {
verifiedNumbers VerifiedNumber[]
verifiedEmails VerifiedEmail[]
hosts Host[]
// organizationId is deprecated. Instead, rely on the Profile to search profiles by organizationId and then get user from the profile.
organizationId Int?
organization Team? @relation("scope", fields: [organizationId], references: [id], onDelete: SetNull)
accessCodes AccessCode[]