cf833a7719
* fix(workspaces): workspace role sync * role changes fixed + validated * seat changes validated * fix tests --------- Co-authored-by: Charles Driesler <chuck@speckle.systems>
732 lines
22 KiB
TypeScript
732 lines
22 KiB
TypeScript
import {
|
|
Workspace,
|
|
WorkspaceAcl,
|
|
WorkspaceDomain,
|
|
WorkspaceJoinRequest,
|
|
WorkspaceWithOptionalRole
|
|
} from '@/modules/workspacesCore/domain/types'
|
|
import {
|
|
CountDomainsByWorkspaceId,
|
|
CountWorkspaceRoleWithOptionalProjectRole,
|
|
CountWorkspaces,
|
|
DeleteWorkspace,
|
|
DeleteWorkspaceDomain,
|
|
DeleteWorkspaceRole,
|
|
GetPaginatedWorkspaceProjects,
|
|
GetPaginatedWorkspaceProjectsArgs,
|
|
GetPaginatedWorkspaceProjectsItems,
|
|
GetPaginatedWorkspaceProjectsTotalCount,
|
|
GetUserDiscoverableWorkspaces,
|
|
GetUserIdsWithRoleInWorkspace,
|
|
GetWorkspace,
|
|
GetWorkspaceBySlug,
|
|
GetWorkspaceBySlugOrId,
|
|
GetWorkspaceCollaborators,
|
|
GetWorkspaceCollaboratorsTotalCount,
|
|
GetWorkspaceCreationState,
|
|
GetWorkspaceDomains,
|
|
GetWorkspaceRoleForUser,
|
|
GetWorkspaceRoles,
|
|
GetWorkspaceRolesForUser,
|
|
GetWorkspaceWithDomains,
|
|
GetWorkspaces,
|
|
GetWorkspacesProjectsCounts,
|
|
GetWorkspacesRolesForUsers,
|
|
QueryWorkspaces,
|
|
StoreWorkspaceDomain,
|
|
UpsertWorkspace,
|
|
UpsertWorkspaceCreationState,
|
|
UpsertWorkspaceRole
|
|
} from '@/modules/workspaces/domain/operations'
|
|
import { Knex } from 'knex'
|
|
import { Roles } from '@speckle/shared'
|
|
import {
|
|
ServerAclRecord,
|
|
BranchRecord,
|
|
StreamAclRecord,
|
|
StreamRecord
|
|
} from '@/modules/core/helpers/types'
|
|
import { WorkspaceInvalidRoleError } from '@/modules/workspaces/errors/workspace'
|
|
import {
|
|
WorkspaceAcl as DbWorkspaceAcl,
|
|
WorkspaceDomains,
|
|
Workspaces
|
|
} from '@/modules/workspaces/helpers/db'
|
|
import {
|
|
knex,
|
|
ServerAcl,
|
|
ServerInvites,
|
|
StreamAcl,
|
|
Streams,
|
|
Users
|
|
} from '@/modules/core/dbSchema'
|
|
import { removePrivateFields } from '@/modules/core/helpers/userHelper'
|
|
import {
|
|
filterByResource,
|
|
InvitesRetrievalValidityFilter
|
|
} from '@/modules/serverinvites/repositories/serverInvites'
|
|
import { WorkspaceInviteResourceType } from '@/modules/workspacesCore/domain/constants'
|
|
import { clamp, has, isObjectLike } from 'lodash'
|
|
import {
|
|
WorkspaceCreationState,
|
|
WorkspaceTeamMember
|
|
} from '@/modules/workspaces/domain/types'
|
|
import {
|
|
decodeCompositeCursor,
|
|
encodeCompositeCursor
|
|
} from '@/modules/shared/helpers/graphqlHelper'
|
|
import { adminOverrideEnabled } from '@/modules/shared/helpers/envHelper'
|
|
|
|
const tables = {
|
|
branches: (db: Knex) => db<BranchRecord>('branches'),
|
|
streams: (db: Knex) => db<StreamRecord>('streams'),
|
|
streamAcl: (db: Knex) => db<StreamAclRecord>('stream_acl'),
|
|
serverAcl: (db: Knex) => db<ServerAclRecord>(ServerAcl.name),
|
|
workspaces: (db: Knex) => db<Workspace>('workspaces'),
|
|
workspaceDomains: (db: Knex) => db<WorkspaceDomain>('workspace_domains'),
|
|
workspacesAcl: (db: Knex) => db<WorkspaceAcl>('workspace_acl'),
|
|
workspaceCreationState: (db: Knex) =>
|
|
db<WorkspaceCreationState>('workspace_creation_state'),
|
|
workspaceJoinRequests: (db: Knex) =>
|
|
db<WorkspaceJoinRequest>('workspace_join_requests')
|
|
}
|
|
|
|
export const getUserDiscoverableWorkspacesFactory =
|
|
({ db }: { db: Knex }): GetUserDiscoverableWorkspaces =>
|
|
async ({ domains, userId }) => {
|
|
if (domains.length === 0) {
|
|
return []
|
|
}
|
|
return (await tables
|
|
.workspaces(db)
|
|
.select('workspaces.id as id', 'name', 'slug', 'description', 'logo')
|
|
.distinctOn('workspaces.id')
|
|
.join('workspace_domains', 'workspace_domains.workspaceId', 'workspaces.id')
|
|
.leftJoin(
|
|
tables.workspacesAcl(db).select('*').where({ userId }).as('acl'),
|
|
'acl.workspaceId',
|
|
'workspaces.id'
|
|
)
|
|
.leftJoin(
|
|
tables
|
|
.workspaceJoinRequests(db)
|
|
.select('*')
|
|
.where({ userId })
|
|
.as('joinRequest'),
|
|
'joinRequest.workspaceId',
|
|
'workspaces.id'
|
|
)
|
|
.whereNull('joinRequest.workspaceId')
|
|
.whereIn('domain', domains)
|
|
.where('discoverabilityEnabled', true)
|
|
.where('verified', true)
|
|
.where('role', null)) as Pick<
|
|
Workspace,
|
|
'id' | 'name' | 'slug' | 'description' | 'logo'
|
|
>[]
|
|
}
|
|
|
|
const workspaceWithRoleBaseQuery = ({
|
|
db,
|
|
userId
|
|
}: {
|
|
db: Knex
|
|
userId?: string
|
|
}): Knex.QueryBuilder<WorkspaceWithOptionalRole, WorkspaceWithOptionalRole[]> => {
|
|
let q = db<WorkspaceWithOptionalRole, WorkspaceWithOptionalRole[]>('workspaces')
|
|
if (userId) {
|
|
q = q
|
|
.select([
|
|
...Object.values(Workspaces.col),
|
|
// Getting first role from grouped results
|
|
knex.raw(`(array_agg("workspace_acl"."role"))[1] as role`)
|
|
])
|
|
.leftJoin(DbWorkspaceAcl.name, function () {
|
|
this.on(DbWorkspaceAcl.col.workspaceId, Workspaces.col.id).andOnVal(
|
|
DbWorkspaceAcl.col.userId,
|
|
userId
|
|
)
|
|
})
|
|
.groupBy(Workspaces.col.id)
|
|
}
|
|
return q
|
|
}
|
|
|
|
export const getWorkspacesFactory =
|
|
({ db }: { db: Knex }): GetWorkspaces =>
|
|
async ({ workspaceIds, userId }) => {
|
|
const q = workspaceWithRoleBaseQuery({ db, userId })
|
|
if (workspaceIds !== undefined) q.whereIn(Workspaces.col.id, workspaceIds)
|
|
const results = await q
|
|
return results
|
|
}
|
|
|
|
export const getWorkspaceFactory =
|
|
({ db }: { db: Knex }): GetWorkspace =>
|
|
async ({ workspaceId, userId }) => {
|
|
const workspace = await workspaceWithRoleBaseQuery({ db, userId })
|
|
.where(Workspaces.col.id, workspaceId)
|
|
.first()
|
|
|
|
return workspace || null
|
|
}
|
|
|
|
export const getWorkspaceBySlugOrIdFactory =
|
|
(deps: { db: Knex }): GetWorkspaceBySlugOrId =>
|
|
async ({ workspaceSlugOrId }) => {
|
|
const { db } = deps
|
|
const workspace = await workspaceWithRoleBaseQuery({ db })
|
|
.where(Workspaces.col.slug, workspaceSlugOrId)
|
|
.orWhere(Workspaces.col.id, workspaceSlugOrId)
|
|
.first()
|
|
|
|
return workspace || null
|
|
}
|
|
|
|
export const getWorkspaceBySlugFactory =
|
|
({ db }: { db: Knex }): GetWorkspaceBySlug =>
|
|
async ({ workspaceSlug, userId }) => {
|
|
const workspace = await workspaceWithRoleBaseQuery({ db, userId })
|
|
.where(Workspaces.col.slug, workspaceSlug)
|
|
.first()
|
|
|
|
return workspace || null
|
|
}
|
|
|
|
const buildWorkspacesQuery = ({ db, search }: { db: Knex; search?: string }) => {
|
|
const query = tables.workspaces(db)
|
|
|
|
if (search) {
|
|
query.andWhere((builder) => {
|
|
builder
|
|
.where('name', 'ILIKE', `%${search}%`)
|
|
.orWhere('slug', 'ILIKE', `%${search}%`)
|
|
})
|
|
}
|
|
return query
|
|
}
|
|
|
|
export const queryWorkspacesFactory =
|
|
({ db }: { db: Knex }): QueryWorkspaces =>
|
|
async ({ limit, cursor, filter }) => {
|
|
const query = buildWorkspacesQuery({ db, search: filter?.search })
|
|
.select()
|
|
.orderBy('createdAt', 'desc')
|
|
.limit(limit)
|
|
|
|
if (cursor) {
|
|
query.andWhere('createdAt', '<', cursor)
|
|
}
|
|
return await query
|
|
}
|
|
|
|
export const countWorkspacesFactory =
|
|
({ db }: { db: Knex }): CountWorkspaces =>
|
|
async ({ filter }) => {
|
|
const query = buildWorkspacesQuery({ db, search: filter?.search })
|
|
|
|
const [res] = await query.count()
|
|
const count = parseInt(res.count.toString())
|
|
return count
|
|
}
|
|
|
|
export const upsertWorkspaceFactory =
|
|
({ db }: { db: Knex }): UpsertWorkspace =>
|
|
async ({ workspace }) => {
|
|
await tables
|
|
.workspaces(db)
|
|
.insert(workspace)
|
|
.onConflict('id')
|
|
.merge([
|
|
'description',
|
|
'logo',
|
|
'slug',
|
|
'name',
|
|
'updatedAt',
|
|
'domainBasedMembershipProtectionEnabled',
|
|
'discoverabilityEnabled'
|
|
])
|
|
}
|
|
|
|
export const deleteWorkspaceFactory =
|
|
({ db }: { db: Knex }): DeleteWorkspace =>
|
|
async ({ workspaceId }) => {
|
|
await tables.workspaces(db).where({ id: workspaceId }).delete()
|
|
}
|
|
|
|
export const getWorkspaceRolesFactory =
|
|
({ db }: { db: Knex }): GetWorkspaceRoles =>
|
|
async ({ workspaceId }) => {
|
|
return await tables.workspacesAcl(db).select('*').where({ workspaceId })
|
|
}
|
|
|
|
export const getWorkspaceRoleForUserFactory =
|
|
({ db }: { db: Knex }): GetWorkspaceRoleForUser =>
|
|
async ({ userId, workspaceId }) => {
|
|
return (
|
|
(await tables
|
|
.workspacesAcl(db)
|
|
.select('*')
|
|
.where({ userId, workspaceId })
|
|
.first()) ?? null
|
|
)
|
|
}
|
|
|
|
export const getWorkspacesRolesForUsersFactory =
|
|
(deps: { db: Knex }): GetWorkspacesRolesForUsers =>
|
|
async (reqs) => {
|
|
const query = tables.workspacesAcl(deps.db).whereIn(
|
|
[DbWorkspaceAcl.col.userId, DbWorkspaceAcl.col.workspaceId],
|
|
reqs.map(({ userId, workspaceId }) => [userId, workspaceId])
|
|
)
|
|
const results = await query
|
|
|
|
return results.reduce((acc, acl) => {
|
|
const { userId, workspaceId } = acl
|
|
if (!acc[workspaceId]) {
|
|
acc[workspaceId] = {}
|
|
}
|
|
|
|
acc[workspaceId][userId] = acl
|
|
return acc
|
|
}, {} as Awaited<ReturnType<GetWorkspacesRolesForUsers>>)
|
|
}
|
|
|
|
export const getWorkspaceRolesForUserFactory =
|
|
({ db }: { db: Knex }): GetWorkspaceRolesForUser =>
|
|
async ({ userId }, options) => {
|
|
const workspaceIdFilter = options?.workspaceIdFilter ?? []
|
|
|
|
const query = tables.workspacesAcl(db).select('*').where({ userId })
|
|
|
|
if (workspaceIdFilter.length > 0) {
|
|
query.whereIn('workspaceId', workspaceIdFilter)
|
|
}
|
|
|
|
return await query
|
|
}
|
|
|
|
export const deleteWorkspaceRoleFactory =
|
|
({ db }: { db: Knex }): DeleteWorkspaceRole =>
|
|
async ({ userId, workspaceId }) => {
|
|
const deletedRoles = await tables
|
|
.workspacesAcl(db)
|
|
.where({ workspaceId, userId })
|
|
.delete('*')
|
|
|
|
if (deletedRoles.length === 0) {
|
|
return null
|
|
}
|
|
|
|
// Given `workspaceId` and `userId` define a primary key for `workspace_acl` table,
|
|
// query returns either 0 or 1 row in all cases
|
|
return deletedRoles[0]
|
|
}
|
|
|
|
export const upsertWorkspaceRoleFactory =
|
|
({ db }: { db: Knex }): UpsertWorkspaceRole =>
|
|
async ({ userId, workspaceId, role, createdAt }) => {
|
|
// Verify requested role is valid workspace role
|
|
const validRoles = Object.values(Roles.Workspace)
|
|
if (!validRoles.includes(role)) {
|
|
throw new WorkspaceInvalidRoleError()
|
|
}
|
|
|
|
await tables
|
|
.workspacesAcl(db)
|
|
.insert({ userId, workspaceId, role, createdAt })
|
|
.onConflict(['userId', 'workspaceId'])
|
|
.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
|
|
}
|
|
|
|
export const getWorkspaceCollaboratorsFactory =
|
|
({ db }: { db: Knex }): GetWorkspaceCollaborators =>
|
|
async ({ workspaceId, filter = {}, cursor, limit = 25 }) => {
|
|
const query = db
|
|
.from(Users.name)
|
|
.select<Array<WorkspaceTeamMember & { workspaceRoleCreatedAt: Date }>>(
|
|
...Users.cols,
|
|
ServerAcl.col.role,
|
|
DbWorkspaceAcl.col.workspaceId, // this field is necessary for projectRoles field resolver
|
|
DbWorkspaceAcl.colAs('role', 'workspaceRole'),
|
|
DbWorkspaceAcl.colAs('createdAt', 'workspaceRoleCreatedAt')
|
|
)
|
|
.join(DbWorkspaceAcl.name, DbWorkspaceAcl.col.userId, Users.col.id)
|
|
.join(ServerAcl.name, ServerAcl.col.userId, Users.col.id)
|
|
.where(DbWorkspaceAcl.col.workspaceId, workspaceId)
|
|
.orderBy('workspaceRoleCreatedAt', 'desc')
|
|
|
|
const { search, roles, seatType, excludeUserIds } = filter || {}
|
|
|
|
if (seatType) {
|
|
query
|
|
.join('workspace_seats', 'workspace_seats.userId', DbWorkspaceAcl.col.userId)
|
|
.andWhere('workspace_seats.type', seatType)
|
|
.andWhere('workspace_seats.workspaceId', workspaceId)
|
|
}
|
|
|
|
if (search) {
|
|
query.andWhere((builder) => {
|
|
builder
|
|
.where(Users.col.name, 'ILIKE', `%${search}%`)
|
|
.orWhere(Users.col.email, 'ILIKE', `%${search}%`)
|
|
})
|
|
}
|
|
|
|
if (roles) {
|
|
query.andWhere((builder) => {
|
|
builder.whereIn(DbWorkspaceAcl.col.role, roles)
|
|
})
|
|
}
|
|
|
|
if (excludeUserIds?.length) {
|
|
query.andWhere((w) => {
|
|
w.whereNotIn(Users.col.id, excludeUserIds)
|
|
})
|
|
}
|
|
|
|
if (cursor) {
|
|
query.andWhere(DbWorkspaceAcl.col.createdAt, '<', cursor)
|
|
}
|
|
|
|
if (limit) {
|
|
query.limit(clamp(limit, 0, 100))
|
|
}
|
|
|
|
const items = (await query).map((i) => ({
|
|
...removePrivateFields(i),
|
|
workspaceRole: i.workspaceRole,
|
|
workspaceRoleCreatedAt: i.workspaceRoleCreatedAt,
|
|
workspaceId: i.workspaceId,
|
|
role: i.role
|
|
}))
|
|
|
|
return items
|
|
}
|
|
|
|
export const workspaceInviteValidityFilter: InvitesRetrievalValidityFilter = (q) => {
|
|
return q
|
|
.leftJoin(
|
|
knex.raw(
|
|
":workspaces: ON :resourceCol: ->> 'resourceType' = :resourceType AND :resourceCol: ->> 'resourceId' = :workspaceIdCol:",
|
|
{
|
|
workspaces: Workspaces.name,
|
|
resourceCol: ServerInvites.col.resource,
|
|
resourceType: WorkspaceInviteResourceType,
|
|
workspaceIdCol: Workspaces.col.id
|
|
}
|
|
)
|
|
)
|
|
.where((w1) => {
|
|
w1.whereNot((w2) =>
|
|
filterByResource(w2, { resourceType: WorkspaceInviteResourceType })
|
|
).orWhereNotNull(Workspaces.col.id)
|
|
})
|
|
}
|
|
|
|
export const storeWorkspaceDomainFactory =
|
|
({ db }: { db: Knex }): StoreWorkspaceDomain =>
|
|
async ({ workspaceDomain }): Promise<void> => {
|
|
await tables.workspaceDomains(db).insert(workspaceDomain)
|
|
}
|
|
|
|
export const getWorkspaceDomainsFactory =
|
|
({ db }: { db: Knex }): GetWorkspaceDomains =>
|
|
({ workspaceIds }) => {
|
|
return tables.workspaceDomains(db).whereIn('workspaceId', workspaceIds)
|
|
}
|
|
|
|
export const countDomainsByWorkspaceIdFactory =
|
|
({ db }: { db: Knex }): CountDomainsByWorkspaceId =>
|
|
async ({ workspaceId }) => {
|
|
const [res] = await tables.workspaceDomains(db).where({ workspaceId }).count()
|
|
return parseInt(res.count.toString())
|
|
}
|
|
|
|
export const deleteWorkspaceDomainFactory =
|
|
({ db }: { db: Knex }): DeleteWorkspaceDomain =>
|
|
async ({ id }) => {
|
|
await tables.workspaceDomains(db).where({ id }).delete()
|
|
}
|
|
|
|
export const getWorkspaceWithDomainsFactory =
|
|
({ db }: { db: Knex }): GetWorkspaceWithDomains =>
|
|
async ({ id }) => {
|
|
const workspace = await tables
|
|
.workspaces(db)
|
|
.select([...Workspaces.cols, WorkspaceDomains.groupArray('domains')])
|
|
.where({ [Workspaces.col.id]: id })
|
|
.leftJoin(
|
|
WorkspaceDomains.name,
|
|
WorkspaceDomains.col.workspaceId,
|
|
Workspaces.col.id
|
|
)
|
|
.groupBy(Workspaces.col.id)
|
|
.first()
|
|
if (!workspace) return null
|
|
return {
|
|
...workspace,
|
|
domains: workspace.domains.filter(
|
|
(domain: WorkspaceDomain | null) => domain !== null
|
|
)
|
|
} as Workspace & { domains: WorkspaceDomain[] }
|
|
}
|
|
|
|
export const getUserIdsWithRoleInWorkspaceFactory =
|
|
({ db }: { db: Knex }): GetUserIdsWithRoleInWorkspace =>
|
|
async ({ workspaceId, workspaceRole }, options) => {
|
|
const query = tables
|
|
.workspacesAcl(db)
|
|
.where(DbWorkspaceAcl.col.workspaceId, workspaceId)
|
|
.where(DbWorkspaceAcl.col.role, workspaceRole)
|
|
.orderBy(DbWorkspaceAcl.col.createdAt)
|
|
|
|
if (options?.limit) {
|
|
query.limit(options.limit)
|
|
}
|
|
|
|
return (
|
|
(await query.select([DbWorkspaceAcl.col.userId])) as Pick<
|
|
WorkspaceAcl,
|
|
'userId'
|
|
>[]
|
|
).map((wsAcl) => wsAcl.userId)
|
|
}
|
|
|
|
export const countWorkspaceRoleWithOptionalProjectRoleFactory =
|
|
({ db }: { db: Knex }): CountWorkspaceRoleWithOptionalProjectRole =>
|
|
async ({ workspaceId, workspaceRole, projectRole, skipUserIds }) => {
|
|
let query
|
|
if (projectRole) {
|
|
query = tables
|
|
.streams(db)
|
|
.join(StreamAcl.name, StreamAcl.col.resourceId, Streams.col.id)
|
|
.join(DbWorkspaceAcl.name, DbWorkspaceAcl.col.userId, StreamAcl.col.userId)
|
|
.where(Streams.col.workspaceId, workspaceId)
|
|
.andWhere(DbWorkspaceAcl.col.role, workspaceRole)
|
|
// make sure to also filter on the workspace_acl workspaceId, to not leak roles across
|
|
.andWhere(DbWorkspaceAcl.col.workspaceId, workspaceId)
|
|
.andWhere(StreamAcl.col.role, projectRole)
|
|
.countDistinct(DbWorkspaceAcl.col.userId)
|
|
} else {
|
|
query = tables
|
|
.workspacesAcl(db)
|
|
.where(DbWorkspaceAcl.col.workspaceId, workspaceId)
|
|
.where(DbWorkspaceAcl.col.role, workspaceRole)
|
|
.count()
|
|
}
|
|
|
|
if (skipUserIds) {
|
|
query.whereNotIn(DbWorkspaceAcl.col.userId, skipUserIds)
|
|
}
|
|
|
|
const [res] = await query
|
|
return parseInt(res.count.toString())
|
|
}
|
|
|
|
export const getWorkspaceCreationStateFactory =
|
|
({ db }: { db: Knex }): GetWorkspaceCreationState =>
|
|
async ({ workspaceId }) => {
|
|
const creationState = await tables
|
|
.workspaceCreationState(db)
|
|
.select()
|
|
.where({ workspaceId })
|
|
.first()
|
|
return creationState || null
|
|
}
|
|
|
|
export const upsertWorkspaceCreationStateFactory =
|
|
({ db }: { db: Knex }): UpsertWorkspaceCreationState =>
|
|
async ({ workspaceCreationState }) => {
|
|
await tables
|
|
.workspaceCreationState(db)
|
|
.insert(workspaceCreationState)
|
|
.onConflict('workspaceId')
|
|
.merge()
|
|
}
|
|
|
|
export const getWorkspacesProjectsCountsFactory =
|
|
(deps: { db: Knex }): GetWorkspacesProjectsCounts =>
|
|
async ({ workspaceIds }) => {
|
|
const ret = workspaceIds.reduce((acc, workspaceId) => {
|
|
acc[workspaceId] = 0
|
|
return acc
|
|
}, {} as Record<string, number>)
|
|
|
|
const q = tables
|
|
.streams(deps.db)
|
|
.select<
|
|
{
|
|
workspaceId: string
|
|
count: string
|
|
}[]
|
|
>([Streams.col.workspaceId, knex.raw('count(*) as count')])
|
|
.whereIn(Streams.col.workspaceId, workspaceIds)
|
|
.groupBy(Streams.col.workspaceId)
|
|
|
|
const res = await q
|
|
|
|
for (const { workspaceId, count } of res) {
|
|
ret[workspaceId] = parseInt(count)
|
|
}
|
|
|
|
return ret
|
|
}
|
|
|
|
const getPaginatedWorkspaceProjectsBaseQueryFactory =
|
|
(deps: { db: Knex }) =>
|
|
(params: Omit<GetPaginatedWorkspaceProjectsArgs, 'cursor' | 'limit'>) => {
|
|
const { workspaceId, userId, filter } = params
|
|
const { search, withProjectRoleOnly } = filter || {}
|
|
|
|
const query = tables
|
|
.streams(deps.db)
|
|
.where(Streams.col.workspaceId, workspaceId)
|
|
.select<StreamRecord[]>(Streams.cols)
|
|
|
|
/**
|
|
* If userId is set:
|
|
* - If no workspace role, user should be server admin w/ admin override enabled
|
|
* - If workspace role is guest, user should have explicit stream roles
|
|
* - If workspace role other than guest, just get all workspace streams
|
|
*
|
|
* If withProjectRoleOnly is set: Require project role always
|
|
*/
|
|
if (userId) {
|
|
query
|
|
.leftJoin(DbWorkspaceAcl.name, (j) => {
|
|
j.on(DbWorkspaceAcl.col.workspaceId, Streams.col.workspaceId).andOnVal(
|
|
DbWorkspaceAcl.col.userId,
|
|
userId
|
|
)
|
|
})
|
|
.andWhere((w) => {
|
|
// Check server_acl exist first, so subsequent checks can be optimized away
|
|
if (adminOverrideEnabled() && !withProjectRoleOnly) {
|
|
w.whereExists(
|
|
tables
|
|
.serverAcl(deps.db)
|
|
.select('*')
|
|
.where(ServerAcl.col.userId, userId)
|
|
.andWhere(ServerAcl.col.role, Roles.Server.Admin)
|
|
)
|
|
}
|
|
|
|
w.orWhere((w2) => {
|
|
// Ensure workspace role exists and its not guest or the user has explicit stream roles
|
|
w2.whereNotNull(DbWorkspaceAcl.col.role).andWhere((w3) => {
|
|
if (!withProjectRoleOnly) {
|
|
w3.whereNot(DbWorkspaceAcl.col.role, Roles.Workspace.Guest)
|
|
}
|
|
|
|
w3.orWhereExists(
|
|
tables
|
|
.streamAcl(deps.db)
|
|
.select('*')
|
|
.where(StreamAcl.col.userId, userId)
|
|
.andWhere(StreamAcl.col.resourceId, knex.ref(Streams.col.id))
|
|
)
|
|
})
|
|
})
|
|
})
|
|
}
|
|
|
|
if (search?.length) {
|
|
query.andWhere((w) => {
|
|
w.where(Streams.col.name, 'ILIKE', `%${search}%`).orWhere(
|
|
Streams.col.description,
|
|
'ILIKE',
|
|
`%${search}%`
|
|
)
|
|
})
|
|
}
|
|
|
|
return query
|
|
}
|
|
|
|
export const getPaginatedWorkspaceProjectsItemsFactory =
|
|
(deps: { db: Knex }): GetPaginatedWorkspaceProjectsItems =>
|
|
async (params) => {
|
|
type CursorType = { updatedAt: string; id: string }
|
|
const query = getPaginatedWorkspaceProjectsBaseQueryFactory(deps)(params)
|
|
|
|
const limit = clamp(params.limit || 25, 1, 50)
|
|
const cursor = decodeCompositeCursor<CursorType>(
|
|
params.cursor,
|
|
(c) => isObjectLike(c) && has(c, 'id') && has(c, 'updatedAt')
|
|
)
|
|
|
|
if (cursor) {
|
|
// filter by date, and if there's duplicate dates, filter by id too
|
|
query.andWhereRaw('(??, ??) < (?, ?)', [
|
|
Streams.col.updatedAt,
|
|
Streams.col.id,
|
|
cursor.updatedAt,
|
|
cursor.id
|
|
])
|
|
}
|
|
|
|
query
|
|
.orderBy([
|
|
{ column: Streams.col.updatedAt, order: 'desc' },
|
|
{ column: Streams.col.id, order: 'desc' }
|
|
])
|
|
.limit(limit)
|
|
|
|
const rows = await query
|
|
const newCursorRow = rows.at(-1)
|
|
const newCursor = newCursorRow
|
|
? encodeCompositeCursor<CursorType>({
|
|
updatedAt: newCursorRow.updatedAt.toISOString(),
|
|
id: newCursorRow.id
|
|
})
|
|
: null
|
|
|
|
return {
|
|
items: rows,
|
|
cursor: newCursor
|
|
}
|
|
}
|
|
|
|
export const getPaginatedWorkspaceProjectsTotalCountFactory =
|
|
(deps: { db: Knex }): GetPaginatedWorkspaceProjectsTotalCount =>
|
|
async (params) => {
|
|
const query = getPaginatedWorkspaceProjectsBaseQueryFactory(deps)(params)
|
|
const [res] = await query.clearSelect().count()
|
|
const count = parseInt(res.count.toString())
|
|
return count
|
|
}
|
|
|
|
export const getPaginatedWorkspaceProjectsFactory =
|
|
(deps: { db: Knex }): GetPaginatedWorkspaceProjects =>
|
|
async (params) => {
|
|
const getItems = getPaginatedWorkspaceProjectsItemsFactory(deps)
|
|
const getTotalCount = getPaginatedWorkspaceProjectsTotalCountFactory(deps)
|
|
|
|
const [items, totalCount] = await Promise.all([
|
|
params.limit !== 0 ? getItems(params) : undefined,
|
|
getTotalCount(params)
|
|
])
|
|
|
|
if (!items) {
|
|
return {
|
|
items: [],
|
|
cursor: null,
|
|
totalCount
|
|
}
|
|
}
|
|
|
|
return {
|
|
...items,
|
|
totalCount
|
|
}
|
|
}
|