import { Workspace, WorkspaceAcl, WorkspaceDomain, WorkspaceWithOptionalRole } from '@/modules/workspacesCore/domain/types' import { CountDomainsByWorkspaceId, CountProjectsVersionsByWorkspaceId, CountWorkspaceRoleWithOptionalProjectRole, DeleteWorkspace, DeleteWorkspaceDomain, DeleteWorkspaceRole, GetUserDiscoverableWorkspaces, GetUserIdsWithRoleInWorkspace, GetWorkspace, GetWorkspaceBySlug, GetWorkspaceCollaborators, GetWorkspaceCollaboratorsTotalCount, GetWorkspaceDomains, GetWorkspaceRoleForUser, GetWorkspaceRoles, GetWorkspaceRolesForUser, GetWorkspaceWithDomains, GetWorkspaces, StoreWorkspaceDomain, UpsertWorkspace, UpsertWorkspaceRole } from '@/modules/workspaces/domain/operations' import { Knex } from 'knex' import { Roles } from '@speckle/shared' import { 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, StreamCommits, 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/workspaces/domain/constants' import { clamp } from 'lodash' import { WorkspaceTeamMember } from '@/modules/workspaces/domain/types' const tables = { streams: (db: Knex) => db('streams'), streamAcl: (db: Knex) => db('stream_acl'), workspaces: (db: Knex) => db('workspaces'), workspaceDomains: (db: Knex) => db('workspace_domains'), workspacesAcl: (db: Knex) => db('workspace_acl') } 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', 'description', 'logo', 'defaultLogoIndex') .distinctOn('workspaces.id') .join('workspace_domains', 'workspace_domains.workspaceId', 'workspaces.id') .leftJoin( tables.workspacesAcl(db).select('*').where({ userId }).as('acl'), 'acl.workspaceId', 'workspaces.id' ) .whereIn('domain', domains) .where('discoverabilityEnabled', true) .where('verified', true) .where('role', null)) as Pick< Workspace, 'id' | 'name' | 'description' | 'logo' | 'defaultLogoIndex' >[] } const workspaceWithRoleBaseQuery = ({ db, userId }: { db: Knex userId?: string }): Knex.QueryBuilder => { let q = db('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 (params: { workspaceIds: string[] /** * Optionally - for each workspace, return the user's role in that workspace */ userId?: string }) => { const { workspaceIds, userId } = params const q = workspaceWithRoleBaseQuery({ db, userId }) const results = await q.whereIn(Workspaces.col.id, workspaceIds) 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 getWorkspaceBySlugFactory = ({ db }: { db: Knex }): GetWorkspaceBySlug => async ({ workspaceSlug, userId }) => { const workspace = await workspaceWithRoleBaseQuery({ db, userId }) .where(Workspaces.col.slug, workspaceSlug) .first() return workspace || null } export const upsertWorkspaceFactory = ({ db }: { db: Knex }): UpsertWorkspace => async ({ workspace }) => { await tables .workspaces(db) .insert(workspace) .onConflict('id') .merge([ 'description', 'logo', 'slug', 'defaultLogoIndex', 'defaultProjectRole', '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 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>( ...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 } = filter || {} 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 (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, workspaceId: i.workspaceId, role: i.role, createdAt: i.workspaceRoleCreatedAt })) 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 => { 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 countProjectsVersionsByWorkspaceIdFactory = ({ db }: { db: Knex }): CountProjectsVersionsByWorkspaceId => async ({ workspaceId }) => { const [res] = await tables .streams(db) .join(StreamCommits.name, StreamCommits.col.streamId, Streams.col.id) .where({ workspaceId }) .count(StreamCommits.col.commitId) return parseInt(res.count.toString()) } 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()) }