-
Notifications
You must be signed in to change notification settings - Fork 453
feat(clerk-js): Add organization basic resources #57
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
09f9012
0ca9a31
d6faaab
2a9edbd
e2eef78
af010ba
480c422
1635132
55cf7c4
fc11087
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| import { Organization } from 'core/resources/internal'; | ||
|
|
||
| describe('Organization', () => { | ||
| it('has the same initial properties', () => { | ||
| const organization = new Organization({ | ||
| object: 'organization', | ||
| id: 'test_id', | ||
| name: 'test_name', | ||
| role: 'basic_member', | ||
| created_at: 12345, | ||
| updated_at: 5678, | ||
| created_by: 'test_user_id', | ||
| instance_id: 'test_instance_id', | ||
| }); | ||
|
|
||
| expect(organization).toMatchSnapshot(); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,140 @@ | ||
| import type { | ||
| GetMembersParams, | ||
| MembershipRole, | ||
| OrganizationInvitationJSON, | ||
| OrganizationJSON, | ||
| OrganizationMembershipJSON, | ||
| OrganizationResource, | ||
| } from '@clerk/types'; | ||
| import { unixEpochToDate } from 'utils/date'; | ||
|
|
||
| import { | ||
| BaseResource, | ||
| OrganizationInvitation, | ||
| OrganizationMembership, | ||
| } from './internal'; | ||
|
|
||
| export class Organization extends BaseResource implements OrganizationResource { | ||
| id!: string; | ||
| name!: string; | ||
| role!: MembershipRole; | ||
| instanceId!: string; | ||
| createdBy!: string; | ||
| createdAt!: Date; | ||
| updatedAt!: Date; | ||
|
|
||
| constructor(data: OrganizationJSON) { | ||
| super(); | ||
| this.fromJSON(data); | ||
| } | ||
|
|
||
| static async create(name: string): Promise<OrganizationResource> { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ❓ IIRC, all resources use the To be honest, I'm not a huge fan of the
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @nikosdouvlis |
||
| const json = ( | ||
| await BaseResource._fetch<OrganizationJSON>({ | ||
| path: '/organizations', | ||
| method: 'POST', | ||
| body: { name } as any, | ||
| }) | ||
| )?.response as unknown as OrganizationJSON; | ||
|
|
||
| return new Organization(json); | ||
| } | ||
|
|
||
| static async retrieve( | ||
| getOrganizationParams?: GetOrganizationParams, | ||
| ): Promise<Organization[]> { | ||
| return await BaseResource._fetch({ | ||
| path: '/me/organizations', | ||
| method: 'GET', | ||
| search: getOrganizationParams as any, | ||
| }) | ||
| .then(res => { | ||
| const organizationsJSON = | ||
| res?.response as unknown as OrganizationJSON[]; | ||
| return organizationsJSON.map(org => new Organization(org)); | ||
| }) | ||
| .catch(() => []); | ||
| } | ||
|
|
||
| getMembers = async ( | ||
| getMemberParams?: GetMembersParams, | ||
| ): Promise<OrganizationMembership[]> => { | ||
| return await BaseResource._fetch({ | ||
| path: `/organizations/${this.id}/memberships`, | ||
| method: 'GET', | ||
| search: getMemberParams as any, | ||
| }) | ||
| .then(res => { | ||
| const members = | ||
| res?.response as unknown as OrganizationMembershipJSON[]; | ||
| return members.map(member => new OrganizationMembership(member)); | ||
| }) | ||
| .catch(() => []); | ||
| }; | ||
|
|
||
| getPendingInvitations = async (): Promise<OrganizationInvitation[]> => { | ||
| return await BaseResource._fetch({ | ||
| path: `/organizations/${this.id}/invitations/pending`, | ||
| method: 'GET', | ||
| }) | ||
| .then(res => { | ||
| const pendingInvitations = | ||
| res?.response as unknown as OrganizationInvitationJSON[]; | ||
| return pendingInvitations.map( | ||
| pendingInvitation => new OrganizationInvitation(pendingInvitation), | ||
| ); | ||
| }) | ||
| .catch(() => []); | ||
| }; | ||
|
|
||
| inviteUser = async (inviteUserParams: InviteUserParams) => { | ||
| return await OrganizationInvitation.create(this.id, inviteUserParams); | ||
| }; | ||
|
|
||
| updateMember = async ({ | ||
| userId, | ||
| role, | ||
| }: UpdateMembershipParams): Promise<OrganizationMembership> => { | ||
| return await BaseResource._fetch({ | ||
| method: 'PATCH', | ||
| path: `/organizations/${this.id}/memberships/${userId}`, | ||
| body: { role } as any, | ||
| }).then( | ||
| res => | ||
| new OrganizationMembership(res?.response as OrganizationMembershipJSON), | ||
| ); | ||
| }; | ||
|
|
||
| removeMember = async (userId: string) => { | ||
| return await this._baseDelete({ | ||
| path: `/organizations/${this.id}/memberships/${userId}`, | ||
| }); | ||
| }; | ||
|
|
||
| protected fromJSON(data: OrganizationJSON): this { | ||
| this.id = data.id; | ||
| this.name = data.name; | ||
| this.role = data.role; | ||
| this.instanceId = data.instance_id; | ||
| this.createdBy = data.created_by; | ||
| this.createdAt = unixEpochToDate(data.created_at); | ||
| this.updatedAt = unixEpochToDate(data.updated_at); | ||
| return this; | ||
| } | ||
| } | ||
|
|
||
| export type GetOrganizationParams = { | ||
| limit?: number; | ||
| offset?: number; | ||
| }; | ||
|
|
||
| export type InviteUserParams = { | ||
| emailAddress: string; | ||
| role: MembershipRole; | ||
| redirectUrl?: string; | ||
| }; | ||
|
|
||
| export type UpdateMembershipParams = { | ||
| userId: string; | ||
| role: MembershipRole; | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| import { OrganizationInvitation } from 'core/resources/internal'; | ||
|
|
||
| describe('OrganizationInvitation', () => { | ||
| it('has the same initial properties', () => { | ||
| const organizationInvitation = new OrganizationInvitation({ | ||
| object: 'organization_invitation', | ||
| email_address: 'test_email', | ||
| id: 'test_id', | ||
| organization_id: 'test_organization_id', | ||
| role: 'basic_member', | ||
| created_at: 12345, | ||
| updated_at: 5678, | ||
| status: 'pending', | ||
| }); | ||
|
|
||
| expect(organizationInvitation).toMatchSnapshot(); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| import { | ||
| MembershipRole, | ||
| OrganizationInvitationJSON, | ||
| OrganizationInvitationResource, | ||
| OrganizationInvitationStatus, | ||
| } from '@clerk/types'; | ||
| import { unixEpochToDate } from 'utils/date'; | ||
|
|
||
| import { BaseResource } from './internal'; | ||
|
|
||
| export class OrganizationInvitation | ||
| extends BaseResource | ||
| implements OrganizationInvitationResource | ||
| { | ||
| id!: string; | ||
| emailAddress!: string; | ||
| organizationId!: string; | ||
| status!: OrganizationInvitationStatus; | ||
| role!: MembershipRole; | ||
| createdAt!: Date; | ||
| updatedAt!: Date; | ||
|
|
||
| static async create( | ||
| organizationId: string, | ||
| { emailAddress, role, redirectUrl }: CreateOrganizationInvitationParams, | ||
| ): Promise<OrganizationInvitationResource> { | ||
| const json = ( | ||
| await BaseResource._fetch<OrganizationInvitationJSON>({ | ||
| path: `/organizations/${organizationId}/invitations`, | ||
| method: 'POST', | ||
| body: { | ||
| email_address: emailAddress, | ||
| role, | ||
| redirect_url: redirectUrl, | ||
| } as any, | ||
| }) | ||
| )?.response as unknown as OrganizationInvitationJSON; | ||
|
|
||
| return new OrganizationInvitation(json); | ||
| } | ||
|
|
||
| constructor(data: OrganizationInvitationJSON) { | ||
| super(); | ||
| this.fromJSON(data); | ||
| } | ||
|
|
||
| revoke = async () => { | ||
| return await this._basePost({ | ||
| path: `/organizations/${this.organizationId}/invitations/${this.id}/revoke`, | ||
| }); | ||
| }; | ||
|
|
||
| protected fromJSON(data: OrganizationInvitationJSON): this { | ||
| this.id = data.id; | ||
| this.emailAddress = data.email_address; | ||
| this.organizationId = data.organization_id; | ||
| this.role = data.role; | ||
| this.status = data.status; | ||
| this.createdAt = unixEpochToDate(data.created_at); | ||
| this.updatedAt = unixEpochToDate(data.updated_at); | ||
| return this; | ||
| } | ||
| } | ||
|
|
||
| export type CreateOrganizationInvitationParams = { | ||
| emailAddress: string; | ||
| role: MembershipRole; | ||
| redirectUrl?: string; | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| import { OrganizationMembership } from 'core/resources/internal'; | ||
|
|
||
| describe('OrganizationMembership', () => { | ||
| it('has the same initial properties', () => { | ||
| const organizationMemberShip = new OrganizationMembership({ | ||
| object: 'organization_membership', | ||
| id: 'test_id', | ||
| created_at: 12345, | ||
| updated_at: 5678, | ||
| role: 'admin', | ||
| public_user_data: { | ||
| object: 'public_user_data', | ||
| first_name: 'test_first_name', | ||
| last_name: 'test_last_name', | ||
| profile_image_url: 'test_url', | ||
| identifier: 'test@identifier.gr', | ||
| id: 'test_user_id', | ||
| }, | ||
| }); | ||
|
|
||
| expect(organizationMemberShip).toMatchSnapshot(); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| import { | ||
| MembershipRole, | ||
| OrganizationMembershipJSON, | ||
| OrganizationMembershipResource, | ||
| PublicUserData, | ||
| } from '@clerk/types'; | ||
| import { unixEpochToDate } from 'utils/date'; | ||
|
|
||
| export class OrganizationMembership implements OrganizationMembershipResource { | ||
| id!: string; | ||
| publicUserData!: PublicUserData; | ||
| role!: MembershipRole; | ||
| createdAt!: Date; | ||
| updatedAt!: Date; | ||
|
|
||
| constructor(data: OrganizationMembershipJSON) { | ||
| this.fromJSON(data); | ||
| } | ||
|
|
||
| protected fromJSON(data: OrganizationMembershipJSON): this { | ||
| this.id = data.id; | ||
| this.publicUserData = { | ||
| firstName: data.public_user_data.first_name, | ||
| lastName: data.public_user_data.last_name, | ||
| profileImageUrl: data.public_user_data.profile_image_url, | ||
| identifier: data.public_user_data.identifier, | ||
| userId: data.public_user_data.user_id, | ||
| }; | ||
| this.role = data.role; | ||
| this.createdAt = unixEpochToDate(data.created_at); | ||
| this.updatedAt = unixEpochToDate(data.updated_at); | ||
| return this; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
❓ @igneel64 Can you please elaborate why do we consider this an unstable method at this point?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
❓ A follow up question, does this method invite a member or creates an org? Something seems to be missing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@SokratisVidros , thanks for the comment.
The use of the
__unstableprefix is intended here. Currently we cannot expose methods on clerk-react from internal clerk-js resources. The only way this can be done is through theClerkinterface.The same interface though is exposed globally. That means that adding a method there, it is also exposed in
window.Clerk.methodName.Since adding methods indiscriminately on the global Clerk object is not the optimal outcome, for now we are adding to those methods the
__unstableprefix to prevent misuse.(Also this method just invites a member on an organization. The organizations can be retrieved from the
useOrganizationshook.)