399c998fd7
* feat(multiregion): replace user replication * chore(multiregion): optimise replication * maybe it's this * postgres is fun * once more * chore(multiregion): only replicate test user creation during multiregion tests * feat: improved replicate_query logic * fix: minor * fix: starting issue * feat: included user create and delete specs to multiregion * feat: removed console logs * fix: user defaults * fix: multiregion test helper * fix: update scenarios for users * refactor(multiregion): swap replicateQuery concept to asMultiregionOperation (#5301) feat(multiregion): introduced asMultregionOperator, refactor test to user builder classes * chore: renamings * fix: remove comments * feat: remove user replication * refactor: simplified spec usages * chore: comments * chore: branches and favs * chore: more tests * chore: more tests * fix linting * fix tests * feat: dropping replication * refactor: moved project delete to service * fix: comment * feat: updateStreamFactory and updateProjectFacotry * deleteProjectFactory + replicateFactory * deleteWorkspaceFactory * fix: selector * fix: tests * fix tests, finished createStreamFactory * feat: simplify changes * fix: remove comment * fix: minor strucutres * fix: moveProjectToRegion * fix: moved branch creation outside of multiregion scope * fix: branch creation * fix: tests * fix: ci tests * fix: removed log form test * fix: on specs, no random regionKeys * review fixes * fix: mr comments * feat: removed test --------- Co-authored-by: Charles Driesler <chuck@speckle.systems>
339 lines
12 KiB
TypeScript
339 lines
12 KiB
TypeScript
import { db } from '@/db/knex'
|
|
import { StreamAcl } from '@/modules/core/dbSchema'
|
|
import { mapDbToGqlProjectVisibility } from '@/modules/core/helpers/project'
|
|
import type { StreamAclRecord, StreamRecord } from '@/modules/core/helpers/types'
|
|
import { getServerInfoFactory } from '@/modules/core/repositories/server'
|
|
import {
|
|
createStreamFactory,
|
|
getStreamCollaboratorsFactory,
|
|
getStreamFactory,
|
|
getStreamRolesFactory,
|
|
grantStreamPermissionsFactory,
|
|
revokeStreamPermissionsFactory
|
|
} from '@/modules/core/repositories/streams'
|
|
import {
|
|
createUserEmailFactory,
|
|
ensureNoPrimaryEmailForUserFactory,
|
|
findEmailFactory
|
|
} from '@/modules/core/repositories/userEmails'
|
|
import { getUserFactory, getUsersFactory } from '@/modules/core/repositories/users'
|
|
import {
|
|
addOrUpdateStreamCollaboratorFactory,
|
|
isStreamCollaboratorFactory,
|
|
removeStreamCollaboratorFactory,
|
|
validateStreamAccessFactory
|
|
} from '@/modules/core/services/streams/access'
|
|
import {
|
|
createStreamReturnRecordFactory,
|
|
legacyCreateStreamFactory
|
|
} from '@/modules/core/services/streams/management'
|
|
import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails'
|
|
import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories'
|
|
import { renderEmail } from '@/modules/emails/services/emailRendering'
|
|
import { sendEmail } from '@/modules/emails/services/sending'
|
|
import { requestNewEmailVerificationFactory } from '@/modules/emails/services/verification/request'
|
|
import {
|
|
deleteInvitesByTargetFactory,
|
|
deleteServerOnlyInvitesFactory,
|
|
findInviteFactory,
|
|
findUserByTargetFactory,
|
|
insertInviteAndDeleteOldFactory,
|
|
updateAllInviteTargetsFactory
|
|
} from '@/modules/serverinvites/repositories/serverInvites'
|
|
import { buildCoreInviteEmailContentsFactory } from '@/modules/serverinvites/services/coreEmailContents'
|
|
import {
|
|
processFinalizedProjectInviteFactory,
|
|
validateProjectInviteBeforeFinalizationFactory
|
|
} from '@/modules/serverinvites/services/coreFinalization'
|
|
import { collectAndValidateCoreTargetsFactory } from '@/modules/serverinvites/services/coreResourceCollection'
|
|
import { createAndSendInviteFactory } from '@/modules/serverinvites/services/creation'
|
|
import {
|
|
finalizeInvitedServerRegistrationFactory,
|
|
finalizeResourceInviteFactory
|
|
} from '@/modules/serverinvites/services/processing'
|
|
import { inviteUsersToProjectFactory } from '@/modules/serverinvites/services/projectInviteManagement'
|
|
import { authorizeResolver } from '@/modules/shared'
|
|
import type { Nullable } from '@/modules/shared/helpers/typeHelper'
|
|
import { getEventBus } from '@/modules/shared/services/eventBus'
|
|
import { getDefaultRegionFactory } from '@/modules/workspaces/repositories/regions'
|
|
import { createWorkspaceProjectFactory } from '@/modules/workspaces/services/projects'
|
|
import type { BasicTestUser } from '@/test/authHelper'
|
|
import { ProjectVisibility } from '@/modules/core/graph/generated/graphql'
|
|
import { faker } from '@faker-js/faker'
|
|
import type { StreamRoles } from '@speckle/shared'
|
|
import { ensureError, Roles } from '@speckle/shared'
|
|
import { omit } from 'lodash-es'
|
|
import { storeProjectRoleFactory } from '@/modules/core/repositories/projects'
|
|
import { asMultiregionalOperation, replicateFactory } from '@/modules/shared/command'
|
|
import { logger } from '@/observability/logging'
|
|
import type { LegacyCreateStream } from '@/modules/core/domain/streams/operations'
|
|
import { getReplicationDbs } from '@/modules/multiregion/utils/dbSelector'
|
|
|
|
const getServerInfo = getServerInfoFactory({ db })
|
|
const getUser = getUserFactory({ db })
|
|
const getStream = getStreamFactory({ db })
|
|
|
|
const buildFinalizeProjectInvite = () =>
|
|
finalizeResourceInviteFactory({
|
|
findInvite: findInviteFactory({ db }),
|
|
validateInvite: validateProjectInviteBeforeFinalizationFactory({
|
|
getProject: getStream
|
|
}),
|
|
processInvite: processFinalizedProjectInviteFactory({
|
|
getProject: getStream,
|
|
addProjectRole: addOrUpdateStreamCollaboratorFactory({
|
|
validateStreamAccess: validateStreamAccessFactory({ authorizeResolver }),
|
|
getUser,
|
|
grantStreamPermissions: grantStreamPermissionsFactory({ db }),
|
|
getStreamRoles: getStreamRolesFactory({ db }),
|
|
emitEvent: getEventBus().emit
|
|
})
|
|
}),
|
|
deleteInvitesByTarget: deleteInvitesByTargetFactory({ db }),
|
|
insertInviteAndDeleteOld: insertInviteAndDeleteOldFactory({ db }),
|
|
emitEvent: (...args) => getEventBus().emit(...args),
|
|
findEmail: findEmailFactory({ db }),
|
|
validateAndCreateUserEmail: validateAndCreateUserEmailFactory({
|
|
createUserEmail: createUserEmailFactory({ db }),
|
|
ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }),
|
|
findEmail: findEmailFactory({ db }),
|
|
updateEmailInvites: finalizeInvitedServerRegistrationFactory({
|
|
deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }),
|
|
updateAllInviteTargets: updateAllInviteTargetsFactory({ db })
|
|
}),
|
|
requestNewEmailVerification: requestNewEmailVerificationFactory({
|
|
findEmail: findEmailFactory({ db }),
|
|
getUser,
|
|
getServerInfo,
|
|
deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({
|
|
db
|
|
}),
|
|
renderEmail,
|
|
sendEmail
|
|
})
|
|
}),
|
|
collectAndValidateResourceTargets: collectAndValidateCoreTargetsFactory({
|
|
getStream
|
|
}),
|
|
getUser,
|
|
getServerInfo
|
|
})
|
|
|
|
const createStream: LegacyCreateStream = async (
|
|
stream: Parameters<LegacyCreateStream>[0] & { regionKey?: string }
|
|
) =>
|
|
asMultiregionalOperation(
|
|
async ({ allDbs, mainDb, emit }) =>
|
|
legacyCreateStreamFactory({
|
|
createStreamReturnRecord: createStreamReturnRecordFactory({
|
|
inviteUsersToProject: inviteUsersToProjectFactory({
|
|
createAndSendInvite: createAndSendInviteFactory({
|
|
findUserByTarget: findUserByTargetFactory({ db: mainDb }),
|
|
insertInviteAndDeleteOld: insertInviteAndDeleteOldFactory({ db: mainDb }),
|
|
collectAndValidateResourceTargets: collectAndValidateCoreTargetsFactory({
|
|
getStream: getStreamFactory({ db: mainDb })
|
|
}),
|
|
buildInviteEmailContents: buildCoreInviteEmailContentsFactory({
|
|
getStream: getStreamFactory({ db: mainDb })
|
|
}),
|
|
emitEvent: emit,
|
|
getUser: getUserFactory({ db: mainDb }),
|
|
getServerInfo: getServerInfoFactory({ db: mainDb }),
|
|
finalizeInvite: buildFinalizeProjectInvite()
|
|
}),
|
|
getUsers: getUsersFactory({ db: mainDb })
|
|
}),
|
|
createStream: replicateFactory(allDbs, createStreamFactory),
|
|
storeProjectRole: storeProjectRoleFactory({ db: mainDb }),
|
|
emitEvent: emit
|
|
})
|
|
})(stream),
|
|
{
|
|
name: 'create stream spec',
|
|
logger,
|
|
description: 'Creates a new stream',
|
|
dbs: await getReplicationDbs({ regionKey: stream.regionKey || null })
|
|
}
|
|
)
|
|
|
|
const validateStreamAccess = validateStreamAccessFactory({ authorizeResolver })
|
|
const isStreamCollaborator = isStreamCollaboratorFactory({
|
|
getStream
|
|
})
|
|
const removeStreamCollaborator = removeStreamCollaboratorFactory({
|
|
validateStreamAccess,
|
|
isStreamCollaborator,
|
|
revokeStreamPermissions: revokeStreamPermissionsFactory({ db }),
|
|
getStreamRoles: getStreamRolesFactory({ db }),
|
|
emitEvent: getEventBus().emit
|
|
})
|
|
|
|
const addOrUpdateStreamCollaborator = addOrUpdateStreamCollaboratorFactory({
|
|
validateStreamAccess,
|
|
getUser,
|
|
grantStreamPermissions: grantStreamPermissionsFactory({ db }),
|
|
getStreamRoles: getStreamRolesFactory({ db }),
|
|
emitEvent: getEventBus().emit
|
|
})
|
|
|
|
export type BasicTestStream = {
|
|
name?: string
|
|
/**
|
|
* @deprecated Use visibility instead
|
|
*/
|
|
isPublic?: boolean
|
|
/**
|
|
* The ID of the owner user. Will be filled in by createTestStream().
|
|
*/
|
|
ownerId: string
|
|
/**
|
|
* The ID of the stream. Will be filled in by createTestStream().
|
|
*/
|
|
id: string
|
|
} & Partial<StreamRecord>
|
|
|
|
/**
|
|
* Create multiple test streams with their IDs filled in
|
|
*/
|
|
export async function createTestStreams(
|
|
streamOwnerPairs: [BasicTestStream, BasicTestUser][]
|
|
) {
|
|
return await Promise.all(streamOwnerPairs.map((p) => createTestStream(p[0], p[1])))
|
|
}
|
|
|
|
/**
|
|
* Create basic stream for testing and update streamObj in-place, via reference, to have a real ID
|
|
*/
|
|
export async function createTestStream<S extends Partial<BasicTestStream>>(
|
|
streamObj: S,
|
|
owner: BasicTestUser
|
|
): Promise<BasicTestStream> {
|
|
let id: string
|
|
|
|
const visibility = streamObj.isPublic
|
|
? ProjectVisibility.Public
|
|
: (streamObj.visibility
|
|
? mapDbToGqlProjectVisibility(streamObj.visibility)
|
|
: undefined) || ProjectVisibility.Private
|
|
|
|
if (streamObj.workspaceId) {
|
|
const createWorkspaceProject = createWorkspaceProjectFactory({
|
|
getDefaultRegion: getDefaultRegionFactory({ db })
|
|
})
|
|
const newProject = await createWorkspaceProject({
|
|
input: {
|
|
name: streamObj.name || faker.commerce.productName(),
|
|
description: streamObj.description,
|
|
visibility,
|
|
workspaceId: streamObj.workspaceId
|
|
},
|
|
ownerId: owner.id
|
|
})
|
|
|
|
id = newProject.id
|
|
} else {
|
|
id = await createStream({
|
|
...omit(streamObj, ['id', 'ownerId', 'visibility']),
|
|
isPublic: visibility === ProjectVisibility.Public,
|
|
ownerId: owner.id
|
|
})
|
|
}
|
|
|
|
streamObj.id = id
|
|
streamObj.ownerId = owner.id
|
|
return {
|
|
...streamObj,
|
|
id,
|
|
ownerId: owner.id
|
|
}
|
|
}
|
|
|
|
export async function leaveStream(streamObj: BasicTestStream, user: BasicTestUser) {
|
|
await removeStreamCollaborator(streamObj.id, user.id, user.id, null).catch((e) => {
|
|
if (ensureError(e).message === 'User is not a stream collaborator') {
|
|
return
|
|
}
|
|
|
|
throw e
|
|
})
|
|
}
|
|
|
|
export async function addToStream(
|
|
streamObj: BasicTestStream,
|
|
user: BasicTestUser,
|
|
role: StreamRoles,
|
|
options?: Partial<{
|
|
owner: BasicTestUser
|
|
}>
|
|
) {
|
|
const { owner } = options || {}
|
|
let ownerId = owner?.id
|
|
if (!ownerId) {
|
|
const getStreamCollaborators = getStreamCollaboratorsFactory({ db })
|
|
const collaborators = await getStreamCollaborators(
|
|
streamObj.id,
|
|
Roles.Stream.Owner,
|
|
{
|
|
limit: 1
|
|
}
|
|
)
|
|
ownerId = collaborators[0]?.id
|
|
}
|
|
if (!ownerId) {
|
|
throw new Error('Attempted to add a collaborator to a stream without an owner')
|
|
}
|
|
|
|
await addOrUpdateStreamCollaborator(streamObj.id, user.id, role, ownerId, null)
|
|
}
|
|
|
|
export async function addAllToStream(
|
|
streamObj: BasicTestStream,
|
|
users: BasicTestUser[] | { user: BasicTestUser; role: StreamRoles }[],
|
|
options?: Partial<{
|
|
owner: BasicTestUser
|
|
}>
|
|
) {
|
|
const { owner } = options || {}
|
|
let ownerId = owner?.id
|
|
if (!ownerId) {
|
|
const getStreamCollaborators = getStreamCollaboratorsFactory({ db })
|
|
const collaborators = await getStreamCollaborators(
|
|
streamObj.id,
|
|
Roles.Stream.Owner,
|
|
{
|
|
limit: 1
|
|
}
|
|
)
|
|
ownerId = collaborators[0]?.id
|
|
}
|
|
if (!ownerId) {
|
|
throw new Error('Attempted to add a collaborator to a stream without an owner')
|
|
}
|
|
|
|
const usersWithRoles = users.map((u) =>
|
|
'user' in u ? u : { user: u, role: Roles.Stream.Contributor }
|
|
)
|
|
await Promise.all(
|
|
usersWithRoles.map(({ user, role }) =>
|
|
addOrUpdateStreamCollaborator(streamObj.id, user.id, role, ownerId!, null)
|
|
)
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Get the role user has for the specified stream
|
|
*/
|
|
export async function getUserStreamRole(
|
|
userId: string,
|
|
streamId: string
|
|
): Promise<Nullable<string>> {
|
|
const entry = await StreamAcl.knex<StreamAclRecord>()
|
|
.where({
|
|
[StreamAcl.col.resourceId]: streamId,
|
|
[StreamAcl.col.userId]: userId
|
|
})
|
|
.first()
|
|
|
|
return entry?.role || null
|
|
}
|