Skip to content

Commit b3a3dcd

Browse files
authored
feat(backend): Introduce OrganizationRoleAPI (#2177)
* feat(backend): Introduce OrganizationRoleAPI * test(backend): Update exports.test.ts * test(clerk-sdk-node): Update exports * test(nextjs): Update exports.test.ts.snap * feat(backend): Address PR comments * chore(backend): Rename OrganizationRole to Role
1 parent 0ce0edc commit b3a3dcd

File tree

11 files changed

+190
-0
lines changed

11 files changed

+190
-0
lines changed

.changeset/cold-comics-serve.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@clerk/backend': patch
3+
---
4+
5+
Add OrganizationRoleAPI for CRUD operations regarding instance level organization roles.
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import { joinPaths } from '../../util/path';
2+
import type { DeletedObject, Role } from '../resources';
3+
import { AbstractAPI } from './AbstractApi';
4+
5+
const basePath = '/organizations_roles';
6+
7+
type GetRoleListParams = {
8+
limit?: number;
9+
offset?: number;
10+
query?: string;
11+
order_by?: string;
12+
};
13+
14+
type CreateParams = {
15+
/**
16+
* A name of a role in a readable friendly format.
17+
* F.e. `Teacher` or `Administrator`
18+
*/
19+
name: string;
20+
21+
/**
22+
* A unique identifier that represents the role.
23+
* F.e. `org:administrator`
24+
*/
25+
key: string;
26+
27+
/**
28+
* A brief description of what the role represents or its intended use.
29+
*/
30+
description: string;
31+
32+
/**
33+
* An array of permission ids that will be assigned to this role.
34+
*/
35+
permissions: string[];
36+
};
37+
38+
type GetOrganizationRoleParams = { roleId: string };
39+
40+
type UpdateParams = {
41+
/**
42+
* A name of a role in a readable friendly format.
43+
* F.e. `Teacher` or `Administrator`
44+
* Passing undefined has no effect to the existing value.
45+
*/
46+
name?: string;
47+
48+
/**
49+
* A unique identifier that represents the role.
50+
* F.e. `org:administrator`
51+
* Passing undefined has no effect to the existing value.
52+
*/
53+
key?: string;
54+
55+
/**
56+
* A brief description of what the role represents or its intended use.
57+
* Passing undefined has no effect to the existing value.
58+
*/
59+
description?: string;
60+
61+
/**
62+
* An array of permission ids that will be assigned to this role.
63+
* Passing undefined has no effect to the permission that already exist.
64+
* Passing an empty array will override the existing permissions.
65+
*/
66+
permissions?: string[];
67+
};
68+
69+
type RemovePermissionParams = {
70+
permissionId: string;
71+
roleId: string;
72+
};
73+
74+
type AssignPermissionParams = RemovePermissionParams;
75+
76+
export class OrganizationRoleAPI extends AbstractAPI {
77+
public async getOrganizationRoleList(params?: GetRoleListParams) {
78+
return this.request<Role[]>({
79+
method: 'GET',
80+
path: basePath,
81+
queryParams: params,
82+
});
83+
}
84+
85+
public async createOrganizationRole(params: CreateParams) {
86+
return this.request<Role>({
87+
method: 'POST',
88+
path: basePath,
89+
bodyParams: params,
90+
});
91+
}
92+
93+
public async getOrganizationRole(params: GetOrganizationRoleParams) {
94+
this.requireId(params.roleId);
95+
96+
return this.request<Role>({
97+
method: 'GET',
98+
path: joinPaths(basePath, params.roleId),
99+
});
100+
}
101+
102+
public async updateOrganizationRole(roleId: string, params: UpdateParams) {
103+
this.requireId(roleId);
104+
return this.request<Role>({
105+
method: 'PATCH',
106+
path: joinPaths(basePath, roleId),
107+
bodyParams: params,
108+
});
109+
}
110+
111+
public async deleteOrganizationRole(roleId: string) {
112+
this.requireId(roleId);
113+
return this.request<DeletedObject>({
114+
method: 'DELETE',
115+
path: joinPaths(basePath, roleId),
116+
});
117+
}
118+
119+
public async assignPermissionToRole(params: AssignPermissionParams) {
120+
const { roleId, permissionId } = params;
121+
this.requireId(roleId);
122+
this.requireId(permissionId);
123+
return this.request<Role>({
124+
method: 'POST',
125+
path: joinPaths(basePath, roleId, 'permission', permissionId),
126+
});
127+
}
128+
129+
public async removePermissionFromRole(params: RemovePermissionParams) {
130+
const { roleId, permissionId } = params;
131+
this.requireId(roleId);
132+
this.requireId(permissionId);
133+
return this.request<Role>({
134+
method: 'DELETE',
135+
path: joinPaths(basePath, roleId, 'permission', permissionId),
136+
});
137+
}
138+
}

packages/backend/src/api/endpoints/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export * from './EmailApi';
77
export * from './InterstitialApi';
88
export * from './InvitationApi';
99
export * from './OrganizationApi';
10+
export * from './OrganizationRoleApi';
1011
export * from './OrganizationPermissionApi';
1112
export * from './PhoneNumberApi';
1213
export * from './RedirectUrlApi';

packages/backend/src/api/factory.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
InvitationAPI,
99
OrganizationAPI,
1010
OrganizationPermissionAPI,
11+
OrganizationRoleAPI,
1112
PhoneNumberAPI,
1213
RedirectUrlAPI,
1314
SessionAPI,
@@ -30,6 +31,7 @@ export function createBackendApiClient(options: CreateBackendApiOptions) {
3031
interstitial: new InterstitialAPI(request),
3132
invitations: new InvitationAPI(request),
3233
organizations: new OrganizationAPI(request),
34+
organizationRoles: new OrganizationRoleAPI(request),
3335
organizationPermissions: new OrganizationPermissionAPI(request),
3436
phoneNumbers: new PhoneNumberAPI(request),
3537
redirectUrls: new RedirectUrlAPI(request),

packages/backend/src/api/resources/JSON.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export enum ObjectType {
2121
Organization = 'organization',
2222
OrganizationInvitation = 'organization_invitation',
2323
OrganizationMembership = 'organization_membership',
24+
Role = 'role',
2425
Permission = 'permission',
2526
PhoneNumber = 'phone_number',
2627
RedirectUrl = 'redirect_url',
@@ -174,6 +175,16 @@ export interface OrganizationMembershipPublicUserDataJSON {
174175
user_id: string;
175176
}
176177

178+
export interface RoleJSON extends ClerkResourceJSON {
179+
object: ObjectType.Role;
180+
name: string;
181+
key: string;
182+
description: string;
183+
permissions: PermissionJSON[];
184+
created_at: number;
185+
updated_at: number;
186+
}
187+
177188
export interface PermissionJSON extends ClerkResourceJSON {
178189
object: ObjectType.Permission;
179190
id: string;
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import type { RoleJSON } from './JSON';
2+
import { Permission } from './Permission';
3+
4+
export class Role {
5+
constructor(
6+
readonly id: string,
7+
readonly name: string,
8+
readonly key: string,
9+
readonly description: string,
10+
readonly permissions: Permission[] = [],
11+
readonly createdAt: number,
12+
readonly updatedAt: number,
13+
) {}
14+
15+
static fromJSON(data: RoleJSON): Role {
16+
return new Role(
17+
data.id,
18+
data.name,
19+
data.key,
20+
data.description,
21+
(data.permissions || []).map(x => Permission.fromJSON(x)),
22+
data.created_at,
23+
data.updated_at,
24+
);
25+
}
26+
}

packages/backend/src/api/resources/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export * from './Invitation';
2222
export * from './JSON';
2323
export * from './OauthAccessToken';
2424
export * from './Organization';
25+
export * from './Role';
2526
export * from './Permission';
2627
export * from './OrganizationInvitation';
2728
export * from './OrganizationMembership';

packages/backend/src/exports.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export default (QUnit: QUnit) => {
2727
'Permission',
2828
'PhoneNumber',
2929
'RedirectUrl',
30+
'Role',
3031
'SMSMessage',
3132
'Session',
3233
'SignInToken',

packages/nextjs/src/server/__tests__/__snapshots__/exports.test.ts.snap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ exports[`/server public exports should not include a breaking change 1`] = `
2121
"Permission",
2222
"PhoneNumber",
2323
"RedirectUrl",
24+
"Role",
2425
"SMSMessage",
2526
"Session",
2627
"SignInToken",

packages/sdk-node/src/__tests__/__snapshots__/exports.test.ts.snap

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ exports[`module exports should not change unless explicitly set 1`] = `
2323
"Permission",
2424
"PhoneNumber",
2525
"RedirectUrl",
26+
"Role",
2627
"SMSMessage",
2728
"Session",
2829
"SignInToken",
@@ -54,6 +55,7 @@ exports[`module exports should not change unless explicitly set 1`] = `
5455
"loadInterstitialFromLocal",
5556
"makeAuthObjectSerializable",
5657
"organizationPermissions",
58+
"organizationRoles",
5759
"organizations",
5860
"phoneNumbers",
5961
"prunePrivateMetadata",

0 commit comments

Comments
 (0)