refactor: fix pagination with stable resolveKey, use reactive default… (#4951)
* refactor: fix pagination with stable resolveKey, use reactive defaultRoles, and remove email permission check * Changes from call * More changes from call * WIP fixing team composite cursor * paginated items fix * minor rename * composite cursor tools improved * fe undoing debugging stuff * extra fixes * invitable collabs fix --------- Co-authored-by: Kristaps Fabians Geikins <fabis94@live.com>
This commit is contained in:
committed by
GitHub
parent
794bd7c7e9
commit
c89fe339ec
@@ -31,7 +31,7 @@
|
||||
}
|
||||
]"
|
||||
:items="members"
|
||||
:loading="loading"
|
||||
:loading="isVeryFirstLoading"
|
||||
:empty-message="
|
||||
search.length || seatTypeFilter || roleFilter
|
||||
? 'No results'
|
||||
@@ -79,7 +79,7 @@
|
||||
<template #email="{ item }">
|
||||
<div class="flex">
|
||||
<span class="text-foreground-2 truncate">
|
||||
{{ canReadMemberEmail ? item.email : '-' }}
|
||||
{{ item.email }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
@@ -156,26 +156,29 @@ const targetUser = ref<SettingsWorkspacesMembersActionsMenu_UserFragment | undef
|
||||
undefined
|
||||
)
|
||||
|
||||
const defaultRoles = shallowRef([Roles.Workspace.Admin, Roles.Workspace.Member])
|
||||
|
||||
const {
|
||||
identifier,
|
||||
onInfiniteLoad,
|
||||
query: { result: membersResult, loading }
|
||||
query: { result: membersResult },
|
||||
isVeryFirstLoading
|
||||
} = usePaginatedQuery({
|
||||
query: settingsWorkspacesMembersSearchQuery,
|
||||
baseVariables: computed(() => ({
|
||||
query: search.value?.length ? search.value : null,
|
||||
limit: 10,
|
||||
slug: props.workspaceSlug,
|
||||
filter: {
|
||||
search: search.value,
|
||||
roles: roleFilter.value
|
||||
? [roleFilter.value]
|
||||
: [Roles.Workspace.Admin, Roles.Workspace.Member],
|
||||
roles: roleFilter.value ? [roleFilter.value] : defaultRoles.value,
|
||||
seatType: seatTypeFilter.value
|
||||
},
|
||||
cursor: null as Nullable<string>
|
||||
})),
|
||||
resolveKey: (vars) => [vars.query || ''],
|
||||
resolveKey: (vars) => ({
|
||||
slug: vars.slug,
|
||||
filter: vars.filter
|
||||
}),
|
||||
resolveCurrentResult: (res) => res?.workspaceBySlug.team,
|
||||
resolveNextPageVariables: (baseVars, cursor) => ({
|
||||
...baseVars,
|
||||
@@ -185,8 +188,5 @@ const {
|
||||
})
|
||||
|
||||
const workspace = computed(() => result.value?.workspaceBySlug)
|
||||
const canReadMemberEmail = computed(
|
||||
() => workspace.value?.permissions.canReadMemberEmail.authorized
|
||||
)
|
||||
const members = computed(() => membersResult.value?.workspaceBySlug.team.items)
|
||||
</script>
|
||||
|
||||
@@ -68,6 +68,9 @@ type SerializableObject = {
|
||||
| SerializableObject
|
||||
| SerializableValue[]
|
||||
| SerializableObject[]
|
||||
| Array<SerializableValue | SerializableObject>
|
||||
| undefined
|
||||
| null
|
||||
}
|
||||
|
||||
type BasicCursorContainer = {
|
||||
@@ -106,6 +109,8 @@ export const usePaginatedQuery = <
|
||||
| SerializableObject
|
||||
| SerializableValue[]
|
||||
| SerializableObject[]
|
||||
| Array<SerializableValue | SerializableObject>
|
||||
|
||||
/**
|
||||
* Predicate for resolving the current paginated result from the query result. Return undefined
|
||||
* if query hasn't finished loading yet.
|
||||
|
||||
@@ -323,7 +323,7 @@ function createCache(): InMemoryCache {
|
||||
merge: (_existing, incoming) => incoming
|
||||
},
|
||||
team: {
|
||||
keyArgs: ['filter', 'limit'],
|
||||
keyArgs: ['limit', 'filter', ['roles', 'search', 'seatType']],
|
||||
merge: buildAbstractCollectionMergeFunction(
|
||||
'WorkspaceCollaboratorCollection'
|
||||
)
|
||||
|
||||
@@ -24,7 +24,6 @@ const buildSourceMaps = ['1', 'true', true, 1].includes(BUILD_SOURCEMAPS)
|
||||
|
||||
// https://v3.nuxtjs.org/api/configuration/nuxt.config
|
||||
export default defineNuxtConfig({
|
||||
// ssr: false, // for debugging set to false (prod should always be true)
|
||||
...(buildSourceMaps ? { sourcemap: true } : {}),
|
||||
modulesDir: ['./node_modules'],
|
||||
typescript: {
|
||||
@@ -56,18 +55,18 @@ export default defineNuxtConfig({
|
||||
redisUrl: '',
|
||||
public: {
|
||||
...featureFlags,
|
||||
apiOrigin: 'UNDEFINED',
|
||||
apiOrigin: '',
|
||||
backendApiOrigin: '',
|
||||
baseUrl: '',
|
||||
mixpanelApiHost: 'UNDEFINED',
|
||||
mixpanelTokenId: 'UNDEFINED',
|
||||
mixpanelApiHost: '',
|
||||
mixpanelTokenId: '',
|
||||
logLevel: NUXT_PUBLIC_LOG_LEVEL,
|
||||
logPretty: isLogPretty,
|
||||
logCsrEmitProps: false,
|
||||
logClientApiToken: '',
|
||||
logClientApiEndpoint: '',
|
||||
speckleServerVersion: SPECKLE_SERVER_VERSION || 'unknown',
|
||||
serverName: 'UNDEFINED',
|
||||
serverName: 'unknown',
|
||||
viewerDebug: false,
|
||||
debugCoreWebVitals: false,
|
||||
datadogAppId: '',
|
||||
|
||||
@@ -276,13 +276,11 @@ export const getModelUploadsItemsFactory =
|
||||
(deps: { db: Knex }): GetModelUploadsItems =>
|
||||
async (params) => {
|
||||
const limit = clamp(params.limit || 0, 0, 100)
|
||||
const { filterByCursor, resolveNewCursor } = getCursorTools()
|
||||
const { applyCursorSortAndFilter, resolveNewCursor } = getCursorTools()
|
||||
|
||||
const q = getModelUploadsBaseQueryFactory(deps)(params)
|
||||
.orderBy(FileUploads.col.convertedLastUpdate, 'desc')
|
||||
.limit(limit)
|
||||
const q = getModelUploadsBaseQueryFactory(deps)(params).limit(limit)
|
||||
|
||||
filterByCursor({
|
||||
applyCursorSortAndFilter({
|
||||
query: q,
|
||||
cursor: params.cursor
|
||||
})
|
||||
|
||||
@@ -76,14 +76,22 @@ export const decodeCompositeCursor = <C extends object>(
|
||||
return null
|
||||
}
|
||||
|
||||
// This is to allow custom column/alias support for compositeCursorTools() - we don't want
|
||||
// to force the user to pass in the entire schema config, just the data we need
|
||||
type LimitedSchemaConfig = Pick<SchemaConfig<any, any, any>, 'col'>
|
||||
|
||||
/**
|
||||
* Simplifies working with composite cursors in SQL queries. Composite cursors are better because they
|
||||
* allow duplicate values (e.g. updatedAt date) in different rows
|
||||
*/
|
||||
export const compositeCursorTools = <
|
||||
Config extends SchemaConfig<any, any, any>,
|
||||
Config extends LimitedSchemaConfig,
|
||||
SelectedCols extends Array<keyof Config['col']>
|
||||
>(args: {
|
||||
/**
|
||||
* Db table schema config OR in case of aliased columns - manual column mapping between final aliases
|
||||
* as keys and table-prefixed column names as values
|
||||
*/
|
||||
schema: Config
|
||||
/**
|
||||
* Order of columns matters - put the primary ordering column first (e.g. updatedAt), then the secondary
|
||||
@@ -107,9 +115,10 @@ export const compositeCursorTools = <
|
||||
)
|
||||
|
||||
/**
|
||||
* Invoke this on the knex querybuilder to filter the query by the cursor
|
||||
* Invoke this on the knex querybuilder to filter the query by the cursor and apply
|
||||
* appropriate ordering
|
||||
*/
|
||||
const filterByCursor = <Query extends Knex.QueryBuilder>(params: {
|
||||
const applyCursorSortAndFilter = <Query extends Knex.QueryBuilder>(params: {
|
||||
query: Query
|
||||
/**
|
||||
* If falsy, filter will be skipped
|
||||
@@ -121,9 +130,16 @@ export const compositeCursorTools = <
|
||||
sort?: 'desc' | 'asc'
|
||||
}) => {
|
||||
const { query, sort = 'desc' } = params
|
||||
|
||||
// Apply orderBy for each cursor column w/ proper sort direction
|
||||
args.cols.forEach((col) => {
|
||||
query.orderBy(args.schema.col[col], sort)
|
||||
})
|
||||
|
||||
const cursor = isString(params.cursor) ? decode(params.cursor) : params.cursor
|
||||
if (!cursor) return query
|
||||
|
||||
// Apply cursor filter
|
||||
const colCount = args.cols.length
|
||||
|
||||
const sql = `(${times(colCount, () => '??').join(', ')}) ${
|
||||
@@ -161,7 +177,7 @@ export const compositeCursorTools = <
|
||||
return {
|
||||
encode,
|
||||
decode,
|
||||
filterByCursor,
|
||||
applyCursorSortAndFilter,
|
||||
resolveNewCursor
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,42 +1,24 @@
|
||||
import {
|
||||
decodeIsoDateCursor,
|
||||
encodeIsoDateCursor,
|
||||
Collection
|
||||
} from '@/modules/shared/helpers/graphqlHelper'
|
||||
import { Collection } from '@/modules/shared/helpers/graphqlHelper'
|
||||
import { MaybeNullOrUndefined } from '@speckle/shared'
|
||||
|
||||
type GetPaginatedItemsArgs = {
|
||||
limit: number
|
||||
cursor?: string
|
||||
cursor?: MaybeNullOrUndefined<string>
|
||||
}
|
||||
|
||||
export const getPaginatedItemsFactory =
|
||||
<TArgs extends GetPaginatedItemsArgs, T extends { createdAt: Date }>({
|
||||
<TArgs extends GetPaginatedItemsArgs, T>({
|
||||
getItems,
|
||||
getTotalCount
|
||||
}: {
|
||||
getItems: (args: TArgs) => Promise<T[]>
|
||||
getItems: (args: TArgs) => Promise<{ items: T[]; cursor: string | null }>
|
||||
getTotalCount: (args: Omit<TArgs, 'cursor' | 'limit'>) => Promise<number>
|
||||
}) =>
|
||||
async (args: TArgs): Promise<Collection<T>> => {
|
||||
const totalCount = await getTotalCount(args)
|
||||
if (args.limit === 0) {
|
||||
return {
|
||||
cursor: null,
|
||||
items: [],
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
const maybeDecodedCursor = args.cursor ? decodeIsoDateCursor(args.cursor) : null
|
||||
const items = await getItems({
|
||||
...args,
|
||||
cursor: maybeDecodedCursor ?? undefined
|
||||
})
|
||||
|
||||
let cursor = null
|
||||
if (items.length === args.limit) {
|
||||
const lastItem = items.at(-1)
|
||||
cursor = lastItem ? encodeIsoDateCursor(lastItem.createdAt) : null
|
||||
}
|
||||
const [totalCount, { items, cursor }] = await Promise.all([
|
||||
getTotalCount(args),
|
||||
args.limit === 0 ? { cursor: null, items: [] } : getItems(args)
|
||||
])
|
||||
|
||||
return {
|
||||
items,
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
describe('paginatedItems @shared', () => {
|
||||
describe('getPaginatedItemsFactory creates a function, that', () => {
|
||||
it('converts undefined cursors to null')
|
||||
it(
|
||||
'calculates a new cursor from createdAt if there could be more items to be queried'
|
||||
)
|
||||
it('returns items and count from dependencies')
|
||||
})
|
||||
})
|
||||
@@ -128,7 +128,9 @@ export type QueryWorkspacesArgs = CountWorkspacesArgs & {
|
||||
limit: number
|
||||
cursor?: string
|
||||
}
|
||||
export type QueryWorkspaces = (args: QueryWorkspacesArgs) => Promise<Workspace[]>
|
||||
export type QueryWorkspaces = (
|
||||
args: QueryWorkspacesArgs
|
||||
) => Promise<{ items: Workspace[]; cursor: string | null }>
|
||||
export type CountWorkspaces = (args: CountWorkspacesArgs) => Promise<number>
|
||||
export type GetProjectWorkspace = (args: {
|
||||
projectId: string
|
||||
@@ -136,10 +138,8 @@ export type GetProjectWorkspace = (args: {
|
||||
|
||||
/** Workspace Roles */
|
||||
|
||||
export type GetWorkspaceCollaboratorsArgs = {
|
||||
export type GetWorkspaceCollaboratorsBaseArgs = {
|
||||
workspaceId: string
|
||||
limit: number
|
||||
cursor?: string
|
||||
filter?: {
|
||||
/**
|
||||
* Optionally filter by workspace role(s)
|
||||
@@ -158,16 +158,17 @@ export type GetWorkspaceCollaboratorsArgs = {
|
||||
hasAccessToEmail?: boolean
|
||||
}
|
||||
|
||||
export type GetWorkspaceCollaborators = (
|
||||
args: GetWorkspaceCollaboratorsArgs
|
||||
) => Promise<WorkspaceTeam>
|
||||
|
||||
type GetWorkspaceCollaboratorsTotalCountArgs = {
|
||||
workspaceId: string
|
||||
export type GetWorkspaceCollaboratorsArgs = GetWorkspaceCollaboratorsBaseArgs & {
|
||||
limit: number
|
||||
cursor?: string
|
||||
}
|
||||
|
||||
export type GetWorkspaceCollaborators = (
|
||||
args: GetWorkspaceCollaboratorsArgs
|
||||
) => Promise<{ items: WorkspaceTeam; cursor: string | null }>
|
||||
|
||||
export type GetWorkspaceCollaboratorsTotalCount = (
|
||||
args: GetWorkspaceCollaboratorsTotalCountArgs
|
||||
args: GetWorkspaceCollaboratorsBaseArgs
|
||||
) => Promise<number>
|
||||
|
||||
type DeleteWorkspaceRoleArgs = {
|
||||
|
||||
@@ -250,7 +250,9 @@ export const onWorkspaceRoleDeletedFactory =
|
||||
updatedByUserId: string
|
||||
}) => {
|
||||
// Resolve a fallback admin
|
||||
const [admin] = await deps.getWorkspaceCollaborators({
|
||||
const {
|
||||
items: [admin]
|
||||
} = await deps.getWorkspaceCollaborators({
|
||||
workspaceId,
|
||||
limit: 1,
|
||||
filter: {
|
||||
@@ -330,7 +332,9 @@ export const onWorkspaceSeatUpdatedFactory =
|
||||
})
|
||||
|
||||
// Resolve a fallback admin
|
||||
const [admin] = await deps.getWorkspaceCollaborators({
|
||||
const {
|
||||
items: [admin]
|
||||
} = await deps.getWorkspaceCollaborators({
|
||||
workspaceId,
|
||||
limit: 1,
|
||||
filter: {
|
||||
@@ -415,7 +419,9 @@ export const onWorkspaceRoleUpdatedFactory =
|
||||
if (!seatType) return
|
||||
|
||||
// Resolve a fallback admin
|
||||
const [admin] = await deps.getWorkspaceCollaborators({
|
||||
const {
|
||||
items: [admin]
|
||||
} = await deps.getWorkspaceCollaborators({
|
||||
workspaceId,
|
||||
limit: 1,
|
||||
filter: {
|
||||
|
||||
@@ -197,8 +197,6 @@ import { getProjectFactory } from '@/modules/core/repositories/projects'
|
||||
import { getProjectRegionKey } from '@/modules/multiregion/utils/regionSelector'
|
||||
import { scheduleJob } from '@/modules/multiregion/services/queue'
|
||||
import { updateWorkspacePlanFactory } from '@/modules/gatekeeper/services/workspacePlans'
|
||||
import { GetWorkspaceCollaboratorsArgs } from '@/modules/workspaces/domain/operations'
|
||||
import { WorkspaceTeamMember } from '@/modules/workspaces/domain/types'
|
||||
import { UsersMeta } from '@/modules/core/dbSchema'
|
||||
import { setUserActiveWorkspaceFactory } from '@/modules/workspaces/repositories/users'
|
||||
import { getGenericRedis } from '@/modules/shared/redis/redis'
|
||||
@@ -2086,10 +2084,7 @@ export = FF_WORKSPACES_MODULE_ENABLED
|
||||
},
|
||||
LimitedWorkspace: {
|
||||
team: async (parent, args) => {
|
||||
const team = await getPaginatedItemsFactory<
|
||||
Pick<GetWorkspaceCollaboratorsArgs, 'workspaceId' | 'limit' | 'cursor'>,
|
||||
WorkspaceTeamMember
|
||||
>({
|
||||
const team = await getPaginatedItemsFactory({
|
||||
getItems: getWorkspaceCollaboratorsFactory({ db }),
|
||||
getTotalCount: getWorkspaceCollaboratorsTotalCountFactory({ db })
|
||||
})({
|
||||
@@ -2107,7 +2102,7 @@ export = FF_WORKSPACES_MODULE_ENABLED
|
||||
roles: [Roles.Workspace.Admin]
|
||||
}
|
||||
})
|
||||
return team
|
||||
return team.items
|
||||
}
|
||||
},
|
||||
ActiveUserMutations: {
|
||||
|
||||
@@ -10,6 +10,7 @@ import { metaHelpers } from '@/modules/core/helpers/meta'
|
||||
import { StreamAclRecord, UserRecord } from '@/modules/core/helpers/types'
|
||||
import { removePrivateFields } from '@/modules/core/helpers/userHelper'
|
||||
import { formatJsonArrayRecords } from '@/modules/shared/helpers/dbHelper'
|
||||
import { compositeCursorTools } from '@/modules/shared/helpers/graphqlHelper'
|
||||
import { SetUserActiveWorkspace } from '@/modules/workspaces/domain/operations'
|
||||
import { WorkspaceTeamMember } from '@/modules/workspaces/domain/types'
|
||||
import { WorkspaceAcl as WorkspaceAclRecord } from '@/modules/workspacesCore/domain/types'
|
||||
@@ -45,6 +46,12 @@ const buildInvitableCollaboratorsByProjectIdQueryFactory =
|
||||
}) => {
|
||||
const query = tables
|
||||
.users(db)
|
||||
.select([
|
||||
...Users.cols,
|
||||
WorkspaceAcl.groupArray('workspaceAcl'),
|
||||
ServerAcl.groupArray('serverAcl'),
|
||||
UserEmails.groupArray('emails')
|
||||
])
|
||||
.join(WorkspaceAcl.name, WorkspaceAcl.col.userId, Users.col.id)
|
||||
.join(Streams.name, Streams.col.workspaceId, WorkspaceAcl.col.workspaceId)
|
||||
.join(ServerAcl.name, ServerAcl.col.userId, Users.col.id)
|
||||
@@ -81,25 +88,27 @@ export const getInvitableCollaboratorsByProjectIdFactory =
|
||||
}
|
||||
cursor?: string
|
||||
limit: number
|
||||
}): Promise<WorkspaceTeamMember[]> => {
|
||||
}): Promise<{ items: WorkspaceTeamMember[]; cursor: string | null }> => {
|
||||
const { workspaceId, projectId, search } = filter
|
||||
const query = buildInvitableCollaboratorsByProjectIdQueryFactory({ db })({
|
||||
workspaceId,
|
||||
projectId,
|
||||
search
|
||||
})
|
||||
if (cursor) {
|
||||
query.andWhere(Users.col.createdAt, '<', cursor)
|
||||
}
|
||||
const { applyCursorSortAndFilter, resolveNewCursor } = compositeCursorTools({
|
||||
schema: Users,
|
||||
cols: ['createdAt', 'id']
|
||||
})
|
||||
|
||||
applyCursorSortAndFilter({
|
||||
query,
|
||||
cursor
|
||||
})
|
||||
|
||||
query.limit(limit)
|
||||
|
||||
const res = await query
|
||||
.orderBy(Users.col.createdAt, 'desc')
|
||||
.limit(limit)
|
||||
.select([
|
||||
...Users.cols,
|
||||
WorkspaceAcl.groupArray('workspaceAcl'),
|
||||
ServerAcl.groupArray('serverAcl'),
|
||||
UserEmails.groupArray('emails')
|
||||
])
|
||||
const nextCursor = resolveNewCursor(res)
|
||||
|
||||
const formattedRes = res.map((row) => {
|
||||
const workspaceAcl = formatJsonArrayRecords(
|
||||
@@ -119,7 +128,7 @@ export const getInvitableCollaboratorsByProjectIdFactory =
|
||||
}
|
||||
})
|
||||
|
||||
return formattedRes
|
||||
return { items: formattedRes, cursor: nextCursor }
|
||||
}
|
||||
|
||||
export const countInvitableCollaboratorsByProjectIdFactory =
|
||||
@@ -134,11 +143,16 @@ export const countInvitableCollaboratorsByProjectIdFactory =
|
||||
}
|
||||
}) => {
|
||||
const { workspaceId, projectId, search } = filter
|
||||
const query = buildInvitableCollaboratorsByProjectIdQueryFactory({ db })({
|
||||
workspaceId,
|
||||
projectId,
|
||||
search
|
||||
})
|
||||
const [res] = await query.count()
|
||||
const query = db
|
||||
.from(
|
||||
buildInvitableCollaboratorsByProjectIdQueryFactory({ db })({
|
||||
workspaceId,
|
||||
projectId,
|
||||
search
|
||||
}).as('sq1')
|
||||
)
|
||||
.count()
|
||||
|
||||
const [res] = await query
|
||||
return parseInt(res?.count?.toString() ?? '0')
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { UserEmails } from '@/modules/core/dbSchema'
|
||||
import { compositeCursorTools } from '@/modules/shared/helpers/graphqlHelper'
|
||||
import {
|
||||
CreateWorkspaceJoinRequest,
|
||||
GetWorkspaceJoinRequest,
|
||||
@@ -96,18 +97,30 @@ export const getAdminWorkspaceJoinRequestsFactory =
|
||||
cursor?: string
|
||||
limit: number
|
||||
}) => {
|
||||
const { applyCursorSortAndFilter, resolveNewCursor } = compositeCursorTools({
|
||||
schema: WorkspaceJoinRequests,
|
||||
cols: ['createdAt', 'userId']
|
||||
})
|
||||
const query = adminWorkspaceJoinRequestsBaseQueryFactory(db)(filter)
|
||||
applyCursorSortAndFilter({
|
||||
query,
|
||||
cursor
|
||||
})
|
||||
|
||||
if (cursor) {
|
||||
query.andWhere(WorkspaceJoinRequests.col.createdAt, '<', cursor)
|
||||
}
|
||||
return await query
|
||||
query
|
||||
.select<WorkspaceJoinRequest[]>([
|
||||
...WorkspaceJoinRequests.cols,
|
||||
UserEmails.col.email
|
||||
])
|
||||
.orderBy(WorkspaceJoinRequests.col.createdAt, 'desc')
|
||||
.limit(limit)
|
||||
|
||||
const items = await query
|
||||
const newCursor = resolveNewCursor(items)
|
||||
|
||||
return {
|
||||
items,
|
||||
cursor: newCursor
|
||||
}
|
||||
}
|
||||
|
||||
export const countAdminWorkspaceJoinRequestsFactory =
|
||||
@@ -138,7 +151,7 @@ const workspaceJoinRequestsBaseQueryFactory =
|
||||
|
||||
export const getWorkspaceJoinRequestsFactory =
|
||||
({ db }: { db: Knex }) =>
|
||||
({
|
||||
async ({
|
||||
filter,
|
||||
cursor,
|
||||
limit
|
||||
@@ -148,14 +161,24 @@ export const getWorkspaceJoinRequestsFactory =
|
||||
limit: number
|
||||
}) => {
|
||||
const query = workspaceJoinRequestsBaseQueryFactory(db)(filter)
|
||||
const { applyCursorSortAndFilter, resolveNewCursor } = compositeCursorTools({
|
||||
schema: WorkspaceJoinRequests,
|
||||
cols: ['createdAt', 'userId']
|
||||
})
|
||||
applyCursorSortAndFilter({
|
||||
query,
|
||||
cursor
|
||||
})
|
||||
|
||||
if (cursor) {
|
||||
query.andWhere(WorkspaceJoinRequests.col.createdAt, '<', cursor)
|
||||
query.select<WorkspaceJoinRequest[]>(WorkspaceJoinRequests.cols).limit(limit)
|
||||
|
||||
const items = await query
|
||||
const newCursor = resolveNewCursor(items)
|
||||
|
||||
return {
|
||||
items,
|
||||
cursor: newCursor
|
||||
}
|
||||
return query
|
||||
.select<WorkspaceJoinRequest[]>(WorkspaceJoinRequests.cols)
|
||||
.orderBy(WorkspaceJoinRequests.col.createdAt, 'desc')
|
||||
.limit(limit)
|
||||
}
|
||||
|
||||
export const countWorkspaceJoinRequestsFactory =
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
GetWorkspaceBySlug,
|
||||
GetWorkspaceBySlugOrId,
|
||||
GetWorkspaceCollaborators,
|
||||
GetWorkspaceCollaboratorsBaseArgs,
|
||||
GetWorkspaceCollaboratorsTotalCount,
|
||||
GetWorkspaceCreationState,
|
||||
GetWorkspaceDomains,
|
||||
@@ -70,7 +71,7 @@ import {
|
||||
UserEmails,
|
||||
Users
|
||||
} from '@/modules/core/dbSchema'
|
||||
import { removePrivateFields } from '@/modules/core/helpers/userHelper'
|
||||
import { removePrivateFields, UserRecord } from '@/modules/core/helpers/userHelper'
|
||||
|
||||
import { clamp, has, isObjectLike } from 'lodash'
|
||||
import {
|
||||
@@ -78,6 +79,7 @@ import {
|
||||
WorkspaceTeamMember
|
||||
} from '@/modules/workspaces/domain/types'
|
||||
import {
|
||||
compositeCursorTools,
|
||||
decodeCompositeCursor,
|
||||
decodeCursor,
|
||||
encodeCompositeCursor,
|
||||
@@ -86,6 +88,7 @@ import {
|
||||
import { adminOverrideEnabled } from '@/modules/shared/helpers/envHelper'
|
||||
|
||||
const tables = {
|
||||
users: (db: Knex) => db<UserRecord>(Users.name),
|
||||
branches: (db: Knex) => db<BranchRecord>('branches'),
|
||||
streams: (db: Knex) => db<StreamRecord>('streams'),
|
||||
streamAcl: (db: Knex) => db<StreamAclRecord>('stream_acl'),
|
||||
@@ -311,15 +314,24 @@ const buildWorkspacesQuery = ({ db, search }: { db: Knex; search?: string }) =>
|
||||
export const queryWorkspacesFactory =
|
||||
({ db }: { db: Knex }): QueryWorkspaces =>
|
||||
async ({ limit, cursor, filter }) => {
|
||||
const { applyCursorSortAndFilter, resolveNewCursor } = compositeCursorTools({
|
||||
schema: Workspaces,
|
||||
cols: ['createdAt', 'id']
|
||||
})
|
||||
|
||||
const query = buildWorkspacesQuery({ db, search: filter?.search })
|
||||
.select()
|
||||
.orderBy('createdAt', 'desc')
|
||||
.limit(limit)
|
||||
|
||||
if (cursor) {
|
||||
query.andWhere('createdAt', '<', cursor)
|
||||
}
|
||||
return await query
|
||||
applyCursorSortAndFilter({
|
||||
query,
|
||||
cursor
|
||||
})
|
||||
|
||||
const res = await query
|
||||
const newCursor = resolveNewCursor(res)
|
||||
|
||||
return { items: res, cursor: newCursor }
|
||||
}
|
||||
|
||||
export const countWorkspacesFactory =
|
||||
@@ -445,19 +457,12 @@ export const upsertWorkspaceRoleFactory =
|
||||
.merge(['role'])
|
||||
}
|
||||
|
||||
export const getWorkspaceCollaboratorsTotalCountFactory =
|
||||
({ db }: { db: Knex }): GetWorkspaceCollaboratorsTotalCount =>
|
||||
async ({ workspaceId }) => {
|
||||
const [res] = await DbWorkspaceAcl.knex(db).where({ workspaceId }).count()
|
||||
const count = parseInt(res.count)
|
||||
return count || 0
|
||||
}
|
||||
const getWorkspaceCollaboratorsBaseQuery =
|
||||
(deps: { db: Knex }) => (params: GetWorkspaceCollaboratorsBaseArgs) => {
|
||||
const { workspaceId, filter = {} } = params
|
||||
|
||||
export const getWorkspaceCollaboratorsFactory =
|
||||
({ db }: { db: Knex }): GetWorkspaceCollaborators =>
|
||||
async ({ workspaceId, filter = {}, cursor, limit = 25, hasAccessToEmail }) => {
|
||||
const query = db
|
||||
.from(Users.name)
|
||||
const query = tables
|
||||
.users(deps.db)
|
||||
.select<Array<WorkspaceTeamMember & { workspaceRoleCreatedAt: Date }>>(
|
||||
...Users.cols,
|
||||
ServerAcl.col.role,
|
||||
@@ -474,7 +479,6 @@ export const getWorkspaceCollaboratorsFactory =
|
||||
// it will not be surfaced by this query
|
||||
//
|
||||
.andWhere(UserEmails.col.primary, true)
|
||||
.orderBy('workspaceRoleCreatedAt', 'desc')
|
||||
|
||||
const { search, roles, seatType, excludeUserIds } = filter || {}
|
||||
|
||||
@@ -505,9 +509,40 @@ export const getWorkspaceCollaboratorsFactory =
|
||||
})
|
||||
}
|
||||
|
||||
if (cursor) {
|
||||
query.andWhere(DbWorkspaceAcl.col.createdAt, '<', cursor)
|
||||
}
|
||||
return query
|
||||
}
|
||||
|
||||
export const getWorkspaceCollaboratorsTotalCountFactory =
|
||||
({ db }: { db: Knex }): GetWorkspaceCollaboratorsTotalCount =>
|
||||
async (params) => {
|
||||
const q = db
|
||||
.from(getWorkspaceCollaboratorsBaseQuery({ db })(params).as('t1'))
|
||||
.count()
|
||||
|
||||
const [res] = await q
|
||||
const count = parseInt(res.count + '')
|
||||
return count || 0
|
||||
}
|
||||
|
||||
export const getWorkspaceCollaboratorsFactory =
|
||||
({ db }: { db: Knex }): GetWorkspaceCollaborators =>
|
||||
async (params) => {
|
||||
const { limit = 25, hasAccessToEmail } = params
|
||||
const query = getWorkspaceCollaboratorsBaseQuery({ db })(params)
|
||||
const { applyCursorSortAndFilter, resolveNewCursor } = compositeCursorTools({
|
||||
schema: {
|
||||
col: {
|
||||
workspaceRoleCreatedAt: DbWorkspaceAcl.col.createdAt,
|
||||
id: Users.col.id
|
||||
}
|
||||
},
|
||||
cols: ['workspaceRoleCreatedAt', 'id']
|
||||
})
|
||||
|
||||
applyCursorSortAndFilter({
|
||||
query,
|
||||
cursor: params.cursor
|
||||
})
|
||||
|
||||
if (limit) {
|
||||
query.limit(clamp(limit, 0, 100))
|
||||
@@ -521,8 +556,9 @@ export const getWorkspaceCollaboratorsFactory =
|
||||
workspaceId: i.workspaceId,
|
||||
role: i.role
|
||||
}))
|
||||
const newCursor = resolveNewCursor(items)
|
||||
|
||||
return items
|
||||
return { items, cursor: newCursor }
|
||||
}
|
||||
|
||||
export const storeWorkspaceDomainFactory =
|
||||
|
||||
@@ -77,7 +77,7 @@ export const sendWorkspaceJoinRequestReceivedEmailFactory =
|
||||
}): SendWorkspaceJoinRequestReceivedEmail =>
|
||||
async (args) => {
|
||||
const { requester, workspace } = args
|
||||
const [serverInfo, workspaceAdmins] = await Promise.all([
|
||||
const [serverInfo, { items: workspaceAdmins }] = await Promise.all([
|
||||
getServerInfo(),
|
||||
getWorkspaceCollaborators({
|
||||
workspaceId: workspace.id,
|
||||
|
||||
@@ -90,7 +90,9 @@ export const requestToJoinWorkspaceFactory =
|
||||
}
|
||||
|
||||
if (workspace.discoverabilityAutoJoinEnabled) {
|
||||
const [workspaceAdmin] = await getWorkspaceTeam({
|
||||
const {
|
||||
items: [workspaceAdmin]
|
||||
} = await getWorkspaceTeam({
|
||||
workspaceId,
|
||||
limit: 1,
|
||||
filter: {
|
||||
|
||||
@@ -250,7 +250,7 @@ describe('Workspace repositories', () => {
|
||||
})
|
||||
|
||||
it('returns all workspace members', async () => {
|
||||
const team = await getWorkspaceCollaborators({
|
||||
const { items: team } = await getWorkspaceCollaborators({
|
||||
workspaceId: testWorkspace.id,
|
||||
limit: 50
|
||||
})
|
||||
@@ -298,7 +298,7 @@ describe('Workspace repositories', () => {
|
||||
})
|
||||
|
||||
it('limits search results to specified workspace', async () => {
|
||||
const result = await getWorkspaceCollaborators({
|
||||
const { items: result } = await getWorkspaceCollaborators({
|
||||
workspaceId: testWorkspaces[2].id,
|
||||
limit: 50,
|
||||
filter: { search: 'John' }
|
||||
|
||||
@@ -86,7 +86,7 @@ describe('Workspace repositories', () => {
|
||||
})
|
||||
|
||||
it('should return all workspace collaborators not members of the project', async () => {
|
||||
const invitable = await getInvitableCollaboratorsByProjectId({
|
||||
const { items: invitable } = await getInvitableCollaboratorsByProjectId({
|
||||
filter: {
|
||||
workspaceId: testWorkspace.id,
|
||||
projectId: testProject.id
|
||||
@@ -100,7 +100,7 @@ describe('Workspace repositories', () => {
|
||||
])
|
||||
})
|
||||
it('should should filter by user name', async () => {
|
||||
const invitable = await getInvitableCollaboratorsByProjectId({
|
||||
const { items: invitable } = await getInvitableCollaboratorsByProjectId({
|
||||
filter: {
|
||||
workspaceId: testWorkspace.id,
|
||||
projectId: testProject.id,
|
||||
@@ -114,7 +114,7 @@ describe('Workspace repositories', () => {
|
||||
])
|
||||
})
|
||||
it('should should filter by user email', async () => {
|
||||
const invitable = await getInvitableCollaboratorsByProjectId({
|
||||
const { items: invitable } = await getInvitableCollaboratorsByProjectId({
|
||||
filter: {
|
||||
workspaceId: testWorkspace.id,
|
||||
projectId: testProject.id,
|
||||
@@ -128,7 +128,7 @@ describe('Workspace repositories', () => {
|
||||
])
|
||||
})
|
||||
it('should should filter by user name and email', async () => {
|
||||
const invitable = await getInvitableCollaboratorsByProjectId({
|
||||
const { items: invitable } = await getInvitableCollaboratorsByProjectId({
|
||||
filter: {
|
||||
workspaceId: testWorkspace.id,
|
||||
projectId: testProject.id,
|
||||
|
||||
@@ -133,7 +133,7 @@ const { FF_WORKSPACES_MODULE_ENABLED } = getFeatureFlags()
|
||||
getWorkspaceWithDomains: async () => null,
|
||||
getUserEmails: async () => [],
|
||||
addOrUpdateWorkspaceRole: async () => {},
|
||||
getWorkspaceTeam: async () => []
|
||||
getWorkspaceTeam: async () => ({ items: [], cursor: null })
|
||||
})({ workspaceId: createRandomString(), userId: createRandomString() })
|
||||
)
|
||||
|
||||
@@ -150,7 +150,7 @@ const { FF_WORKSPACES_MODULE_ENABLED } = getFeatureFlags()
|
||||
getWorkspaceWithDomains: async () => null,
|
||||
getUserEmails: async () => [],
|
||||
addOrUpdateWorkspaceRole: async () => {},
|
||||
getWorkspaceTeam: async () => []
|
||||
getWorkspaceTeam: async () => ({ items: [], cursor: null })
|
||||
})({ workspaceId: createRandomString(), userId: createRandomString() })
|
||||
)
|
||||
|
||||
@@ -185,7 +185,7 @@ const { FF_WORKSPACES_MODULE_ENABLED } = getFeatureFlags()
|
||||
workspace as unknown as WorkspaceWithDomains,
|
||||
getUserEmails: async () => [],
|
||||
addOrUpdateWorkspaceRole: async () => {},
|
||||
getWorkspaceTeam: async () => []
|
||||
getWorkspaceTeam: async () => ({ items: [], cursor: null })
|
||||
})({ workspaceId: createRandomString(), userId: createRandomString() })
|
||||
)
|
||||
|
||||
@@ -243,7 +243,7 @@ const { FF_WORKSPACES_MODULE_ENABLED } = getFeatureFlags()
|
||||
getUserEmails: async () =>
|
||||
[{ email: user.email, verified: true }] as unknown as UserEmail[],
|
||||
addOrUpdateWorkspaceRole: async () => {},
|
||||
getWorkspaceTeam: async () => []
|
||||
getWorkspaceTeam: async () => ({ items: [], cursor: null })
|
||||
})({ workspaceId: workspace.id, userId: user.id })
|
||||
).to.equal(true)
|
||||
|
||||
@@ -314,7 +314,7 @@ const { FF_WORKSPACES_MODULE_ENABLED } = getFeatureFlags()
|
||||
getUserEmails: async () =>
|
||||
[{ email: user.email, verified: true }] as unknown as UserEmail[],
|
||||
addOrUpdateWorkspaceRole: async () => {},
|
||||
getWorkspaceTeam: async () => []
|
||||
getWorkspaceTeam: async () => ({ items: [], cursor: null })
|
||||
})
|
||||
|
||||
expect(
|
||||
|
||||
@@ -463,6 +463,11 @@ describe('Workspaces GQL CRUD', () => {
|
||||
limit: 10,
|
||||
cursor: resA.data?.workspace.team.cursor
|
||||
})
|
||||
const resC = await largeWorkspaceApollo.execute(GetWorkspaceTeamDocument, {
|
||||
workspaceId: largeWorkspace.id,
|
||||
limit: 10,
|
||||
cursor: resB.data?.workspace.team.cursor
|
||||
})
|
||||
|
||||
expect(resA).to.not.haveGraphQLErrors()
|
||||
expect(resA.data?.workspace.team.items.length).to.equal(2)
|
||||
@@ -475,7 +480,11 @@ describe('Workspaces GQL CRUD', () => {
|
||||
|
||||
expect(resB).to.not.haveGraphQLErrors()
|
||||
expect(resB.data?.workspace.team.items.length).to.equal(4)
|
||||
expect(resB.data?.workspace.team.cursor).to.be.null
|
||||
expect(resB.data?.workspace.team.cursor).to.be.not.null
|
||||
|
||||
expect(resC).to.not.haveGraphQLErrors()
|
||||
expect(resC.data?.workspace.team.items.length).to.equal(0)
|
||||
expect(resC.data?.workspace.team.cursor).to.be.null
|
||||
})
|
||||
|
||||
it('should return correct total count', async () => {
|
||||
|
||||
Reference in New Issue
Block a user