Skip to content

Commit dddbd7d

Browse files
authored
feat: v2 membership attributes (calcom#21131)
* refactor: getByUserId.handler.ts * feat: return org membership attributes * docs * export new library function * test: attributes * chore: bump libraries * refactor: value -> option
1 parent ae2d85d commit dddbd7d

20 files changed

Lines changed: 4258 additions & 136 deletions

apps/api/v2/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
"@axiomhq/winston": "^1.2.0",
3939
"@calcom/platform-constants": "*",
4040
"@calcom/platform-enums": "*",
41-
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.196",
41+
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.197",
4242
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
4343
"@calcom/platform-types": "*",
4444
"@calcom/platform-utils": "*",

apps/api/v2/src/modules/organizations/memberships/organizations-membership.controller.e2e-spec.ts

Lines changed: 196 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { CreateOrgMembershipOutput } from "@/modules/organizations/memberships/o
66
import { DeleteOrgMembership } from "@/modules/organizations/memberships/outputs/delete-membership.output";
77
import { GetAllOrgMemberships } from "@/modules/organizations/memberships/outputs/get-all-memberships.output";
88
import { GetOrgMembership } from "@/modules/organizations/memberships/outputs/get-membership.output";
9+
import { OrgUserAttribute } from "@/modules/organizations/memberships/outputs/organization-membership.output";
910
import { UpdateOrgMembership } from "@/modules/organizations/memberships/outputs/update-membership.output";
1011
import { PrismaModule } from "@/modules/prisma/prisma.module";
1112
import { TeamMembershipOutput } from "@/modules/teams/memberships/outputs/team-membership.output";
@@ -14,17 +15,16 @@ import { UsersModule } from "@/modules/users/users.module";
1415
import { INestApplication } from "@nestjs/common";
1516
import { NestExpressApplication } from "@nestjs/platform-express";
1617
import { Test } from "@nestjs/testing";
17-
import { User } from "@prisma/client";
18+
import { Attribute, AttributeOption, User } from "@prisma/client";
1819
import * as request from "supertest";
20+
import { AttributeRepositoryFixture } from "test/fixtures/repository/attributes.repository.fixture";
1921
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
2022
import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture";
21-
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
2223
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
2324
import { randomString } from "test/utils/randomString";
2425
import { withApiAuth } from "test/utils/withApiAuth";
2526

2627
import { SUCCESS_STATUS } from "@calcom/platform-constants";
27-
import { ApiSuccessResponse } from "@calcom/platform-types";
2828
import { Membership, Team } from "@calcom/prisma/client";
2929

3030
describe("Organizations Memberships Endpoints", () => {
@@ -33,7 +33,8 @@ describe("Organizations Memberships Endpoints", () => {
3333

3434
let userRepositoryFixture: UserRepositoryFixture;
3535
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
36-
let membershipsRepositoryFixture: MembershipRepositoryFixture;
36+
let membershipRepositoryFixture: MembershipRepositoryFixture;
37+
let attributesRepositoryFixture: AttributeRepositoryFixture;
3738

3839
let org: Team;
3940
let membership: Membership;
@@ -49,6 +50,17 @@ describe("Organizations Memberships Endpoints", () => {
4950

5051
let userToInviteViaApi: User;
5152

53+
let textAttribute: Attribute;
54+
let multiSelectAttribute: Attribute;
55+
let numberAttribute: Attribute;
56+
let singleSelectAttribute: Attribute;
57+
58+
let textAttributeOption: AttributeOption;
59+
let multiSelectAttributeOption: AttributeOption;
60+
let multiSelectAttributeOption2: AttributeOption;
61+
let numberAttributeOption: AttributeOption;
62+
let singleSelectAttributeOption: AttributeOption;
63+
5264
const metadata = {
5365
some: "key",
5466
};
@@ -64,7 +76,8 @@ describe("Organizations Memberships Endpoints", () => {
6476

6577
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
6678
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
67-
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
79+
membershipRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
80+
attributesRepositoryFixture = new AttributeRepositoryFixture(moduleRef);
6881

6982
user = await userRepositoryFixture.create({
7083
email: userEmail,
@@ -91,16 +104,42 @@ describe("Organizations Memberships Endpoints", () => {
91104
isOrganization: true,
92105
});
93106

94-
membership = await membershipsRepositoryFixture.create({
107+
await setupAttributes();
108+
109+
membership = await membershipRepositoryFixture.create({
95110
role: "ADMIN",
96111
user: { connect: { id: user.id } },
97112
team: { connect: { id: org.id } },
98113
});
99114

100-
membership2 = await membershipsRepositoryFixture.create({
115+
membership2 = await membershipRepositoryFixture.create({
101116
role: "MEMBER",
102117
user: { connect: { id: user2.id } },
103118
team: { connect: { id: org.id } },
119+
AttributeToUser: {
120+
create: [
121+
{
122+
attributeOption: { connect: { id: textAttributeOption.id } },
123+
weight: 100,
124+
},
125+
{
126+
attributeOption: { connect: { id: multiSelectAttributeOption.id } },
127+
weight: 100,
128+
},
129+
{
130+
attributeOption: { connect: { id: multiSelectAttributeOption2.id } },
131+
weight: null,
132+
},
133+
{
134+
attributeOption: { connect: { id: numberAttributeOption.id } },
135+
weight: 100,
136+
},
137+
{
138+
attributeOption: { connect: { id: singleSelectAttributeOption.id } },
139+
weight: 100,
140+
},
141+
],
142+
},
104143
});
105144

106145
app = moduleRef.createNestApplication();
@@ -109,6 +148,87 @@ describe("Organizations Memberships Endpoints", () => {
109148
await app.init();
110149
});
111150

151+
async function setupAttributes() {
152+
textAttribute = await attributesRepositoryFixture.create({
153+
team: { connect: { id: org.id } },
154+
type: "TEXT",
155+
name: "team",
156+
slug: `team-${randomString()}`,
157+
enabled: true,
158+
usersCanEditRelation: false,
159+
isWeightsEnabled: false,
160+
isLocked: false,
161+
});
162+
163+
multiSelectAttribute = await attributesRepositoryFixture.create({
164+
team: { connect: { id: org.id } },
165+
type: "MULTI_SELECT",
166+
name: "skills",
167+
slug: `skills-${randomString()}`,
168+
enabled: true,
169+
usersCanEditRelation: false,
170+
isWeightsEnabled: false,
171+
isLocked: false,
172+
});
173+
174+
numberAttribute = await attributesRepositoryFixture.create({
175+
team: { connect: { id: org.id } },
176+
type: "NUMBER",
177+
name: "age",
178+
slug: `age-${randomString()}`,
179+
enabled: true,
180+
usersCanEditRelation: false,
181+
isWeightsEnabled: false,
182+
isLocked: false,
183+
});
184+
185+
singleSelectAttribute = await attributesRepositoryFixture.create({
186+
team: { connect: { id: org.id } },
187+
type: "SINGLE_SELECT",
188+
name: "frontend",
189+
slug: `frontend-${randomString()}`,
190+
enabled: true,
191+
usersCanEditRelation: false,
192+
isWeightsEnabled: false,
193+
isLocked: false,
194+
});
195+
196+
textAttributeOption = await attributesRepositoryFixture.createOption({
197+
attribute: { connect: { id: textAttribute.id } },
198+
value: "coders",
199+
slug: "coders",
200+
isGroup: false,
201+
});
202+
203+
multiSelectAttributeOption = await attributesRepositoryFixture.createOption({
204+
attribute: { connect: { id: multiSelectAttribute.id } },
205+
value: "javascript",
206+
slug: "javascript",
207+
isGroup: false,
208+
});
209+
210+
multiSelectAttributeOption2 = await attributesRepositoryFixture.createOption({
211+
attribute: { connect: { id: multiSelectAttribute.id } },
212+
value: "typescript",
213+
slug: "typescript",
214+
isGroup: false,
215+
});
216+
217+
numberAttributeOption = await attributesRepositoryFixture.createOption({
218+
attribute: { connect: { id: numberAttribute.id } },
219+
value: "18",
220+
slug: "18",
221+
isGroup: false,
222+
});
223+
224+
singleSelectAttributeOption = await attributesRepositoryFixture.createOption({
225+
attribute: { connect: { id: singleSelectAttribute.id } },
226+
value: "yes",
227+
slug: "yes",
228+
isGroup: false,
229+
});
230+
}
231+
112232
it("should be defined", () => {
113233
expect(userRepositoryFixture).toBeDefined();
114234
expect(organizationsRepositoryFixture).toBeDefined();
@@ -139,9 +259,54 @@ describe("Organizations Memberships Endpoints", () => {
139259
expect(responseBody.data[1].user.email).toEqual(user2.email);
140260
expect(responseBody.data[1].user.username).toEqual(user2.username);
141261
expect(responseBody.data[1].teamId).toEqual(org.id);
262+
userHasCorrectAttributes(responseBody.data[1].attributes);
142263
});
143264
});
144265

266+
function userHasCorrectAttributes(attributes: OrgUserAttribute[]) {
267+
expect(attributes.length).toEqual(4);
268+
const responseNumberAttribute = attributes.find((attr) => attr.type === "number");
269+
const responseSingleSelectAttribute = attributes.find((attr) => attr.type === "singleSelect");
270+
const responseMultiSelectAttribute = attributes.find((attr) => attr.type === "multiSelect");
271+
const responseTextAttribute = attributes.find((attr) => attr.type === "text");
272+
expect(responseNumberAttribute).toEqual({
273+
id: numberAttribute.id,
274+
name: numberAttribute.name,
275+
optionId: numberAttributeOption.id,
276+
option: +numberAttributeOption.value,
277+
type: "number",
278+
});
279+
expect(responseSingleSelectAttribute).toEqual({
280+
id: singleSelectAttribute.id,
281+
name: singleSelectAttribute.name,
282+
optionId: singleSelectAttributeOption.id,
283+
option: singleSelectAttributeOption.value,
284+
type: "singleSelect",
285+
});
286+
expect(responseMultiSelectAttribute).toEqual({
287+
id: multiSelectAttribute.id,
288+
name: multiSelectAttribute.name,
289+
options: [
290+
{
291+
optionId: multiSelectAttributeOption2.id,
292+
option: multiSelectAttributeOption2.value,
293+
},
294+
{
295+
optionId: multiSelectAttributeOption.id,
296+
option: multiSelectAttributeOption.value,
297+
},
298+
],
299+
type: "multiSelect",
300+
});
301+
expect(responseTextAttribute).toEqual({
302+
id: textAttribute.id,
303+
name: textAttribute.name,
304+
optionId: textAttributeOption.id,
305+
option: textAttributeOption.value,
306+
type: "text",
307+
});
308+
}
309+
145310
it("should get all the memberships of the org paginated", async () => {
146311
return request(app.getHttpServer())
147312
.get(`/v2/organizations/${org.id}/memberships?skip=1&take=1`)
@@ -182,6 +347,25 @@ describe("Organizations Memberships Endpoints", () => {
182347
});
183348
});
184349

350+
it("should get the membership of the org", async () => {
351+
return request(app.getHttpServer())
352+
.get(`/v2/organizations/${org.id}/memberships/${membership2.id}`)
353+
.expect(200)
354+
.then((response) => {
355+
const responseBody: GetOrgMembership = response.body;
356+
expect(responseBody.status).toEqual(SUCCESS_STATUS);
357+
expect(responseBody.data.id).toEqual(membership2.id);
358+
expect(responseBody.data.userId).toEqual(user2.id);
359+
expect(responseBody.data.role).toEqual("MEMBER");
360+
expect(responseBody.data.user.bio).toEqual(bio);
361+
expect(responseBody.data.user.metadata).toEqual(metadata);
362+
expect(responseBody.data.user.email).toEqual(user2.email);
363+
expect(responseBody.data.user.username).toEqual(user2.username);
364+
expect(responseBody.data.teamId).toEqual(org.id);
365+
userHasCorrectAttributes(responseBody.data.attributes);
366+
});
367+
});
368+
185369
it("should create the membership of the org", async () => {
186370
return request(app.getHttpServer())
187371
.post(`/v2/organizations/${org.id}/memberships`)
@@ -259,7 +443,8 @@ describe("Organizations Memberships Endpoints", () => {
259443

260444
let userRepositoryFixture: UserRepositoryFixture;
261445
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
262-
let membershipsRepositoryFixture: MembershipRepositoryFixture;
446+
let membershipRepositoryFixture: MembershipRepositoryFixture;
447+
let attributesRepositoryFixture: AttributeRepositoryFixture;
263448

264449
let org: Team;
265450
let membership: Membership;
@@ -277,7 +462,8 @@ describe("Organizations Memberships Endpoints", () => {
277462

278463
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
279464
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
280-
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
465+
membershipRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
466+
attributesRepositoryFixture = new AttributeRepositoryFixture(moduleRef);
281467

282468
user = await userRepositoryFixture.create({
283469
email: userEmail,
@@ -289,7 +475,7 @@ describe("Organizations Memberships Endpoints", () => {
289475
isOrganization: true,
290476
});
291477

292-
membership = await membershipsRepositoryFixture.create({
478+
membership = await membershipRepositoryFixture.create({
293479
role: "MEMBER",
294480
user: { connect: { id: user.id } },
295481
team: { connect: { id: org.id } },

apps/api/v2/src/modules/organizations/memberships/organizations-membership.controller.ts

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import { GetAllOrgMemberships } from "@/modules/organizations/memberships/output
2020
import { GetOrgMembership } from "@/modules/organizations/memberships/outputs/get-membership.output";
2121
import { UpdateOrgMembership } from "@/modules/organizations/memberships/outputs/update-membership.output";
2222
import { OrganizationsMembershipService } from "@/modules/organizations/memberships/services/organizations-membership.service";
23-
import { TeamMembershipOutput } from "@/modules/teams/memberships/outputs/team-membership.output";
2423
import {
2524
Controller,
2625
UseGuards,
@@ -36,7 +35,6 @@ import {
3635
HttpStatus,
3736
} from "@nestjs/common";
3837
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
39-
import { plainToClass } from "class-transformer";
4038

4139
import { SUCCESS_STATUS } from "@calcom/platform-constants";
4240
import { SkipTakePagination } from "@calcom/platform-types";
@@ -70,9 +68,7 @@ export class OrganizationsMembershipsController {
7068
);
7169
return {
7270
status: SUCCESS_STATUS,
73-
data: memberships.map((membership) =>
74-
plainToClass(TeamMembershipOutput, membership, { strategy: "excludeAll" })
75-
),
71+
data: memberships,
7672
};
7773
}
7874

@@ -88,7 +84,7 @@ export class OrganizationsMembershipsController {
8884
const membership = await this.organizationsMembershipService.createOrgMembership(orgId, body);
8985
return {
9086
status: SUCCESS_STATUS,
91-
data: plainToClass(TeamMembershipOutput, membership, { strategy: "excludeAll" }),
87+
data: membership,
9288
};
9389
}
9490

@@ -105,7 +101,7 @@ export class OrganizationsMembershipsController {
105101
const membership = await this.organizationsMembershipService.getOrgMembership(orgId, membershipId);
106102
return {
107103
status: SUCCESS_STATUS,
108-
data: plainToClass(TeamMembershipOutput, membership, { strategy: "excludeAll" }),
104+
data: membership,
109105
};
110106
}
111107

@@ -122,7 +118,7 @@ export class OrganizationsMembershipsController {
122118
const membership = await this.organizationsMembershipService.deleteOrgMembership(orgId, membershipId);
123119
return {
124120
status: SUCCESS_STATUS,
125-
data: plainToClass(TeamMembershipOutput, membership, { strategy: "excludeAll" }),
121+
data: membership,
126122
};
127123
}
128124

@@ -144,7 +140,7 @@ export class OrganizationsMembershipsController {
144140
);
145141
return {
146142
status: SUCCESS_STATUS,
147-
data: plainToClass(TeamMembershipOutput, membership, { strategy: "excludeAll" }),
143+
data: membership,
148144
};
149145
}
150146
}

0 commit comments

Comments
 (0)