feat: filter users by assigned attribute options (#20659)
* feat: assigned attributes options base * feat: assigned attributes options base e2e * feat: filter assigned options by other assigned options * feat: filter assigned options by other assigned options * fixup! feat: filter assigned options by other assigned options * feat: get org users by assigned attributes * adding missing input file * push generated docs * feat: add filter org users by teamIds * feat: add filter assigned attribute options by teamIds * chore; update doc * chore: remove console log * chore: minor e2e fix * chore: minor e2e fix * chore: improve docs * fix: assigned options by teamIds + optionIds
This commit is contained in:
1 parent
a8fe3a6466
commit
5c7f1aebb2
16 files changed
+1435
-26
No files matched your search
-2
@@ -16,8 +16,6 @@ export class DestinationCalendarsService {
|
||||
userId: number,
|
||||
delegationCredentialId?: string
|
||||
) {
|
||||
// note(Lauris): todo remove the log but leaving this now to confirm delegationCredentialId is received
|
||||
console.log("debug: delegationCredentialId", delegationCredentialId);
|
||||
const userCalendars = await this.calendarsService.getCalendars(userId);
|
||||
const allCalendars: Calendar[] = userCalendars.connectedCalendars
|
||||
.map((cal: ConnectedCalendar) => cal.calendars ?? [])
|
||||
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
import { ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { Transform } from "class-transformer";
|
||||
import { IsOptional, IsArray, ArrayMinSize, IsString, IsNumber } from "class-validator";
|
||||
|
||||
export class GetAssignedAttributeOptions {
|
||||
@ApiPropertyOptional({ type: Number, description: "Number of responses to skip" })
|
||||
@Transform(({ value }) => value && parseInt(value))
|
||||
@IsOptional()
|
||||
skip?: number;
|
||||
|
||||
@ApiPropertyOptional({ type: Number, description: "Number of responses to take" })
|
||||
@Transform(({ value }) => value && parseInt(value))
|
||||
@IsOptional()
|
||||
take?: number;
|
||||
|
||||
@IsOptional()
|
||||
@Transform(({ value }) => {
|
||||
if (typeof value === "string") {
|
||||
return value.split(",").map((optId: string) => optId);
|
||||
}
|
||||
return value;
|
||||
})
|
||||
@ApiPropertyOptional({
|
||||
type: [String],
|
||||
description: "Filter by assigned attribute option ids. ids must be separated by a comma.",
|
||||
example: "?assignedOptionIds=aaaaaaaa-bbbb-cccc-dddd-eeeeee1eee,aaaaaaaa-bbbb-cccc-dddd-eeeeee2eee",
|
||||
})
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
@ArrayMinSize(1, { message: "assignedOptionIds must contain at least 1 attribute option id" })
|
||||
assignedOptionIds?: string[];
|
||||
|
||||
@IsOptional()
|
||||
@Transform(({ value }) => {
|
||||
if (typeof value === "string") {
|
||||
return value.split(",").map((teamId: string) => parseInt(teamId));
|
||||
}
|
||||
return value;
|
||||
})
|
||||
@ApiPropertyOptional({
|
||||
type: [Number],
|
||||
description: "Filter by teamIds. Team ids must be separated by a comma.",
|
||||
example: "?teamIds=100,200",
|
||||
})
|
||||
@IsArray()
|
||||
@IsNumber({}, { each: true })
|
||||
@ArrayMinSize(1, { message: "teamIds must contain at least 1 team id" })
|
||||
teamIds?: number[];
|
||||
}
|
||||
+78
@@ -7,6 +7,8 @@ import { Injectable, Logger, NotFoundException } from "@nestjs/common";
|
||||
|
||||
import { slugify } from "@calcom/platform-libraries";
|
||||
|
||||
import { GetOrganizationAttributeAssignedOptionsProp } from "./services/organization-attributes-option.service";
|
||||
|
||||
@Injectable()
|
||||
export class OrganizationAttributeOptionRepository {
|
||||
private readonly logger = new Logger("OrganizationAttributeOptionRepository");
|
||||
@@ -90,6 +92,82 @@ export class OrganizationAttributeOptionRepository {
|
||||
return options;
|
||||
}
|
||||
|
||||
async getOrganizationAttributeAssignedOptions({
|
||||
attributeId,
|
||||
attributeSlug,
|
||||
filters,
|
||||
organizationId,
|
||||
skip,
|
||||
take,
|
||||
}: GetOrganizationAttributeAssignedOptionsProp) {
|
||||
const options = await this.dbRead.prisma.attributeOption.findMany({
|
||||
where: {
|
||||
attribute: {
|
||||
...(attributeId && { id: attributeId }),
|
||||
...(attributeSlug && { slug: attributeSlug }),
|
||||
teamId: organizationId,
|
||||
},
|
||||
assignedUsers: {
|
||||
some: {}, // empty {} statement checks if option is assigned to at least one user
|
||||
...(filters?.teamIds && {
|
||||
some: { member: { user: { teams: { some: { teamId: { in: filters.teamIds } } } } } },
|
||||
}),
|
||||
},
|
||||
},
|
||||
include: { assignedUsers: { include: { member: true } } },
|
||||
skip,
|
||||
take,
|
||||
});
|
||||
|
||||
// only return options that are assigned to users alongside assignedOptionIds filter
|
||||
if (filters?.assignedOptionIds) {
|
||||
const filteredAssignedOptions = await this.dbRead.prisma.attributeOption.findMany({
|
||||
where: {
|
||||
attribute: { teamId: organizationId },
|
||||
assignedUsers: {
|
||||
every: {
|
||||
attributeOptionId: { in: filters?.assignedOptionIds },
|
||||
},
|
||||
...(filters?.teamIds && {
|
||||
some: { member: { user: { teams: { some: { teamId: { in: filters.teamIds } } } } } },
|
||||
}),
|
||||
},
|
||||
},
|
||||
include: { assignedUsers: { include: { member: true } } },
|
||||
});
|
||||
|
||||
if (!filteredAssignedOptions?.length) {
|
||||
throw new NotFoundException(
|
||||
"Options provided in assignedOptionIds are not assigned to anyone, or the users are not part of the teams specified in teamIds filter."
|
||||
);
|
||||
}
|
||||
|
||||
const matchingUserIds = filteredAssignedOptions.flatMap((opt) =>
|
||||
opt.assignedUsers.flatMap((assignedUser) => assignedUser.member.userId)
|
||||
);
|
||||
// reduce remove options that are not assigned alongside assignedOptionIds filter
|
||||
return options.reduce((acc, opt) => {
|
||||
if (opt.assignedUsers.some((assignedUser) => matchingUserIds.includes(assignedUser.member.userId))) {
|
||||
return [
|
||||
...acc,
|
||||
{
|
||||
...opt,
|
||||
assignedUserIds: opt.assignedUsers.map((attributeToUser) => attributeToUser.member.userId),
|
||||
},
|
||||
];
|
||||
}
|
||||
return acc;
|
||||
}, [] as typeof options);
|
||||
}
|
||||
|
||||
return options.map((opt) => {
|
||||
return {
|
||||
...opt,
|
||||
assignedUserIds: opt.assignedUsers.map((attributeToUser) => attributeToUser.member.userId),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async assignOrganizationAttributeOptionToUser({
|
||||
organizationId,
|
||||
membershipId,
|
||||
|
||||
+233
-8
@@ -3,6 +3,8 @@ import { AppModule } from "@/app.module";
|
||||
import { CreateOrganizationAttributeOptionInput } from "@/modules/organizations/attributes/options/inputs/create-organization-attribute-option.input";
|
||||
import { AssignOrganizationAttributeOptionToUserInput } from "@/modules/organizations/attributes/options/inputs/organizations-attributes-options-assign.input";
|
||||
import { UpdateOrganizationAttributeOptionInput } from "@/modules/organizations/attributes/options/inputs/update-organizaiton-attribute-option.input.ts";
|
||||
import { AssignedOptionOutput } from "@/modules/organizations/attributes/options/outputs/assigned-options.output";
|
||||
import { OptionOutput } from "@/modules/organizations/attributes/options/outputs/option.output";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||
import { UsersModule } from "@/modules/users/users.module";
|
||||
@@ -13,12 +15,13 @@ import * as request from "supertest";
|
||||
import { AttributeRepositoryFixture } from "test/fixtures/repository/attributes.repository.fixture";
|
||||
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
|
||||
import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.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 { withApiAuth } from "test/utils/withApiAuth";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { User, Team, Membership } from "@calcom/prisma/client";
|
||||
import { User, Team, Membership, Attribute, AttributeOption } from "@calcom/prisma/client";
|
||||
|
||||
describe("Organizations Attributes Options Endpoints", () => {
|
||||
describe("User lacks required role", () => {
|
||||
@@ -95,19 +98,37 @@ describe("Organizations Attributes Options Endpoints", () => {
|
||||
let app: INestApplication;
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
|
||||
let teamsRepositoryFixtures: TeamRepositoryFixture;
|
||||
|
||||
let membershipFixtures: MembershipRepositoryFixture;
|
||||
let attributeRepositoryFixture: AttributeRepositoryFixture;
|
||||
|
||||
const userEmail = `organization-attributes-options-admin-${randomString()}@api.com`;
|
||||
const userEmail2 = `organization-attributes-options-member-${randomString()}@api.com`;
|
||||
|
||||
let user: User;
|
||||
let user2: User;
|
||||
let org: Team;
|
||||
let team: Team;
|
||||
let membership: Membership;
|
||||
let membership2: Membership;
|
||||
|
||||
let attributeId: string;
|
||||
let attributeSlug: string;
|
||||
|
||||
let createdOption: any;
|
||||
let createdOption2: any;
|
||||
let attribute2: Attribute;
|
||||
let attribute2Option: AttributeOption;
|
||||
|
||||
const createOptionInput: CreateOrganizationAttributeOptionInput = {
|
||||
value: "option1",
|
||||
slug: "option1",
|
||||
slug: `option1-${randomString()}`,
|
||||
};
|
||||
|
||||
const createOption2Input: CreateOrganizationAttributeOptionInput = {
|
||||
value: "option2",
|
||||
slug: `option2${randomString()}`,
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
@@ -120,6 +141,7 @@ describe("Organizations Attributes Options Endpoints", () => {
|
||||
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
|
||||
teamsRepositoryFixtures = new TeamRepositoryFixture(moduleRef);
|
||||
membershipFixtures = new MembershipRepositoryFixture(moduleRef);
|
||||
attributeRepositoryFixture = new AttributeRepositoryFixture(moduleRef);
|
||||
|
||||
@@ -128,23 +150,50 @@ describe("Organizations Attributes Options Endpoints", () => {
|
||||
isOrganization: true,
|
||||
});
|
||||
|
||||
team = await teamsRepositoryFixtures.create({ name: "org team", parent: { connect: { id: org.id } } });
|
||||
|
||||
user = await userRepositoryFixture.create({
|
||||
email: userEmail,
|
||||
username: userEmail,
|
||||
organization: { connect: { id: org.id } },
|
||||
});
|
||||
|
||||
user2 = await userRepositoryFixture.create({
|
||||
email: userEmail2,
|
||||
username: userEmail2,
|
||||
organization: { connect: { id: org.id } },
|
||||
});
|
||||
|
||||
membership = await membershipFixtures.addUserToOrg(user, org, "ADMIN", true);
|
||||
membership2 = await membershipFixtures.addUserToOrg(user2, org, "ADMIN", true);
|
||||
await membershipFixtures.create({
|
||||
role: "MEMBER",
|
||||
accepted: true,
|
||||
team: { connect: { id: team.id } },
|
||||
user: { connect: { id: user.id } },
|
||||
});
|
||||
|
||||
// Create an attribute for testing
|
||||
const attribute = await attributeRepositoryFixture.create({
|
||||
name: "Test Attribute",
|
||||
team: { connect: { id: org.id } },
|
||||
type: "TEXT",
|
||||
slug: "test-attribute",
|
||||
slug: `test-attribute-${randomString()}`,
|
||||
});
|
||||
|
||||
attribute2 = await attributeRepositoryFixture.create({
|
||||
name: "Test Attribute 2",
|
||||
team: { connect: { id: org.id } },
|
||||
type: "TEXT",
|
||||
slug: `test-attribute-2-${randomString()}`,
|
||||
});
|
||||
attributeId = attribute.id;
|
||||
|
||||
attributeSlug = attribute.slug;
|
||||
attribute2Option = await attributeRepositoryFixture.createOption({
|
||||
slug: `optionA-${randomString()}`,
|
||||
value: "optionA",
|
||||
attribute: { connect: { id: attribute2.id } },
|
||||
});
|
||||
app = moduleRef.createNestApplication();
|
||||
bootstrap(app as NestExpressApplication);
|
||||
|
||||
@@ -164,16 +213,31 @@ describe("Organizations Attributes Options Endpoints", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should create attribute option", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/organizations/${org.id}/attributes/${attributeId}/options`)
|
||||
.send(createOption2Input)
|
||||
.expect(201)
|
||||
.then((response) => {
|
||||
expect(response.body.status).toEqual(SUCCESS_STATUS);
|
||||
createdOption2 = response.body.data;
|
||||
expect(createdOption2.value).toEqual(createOption2Input.value);
|
||||
expect(createdOption2.slug).toEqual(createOption2Input.slug);
|
||||
});
|
||||
});
|
||||
|
||||
it("should get attribute options", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/organizations/${org.id}/attributes/${attributeId}/options`)
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
expect(response.body.status).toEqual(SUCCESS_STATUS);
|
||||
const options = response.body.data;
|
||||
expect(options.length).toEqual(1);
|
||||
expect(options[0].value).toEqual(createOptionInput.value);
|
||||
expect(options[0].slug).toEqual(createOptionInput.slug);
|
||||
const options = response.body.data as OptionOutput[];
|
||||
expect(options.length).toEqual(2);
|
||||
expect(options.find((opt) => opt.value === createOptionInput.value)).toBeDefined();
|
||||
expect(options.find((opt) => opt.slug === createOptionInput.slug)).toBeDefined();
|
||||
expect(options.find((opt) => opt.value === createOption2Input.value)).toBeDefined();
|
||||
expect(options.find((opt) => opt.slug === createOption2Input.slug)).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -209,6 +273,38 @@ describe("Organizations Attributes Options Endpoints", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should assign attribute option to user2", async () => {
|
||||
const assignInput: AssignOrganizationAttributeOptionToUserInput = {
|
||||
attributeId: attributeId,
|
||||
attributeOptionId: createdOption2.id,
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/organizations/${org.id}/attributes/options/${user2.id}`)
|
||||
.send(assignInput)
|
||||
.expect(201)
|
||||
.then((response) => {
|
||||
expect(response.body.status).toEqual(SUCCESS_STATUS);
|
||||
expect(response.body.data).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it("should assign attribute option to user2", async () => {
|
||||
const assignInput: AssignOrganizationAttributeOptionToUserInput = {
|
||||
attributeId: attribute2.id,
|
||||
attributeOptionId: attribute2Option.id,
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/organizations/${org.id}/attributes/options/${user2.id}`)
|
||||
.send(assignInput)
|
||||
.expect(201)
|
||||
.then((response) => {
|
||||
expect(response.body.status).toEqual(SUCCESS_STATUS);
|
||||
expect(response.body.data).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it("should get attribute options for user", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/organizations/${org.id}/attributes/options/${user.id}`)
|
||||
@@ -221,6 +317,133 @@ describe("Organizations Attributes Options Endpoints", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should get attribute options for user2", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/organizations/${org.id}/attributes/options/${user2.id}`)
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
expect(response.body.status).toEqual(SUCCESS_STATUS);
|
||||
const userOptions = response.body.data as AttributeOption[];
|
||||
expect(userOptions.length).toEqual(2);
|
||||
expect(userOptions.find((opt) => opt.id === createdOption2.id)).toBeDefined();
|
||||
expect(userOptions.find((opt) => opt.id === attribute2Option.id)).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("should get attribute all assigned options", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/organizations/${org.id}/attributes/${attributeId}/options/assigned`)
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
expect(response.body.status).toEqual(SUCCESS_STATUS);
|
||||
const assignedOptions = response.body.data as AssignedOptionOutput[];
|
||||
expect(assignedOptions?.length).toEqual(2);
|
||||
|
||||
expect(assignedOptions.find((opt) => createdOption.id === opt.id)).toBeDefined();
|
||||
expect(
|
||||
assignedOptions
|
||||
.find((opt) => createdOption.id === opt.id)
|
||||
?.assignedUserIds.find((id) => id === user.id)
|
||||
).toBeDefined();
|
||||
|
||||
expect(assignedOptions.find((opt) => createdOption2.id === opt.id)).toBeDefined();
|
||||
expect(
|
||||
assignedOptions
|
||||
.find((opt) => createdOption2.id === opt.id)
|
||||
?.assignedUserIds.find((id) => id === user2.id)
|
||||
).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("should get attribute all assigned options filtered by team id", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/organizations/${org.id}/attributes/${attributeId}/options/assigned?teamIds=${team.id}`)
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
expect(response.body.status).toEqual(SUCCESS_STATUS);
|
||||
const assignedOptions = response.body.data as AssignedOptionOutput[];
|
||||
expect(assignedOptions?.length).toEqual(1);
|
||||
|
||||
expect(assignedOptions.find((opt) => createdOption.id === opt.id)).toBeDefined();
|
||||
expect(
|
||||
assignedOptions
|
||||
.find((opt) => createdOption.id === opt.id)
|
||||
?.assignedUserIds.find((id) => id === user.id)
|
||||
).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("should get attribute all assigned options by attribute slug", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/organizations/${org.id}/attributes/slugs/${attributeSlug}/options/assigned`)
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
expect(response.body.status).toEqual(SUCCESS_STATUS);
|
||||
const assignedOptions = response.body.data as AssignedOptionOutput[];
|
||||
expect(assignedOptions?.length).toEqual(2);
|
||||
|
||||
expect(assignedOptions.find((opt) => createdOption.id === opt.id)).toBeDefined();
|
||||
expect(
|
||||
assignedOptions
|
||||
.find((opt) => createdOption.id === opt.id)
|
||||
?.assignedUserIds.find((id) => id === user.id)
|
||||
).toBeDefined();
|
||||
|
||||
expect(assignedOptions.find((opt) => createdOption2.id === opt.id)).toBeDefined();
|
||||
expect(
|
||||
assignedOptions
|
||||
.find((opt) => createdOption2.id === opt.id)
|
||||
?.assignedUserIds.find((id) => id === user2.id)
|
||||
).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("should get attribute all assigned options filtered by other assigned options", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(
|
||||
`/v2/organizations/${org.id}/attributes/${attributeId}/options/assigned?assignedOptionIds=${attribute2Option.id}`
|
||||
)
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
expect(response.body.status).toEqual(SUCCESS_STATUS);
|
||||
const assignedOptions = response.body.data as AssignedOptionOutput[];
|
||||
expect(assignedOptions?.length).toEqual(1);
|
||||
expect(assignedOptions.find((opt) => createdOption2.id === opt.id)).toBeDefined();
|
||||
expect(
|
||||
assignedOptions
|
||||
.find((opt) => createdOption2.id === opt.id)
|
||||
?.assignedUserIds.find((id) => id === user2.id)
|
||||
).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("should not get attribute all assigned options filtered by other assigned options and teamIds in which the user is not part of", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(
|
||||
`/v2/organizations/${org.id}/attributes/${attributeId}/options/assigned?assignedOptionIds=${attribute2Option.id}&teamIds=${team.id}`
|
||||
)
|
||||
.expect(404);
|
||||
});
|
||||
|
||||
it("should get attribute all assigned options filtered by other assigned options and teamIds", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(
|
||||
`/v2/organizations/${org.id}/attributes/${attributeId}/options/assigned?assignedOptionIds=${createdOption.id}&teamIds=${team.id}`
|
||||
)
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
expect(response.body.status).toEqual(SUCCESS_STATUS);
|
||||
const assignedOptions = response.body.data as AssignedOptionOutput[];
|
||||
expect(assignedOptions?.length).toEqual(1);
|
||||
expect(assignedOptions.find((opt) => createdOption.id === opt.id)).toBeDefined();
|
||||
expect(
|
||||
assignedOptions
|
||||
.find((opt) => createdOption.id === opt.id)
|
||||
?.assignedUserIds.find((id) => id === user.id)
|
||||
).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("should unassign attribute option from user", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.delete(`/v2/organizations/${org.id}/attributes/options/${user.id}/${createdOption.id}`)
|
||||
@@ -243,7 +466,9 @@ describe("Organizations Attributes Options Endpoints", () => {
|
||||
|
||||
afterAll(async () => {
|
||||
await membershipFixtures.delete(membership.id);
|
||||
await membershipFixtures.delete(membership2.id);
|
||||
await userRepositoryFixture.deleteByEmail(user.email);
|
||||
await userRepositoryFixture.deleteByEmail(user2.email);
|
||||
await organizationsRepositoryFixture.delete(org.id);
|
||||
await app.close();
|
||||
});
|
||||
|
||||
+62
-1
@@ -8,19 +8,32 @@ import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-a
|
||||
import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard";
|
||||
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
|
||||
import { CreateOrganizationAttributeOptionInput } from "@/modules/organizations/attributes/options/inputs/create-organization-attribute-option.input";
|
||||
import { GetAssignedAttributeOptions } from "@/modules/organizations/attributes/options/inputs/get-assigned-attribute-options.input";
|
||||
import { AssignOrganizationAttributeOptionToUserInput } from "@/modules/organizations/attributes/options/inputs/organizations-attributes-options-assign.input";
|
||||
import { UpdateOrganizationAttributeOptionInput } from "@/modules/organizations/attributes/options/inputs/update-organizaiton-attribute-option.input.ts";
|
||||
import {
|
||||
AssignOptionUserOutput,
|
||||
UnassignOptionUserOutput,
|
||||
} from "@/modules/organizations/attributes/options/outputs/assign-option-user.output";
|
||||
import { GetAllAttributeAssignedOptionOutput } from "@/modules/organizations/attributes/options/outputs/assigned-options.output";
|
||||
import { CreateAttributeOptionOutput } from "@/modules/organizations/attributes/options/outputs/create-option.output";
|
||||
import { DeleteAttributeOptionOutput } from "@/modules/organizations/attributes/options/outputs/delete-option.output";
|
||||
import { GetOptionUserOutput } from "@/modules/organizations/attributes/options/outputs/get-option-user.output";
|
||||
import { GetAllAttributeOptionOutput } from "@/modules/organizations/attributes/options/outputs/get-option.output";
|
||||
import { UpdateAttributeOptionOutput } from "@/modules/organizations/attributes/options/outputs/update-option.output";
|
||||
import { OrganizationAttributeOptionService } from "@/modules/organizations/attributes/options/services/organization-attributes-option.service";
|
||||
import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post, UseGuards } from "@nestjs/common";
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Param,
|
||||
ParseIntPipe,
|
||||
Patch,
|
||||
Post,
|
||||
Query,
|
||||
UseGuards,
|
||||
} from "@nestjs/common";
|
||||
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
@@ -118,6 +131,54 @@ export class OrganizationsAttributesOptionsController {
|
||||
};
|
||||
}
|
||||
|
||||
@Roles("ORG_MEMBER")
|
||||
@PlatformPlan("ESSENTIALS")
|
||||
@Get("/attributes/:attributeId/options/assigned")
|
||||
@ApiOperation({ summary: "Get by attribute id all of the attribute options that are assigned to users" })
|
||||
async getOrganizationAttributeAssignedOptions(
|
||||
@Param("orgId", ParseIntPipe) orgId: number,
|
||||
@Param("attributeId") attributeId: string,
|
||||
@Query() queryParams: GetAssignedAttributeOptions
|
||||
): Promise<GetAllAttributeAssignedOptionOutput> {
|
||||
const { skip, take, ...rest } = queryParams;
|
||||
const attributeOptions =
|
||||
await this.organizationsAttributesOptionsService.getOrganizationAttributeAssignedOptions({
|
||||
organizationId: orgId,
|
||||
attributeId,
|
||||
skip: skip ?? 0,
|
||||
take: take ?? 250,
|
||||
filters: rest,
|
||||
});
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: attributeOptions,
|
||||
};
|
||||
}
|
||||
|
||||
@Roles("ORG_MEMBER")
|
||||
@PlatformPlan("ESSENTIALS")
|
||||
@Get("/attributes/slugs/:attributeSlug/options/assigned")
|
||||
@ApiOperation({ summary: "Get by attribute slug all of the attribute options that are assigned to users" })
|
||||
async getOrganizationAttributeAssignedOptionsBySlug(
|
||||
@Param("orgId", ParseIntPipe) orgId: number,
|
||||
@Param("attributeSlug") attributeSlug: string,
|
||||
@Query() queryParams: GetAssignedAttributeOptions
|
||||
): Promise<GetAllAttributeAssignedOptionOutput> {
|
||||
const { skip, take, ...rest } = queryParams;
|
||||
const attributeOptions =
|
||||
await this.organizationsAttributesOptionsService.getOrganizationAttributeAssignedOptions({
|
||||
organizationId: orgId,
|
||||
attributeSlug,
|
||||
skip: skip ?? 0,
|
||||
take: take ?? 250,
|
||||
filters: rest,
|
||||
});
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: attributeOptions,
|
||||
};
|
||||
}
|
||||
|
||||
@Roles("ORG_ADMIN")
|
||||
@PlatformPlan("ESSENTIALS")
|
||||
@Post("/attributes/options/:userId")
|
||||
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
import { BaseOutputDTO } from "@/modules/organizations/attributes/index/outputs/base.output";
|
||||
import { OptionOutput } from "@/modules/organizations/attributes/options/outputs/option.output";
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { Expose, Type } from "class-transformer";
|
||||
import { IsArray, ValidateNested } from "class-validator";
|
||||
|
||||
export class AssignedOptionOutput extends OptionOutput {
|
||||
@Expose()
|
||||
@IsArray()
|
||||
@ApiProperty({
|
||||
type: Array,
|
||||
required: true,
|
||||
description: "Ids of the users assigned to the attribute option.",
|
||||
example: [124, 224],
|
||||
})
|
||||
assignedUserIds!: number[];
|
||||
}
|
||||
|
||||
export class GetAllAttributeAssignedOptionOutput extends BaseOutputDTO {
|
||||
@Expose()
|
||||
@ValidateNested()
|
||||
@Type(() => OptionOutput)
|
||||
data!: AssignedOptionOutput[];
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
import { BaseOutputDTO } from "@/modules/organizations/attributes/index/outputs/base.output";
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsString, IsInt, IsEnum, IsBoolean } from "class-validator";
|
||||
import { Expose } from "class-transformer";
|
||||
import { IsString } from "class-validator";
|
||||
|
||||
export class OptionOutput {
|
||||
@Expose()
|
||||
@IsString()
|
||||
@ApiProperty({
|
||||
type: String,
|
||||
@@ -12,10 +13,12 @@ export class OptionOutput {
|
||||
})
|
||||
id!: string;
|
||||
|
||||
@Expose()
|
||||
@IsString()
|
||||
@ApiProperty({ type: String, required: true, description: "The ID of the attribute", example: "attr_id" })
|
||||
attributeId!: string;
|
||||
|
||||
@Expose()
|
||||
@IsString()
|
||||
@ApiProperty({
|
||||
type: String,
|
||||
@@ -25,6 +28,7 @@ export class OptionOutput {
|
||||
})
|
||||
value!: string;
|
||||
|
||||
@Expose()
|
||||
@IsString()
|
||||
@ApiProperty({
|
||||
type: String,
|
||||
|
||||
+43
@@ -3,8 +3,10 @@ import { CreateOrganizationAttributeOptionInput } from "@/modules/organizations/
|
||||
import { AssignOrganizationAttributeOptionToUserInput } from "@/modules/organizations/attributes/options/inputs/organizations-attributes-options-assign.input";
|
||||
import { UpdateOrganizationAttributeOptionInput } from "@/modules/organizations/attributes/options/inputs/update-organizaiton-attribute-option.input.ts";
|
||||
import { OrganizationAttributeOptionRepository } from "@/modules/organizations/attributes/options/organization-attribute-options.repository";
|
||||
import { AssignedOptionOutput } from "@/modules/organizations/attributes/options/outputs/assigned-options.output";
|
||||
import { OrganizationsMembershipService } from "@/modules/organizations/memberships/services/organizations-membership.service";
|
||||
import { BadRequestException, Injectable, Logger, NotFoundException } from "@nestjs/common";
|
||||
import { plainToClass } from "class-transformer";
|
||||
|
||||
const TYPE_SUPPORTS_VALUE = new Set(["TEXT", "NUMBER"]);
|
||||
|
||||
@@ -111,4 +113,45 @@ export class OrganizationAttributeOptionService {
|
||||
userId
|
||||
);
|
||||
}
|
||||
|
||||
async getOrganizationAttributeAssignedOptions({
|
||||
organizationId,
|
||||
attributeId,
|
||||
attributeSlug,
|
||||
skip,
|
||||
take,
|
||||
filters,
|
||||
}: GetOrganizationAttributeAssignedOptionsProp) {
|
||||
const options = await this.organizationAttributeOptionRepository.getOrganizationAttributeAssignedOptions({
|
||||
organizationId,
|
||||
...(attributeId ? { attributeId } : { attributeSlug }),
|
||||
skip,
|
||||
take,
|
||||
filters,
|
||||
});
|
||||
return options.map((opt) => plainToClass(AssignedOptionOutput, opt, { strategy: "excludeAll" }));
|
||||
}
|
||||
}
|
||||
|
||||
// Discriminative Union on attributeSlug / attributeId
|
||||
export type GetOrganizationAttributeAssignedOptionsProp =
|
||||
| GetOrganizationAttributeAssignedOptionsPropById
|
||||
| GetOrganizationAttributeAssignedOptionsPropBySlug;
|
||||
|
||||
type GetOrganizationAttributeAssignedOptionsPropById = {
|
||||
organizationId: number;
|
||||
attributeId: string;
|
||||
attributeSlug?: undefined;
|
||||
skip: number;
|
||||
take: number;
|
||||
filters?: { assignedOptionIds?: string[]; teamIds?: number[] };
|
||||
};
|
||||
|
||||
type GetOrganizationAttributeAssignedOptionsPropBySlug = {
|
||||
organizationId: number;
|
||||
attributeId?: undefined;
|
||||
attributeSlug?: string;
|
||||
skip: number;
|
||||
take: number;
|
||||
filters?: { assignedOptionIds?: string[]; teamIds?: number[] };
|
||||
};
|
||||
+3
-1
@@ -66,9 +66,11 @@ export class OrganizationsUsersController {
|
||||
@Param("orgId", ParseIntPipe) orgId: number,
|
||||
@Query() query: GetOrganizationsUsersInput
|
||||
): Promise<GetOrganizationUsersResponseDTO> {
|
||||
const { emails, assignedOptionIds, attributeQueryOperator, teamIds } = query ?? {};
|
||||
const users = await this.organizationsUsersService.getUsers(
|
||||
orgId,
|
||||
query.emails,
|
||||
emails,
|
||||
{ assignedOptionIds, attributeQueryOperator, teamIds },
|
||||
query.skip ?? 0,
|
||||
query.take ?? 250
|
||||
);
|
||||
|
||||
+246
-8
@@ -1,6 +1,7 @@
|
||||
import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { EmailService } from "@/modules/email/email.service";
|
||||
import { GetOrganizationsUsersInput } from "@/modules/organizations/users/index/inputs/get-organization-users.input";
|
||||
import { GetOrgUsersWithProfileOutput } from "@/modules/organizations/users/index/outputs/get-organization-users.output";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||
@@ -10,6 +11,7 @@ import { INestApplication } from "@nestjs/common";
|
||||
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||
import { Test } from "@nestjs/testing";
|
||||
import * as request from "supertest";
|
||||
import { AttributeRepositoryFixture } from "test/fixtures/repository/attributes.repository.fixture";
|
||||
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
|
||||
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
|
||||
import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture";
|
||||
@@ -20,7 +22,7 @@ import { randomString } from "test/utils/randomString";
|
||||
import { withApiAuth } from "test/utils/withApiAuth";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { User, Team, EventType } from "@calcom/prisma/client";
|
||||
import { User, Team, EventType, AttributeOption } from "@calcom/prisma/client";
|
||||
|
||||
describe("Organizations Users Endpoints", () => {
|
||||
const bio = "I am a bio";
|
||||
@@ -114,8 +116,6 @@ describe("Organizations Users Endpoints", () => {
|
||||
await Promise.all([userRepositoryFixture.deleteByEmail(user.email)]);
|
||||
await organizationsRepositoryFixture.delete(org.id);
|
||||
await app.close();
|
||||
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
describe("Admin role", () => {
|
||||
@@ -249,11 +249,6 @@ describe("Organizations Users Endpoints", () => {
|
||||
|
||||
expect(body.status).toBe(SUCCESS_STATUS);
|
||||
expect(userData.length).toBe(4);
|
||||
console.log(
|
||||
"profiles",
|
||||
{ userData },
|
||||
userData.map((u) => u.profile)
|
||||
);
|
||||
// Find and verify each member's data
|
||||
const member0 = userData.find((u) => u.profile.username === orgMembersData[0].username);
|
||||
const member1 = userData.find((u) => u.profile.username === orgMembersData[1].username);
|
||||
@@ -543,7 +538,250 @@ describe("Organizations Users Endpoints", () => {
|
||||
await userRepositoryFixture.deleteByEmail(createdUser.email);
|
||||
await organizationsRepositoryFixture.delete(org.id);
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Org Members with assigned attributes", () => {
|
||||
let app: INestApplication;
|
||||
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
|
||||
let membershipFixtures: MembershipRepositoryFixture;
|
||||
let teamsRepositoryFixtures: TeamRepositoryFixture;
|
||||
let profileRepositoryFixture: ProfileRepositoryFixture;
|
||||
let attributeRepositoryFixture: AttributeRepositoryFixture;
|
||||
|
||||
const authEmail = `organizations-users-auth-${randomString()}@api.com`;
|
||||
const user2Email = `organizations-users2-auth-${randomString()}@api.com`;
|
||||
|
||||
let user: User;
|
||||
let user2: User;
|
||||
|
||||
let org: Team;
|
||||
let team: Team;
|
||||
let assignedOption1: AttributeOption;
|
||||
let assignedOption2: AttributeOption;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withApiAuth(
|
||||
authEmail,
|
||||
Test.createTestingModule({
|
||||
imports: [AppModule, PrismaModule, UsersModule, TokensModule],
|
||||
})
|
||||
).compile();
|
||||
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
|
||||
teamsRepositoryFixtures = new TeamRepositoryFixture(moduleRef);
|
||||
|
||||
membershipFixtures = new MembershipRepositoryFixture(moduleRef);
|
||||
profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
|
||||
|
||||
org = await organizationsRepositoryFixture.create({
|
||||
name: `organizations-users-organization-${randomString()}`,
|
||||
isOrganization: true,
|
||||
});
|
||||
|
||||
team = await teamsRepositoryFixtures.create({ name: "org team", parent: { connect: { id: org.id } } });
|
||||
|
||||
user = await userRepositoryFixture.create({
|
||||
email: authEmail,
|
||||
username: authEmail,
|
||||
organization: { connect: { id: org.id } },
|
||||
});
|
||||
|
||||
await profileRepositoryFixture.create({
|
||||
uid: `usr-${user.id}`,
|
||||
username: authEmail,
|
||||
organization: {
|
||||
connect: {
|
||||
id: org.id,
|
||||
},
|
||||
},
|
||||
user: {
|
||||
connect: {
|
||||
id: user.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const membership = await membershipFixtures.addUserToOrg(user, org, "ADMIN", true);
|
||||
|
||||
await membershipFixtures.create({
|
||||
role: "MEMBER",
|
||||
accepted: true,
|
||||
team: { connect: { id: team.id } },
|
||||
user: { connect: { id: user.id } },
|
||||
});
|
||||
|
||||
user2 = await userRepositoryFixture.create({
|
||||
email: user2Email,
|
||||
username: user2Email,
|
||||
organization: { connect: { id: org.id } },
|
||||
});
|
||||
|
||||
await profileRepositoryFixture.create({
|
||||
uid: `usr-${user2.id}`,
|
||||
username: user2Email,
|
||||
organization: {
|
||||
connect: {
|
||||
id: org.id,
|
||||
},
|
||||
},
|
||||
user: {
|
||||
connect: {
|
||||
id: user2.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const membership2 = await membershipFixtures.addUserToOrg(user2, org, "ADMIN", true);
|
||||
|
||||
attributeRepositoryFixture = new AttributeRepositoryFixture(moduleRef);
|
||||
const attribute = await attributeRepositoryFixture.create({
|
||||
name: "Test Attribute",
|
||||
team: { connect: { id: org.id } },
|
||||
type: "TEXT",
|
||||
slug: `test-attribute-${randomString()}`,
|
||||
});
|
||||
|
||||
const attribute2 = await attributeRepositoryFixture.create({
|
||||
name: "Test Attribute 2",
|
||||
team: { connect: { id: org.id } },
|
||||
type: "TEXT",
|
||||
slug: `test-attribute-2-${randomString()}`,
|
||||
});
|
||||
const attributeId = attribute.id;
|
||||
|
||||
assignedOption1 = await attributeRepositoryFixture.createOption({
|
||||
slug: "option1",
|
||||
value: "option1",
|
||||
attribute: { connect: { id: attribute.id } },
|
||||
assignedUsers: {
|
||||
create: {
|
||||
memberId: membership.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
assignedOption2 = await attributeRepositoryFixture.createOption({
|
||||
slug: "optionA",
|
||||
value: "optionA",
|
||||
attribute: { connect: { id: attribute2.id } },
|
||||
assignedUsers: { createMany: { data: [{ memberId: membership.id }, { memberId: membership2.id }] } },
|
||||
});
|
||||
|
||||
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 get users with all specified assigned attribute options", async () => {
|
||||
const { body } = await request(app.getHttpServer())
|
||||
.get(`/v2/organizations/${org.id}/users`)
|
||||
.query({
|
||||
assignedOptionIds: [assignedOption1.id],
|
||||
attributeQueryOperator: "AND",
|
||||
} as GetOrganizationsUsersInput)
|
||||
.set("Content-Type", "application/json")
|
||||
.set("Accept", "application/json");
|
||||
|
||||
const userData = body.data;
|
||||
|
||||
expect(body.status).toBe(SUCCESS_STATUS);
|
||||
expect(userData.length).toBe(1);
|
||||
|
||||
const userWithAssignedOptions = userData.find(
|
||||
(u: GetOrgUsersWithProfileOutput) => u.email === user.email
|
||||
);
|
||||
expect(userWithAssignedOptions).toBeDefined();
|
||||
expect(userWithAssignedOptions?.email).toBe(user.email);
|
||||
expect(userWithAssignedOptions?.profile.username).toBe(user.username);
|
||||
});
|
||||
|
||||
it("should get users with at least one of the specified assigned attribute options", async () => {
|
||||
const { body } = await request(app.getHttpServer())
|
||||
.get(`/v2/organizations/${org.id}/users`)
|
||||
.query({
|
||||
assignedOptionIds: [assignedOption1.id, assignedOption2.id],
|
||||
attributeQueryOperator: "OR",
|
||||
} as GetOrganizationsUsersInput)
|
||||
.set("Content-Type", "application/json")
|
||||
.set("Accept", "application/json");
|
||||
|
||||
const userData = body.data;
|
||||
|
||||
expect(body.status).toBe(SUCCESS_STATUS);
|
||||
expect(userData.length).toBe(2);
|
||||
|
||||
const userWithAssignedOptions = userData.find(
|
||||
(u: GetOrgUsersWithProfileOutput) => u.email === user.email
|
||||
);
|
||||
expect(userWithAssignedOptions).toBeDefined();
|
||||
expect(userWithAssignedOptions?.email).toBe(user.email);
|
||||
expect(userWithAssignedOptions?.profile.username).toBe(user.username);
|
||||
|
||||
const userWithAssignedOptions2 = userData.find(
|
||||
(u: GetOrgUsersWithProfileOutput) => u.email === user2.email
|
||||
);
|
||||
expect(userWithAssignedOptions2).toBeDefined();
|
||||
expect(userWithAssignedOptions2?.email).toBe(user2.email);
|
||||
expect(userWithAssignedOptions2?.profile.username).toBe(user2.username);
|
||||
});
|
||||
|
||||
it("should get users with at least one of the specified assigned attribute options filtered by teams", async () => {
|
||||
const { body } = await request(app.getHttpServer())
|
||||
.get(`/v2/organizations/${org.id}/users`)
|
||||
.query({
|
||||
assignedOptionIds: [assignedOption1.id, assignedOption2.id],
|
||||
attributeQueryOperator: "OR",
|
||||
teamIds: [team.id],
|
||||
} as GetOrganizationsUsersInput)
|
||||
.set("Content-Type", "application/json")
|
||||
.set("Accept", "application/json");
|
||||
|
||||
const userData = body.data;
|
||||
|
||||
expect(body.status).toBe(SUCCESS_STATUS);
|
||||
expect(userData.length).toBe(1);
|
||||
|
||||
const userWithAssignedOptions = userData.find(
|
||||
(u: GetOrgUsersWithProfileOutput) => u.email === user.email
|
||||
);
|
||||
expect(userWithAssignedOptions).toBeDefined();
|
||||
expect(userWithAssignedOptions?.email).toBe(user.email);
|
||||
expect(userWithAssignedOptions?.profile.username).toBe(user.username);
|
||||
});
|
||||
|
||||
it("should get users with none of the specified assigned attribute options", async () => {
|
||||
const { body } = await request(app.getHttpServer())
|
||||
.get(`/v2/organizations/${org.id}/users`)
|
||||
.query({
|
||||
assignedOptionIds: [assignedOption1.id, assignedOption2.id],
|
||||
attributeQueryOperator: "NONE",
|
||||
} as GetOrganizationsUsersInput)
|
||||
.set("Content-Type", "application/json")
|
||||
.set("Accept", "application/json");
|
||||
|
||||
const userData = body.data;
|
||||
|
||||
expect(body.status).toBe(SUCCESS_STATUS);
|
||||
expect(userData.length).toBe(0);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// await membershipFixtures.delete(membership.id);
|
||||
await userRepositoryFixture.deleteByEmail(user.email);
|
||||
await userRepositoryFixture.deleteByEmail(user2.email);
|
||||
await organizationsRepositoryFixture.delete(org.id);
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
|
||||
+50
-1
@@ -1,3 +1,52 @@
|
||||
import { GetUsersInput } from "@/modules/users/inputs/get-users.input";
|
||||
import { ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { Transform } from "class-transformer";
|
||||
import { Expose } from "class-transformer";
|
||||
import { IsOptional, IsArray, ArrayMinSize, IsString, IsIn, IsNumber } from "class-validator";
|
||||
|
||||
export class GetOrganizationsUsersInput extends GetUsersInput {}
|
||||
export class GetOrganizationsUsersInput extends GetUsersInput {
|
||||
@Expose()
|
||||
@IsOptional()
|
||||
@Transform(({ value }) => {
|
||||
if (typeof value === "string") {
|
||||
return value.split(",").map((optId: string) => optId);
|
||||
}
|
||||
return value;
|
||||
})
|
||||
@ApiPropertyOptional({
|
||||
type: [String],
|
||||
description: "Filter by assigned attribute option ids. ids must be separated by a comma.",
|
||||
example: "?assignedOptionIds=aaaaaaaa-bbbb-cccc-dddd-eeeeee1eee,aaaaaaaa-bbbb-cccc-dddd-eeeeee2eee",
|
||||
})
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
@ArrayMinSize(1, { message: "assignedOptionIds must contain at least 1 attribute option id" })
|
||||
assignedOptionIds?: string[];
|
||||
|
||||
@ApiPropertyOptional({
|
||||
type: String,
|
||||
description: "Query operator used to filter assigned options, AND by default.",
|
||||
example: "NONE",
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsIn(["OR", "AND", "NONE"])
|
||||
attributeQueryOperator: "AND" | "OR" | "NONE" = "AND"; // Default value
|
||||
|
||||
@IsOptional()
|
||||
@Transform(({ value }) => {
|
||||
if (typeof value === "string") {
|
||||
return value.split(",").map((teamId: string) => parseInt(teamId));
|
||||
}
|
||||
return value;
|
||||
})
|
||||
@ApiPropertyOptional({
|
||||
type: [Number],
|
||||
description: "Filter by teamIds. Team ids must be separated by a comma.",
|
||||
example: "?teamIds=100,200",
|
||||
})
|
||||
@IsArray()
|
||||
@IsNumber({}, { each: true })
|
||||
@ArrayMinSize(1, { message: "teamIds must contain at least 1 team id" })
|
||||
teamIds?: number[];
|
||||
}
|
||||
+54
-1
@@ -17,11 +17,64 @@ export class OrganizationsUsersRepository {
|
||||
};
|
||||
}
|
||||
|
||||
async getOrganizationUsersByEmails(orgId: number, emailArray?: string[], skip?: number, take?: number) {
|
||||
async getOrganizationUsersByEmailsAndAttributeFilters(
|
||||
orgId: number,
|
||||
filters: {
|
||||
teamIds?: number[];
|
||||
assignedOptionIds: string[];
|
||||
attributeQueryOperator?: "AND" | "OR" | "NONE";
|
||||
},
|
||||
emailArray?: string[],
|
||||
skip?: number,
|
||||
take?: number
|
||||
) {
|
||||
const { teamIds, assignedOptionIds, attributeQueryOperator } = filters ?? {};
|
||||
const attributeToUsersWithProfile = await this.dbRead.prisma.attributeToUser.findMany({
|
||||
include: {
|
||||
member: { include: { user: { include: { profiles: { where: { organizationId: orgId } } } } } },
|
||||
},
|
||||
distinct: ["memberId"],
|
||||
where: {
|
||||
member: {
|
||||
teamId: orgId,
|
||||
...(teamIds && { user: { teams: { some: { teamId: { in: teamIds } } } } }),
|
||||
// Filter to only get users which have ALL of the assigned attribue options
|
||||
...(attributeQueryOperator === "AND" && {
|
||||
AND: assignedOptionIds.map((optionId) => ({
|
||||
AttributeToUser: { some: { attributeOptionId: optionId } },
|
||||
})),
|
||||
}),
|
||||
},
|
||||
...(emailArray && emailArray.length ? { email: { in: emailArray } } : {}),
|
||||
// Filter to get users which have AT LEAST ONE of the assigned attribue options
|
||||
...(attributeQueryOperator === "OR" && {
|
||||
attributeOption: { id: { in: assignedOptionIds } },
|
||||
}),
|
||||
// Filter to get users that have NONE the assigned attribue options
|
||||
...(attributeQueryOperator === "NONE" && {
|
||||
NOT: {
|
||||
attributeOption: { id: { in: assignedOptionIds } },
|
||||
},
|
||||
}),
|
||||
},
|
||||
skip,
|
||||
take,
|
||||
});
|
||||
return attributeToUsersWithProfile.map((attributeToUser) => attributeToUser.member.user);
|
||||
}
|
||||
|
||||
async getOrganizationUsersByEmails(
|
||||
orgId: number,
|
||||
emailArray?: string[],
|
||||
teamIds?: number[],
|
||||
skip?: number,
|
||||
take?: number
|
||||
) {
|
||||
return await this.dbRead.prisma.user.findMany({
|
||||
where: {
|
||||
...this.filterOnOrgMembership(orgId),
|
||||
...(emailArray && emailArray.length ? { email: { in: emailArray } } : {}),
|
||||
...(teamIds && { teams: { some: { teamId: { in: teamIds } } } }),
|
||||
},
|
||||
include: {
|
||||
profiles: {
|
||||
|
||||
+26
-1
@@ -16,12 +16,37 @@ export class OrganizationsUsersService {
|
||||
private readonly emailService: EmailService
|
||||
) {}
|
||||
|
||||
async getUsers(orgId: number, emailInput?: string[], skip?: number, take?: number) {
|
||||
async getUsers(
|
||||
orgId: number,
|
||||
emailInput?: string[],
|
||||
filters?: {
|
||||
teamIds?: number[];
|
||||
assignedOptionIds?: string[];
|
||||
attributeQueryOperator?: "AND" | "OR" | "NONE";
|
||||
},
|
||||
skip?: number,
|
||||
take?: number
|
||||
) {
|
||||
const emailArray = !emailInput ? [] : emailInput;
|
||||
|
||||
if (filters?.assignedOptionIds && filters?.assignedOptionIds?.length) {
|
||||
return await this.organizationsUsersRepository.getOrganizationUsersByEmailsAndAttributeFilters(
|
||||
orgId,
|
||||
{
|
||||
assignedOptionIds: filters.assignedOptionIds,
|
||||
attributeQueryOperator: filters?.attributeQueryOperator ?? "AND",
|
||||
teamIds: filters?.teamIds,
|
||||
},
|
||||
emailArray,
|
||||
skip,
|
||||
take
|
||||
);
|
||||
}
|
||||
|
||||
const users = await this.organizationsUsersRepository.getOrganizationUsersByEmails(
|
||||
orgId,
|
||||
emailArray,
|
||||
filters?.teamIds,
|
||||
skip,
|
||||
take
|
||||
);
|
||||
|
||||
@@ -11,7 +11,6 @@ export class OrgUsersOOORepository {
|
||||
sort?: { sortStart?: "asc" | "desc"; sortEnd?: "asc" | "desc" },
|
||||
filters?: { email?: string }
|
||||
) {
|
||||
console.log({ sort, filters });
|
||||
return this.dbRead.prisma.outOfOfficeEntry.findMany({
|
||||
where: {
|
||||
user: {
|
||||
|
||||
@@ -1110,6 +1110,190 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v2/organizations/{orgId}/attributes/{attributeId}/options/assigned": {
|
||||
"get": {
|
||||
"operationId": "OrganizationsAttributesOptionsController_getOrganizationAttributeAssignedOptions",
|
||||
"summary": "Get by attribute id all of the attribute options that are assigned to users",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "Authorization",
|
||||
"in": "header",
|
||||
"description": "value must be `Bearer <token>` where `<token>` is api key prefixed with cal_",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "orgId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "attributeId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "skip",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Number of responses to skip",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "take",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Number of responses to take",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "assignedOptionIds",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Filter by assigned attribute option ids. ids must be separated by a comma.",
|
||||
"example": "?assignedOptionIds=aaaaaaaa-bbbb-cccc-dddd-eeeeee1eee,aaaaaaaa-bbbb-cccc-dddd-eeeeee2eee",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "teamIds",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Filter by teamIds. Team ids must be separated by a comma.",
|
||||
"example": "?teamIds=100,200",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/GetAllAttributeAssignedOptionOutput"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Orgs / Attributes / Options"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v2/organizations/{orgId}/attributes/slugs/{attributeSlug}/options/assigned": {
|
||||
"get": {
|
||||
"operationId": "OrganizationsAttributesOptionsController_getOrganizationAttributeAssignedOptionsBySlug",
|
||||
"summary": "Get by attribute slug all of the attribute options that are assigned to users",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "Authorization",
|
||||
"in": "header",
|
||||
"description": "value must be `Bearer <token>` where `<token>` is api key prefixed with cal_",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "orgId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "attributeSlug",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "skip",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Number of responses to skip",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "take",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Number of responses to take",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "assignedOptionIds",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Filter by assigned attribute option ids. ids must be separated by a comma.",
|
||||
"example": "?assignedOptionIds=aaaaaaaa-bbbb-cccc-dddd-eeeeee1eee,aaaaaaaa-bbbb-cccc-dddd-eeeeee2eee",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "teamIds",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Filter by teamIds. Team ids must be separated by a comma.",
|
||||
"example": "?teamIds=100,200",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/GetAllAttributeAssignedOptionOutput"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Orgs / Attributes / Options"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v2/organizations/{orgId}/attributes/options/{userId}": {
|
||||
"post": {
|
||||
"operationId": "OrganizationsAttributesOptionsController_assignOrganizationAttributeOptionToUser",
|
||||
@@ -4777,6 +4961,48 @@
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "assignedOptionIds",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Filter by assigned attribute option ids. ids must be separated by a comma.",
|
||||
"example": "?assignedOptionIds=aaaaaaaa-bbbb-cccc-dddd-eeeeee1eee,aaaaaaaa-bbbb-cccc-dddd-eeeeee2eee",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "attributeQueryOperator",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Query operator used to filter assigned options, AND by default.",
|
||||
"example": "NONE",
|
||||
"schema": {
|
||||
"default": "AND",
|
||||
"enum": [
|
||||
"OR",
|
||||
"AND",
|
||||
"NONE"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "teamIds",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Filter by teamIds. Team ids must be separated by a comma.",
|
||||
"example": "?teamIds=100,200",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
@@ -18416,6 +18642,72 @@
|
||||
"data"
|
||||
]
|
||||
},
|
||||
"AssignedOptionOutput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "The ID of the option",
|
||||
"example": "attr_option_id"
|
||||
},
|
||||
"attributeId": {
|
||||
"type": "string",
|
||||
"description": "The ID of the attribute",
|
||||
"example": "attr_id"
|
||||
},
|
||||
"value": {
|
||||
"type": "string",
|
||||
"description": "The value of the option",
|
||||
"example": "option_value"
|
||||
},
|
||||
"slug": {
|
||||
"type": "string",
|
||||
"description": "The slug of the option",
|
||||
"example": "option-slug"
|
||||
},
|
||||
"assignedUserIds": {
|
||||
"description": "Ids of the users assigned to the attribute option.",
|
||||
"example": [
|
||||
124,
|
||||
224
|
||||
],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"attributeId",
|
||||
"value",
|
||||
"slug",
|
||||
"assignedUserIds"
|
||||
]
|
||||
},
|
||||
"GetAllAttributeAssignedOptionOutput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "success",
|
||||
"enum": [
|
||||
"success",
|
||||
"error"
|
||||
]
|
||||
},
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/AssignedOptionOutput"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status",
|
||||
"data"
|
||||
]
|
||||
},
|
||||
"AssignOrganizationAttributeOptionToUserInput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -1066,6 +1066,186 @@
|
||||
"tags": ["Orgs / Attributes / Options"]
|
||||
}
|
||||
},
|
||||
"/v2/organizations/{orgId}/attributes/{attributeId}/options/assigned": {
|
||||
"get": {
|
||||
"operationId": "OrganizationsAttributesOptionsController_getOrganizationAttributeAssignedOptions",
|
||||
"summary": "Get by attribute id all of the attribute options that are assigned to users",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "Authorization",
|
||||
"in": "header",
|
||||
"description": "value must be `Bearer <token>` where `<token>` is api key prefixed with cal_",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "orgId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "attributeId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "skip",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Number of responses to skip",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "take",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Number of responses to take",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "assignedOptionIds",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Filter by assigned attribute option ids. ids must be separated by a comma.",
|
||||
"example": "?assignedOptionIds=aaaaaaaa-bbbb-cccc-dddd-eeeeee1eee,aaaaaaaa-bbbb-cccc-dddd-eeeeee2eee",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "teamIds",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Filter by teamIds. Team ids must be separated by a comma.",
|
||||
"example": "?teamIds=100,200",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/GetAllAttributeAssignedOptionOutput"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": ["Orgs / Attributes / Options"]
|
||||
}
|
||||
},
|
||||
"/v2/organizations/{orgId}/attributes/slugs/{attributeSlug}/options/assigned": {
|
||||
"get": {
|
||||
"operationId": "OrganizationsAttributesOptionsController_getOrganizationAttributeAssignedOptionsBySlug",
|
||||
"summary": "Get by attribute slug all of the attribute options that are assigned to users",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "Authorization",
|
||||
"in": "header",
|
||||
"description": "value must be `Bearer <token>` where `<token>` is api key prefixed with cal_",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "orgId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "attributeSlug",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "skip",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Number of responses to skip",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "take",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Number of responses to take",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "assignedOptionIds",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Filter by assigned attribute option ids. ids must be separated by a comma.",
|
||||
"example": "?assignedOptionIds=aaaaaaaa-bbbb-cccc-dddd-eeeeee1eee,aaaaaaaa-bbbb-cccc-dddd-eeeeee2eee",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "teamIds",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Filter by teamIds. Team ids must be separated by a comma.",
|
||||
"example": "?teamIds=100,200",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/GetAllAttributeAssignedOptionOutput"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": ["Orgs / Attributes / Options"]
|
||||
}
|
||||
},
|
||||
"/v2/organizations/{orgId}/attributes/options/{userId}": {
|
||||
"post": {
|
||||
"operationId": "OrganizationsAttributesOptionsController_assignOrganizationAttributeOptionToUser",
|
||||
@@ -4569,6 +4749,44 @@
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "assignedOptionIds",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Filter by assigned attribute option ids. ids must be separated by a comma.",
|
||||
"example": "?assignedOptionIds=aaaaaaaa-bbbb-cccc-dddd-eeeeee1eee,aaaaaaaa-bbbb-cccc-dddd-eeeeee2eee",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "attributeQueryOperator",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Query operator used to filter assigned options, AND by default.",
|
||||
"example": "NONE",
|
||||
"schema": {
|
||||
"default": "AND",
|
||||
"enum": ["OR", "AND", "NONE"],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "teamIds",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Filter by teamIds. Team ids must be separated by a comma.",
|
||||
"example": "?teamIds=100,200",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
@@ -16927,6 +17145,57 @@
|
||||
},
|
||||
"required": ["status", "data"]
|
||||
},
|
||||
"AssignedOptionOutput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "The ID of the option",
|
||||
"example": "attr_option_id"
|
||||
},
|
||||
"attributeId": {
|
||||
"type": "string",
|
||||
"description": "The ID of the attribute",
|
||||
"example": "attr_id"
|
||||
},
|
||||
"value": {
|
||||
"type": "string",
|
||||
"description": "The value of the option",
|
||||
"example": "option_value"
|
||||
},
|
||||
"slug": {
|
||||
"type": "string",
|
||||
"description": "The slug of the option",
|
||||
"example": "option-slug"
|
||||
},
|
||||
"assignedUserIds": {
|
||||
"description": "Ids of the users assigned to the attribute option.",
|
||||
"example": [124, 224],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["id", "attributeId", "value", "slug", "assignedUserIds"]
|
||||
},
|
||||
"GetAllAttributeAssignedOptionOutput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "success",
|
||||
"enum": ["success", "error"]
|
||||
},
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/AssignedOptionOutput"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["status", "data"]
|
||||
},
|
||||
"AssignOrganizationAttributeOptionToUserInput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
Reference in new issue
Block a user