diff --git a/packages/server/modules/core/domain/users/operations.ts b/packages/server/modules/core/domain/users/operations.ts index 1ea3e71ab..33c09da82 100644 --- a/packages/server/modules/core/domain/users/operations.ts +++ b/packages/server/modules/core/domain/users/operations.ts @@ -175,3 +175,28 @@ export type AdminUserList = (args: AdminUserListArgs) => Promise<{ items: UserWithRole[] cursor: string | null }> + +export type LegacyAdminUsersPaginationParams = { + limit: number + offset: number + query: string | null +} + +export type LegacyAdminUsersListItem = { + registeredUser: Nullable + invitedUser: Nullable<{ + id: string + email: string + invitedById: string + }> + id: string +} + +type LegacyAdminUsersListCollection = { + totalCount: number + items: Array +} + +export type LegacyGetAdminUsersListCollection = ( + params: LegacyAdminUsersPaginationParams +) => Promise diff --git a/packages/server/modules/core/graph/resolvers/users.js b/packages/server/modules/core/graph/resolvers/users.js index 4b6a499a2..faf660bfe 100644 --- a/packages/server/modules/core/graph/resolvers/users.js +++ b/packages/server/modules/core/graph/resolvers/users.js @@ -1,10 +1,6 @@ const { ActionTypes } = require('@/modules/activitystream/helpers/types') const { validateScopes } = require(`@/modules/shared`) const zxcvbn = require('zxcvbn') -const { - getAdminUsersListCollection, - getTotalCounts -} = require('@/modules/core/services/users/adminUsersListService') const { Roles, Scopes } = require('@speckle/shared') const { legacyGetUserFactory, @@ -16,7 +12,9 @@ const { getUserRoleFactory, updateUserServerRoleFactory, searchUsersFactory, - markOnboardingCompleteFactory + markOnboardingCompleteFactory, + legacyGetPaginatedUsersCountFactory, + legacyGetPaginatedUsersFactory } = require('@/modules/core/repositories/users') const { UsersMeta } = require('@/modules/core/dbSchema') const { getServerInfo } = require('@/modules/core/services/generic') @@ -42,6 +40,9 @@ const { getUserDeletableStreamsFactory } = require('@/modules/core/repositories/streams') const { dbLogger } = require('@/logging/logging') +const { + getAdminUsersListCollectionFactory +} = require('@/modules/core/services/users/legacyAdminUsersList') const getUser = legacyGetUserFactory({ db }) const getUserByEmail = legacyGetUserByEmailFactory({ db }) @@ -70,6 +71,12 @@ const changeUserRole = changeUserRoleFactory({ }) const searchUsers = searchUsersFactory({ db }) const markOnboardingComplete = markOnboardingCompleteFactory({ db }) +const getAdminUsersListCollection = getAdminUsersListCollectionFactory({ + countUsers: legacyGetPaginatedUsersCountFactory({ db }), + countServerInvites: countServerInvitesFactory({ db }), + findServerInvites: findServerInvitesFactory({ db }), + getUsers: legacyGetPaginatedUsersFactory({ db }) +}) /** @type {import('@/modules/core/graph/generated/graphql').Resolvers} */ module.exports = { @@ -109,12 +116,7 @@ module.exports = { }, async adminUsers(_parent, args) { - return await getAdminUsersListCollection({ - findServerInvites: findServerInvitesFactory({ db }), - getTotalCounts: getTotalCounts({ - countServerInvites: countServerInvitesFactory({ db }) - }) - })(args) + return await getAdminUsersListCollection(args) }, async userSearch(parent, args, context) { diff --git a/packages/server/modules/core/services/users/adminUsersListService.js b/packages/server/modules/core/services/users/adminUsersListService.js deleted file mode 100644 index 9b386b825..000000000 --- a/packages/server/modules/core/services/users/adminUsersListService.js +++ /dev/null @@ -1,205 +0,0 @@ -const { db } = require('@/db/knex') -const { - legacyGetPaginatedUsersCountFactory, - legacyGetPaginatedUsersFactory -} = require('@/modules/core/repositories/users') -const { resolveTarget } = require('@/modules/serverinvites/helpers/core') -const { clamp } = require('lodash') - -/** - * @typedef {{ - * id: string, - * email: string, - * invitedById: string - * }} ServerInviteGraphqlReturnType - */ - -/** - * @typedef {{ - * registeredUser: import("@/modules/core/helpers/userHelper").UserRecord | null, - * invitedUser: ServerInviteGraphqlReturnType | null, - * id: string - * }} AdminUsersListItem - */ - -/** - * @typedef {{ - * totalCount: number, - * items: Array - * }} AdminUsersListCollection - */ - -/** - * @typedef {{ - * limit: number, - * offset: number, - * query: string | null - * }} PaginationParams - */ - -/** - * @typedef {{ - * userCount: number, - * inviteCount: number, - * totalCount: number - * }} TotalCounts - */ - -/** - * @typedef {{ - * invitesFilter: {offset: number, limit: number} | null, - * usersFilter: {offset: number, limit: number} | null - * }} UsersInvitesFilters - */ - -/** - * Sanitizing params to ensure limits aren't too high etc. - * @param {PaginationParams} params - * @returns {PaginationParams} - */ -function sanitizeParams(params) { - params.limit = clamp(params.limit || 10, 1, 200) - params.offset = Math.max(params.offset || 0, 0) -} - -/** - * Get total users & invites that we can find using these params - * @param {{ countServerInvites: import('@/modules/serverinvites/domain/operations').CountServerInvites}} param0 - */ -function getTotalCounts({ countServerInvites }) { - const countUsers = legacyGetPaginatedUsersCountFactory({ db }) - - /** - * Get total users & invites that we can find using these params - * @param {PaginationParams} params - * @returns {Promise} - */ - return async (params) => { - const { query } = params - - const [userCount, inviteCount] = await Promise.all([ - // Actual users - countUsers(query), - // Invites - countServerInvites(query) - ]) - const totalCount = userCount + inviteCount - - return { userCount, inviteCount, totalCount } - } -} - -/** - * Resolve limits & offsets for user & invite queries. All invites will always appear first, - * and only once there are no more results will users start appearing in the list - * @param {PaginationParams} params - * @param {TotalCounts} totalCounts - * @returns {UsersInvitesFilters} - */ -function resolveLimitsAndOffsets(params, totalCounts) { - const { offset, limit } = params - const { inviteCount } = totalCounts - - const inviteOffset = offset < inviteCount ? offset : null - const inviteLimit = - inviteOffset !== null ? Math.min(inviteCount - offset, limit) : null - - const userOffset = inviteOffset !== null ? 0 : offset - inviteCount - const userLimit = limit - (inviteLimit || 0) - - return { - invitesFilter: inviteLimit ? { limit: inviteLimit, offset: inviteOffset } : null, - usersFilter: userLimit ? { limit: userLimit, offset: userOffset } : null - } -} - -/** - * @param {import('@/modules/core/helpers/userHelper').UserRecord} user - * @returns {AdminUsersListItem} - */ -function mapUserToListItem(user) { - return { - invitedUser: null, - registeredUser: user, - id: `user:${user.id}` - } -} - -/** - * @param {import('@/modules/serverinvites/domain/types').ServerInviteRecord} invite - * @returns {AdminUsersListItem} - */ -function mapInviteToListItem(invite) { - return { - registeredUser: null, - invitedUser: { - id: invite.id, - invitedById: invite.inviterId, - email: resolveTarget(invite.target).userEmail - }, - id: `invite:${invite.id}` - } -} - -/** - * - * @param {{findServerInvites: import('@/modules/serverinvites/domain/operations').FindServerInvites}} param0 - */ -function retrieveItems({ findServerInvites }) { - const getUsers = legacyGetPaginatedUsersFactory({ db }) - - /** - * Retrieve all list items from DB and convert them to the target model - * @param {PaginationParams} params - * @param {TotalCounts} counts - * @returns {Promise} - */ - return async (params, counts) => { - const { invitesFilter, usersFilter } = resolveLimitsAndOffsets(params, counts) - const { query } = params - - const [invites, users] = await Promise.all([ - // Invites - invitesFilter - ? findServerInvites(query, invitesFilter.limit, invitesFilter.offset) - : [], - // Users - usersFilter ? getUsers(usersFilter.limit, usersFilter.offset, query) : [] - ]) - - return [ - // Invites first - ...invites.map((i) => mapInviteToListItem(i)), - // Users after - ...users.map((u) => mapUserToListItem(u)) - ] - } -} - -/** - * - * @param {{ getTotalCounts: (params: PaginationParams) => Promise, findServerInvites: import('@/modules/serverinvites/domain/operations').FindServerInvites}} param0 - */ -function getAdminUsersListCollection({ getTotalCounts, findServerInvites }) { - /** - * Resolve admin users list data using the specified filter params - * @param {PaginationParams} params - * @returns {Promise} - */ - return async (params) => { - sanitizeParams(params) - - const totalCounts = await getTotalCounts(params) - const items = await retrieveItems({ findServerInvites })(params, totalCounts) - - return { - items, - totalCount: totalCounts.totalCount - } - } -} - -module.exports = { - getAdminUsersListCollection, - getTotalCounts -} diff --git a/packages/server/modules/core/services/users/legacyAdminUsersList.ts b/packages/server/modules/core/services/users/legacyAdminUsersList.ts new file mode 100644 index 000000000..34538b3b2 --- /dev/null +++ b/packages/server/modules/core/services/users/legacyAdminUsersList.ts @@ -0,0 +1,147 @@ +import { + LegacyAdminUsersListItem, + LegacyAdminUsersPaginationParams, + LegacyGetAdminUsersListCollection, + LegacyGetPaginatedUsers, + LegacyGetPaginatedUsersCount +} from '@/modules/core/domain/users/operations' +import { UserRecord } from '@/modules/core/helpers/userHelper' +import { + CountServerInvites, + FindServerInvites +} from '@/modules/serverinvites/domain/operations' +import { ServerInviteRecord } from '@/modules/serverinvites/domain/types' +import { resolveTarget } from '@/modules/serverinvites/helpers/core' +import { clamp } from 'lodash' + +type LegacyGetUsersInvitesTotalCounts = { + userCount: number + inviteCount: number + totalCount: number +} + +type GetTotalCountsDeps = { + countUsers: LegacyGetPaginatedUsersCount + countServerInvites: CountServerInvites +} + +/** + * Get total users & invites that we can find using these params + */ +const getTotalCountsFactory = + (deps: GetTotalCountsDeps) => + async ( + params: LegacyAdminUsersPaginationParams + ): Promise => { + const { query } = params + + const [userCount, inviteCount] = await Promise.all([ + // Actual users + deps.countUsers(query), + // Invites + deps.countServerInvites(query) + ]) + const totalCount = userCount + inviteCount + + return { userCount, inviteCount, totalCount } + } + +/** + * Sanitizing params to ensure limits aren't too high etc. + */ +function sanitizeParams(params: LegacyAdminUsersPaginationParams) { + params.limit = clamp(params.limit || 10, 1, 200) + params.offset = Math.max(params.offset || 0, 0) +} + +/** + * Resolve limits & offsets for user & invite queries. All invites will always appear first, + * and only once there are no more results will users start appearing in the list + */ +function resolveLimitsAndOffsets( + params: LegacyAdminUsersPaginationParams, + totalCounts: LegacyGetUsersInvitesTotalCounts +) { + const { offset, limit } = params + const { inviteCount } = totalCounts + + const inviteOffset = offset < inviteCount ? offset : null + const inviteLimit = + inviteOffset !== null ? Math.min(inviteCount - offset, limit) : null + + const userOffset = inviteOffset !== null ? 0 : offset - inviteCount + const userLimit = limit - (inviteLimit || 0) + + return { + invitesFilter: inviteLimit ? { limit: inviteLimit, offset: inviteOffset } : null, + usersFilter: userLimit ? { limit: userLimit, offset: userOffset } : null + } +} + +function mapUserToListItem(user: UserRecord): LegacyAdminUsersListItem { + return { + invitedUser: null, + registeredUser: user, + id: `user:${user.id}` + } +} + +function mapInviteToListItem(invite: ServerInviteRecord): LegacyAdminUsersListItem { + return { + registeredUser: null, + invitedUser: { + id: invite.id, + invitedById: invite.inviterId, + email: resolveTarget(invite.target).userEmail! + }, + id: `invite:${invite.id}` + } +} + +type RetrieveItemsDeps = { + findServerInvites: FindServerInvites + getUsers: LegacyGetPaginatedUsers +} + +const retrieveItemsFactory = + (deps: RetrieveItemsDeps) => + async ( + params: LegacyAdminUsersPaginationParams, + counts: LegacyGetUsersInvitesTotalCounts + ) => { + const { invitesFilter, usersFilter } = resolveLimitsAndOffsets(params, counts) + const { query } = params + + const [invites, users] = await Promise.all([ + // Invites + invitesFilter + ? deps.findServerInvites(query, invitesFilter.limit, invitesFilter.offset || 0) + : [], + // Users + usersFilter ? deps.getUsers(usersFilter.limit, usersFilter.offset, query) : [] + ]) + + return [ + // Invites first + ...invites.map((i) => mapInviteToListItem(i)), + // Users after + ...users.map((u) => mapUserToListItem(u)) + ] + } + +/** + * Resolve admin users list data using the specified filter params + */ +export const getAdminUsersListCollectionFactory = + (deps: GetTotalCountsDeps & RetrieveItemsDeps): LegacyGetAdminUsersListCollection => + async (params) => { + sanitizeParams(params) + + const totalCounts = await getTotalCountsFactory(deps)(params) + const items = await retrieveItemsFactory(deps)(params, totalCounts) + + return { + items, + totalCount: totalCounts.totalCount + } + }