-
Notifications
You must be signed in to change notification settings - Fork 74
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add user organization domain/route logic (#2327)
Adds new `UserOrganizations` repository, with relevant methods for sending, accepting and declining invites and updating/removing users has been added to the domain layer. Respective routes for each have been included in a new `UserOrganizationsController`: - Add `IUserOrganizationsRepository` with implementation - Add `UserOrganizationController` with routes for inviting, accepting and declining user invites, modifying roles and removing users - Add `UserOrganizationService` to route repository to controller - Associated entity updates - Add appropriate test coverage
- Loading branch information
Showing
27 changed files
with
2,619 additions
and
34 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -431,4 +431,7 @@ export default () => ({ | |
}, | ||
}, | ||
}, | ||
users: { | ||
maxInvites: 50, | ||
}, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,22 +1,71 @@ | ||
/* eslint-disable @typescript-eslint/no-explicit-any */ | ||
import type { ValueTransformer } from 'typeorm'; | ||
|
||
export function getEnumKey< | ||
T extends { [key: string]: number | string; [key: number]: string }, | ||
>(enumObj: T, value: number): keyof T { | ||
/** | ||
* Note: numeric enums have reverse mappings so we can't interact with them | ||
* as we would normally do an object on both a type and runtime level. | ||
* Object.keys will return all keys/values, as will keyof. | ||
*/ | ||
|
||
type NumericEnum = { [key: string]: number | string; [key: number]: string }; | ||
|
||
export function getEnumKey<T extends NumericEnum>( | ||
enumObj: T, | ||
value: number, | ||
): keyof T { | ||
const key = enumObj[value]; | ||
if (typeof key !== 'string') { | ||
throw new Error(`Invalid enum value: ${value}`); | ||
} | ||
return key; | ||
} | ||
|
||
export const databaseEnumTransformer = < | ||
T extends { [key: string]: number | string; [key: number]: string }, | ||
>( | ||
export const databaseEnumTransformer = <T extends NumericEnum>( | ||
enumObj: T, | ||
): ValueTransformer => { | ||
return { | ||
to: (value: keyof typeof enumObj) => enumObj[value], | ||
from: (value: number): keyof T => getEnumKey(enumObj, value), | ||
}; | ||
}; | ||
|
||
/** | ||
* The following is a workaround to get only the string keys of a numeric enum | ||
* as a tuple, as `keyof` will return all keys/values due to reverse mapping. | ||
* | ||
* Returned as a tuple, it is useful for zod enum validation, e.g. z.enum([...]) | ||
* | ||
* @param enumObj - numeric enum object | ||
* @returns string keys as a strictly typed tuple | ||
*/ | ||
export function getStringEnumKeys<T extends NumericEnum>( | ||
enumObj: T, | ||
): NumericEnumKeysTuple<T> { | ||
return Object.keys(enumObj).filter((key) => | ||
isNaN(Number(key)), | ||
) as NumericEnumKeysTuple<T>; | ||
} | ||
|
||
type NumericEnumKeysTuple<T> = UnionToTuple<ExcludeNumberKeys<T>>; | ||
|
||
type ExcludeNumberKeys<T> = Exclude<keyof T, `${number}`>; | ||
|
||
// Recursively removes last element of union and pushes it to tuple | ||
type UnionToTuple<T, R extends Array<any> = []> = [T] extends [never] | ||
? R | ||
: UnionToTuple<Exclude<T, LastOf<T>>, [LastOf<T>, ...R]>; | ||
|
||
// Gets the last element of a union | ||
type LastOf<T> = | ||
UnionToIntersection<T extends any ? (x: T) => 0 : never> extends ( | ||
x: infer Last, | ||
) => 0 | ||
? Last | ||
: never; | ||
|
||
// Converts a union to an intersection of functions | ||
type UnionToIntersection<U> = (U extends any ? (x: U) => any : never) extends ( | ||
x: infer I, | ||
) => any | ||
? I | ||
: never; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,15 +1,24 @@ | ||
import { z } from 'zod'; | ||
import { RowSchema } from '@/datasources/db/v2/entities/row.entity'; | ||
import { UserOrganizationSchema } from '@/domain/users/entities/user-organization.entity'; | ||
import { getStringEnumKeys } from '@/domain/common/utils/enum'; | ||
import type { UserOrganization } from '@/domain/users/entities/user-organization.entity'; | ||
|
||
export enum OrganizationStatus { | ||
ACTIVE = 1, | ||
} | ||
export const OrganizationStatusKeys = Object.keys(OrganizationStatus) as [ | ||
keyof typeof OrganizationStatus, | ||
]; | ||
|
||
export type Organization = z.infer<typeof OrganizationSchema>; | ||
|
||
export const OrganizationSchema = RowSchema.extend({ | ||
status: z.enum(OrganizationStatusKeys), | ||
// We need explicitly define ZodType due to recursion | ||
export const OrganizationSchema: z.ZodType< | ||
z.infer<typeof RowSchema> & { | ||
name: string; | ||
status: keyof typeof OrganizationStatus; | ||
userOrganizations: Array<UserOrganization>; | ||
} | ||
> = RowSchema.extend({ | ||
name: z.string().max(255), | ||
status: z.enum(getStringEnumKeys(OrganizationStatus)), | ||
userOrganizations: z.array(z.lazy(() => UserOrganizationSchema)), | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import type { Organization } from '@/domain/organizations/entities/organization.entity'; | ||
import type { UserOrganization } from '@/domain/users/entities/user-organization.entity'; | ||
import type { User } from '@/domain/users/entities/user.entity'; | ||
|
||
export type Invitation = { | ||
userId: User['id']; | ||
orgId: Organization['id']; | ||
role: UserOrganization['role']; | ||
status: UserOrganization['status']; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,37 @@ | ||
import { z } from 'zod'; | ||
import { RowSchema } from '@/datasources/db/v2/entities/row.entity'; | ||
import { OrganizationSchema } from '@/domain/organizations/entities/organization.entity'; | ||
import { UserSchema } from '@/domain/users/entities/user.entity'; | ||
import { getStringEnumKeys } from '@/domain/common/utils/enum'; | ||
import type { Organization } from '@/domain/organizations/entities/organization.entity'; | ||
import type { User } from '@/domain/users/entities/user.entity'; | ||
|
||
export enum UserOrganizationRole { | ||
ADMIN = 1, | ||
MEMBER = 2, | ||
} | ||
|
||
export enum UserOrganizationStatus { | ||
PENDING = 0, | ||
INVITED = 0, | ||
ACTIVE = 1, | ||
DECLINED = 2, | ||
} | ||
|
||
// We need explicitly define ZodType due to recursion | ||
export const UserOrganizationSchema: z.ZodType< | ||
z.infer<typeof RowSchema> & { | ||
user: User; | ||
organization: Organization; | ||
name: string | null; | ||
role: keyof typeof UserOrganizationRole; | ||
status: keyof typeof UserOrganizationStatus; | ||
} | ||
> = RowSchema.extend({ | ||
user: z.lazy(() => UserSchema), | ||
organization: z.lazy(() => OrganizationSchema), | ||
name: z.string().nullable(), | ||
role: z.enum(getStringEnumKeys(UserOrganizationRole)), | ||
status: z.enum(getStringEnumKeys(UserOrganizationStatus)), | ||
}); | ||
|
||
export type UserOrganization = z.infer<typeof UserOrganizationSchema>; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,15 +1,27 @@ | ||
import { z } from 'zod'; | ||
import { RowSchema } from '@/datasources/db/v1/entities/row.entity'; | ||
import { RowSchema } from '@/datasources/db/v2/entities/row.entity'; | ||
import { WalletSchema } from '@/domain/wallets/entities/wallet.entity'; | ||
import { UserOrganizationSchema } from '@/domain/users/entities/user-organization.entity'; | ||
import { getStringEnumKeys } from '@/domain/common/utils/enum'; | ||
import type { Wallet } from '@/domain/wallets/entities/wallet.entity'; | ||
import type { UserOrganization } from '@/domain/users/entities/user-organization.entity'; | ||
|
||
export enum UserStatus { | ||
PENDING = 0, | ||
ACTIVE = 1, | ||
} | ||
export const UserStatusKeys = Object.keys(UserStatus) as [ | ||
keyof typeof UserStatus, | ||
]; | ||
|
||
export type User = z.infer<typeof UserSchema>; | ||
|
||
export const UserSchema = RowSchema.extend({ | ||
status: z.enum(UserStatusKeys), | ||
// We need explicitly define ZodType due to recursion | ||
export const UserSchema: z.ZodType< | ||
z.infer<typeof RowSchema> & { | ||
status: keyof typeof UserStatus; | ||
wallets: Array<Wallet>; | ||
userOrganizations: Array<UserOrganization>; | ||
} | ||
> = RowSchema.extend({ | ||
status: z.enum(getStringEnumKeys(UserStatus)), | ||
wallets: z.array(WalletSchema), | ||
userOrganizations: z.array(z.lazy(() => UserOrganizationSchema)), | ||
}); |
76 changes: 76 additions & 0 deletions
76
src/domain/users/user-organizations.repository.interface.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
import type { UserOrganization as DbUserOrganization } from '@/datasources/users/entities/user-organizations.entity.db'; | ||
import type { AuthPayload } from '@/domain/auth/entities/auth-payload.entity'; | ||
import type { Organization } from '@/domain/organizations/entities/organization.entity'; | ||
import type { Invitation } from '@/domain/users/entities/invitation.entity'; | ||
import type { UserOrganization } from '@/domain/users/entities/user-organization.entity'; | ||
import type { User } from '@/domain/users/entities/user.entity'; | ||
import type { | ||
FindOptionsWhere, | ||
FindOptionsRelations, | ||
FindManyOptions, | ||
} from 'typeorm'; | ||
|
||
export const IUsersOrganizationsRepository = Symbol( | ||
'IUsersOrganizationsRepository', | ||
); | ||
|
||
export interface IUsersOrganizationsRepository { | ||
findOneOrFail( | ||
where: | ||
| Array<FindOptionsWhere<UserOrganization>> | ||
| FindOptionsWhere<UserOrganization>, | ||
relations?: FindOptionsRelations<UserOrganization>, | ||
): Promise<DbUserOrganization>; | ||
|
||
findOne( | ||
where: | ||
| Array<FindOptionsWhere<UserOrganization>> | ||
| FindOptionsWhere<UserOrganization>, | ||
relations?: FindOptionsRelations<UserOrganization>, | ||
): Promise<DbUserOrganization | null>; | ||
|
||
findOrFail( | ||
args?: FindManyOptions<DbUserOrganization>, | ||
): Promise<[DbUserOrganization, ...Array<DbUserOrganization>]>; | ||
|
||
find( | ||
args?: FindManyOptions<DbUserOrganization>, | ||
): Promise<Array<DbUserOrganization>>; | ||
|
||
inviteUsers(args: { | ||
authPayload: AuthPayload; | ||
orgId: Organization['id']; | ||
users: Array<{ | ||
address: `0x${string}`; | ||
role: UserOrganization['role']; | ||
}>; | ||
}): Promise<Array<Invitation>>; | ||
|
||
acceptInvite(args: { | ||
authPayload: AuthPayload; | ||
orgId: Organization['id']; | ||
}): Promise<void>; | ||
|
||
declineInvite(args: { | ||
authPayload: AuthPayload; | ||
orgId: Organization['id']; | ||
}): Promise<void>; | ||
|
||
findAuthorizedUserOrgsOrFail(args: { | ||
authPayload: AuthPayload; | ||
orgId: Organization['id']; | ||
}): Promise<Array<UserOrganization>>; | ||
|
||
updateRole(args: { | ||
authPayload: AuthPayload; | ||
orgId: Organization['id']; | ||
userId: User['id']; | ||
role: UserOrganization['role']; | ||
}): Promise<void>; | ||
|
||
removeUser(args: { | ||
authPayload: AuthPayload; | ||
orgId: Organization['id']; | ||
userId: User['id']; | ||
}): Promise<void>; | ||
} |
Oops, something went wrong.