Merge pull request #3297 from specklesystems/fabians/core-ioc-66

chore(server): core IoC #65 - adminUserListFactory
This commit is contained in:
Alessandro Magionami
2024-10-16 11:02:53 +02:00
committed by GitHub
6 changed files with 112 additions and 70 deletions
@@ -4,7 +4,7 @@ import {
UserWithOptionalRole
} from '@/modules/core/domain/users/types'
import { UserUpdateInput } from '@/modules/core/graph/generated/graphql'
import { ServerAclRecord } from '@/modules/core/helpers/types'
import { ServerAclRecord, UserWithRole } from '@/modules/core/helpers/types'
import { Nullable, NullableKeysToOptional, ServerRoles } from '@speckle/shared'
export type GetUserParams = Partial<{
@@ -55,6 +55,20 @@ export type CountAdminUsers = () => Promise<number>
export type IsLastAdminUser = (userId: string) => Promise<boolean>
type UserQuery = {
query: string | null
role?: ServerRoles | null
}
export type ListPaginatedUsersPage = (
params: {
limit: number
cursor?: Date | null
} & UserQuery
) => Promise<UserWithRole[]>
export type CountUsers = (params: UserQuery) => Promise<number>
export type StoreUserAcl = (params: {
acl: ServerAclRecord
}) => Promise<ServerAclRecord>
@@ -137,3 +151,16 @@ export type SearchLimitedUsers = (
users: LimitedUser[]
cursor: Nullable<string>
}>
type AdminUserListArgs = {
cursor: string | null
query: string | null
limit: number
role: ServerRoles | null
}
export type AdminUserList = (args: AdminUserListArgs) => Promise<{
totalCount: number
items: UserWithRole[]
cursor: string | null
}>
@@ -2,16 +2,22 @@ import { db } from '@/db/knex'
import { Resolvers } from '@/modules/core/graph/generated/graphql'
import { mapServerRoleToValue } from '@/modules/core/helpers/graphTypes'
import { toProjectIdWhitelist } from '@/modules/core/helpers/token'
import { countUsersFactory, listUsersFactory } from '@/modules/core/repositories/users'
import {
adminInviteList,
adminProjectList,
adminUserList
adminUserListFactory
} from '@/modules/core/services/admin'
import {
getTotalStreamCountFactory,
getTotalUserCountFactory
} from '@/modules/stats/repositories'
const adminUserList = adminUserListFactory({
listUsers: listUsersFactory({ db }),
countUsers: countUsersFactory({ db })
})
export = {
Query: {
admin: () => ({})
@@ -12,6 +12,7 @@ import { markUserEmailAsVerifiedFactory } from '@/modules/core/services/users/em
import { UserWithOptionalRole } from '@/modules/core/domain/users/types'
import {
CountAdminUsers,
CountUsers,
DeleteUserRecord,
GetUser,
GetUserByEmail,
@@ -23,6 +24,7 @@ import {
LegacyGetPaginatedUsersCount,
LegacyGetUser,
LegacyGetUserByEmail,
ListPaginatedUsersPage,
SearchLimitedUsers,
StoreUser,
StoreUserAcl,
@@ -97,57 +99,53 @@ export const getUsersFactory =
return (await q).map((u) => (skipClean ? u : sanitizeUserRecord(u)))
}
type UserQuery = {
query: string | null
role?: ServerRoles | null
}
/**
* List users
*/
export async function listUsers({
limit,
cursor,
query,
role
}: {
limit: number
cursor?: Date | null
} & UserQuery): Promise<UserWithRole[]> {
const sanitizedLimit = clamp(limit, 1, 200)
export const listUsersFactory =
(deps: { db: Knex }): ListPaginatedUsersPage =>
async ({ limit, cursor, query, role }) => {
const sanitizedLimit = clamp(limit, 1, 200)
const userCols = omit(Users.col, ['email', 'verified'])
const q = Users.knex<UserWithRole[]>()
.orderBy(Users.col.createdAt, 'desc')
.limit(sanitizedLimit)
.columns([
...Object.values(userCols),
// Getting first role from grouped results
knex.raw(`(array_agg("server_acl"."role"))[1] as role`),
knex.raw(`(array_agg("user_emails"."email"))[1] as email`),
knex.raw(`(array_agg("user_emails"."verified"))[1] as verified`)
])
.leftJoin(ServerAcl.name, ServerAcl.col.userId, Users.col.id)
.leftJoin(UserEmails.name, UserEmails.col.userId, Users.col.id)
.where({ [UserEmails.col.primary]: true })
.groupBy(Users.col.id)
if (cursor) q.where(Users.col.createdAt, '<', cursor)
const users: UserWithRole[] = await getUsersBaseQuery(q, { searchQuery: query, role })
return users.map((u) => sanitizeUserRecord(u))
}
const userCols = omit(Users.col, ['email', 'verified'])
const q = tables
.users(deps.db)
.orderBy(Users.col.createdAt, 'desc')
.limit(sanitizedLimit)
.columns([
...Object.values(userCols),
// Getting first role from grouped results
knex.raw(`(array_agg("server_acl"."role"))[1] as role`),
knex.raw(`(array_agg("user_emails"."email"))[1] as email`),
knex.raw(`(array_agg("user_emails"."verified"))[1] as verified`)
])
.leftJoin(ServerAcl.name, ServerAcl.col.userId, Users.col.id)
.leftJoin(UserEmails.name, UserEmails.col.userId, Users.col.id)
.where({ [UserEmails.col.primary]: true })
.groupBy(Users.col.id)
if (cursor) q.where(Users.col.createdAt, '<', cursor)
const users: UserWithRole[] = await getUsersBaseQuery(q, {
searchQuery: query,
role
})
return users.map((u) => sanitizeUserRecord(u))
}
export async function countUsers(args: UserQuery): Promise<number> {
const q = Users.knex()
.leftJoin(ServerAcl.name, ServerAcl.col.userId, Users.col.id)
.leftJoin(UserEmails.name, UserEmails.col.userId, Users.col.id)
.countDistinct(Users.col.id)
export const countUsersFactory =
(deps: { db: Knex }): CountUsers =>
async (args) => {
const q = tables
.users(deps.db)
.leftJoin(ServerAcl.name, ServerAcl.col.userId, Users.col.id)
.leftJoin(UserEmails.name, UserEmails.col.userId, Users.col.id)
.countDistinct(Users.col.id)
const result = await getUsersBaseQuery(q, {
searchQuery: args.query,
role: args.role
})
return parseInt(result[0]['count'])
}
const result = await getUsersBaseQuery(q, {
searchQuery: args.query,
role: args.role
})
return parseInt(result[0]['count'])
}
/**
* Get user by ID
+30 -21
View File
@@ -1,8 +1,12 @@
import db from '@/db/knex'
import {
AdminUserList,
CountUsers,
ListPaginatedUsersPage
} from '@/modules/core/domain/users/operations'
import { ServerInviteGraphQLReturnType } from '@/modules/core/helpers/graphTypes'
import { StreamRecord, UserRecord } from '@/modules/core/helpers/types'
import { StreamRecord } from '@/modules/core/helpers/types'
import { legacyGetStreamsFactory } from '@/modules/core/repositories/streams'
import { listUsers, countUsers } from '@/modules/core/repositories/users'
import { ServerInviteRecord } from '@/modules/serverinvites/domain/types'
import {
countServerInvitesFactory,
@@ -34,37 +38,42 @@ type AdminUserListArgs = CollectionQueryArgs & {
role: ServerRoles | null
}
export class CursorParsingError extends BaseError {
class CursorParsingError extends BaseError {
static defaultMessage = 'Invalid cursor provided'
static code = 'INVALID_CURSOR_VALUE'
static statusCode = 400
}
export const parseCursorToDate = (cursor: string): Date => {
const parseCursorToDate = (cursor: string): Date => {
const timestamp = Date.parse(Buffer.from(cursor, 'base64').toString('utf-8'))
if (isNaN(timestamp)) throw new CursorParsingError()
return new Date(timestamp)
}
export const convertDateToCursor = (date: Date): string =>
const convertDateToCursor = (date: Date): string =>
Buffer.from(date.toISOString()).toString('base64')
export const adminUserList = async (
args: AdminUserListArgs
): Promise<Collection<UserRecord>> => {
const parsedCursor = args.cursor ? parseCursorToDate(args.cursor) : null
const [items, totalCount] = await Promise.all([
listUsers({
role: args.role,
cursor: parsedCursor,
limit: args.limit,
query: args.query ?? null
}),
countUsers(args)
])
const cursor = items.length ? convertDateToCursor(items.slice(-1)[0].createdAt) : null
return { totalCount, items, cursor }
}
export const adminUserListFactory =
(deps: {
listUsers: ListPaginatedUsersPage
countUsers: CountUsers
}): AdminUserList =>
async (args: AdminUserListArgs) => {
const parsedCursor = args.cursor ? parseCursorToDate(args.cursor) : null
const [items, totalCount] = await Promise.all([
deps.listUsers({
role: args.role,
cursor: parsedCursor,
limit: args.limit,
query: args.query ?? null
}),
deps.countUsers(args)
])
const cursor = items.length
? convertDateToCursor(items.slice(-1)[0].createdAt)
: null
return { totalCount, items, cursor }
}
type AdminProjectListArgs = HasCursor & {
query: string | null
@@ -16,7 +16,7 @@ import {
getUserByEmailFactory,
getUserFactory,
getUsersFactory,
listUsers,
listUsersFactory,
storeUserAclFactory,
storeUserFactory
} from '@/modules/core/repositories/users'
@@ -63,6 +63,7 @@ const createUser = createUserFactory({
usersEventsEmitter: UsersEmitter.emit
})
const getUserByEmail = getUserByEmailFactory({ db })
const listUsers = listUsersFactory({ db })
describe('Find users @core', () => {
describe('getUsers', () => {
@@ -8,7 +8,7 @@ import {
legacyGetPaginatedUsersCountFactory,
legacyGetPaginatedUsersFactory,
legacyGetUserByEmailFactory,
listUsers,
listUsersFactory,
markUserAsVerified,
storeUserAclFactory,
storeUserFactory
@@ -83,6 +83,7 @@ const createUser = createUserFactory({
})
const getUserByEmail = getUserByEmailFactory({ db })
const legacyGetUserByEmail = legacyGetUserByEmailFactory({ db })
const listUsers = listUsersFactory({ db })
describe('Core @user-emails', () => {
before(async () => {