complete workspaces

This commit is contained in:
Iain Sproat
2025-04-15 10:48:26 +01:00
parent 197e9271c5
commit afd8895a81
@@ -209,6 +209,7 @@ import {
mapAuthToServerError,
throwIfAuthNotOk
} from '@/modules/shared/helpers/errorHelper'
import { withOperationLogging } from '@/observability/domain/businessLogging'
const eventBus = getEventBus()
const getServerInfo = getServerInfoFactory({ db })
@@ -368,9 +369,10 @@ export = FF_WORKSPACES_MODULE_ENABLED
},
ProjectInviteMutations: {
async createForWorkspace(_parent, args, ctx) {
const projectId = args.projectId
await authorizeResolver(
ctx.userId,
args.projectId,
projectId,
Roles.Stream.Owner,
ctx.resourceAccessRules
)
@@ -382,6 +384,12 @@ export = FF_WORKSPACES_MODULE_ENABLED
)
}
const logger = ctx.log.child({
projectId,
streamId: projectId, //legacy
inviteCount
})
const createProjectInvite = createProjectInviteFactory({
createAndSendInvite: buildCreateAndSendServerOrProjectInvite(),
getStream
@@ -401,35 +409,57 @@ export = FF_WORKSPACES_MODULE_ENABLED
)
}
return createProjectInvite({
input: {
...i,
projectId: args.projectId
},
inviterId: ctx.userId!,
inviterResourceAccessRules: ctx.resourceAccessRules,
secondaryResourceRoles: workspaceRole
? {
[WorkspaceInviteResourceType]: workspaceRole as WorkspaceRoles
}
: undefined,
allowWorkspacedProjects: true
})
return withOperationLogging(
async () =>
await createProjectInvite({
input: {
...i,
projectId
},
inviterId: ctx.userId!,
inviterResourceAccessRules: ctx.resourceAccessRules,
secondaryResourceRoles: workspaceRole
? {
[WorkspaceInviteResourceType]:
workspaceRole as WorkspaceRoles
}
: undefined,
allowWorkspacedProjects: true
}),
{
logger,
operationName: 'createWorkspaceProjectInviteFromBatch',
operationDescription: 'Create workspace project invite from batch'
}
)
})
)
}
return ctx.loaders.streams.getStream.load(args.projectId)
return ctx.loaders.streams.getStream.load(projectId)
}
},
AdminMutations: {
updateWorkspacePlan: async (_parent, { input }) => {
updateWorkspacePlan: async (_parent, { input }, ctx) => {
const { workspaceId, plan: name, status } = input
const logger = ctx.log.child({
workspaceId,
workspacePlanName: name
})
await updateWorkspacePlanFactory({
const updateWorkspacePlan = updateWorkspacePlanFactory({
getWorkspace: getWorkspaceFactory({ db }),
upsertWorkspacePlan: upsertWorkspacePlanFactory({ db }),
emitEvent: getEventBus().emit
})({ workspaceId, name, status })
})
await withOperationLogging(
async () => await updateWorkspacePlan({ workspaceId, name, status }),
{
logger,
operationName: 'updateWorkspacePlan',
operationDescription: 'Update workspace plan'
}
)
return true
}
},
@@ -443,6 +473,8 @@ export = FF_WORKSPACES_MODULE_ENABLED
enableDomainDiscoverabilityForDomain
} = args.input
const logger = context.log
const createWorkspace = commandFactory({
db,
eventBus,
@@ -518,7 +550,11 @@ export = FF_WORKSPACES_MODULE_ENABLED
}
})
return await createWorkspace()
return await withOperationLogging(async () => await createWorkspace(), {
logger,
operationName: 'createWorkspace',
operationDescription: 'Create workspace'
})
},
delete: async (_parent, args, context) => {
const { workspaceId } = args
@@ -530,6 +566,10 @@ export = FF_WORKSPACES_MODULE_ENABLED
context.resourceAccessRules
)
const logger = context.log.child({
workspaceId
})
const workspacePlan = await getWorkspacePlanFactory({ db })({ workspaceId })
if (workspacePlan) {
switch (workspacePlan.name) {
@@ -582,10 +622,24 @@ export = FF_WORKSPACES_MODULE_ENABLED
const region = await getDefaultRegionFactory({ db })({ workspaceId })
if (region) {
const regionDb = await getRegionDb({ regionKey: region.key })
await deleteWorkspaceFrom(regionDb)({ workspaceId })
await withOperationLogging(
async () => await deleteWorkspaceFrom(regionDb)({ workspaceId }),
{
logger: logger.child({ regionKey: region.key }),
operationName: 'deleteWorkspaceFromRegion',
operationDescription: 'Delete workspace from region'
}
)
}
await deleteWorkspaceFrom(db)({ workspaceId })
await withOperationLogging(
async () => await deleteWorkspaceFrom(db)({ workspaceId }),
{
logger,
operationName: 'deleteWorkspace',
operationDescription: 'Delete workspace'
}
)
return true
},
@@ -599,6 +653,10 @@ export = FF_WORKSPACES_MODULE_ENABLED
context.resourceAccessRules
)
const logger = context.log.child({
workspaceId
})
const updateWorkspace = updateWorkspaceFactory({
validateSlug: validateSlugFactory({
getWorkspaceBySlug: getWorkspaceBySlugFactory({ db })
@@ -612,10 +670,18 @@ export = FF_WORKSPACES_MODULE_ENABLED
emitWorkspaceEvent: getEventBus().emit
})
const workspace = await updateWorkspace({
workspaceId,
workspaceInput: omit(workspaceInput, ['defaultProjectRole'])
})
const workspace = await withOperationLogging(
async () =>
await updateWorkspace({
workspaceId,
workspaceInput: omit(workspaceInput, ['defaultProjectRole'])
}),
{
logger,
operationName: 'updateWorkspace',
operationDescription: 'Update workspace'
}
)
return workspace
},
@@ -629,6 +695,10 @@ export = FF_WORKSPACES_MODULE_ENABLED
context.resourceAccessRules
)
const logger = context.log.child({
workspaceId
})
if (!role) {
// this is currently not working with the command factory
// TODO: include the onWorkspaceRoleDeletedFactory listener service
@@ -638,7 +708,18 @@ export = FF_WORKSPACES_MODULE_ENABLED
getWorkspaceRoles: getWorkspaceRolesFactory({ db: trx }),
emitWorkspaceEvent: getEventBus().emit
})
await withTransaction(deleteWorkspaceRole({ workspaceId, userId }), trx)
await withOperationLogging(
async () =>
await withTransaction(
deleteWorkspaceRole({ workspaceId, userId }),
trx
),
{
logger,
operationName: 'deleteWorkspaceRole',
operationDescription: 'Delete workspace role'
}
)
} else {
if (!isWorkspaceRole(role)) {
throw new WorkspaceInvalidRoleError()
@@ -662,12 +743,20 @@ export = FF_WORKSPACES_MODULE_ENABLED
})
})
})
await updateWorkspaceRole({
userId,
workspaceId,
role,
updatedByUserId: context.userId!
})
await withOperationLogging(
async () =>
await updateWorkspaceRole({
userId,
workspaceId,
role,
updatedByUserId: context.userId!
}),
{
logger,
operationName: 'updateWorkspaceRole',
operationDescription: 'Update workspace role'
}
)
}
return await getWorkspaceFactory({ db })({
@@ -676,38 +765,57 @@ export = FF_WORKSPACES_MODULE_ENABLED
})
},
addDomain: async (_parent, args, context) => {
const workspaceId = args.input.workspaceId
await authorizeResolver(
context.userId!,
args.input.workspaceId,
workspaceId,
Roles.Workspace.Admin,
context.resourceAccessRules
)
await addDomainToWorkspaceFactory({
getWorkspace: getWorkspaceFactory({ db }),
findEmailsByUserId: findEmailsByUserIdFactory({ db }),
storeWorkspaceDomain: storeWorkspaceDomainFactory({ db }),
getDomains: getWorkspaceDomainsFactory({ db }),
emitWorkspaceEvent: getEventBus().emit
})({
workspaceId: args.input.workspaceId,
userId: context.userId!,
domain: args.input.domain
const logger = context.log.child({
workspaceId
})
await withOperationLogging(
async () =>
await addDomainToWorkspaceFactory({
getWorkspace: getWorkspaceFactory({ db }),
findEmailsByUserId: findEmailsByUserIdFactory({ db }),
storeWorkspaceDomain: storeWorkspaceDomainFactory({ db }),
getDomains: getWorkspaceDomainsFactory({ db }),
emitWorkspaceEvent: getEventBus().emit
})({
workspaceId,
userId: context.userId!,
domain: args.input.domain
}),
{
logger,
operationName: 'addDomainToWorkspace',
operationDescription: 'Add domain to workspace'
}
)
return await getWorkspaceFactory({ db })({
workspaceId: args.input.workspaceId,
workspaceId,
userId: context.userId
})
},
async deleteDomain(_parent, args, context) {
const workspaceId = args.input.workspaceId
await authorizeResolver(
context.userId!,
args.input.workspaceId,
Roles.Workspace.Admin,
context.resourceAccessRules
)
await deleteWorkspaceDomainFactory({
const logger = context.log.child({
workspaceId
})
const deleteWorkspaceDomain = deleteWorkspaceDomainFactory({
deleteWorkspaceDomain: repoDeleteWorkspaceDomainFactory({ db }),
countDomainsByWorkspaceId: countDomainsByWorkspaceIdFactory({
db
@@ -724,29 +832,56 @@ export = FF_WORKSPACES_MODULE_ENABLED
upsertWorkspace: upsertWorkspaceFactory({ db }),
emitWorkspaceEvent: getEventBus().emit
})
})({ workspaceId: args.input.workspaceId, domainId: args.input.id })
})
await withOperationLogging(
async () =>
await deleteWorkspaceDomain({ workspaceId, domainId: args.input.id }),
{
logger,
operationName: 'deleteWorkspaceDomain',
operationDescription: 'Delete domain from workspace'
}
)
return await getWorkspaceFactory({ db })({
workspaceId: args.input.workspaceId,
workspaceId,
userId: context.userId
})
},
deleteSsoProvider: async (_parent, args, context) => {
const workspaceId = args.workspaceId
await authorizeResolver(
context.userId,
args.workspaceId,
workspaceId,
Roles.Workspace.Admin,
context.resourceAccessRules
)
await deleteSsoProviderFactory({ db })({ workspaceId: args.workspaceId })
const logger = context.log.child({
workspaceId
})
await withOperationLogging(
async () => await deleteSsoProviderFactory({ db })({ workspaceId }),
{
logger,
operationName: 'deleteWorkspaceSsoProvider',
operationDescription: 'Delete SSO provider from workspace'
}
)
return true
},
async join(_parent, args, context) {
if (!context.userId) throw new WorkspaceJoinNotAllowedError()
const workspaceId = args.input.workspaceId
await joinWorkspaceFactory({
const logger = context.log.child({
workspaceId
})
const joinWorkspace = joinWorkspaceFactory({
getUserEmails: findEmailsByUserIdFactory({ db }),
getWorkspaceWithDomains: getWorkspaceWithDomainsFactory({ db }),
upsertWorkspaceRole: upsertWorkspaceRoleFactory({ db }),
@@ -756,14 +891,28 @@ export = FF_WORKSPACES_MODULE_ENABLED
getWorkspaceUserSeat: getWorkspaceUserSeatFactory({ db }),
eventEmit: getEventBus().emit
})
})({ userId: context.userId, workspaceId: args.input.workspaceId })
})
await withOperationLogging(
async () => await joinWorkspace({ userId: context.userId!, workspaceId }),
{
logger,
operationName: 'joinWorkspace',
operationDescription: 'Join workspace'
}
)
return await getWorkspaceFactory({ db })({
workspaceId: args.input.workspaceId,
workspaceId,
userId: context.userId
})
},
leave: async (_parent, args, context) => {
const workspaceId = args.id
const logger = context.log.child({
workspaceId
})
// this is currently not working with the command factory
// TODO: include the onWorkspaceRoleDeletedFactory listener service
const trx = await db.transaction()
@@ -772,35 +921,77 @@ export = FF_WORKSPACES_MODULE_ENABLED
getWorkspaceRoles: getWorkspaceRolesFactory({ db: trx }),
emitWorkspaceEvent: getEventBus().emit
})
await withTransaction(
deleteWorkspaceRole({ workspaceId: args.id, userId: context.userId! }),
trx
await withOperationLogging(
async () =>
await withTransaction(
deleteWorkspaceRole({ workspaceId, userId: context.userId! }),
trx
),
{
logger,
operationName: 'leaveWorkspace',
operationDescription: 'Leave workspace'
}
)
return true
},
updateCreationState: async (_parent, args, context) => {
const workspaceId = args.input.workspaceId
await authorizeResolver(
context.userId!,
args.input.workspaceId,
Roles.Workspace.Admin,
context.resourceAccessRules
)
await upsertWorkspaceCreationStateFactory({ db })({
workspaceCreationState: args.input
const logger = context.log.child({
workspaceId
})
await withOperationLogging(
async () =>
await upsertWorkspaceCreationStateFactory({ db })({
workspaceCreationState: args.input
}),
{
logger,
operationName: 'updateWorkspaceCreationState',
operationDescription: 'Update workspace creation state'
}
)
return true
},
invites: () => ({}),
projects: () => ({}),
dismiss: async (_parent, args, ctx) => {
return await dismissWorkspaceJoinRequestFactory({
const workspaceId = args.input.workspaceId
const logger = ctx.log.child({
workspaceId
})
const dismissWorkspaceJoinRequest = dismissWorkspaceJoinRequestFactory({
getWorkspace: getWorkspaceFactory({ db }),
updateWorkspaceJoinRequestStatus: updateWorkspaceJoinRequestStatusFactory({
db
})
})({ userId: ctx.userId!, workspaceId: args.input.workspaceId })
})
return await withOperationLogging(
async () =>
await dismissWorkspaceJoinRequest({
userId: ctx.userId!,
workspaceId
}),
{
logger,
operationName: 'dismissWorkspaceJoinRequest',
operationDescription: 'Dismiss workspace join request'
}
)
},
requestToJoin: async (_parent, args, ctx) => {
const workspaceId = args.input.workspaceId
const logger = ctx.log.child({
workspaceId
})
const requestToJoin = commandFactory({
db,
operationFactory: ({ db }) => {
@@ -826,10 +1017,18 @@ export = FF_WORKSPACES_MODULE_ENABLED
})
}
})
return await requestToJoin({
userId: ctx.userId!,
workspaceId: args.input.workspaceId
})
return await withOperationLogging(
async () =>
await requestToJoin({
userId: ctx.userId!,
workspaceId
}),
{
logger,
operationName: 'requestToJoinWorkspace',
operationDescription: 'Request to join workspace'
}
)
}
},
WorkspaceInviteMutations: {
@@ -845,6 +1044,11 @@ export = FF_WORKSPACES_MODULE_ENABLED
ctx.resourceAccessRules
)
const logger = ctx.log.child({
workspaceId,
inviteId
})
const resendInviteEmail = resendInviteEmailFactory({
buildInviteEmailContents: buildWorkspaceInviteEmailContentsFactory({
getStream,
@@ -860,30 +1064,52 @@ export = FF_WORKSPACES_MODULE_ENABLED
getServerInfo
})
await resendInviteEmail({
inviteId,
resourceFilter: {
resourceType: WorkspaceInviteResourceType,
resourceId: workspaceId
await withOperationLogging(
async () =>
await resendInviteEmail({
inviteId,
resourceFilter: {
resourceType: WorkspaceInviteResourceType,
resourceId: workspaceId
}
}),
{
logger,
operationName: 'resendWorkspaceInvite',
operationDescription: 'Resend workspace invite'
}
})
)
return true
},
create: async (_parent, args, ctx) => {
const workspaceId = args.workspaceId
const logger = ctx.log.child({
workspaceId
})
const createInvite = createWorkspaceInviteFactory({
createAndSendInvite: buildCreateAndSendWorkspaceInvite()
})
await createInvite({
workspaceId: args.workspaceId,
input: args.input,
inviterId: ctx.userId!,
inviterResourceAccessRules: ctx.resourceAccessRules
})
await withOperationLogging(
async () =>
await createInvite({
workspaceId,
input: args.input,
inviterId: ctx.userId!,
inviterResourceAccessRules: ctx.resourceAccessRules
}),
{
logger,
operationName: 'createWorkspaceInvite',
operationDescription: 'Create workspace invite'
}
)
return ctx.loaders.workspaces!.getWorkspace.load(args.workspaceId)
return ctx.loaders.workspaces!.getWorkspace.load(workspaceId)
},
batchCreate: async (_parent, args, ctx) => {
const workspaceId = args.workspaceId
const inviteCount = args.input.length
if (inviteCount > 10 && ctx.role !== Roles.Server.Admin) {
throw new InviteCreateValidationError(
@@ -891,6 +1117,11 @@ export = FF_WORKSPACES_MODULE_ENABLED
)
}
const logger = ctx.log.child({
workspaceId,
inviteCount
})
const createInvite = createWorkspaceInviteFactory({
createAndSendInvite: buildCreateAndSendWorkspaceInvite()
})
@@ -899,12 +1130,23 @@ export = FF_WORKSPACES_MODULE_ENABLED
for (const batch of inputBatches) {
await Promise.all(
batch.map((i) =>
createInvite({
workspaceId: args.workspaceId,
input: i,
inviterId: ctx.userId!,
inviterResourceAccessRules: ctx.resourceAccessRules
})
withOperationLogging(
async () =>
createInvite({
workspaceId,
input: i,
inviterId: ctx.userId!,
inviterResourceAccessRules: ctx.resourceAccessRules
}),
{
logger: logger.child({
targetUserId: i.userId,
targetEmail: i.email
}),
operationName: 'createWorkspaceInviteFromBatch',
operationDescription: 'Create workspace invite from batch'
}
)
)
)
}
@@ -912,6 +1154,8 @@ export = FF_WORKSPACES_MODULE_ENABLED
return ctx.loaders.workspaces!.getWorkspace.load(args.workspaceId)
},
use: async (_parent, args, ctx) => {
const logger = ctx.log
const finalizeInvite = finalizeResourceInviteFactory({
findInvite: findInviteFactory({
db,
@@ -958,25 +1202,40 @@ export = FF_WORKSPACES_MODULE_ENABLED
getServerInfo
})
await finalizeInvite({
finalizerUserId: ctx.userId!,
finalizerResourceAccessLimits: ctx.resourceAccessRules,
token: args.input.token,
accept: args.input.accept,
resourceType: WorkspaceInviteResourceType,
allowAttachingNewEmail: args.input.addNewEmail ?? undefined
})
await withOperationLogging(
async () =>
await finalizeInvite({
finalizerUserId: ctx.userId!,
finalizerResourceAccessLimits: ctx.resourceAccessRules,
token: args.input.token,
accept: args.input.accept,
resourceType: WorkspaceInviteResourceType,
allowAttachingNewEmail: args.input.addNewEmail ?? undefined
}),
{
logger,
operationName: 'useWorkspaceInvite',
operationDescription: 'Use workspace invite'
}
)
return true
},
cancel: async (_parent, args, ctx) => {
const workspaceId = args.workspaceId
const inviteId = args.inviteId
await authorizeResolver(
ctx.userId,
args.workspaceId,
workspaceId,
Roles.Workspace.Admin,
ctx.resourceAccessRules
)
const logger = ctx.log.child({
workspaceId,
inviteId
})
const cancelInvite = cancelResourceInviteFactory({
findInvite: findInviteFactory({
db,
@@ -989,14 +1248,22 @@ export = FF_WORKSPACES_MODULE_ENABLED
emitEvent: getEventBus().emit
})
await cancelInvite({
resourceId: args.workspaceId,
inviteId: args.inviteId,
cancelerId: ctx.userId!,
resourceType: WorkspaceInviteResourceType,
cancelerResourceAccessLimits: ctx.resourceAccessRules
})
return ctx.loaders.workspaces!.getWorkspace.load(args.workspaceId)
await withOperationLogging(
async () =>
await cancelInvite({
resourceId: args.workspaceId,
inviteId,
cancelerId: ctx.userId!,
resourceType: WorkspaceInviteResourceType,
cancelerResourceAccessLimits: ctx.resourceAccessRules
}),
{
logger,
operationName: 'cancelWorkspaceInvite',
operationDescription: 'Cancel workspace invite'
}
)
return ctx.loaders.workspaces!.getWorkspace.load(workspaceId)
}
},
WorkspaceProjectMutations: {
@@ -1009,6 +1276,8 @@ export = FF_WORKSPACES_MODULE_ENABLED
throw new RateLimitError(rateLimitResult)
}
const logger = context.log
const canCreate = await context.authPolicies.workspace.canCreateProject({
userId: context.userId,
workspaceId: args.input.workspaceId
@@ -1018,29 +1287,57 @@ export = FF_WORKSPACES_MODULE_ENABLED
const createWorkspaceProject = createWorkspaceProjectFactory({
getDefaultRegion: getDefaultRegionFactory({ db })
})
const project = await createWorkspaceProject({
input: args.input,
ownerId: context.userId!
})
const project = await withOperationLogging(
async () =>
await createWorkspaceProject({
input: args.input,
ownerId: context.userId!
}),
{
logger,
operationName: 'createWorkspaceProject',
operationDescription: 'Create workspace project'
}
)
return project
},
updateRole: async (_parent, args, context) => {
const projectId = args.input.projectId
await authorizeResolver(
context.userId,
args.input.projectId,
Roles.Stream.Owner,
context.resourceAccessRules
)
return await updateStreamRoleAndNotify(
args.input,
context.userId!,
context.resourceAccessRules
const logger = context.log.child({
projectId,
streamId: projectId //legacy
})
return await withOperationLogging(
async () =>
await updateStreamRoleAndNotify(
args.input,
context.userId!,
context.resourceAccessRules
),
{
logger,
operationName: 'updateProjectRole',
operationDescription: 'Update workspace project role'
}
)
},
moveToWorkspace: async (_parent, args, context) => {
const { projectId, workspaceId } = args
const logger = context.log.child({
projectId,
streamId: projectId, //legacy
workspaceId
})
const canMoveToWorkspace =
await context.authPolicies.project.canMoveToWorkspace({
userId: context.userId,
@@ -1083,11 +1380,19 @@ export = FF_WORKSPACES_MODULE_ENABLED
})
})
const updatedProject = await moveProjectToWorkspace({
projectId,
workspaceId,
movedByUserId: context.userId!
})
const updatedProject = await withOperationLogging(
async () =>
await moveProjectToWorkspace({
projectId,
workspaceId,
movedByUserId: context.userId!
}),
{
logger,
operationName: 'moveProjectToWorkspace',
operationDescription: 'Move project to workspace'
}
)
// Trigger project region change, if necessary
if (FF_MOVE_PROJECT_REGION_ENABLED) {