Merge pull request #3297 from specklesystems/fabians/core-ioc-66
chore(server): core IoC #65 - adminUserListFactory
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user