diff --git a/packages/server/modules/workspaces/graph/resolvers/workspaces.ts b/packages/server/modules/workspaces/graph/resolvers/workspaces.ts index da872b495..9d7090d3c 100644 --- a/packages/server/modules/workspaces/graph/resolvers/workspaces.ts +++ b/packages/server/modules/workspaces/graph/resolvers/workspaces.ts @@ -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) {