fix(api): retry commands that clash with a concurrent write

Services run commands through a CommandRunner that runs one again (up to
5 times, waiting a little longer each time) when its aggregate changed
underneath, e.g. a saga writing to the same ticket. Each command loads its
aggregate fresh and stores its events in one transaction, so a
ConcurrencyError means nothing was stored and running it again is safe.
People no longer get a 409 for it, and the e2e suite stops flaking.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-15 14:00:08 +02:00
parent 76312c079d
commit e14f445e6b
23 changed files with 208 additions and 114 deletions

View File

@@ -1,5 +1,6 @@
import { CommandRunner } from '@task-stream/api/shared/application';
import { Injectable } from '@nestjs/common';
import { CommandBus, QueryBus } from '@nestjs/cqrs';
import { QueryBus } from '@nestjs/cqrs';
import {
AutomationInput,
CreateAutomationCommand,
@@ -14,7 +15,7 @@ import { Automation } from './ports/automations.repository';
@Injectable()
export class AutomationsService {
constructor(
private readonly commandBus: CommandBus,
private readonly commands: CommandRunner,
private readonly queryBus: QueryBus,
) {}
@@ -27,7 +28,7 @@ export class AutomationsService {
projectId: string,
rule: AutomationInput,
): Promise<Automation> {
return this.commandBus.execute(
return this.commands.execute(
new CreateAutomationCommand(actorId, projectId, rule),
);
}
@@ -37,20 +38,18 @@ export class AutomationsService {
automationId: string,
changes: { name?: string; active?: boolean },
): Promise<Automation> {
return this.commandBus.execute(
return this.commands.execute(
new UpdateAutomationCommand(actorId, automationId, changes),
);
}
/** Runs a rule on a ticket; the error, if any, as text. */
run(automation: Automation, issueId: string): Promise<string | null> {
return this.commandBus.execute(
new RunAutomationCommand(automation, issueId),
);
return this.commands.execute(new RunAutomationCommand(automation, issueId));
}
delete(actorId: string, automationId: string): Promise<void> {
return this.commandBus.execute(
return this.commands.execute(
new DeleteAutomationCommand(actorId, automationId),
);
}

View File

@@ -1,5 +1,6 @@
import { CommandRunner } from '@task-stream/api/shared/application';
import { Injectable } from '@nestjs/common';
import { CommandBus, QueryBus } from '@nestjs/cqrs';
import { QueryBus } from '@nestjs/cqrs';
import {
CreateCalendarFeedCommand,
GetCalendarIcsQuery,
@@ -12,7 +13,7 @@ import { CalendarFeed } from './ports/calendar-feeds.repository';
@Injectable()
export class CalendarService {
constructor(
private readonly commandBus: CommandBus,
private readonly commands: CommandRunner,
private readonly queryBus: QueryBus,
) {}
@@ -22,11 +23,11 @@ export class CalendarService {
/** A new secret URL; the previous one, if any, stops working. */
create(userId: string): Promise<CalendarFeed> {
return this.commandBus.execute(new CreateCalendarFeedCommand(userId));
return this.commands.execute(new CreateCalendarFeedCommand(userId));
}
revoke(userId: string): Promise<void> {
return this.commandBus.execute(new RevokeCalendarFeedCommand(userId));
return this.commands.execute(new RevokeCalendarFeedCommand(userId));
}
/** The calendar behind `token`, or null if no feed has it. */

View File

@@ -2,6 +2,7 @@ import { CommandBus, CqrsModule, EventBus } from '@nestjs/cqrs';
import { Test } from '@nestjs/testing';
import {
AggregateRehydrator,
CommandRunner,
EventStore,
} from '@task-stream/api/shared/application';
import {
@@ -151,6 +152,7 @@ async function setup() {
},
{ provide: GitIntegrationsRepository, useValue: git },
AggregateRehydrator,
CommandRunner,
{ provide: EventStore, useValue: store },
{ provide: AttachmentStorage, useValue: files },
{ provide: UserDirectory, useValue: users },

View File

@@ -1,5 +1,6 @@
import { CommandRunner } from '@task-stream/api/shared/application';
import { Injectable } from '@nestjs/common';
import { CommandBus, QueryBus } from '@nestjs/cqrs';
import { QueryBus } from '@nestjs/cqrs';
import { CommentReadModel } from '@task-stream/api/tracker/domain';
import { AddCommentCommand } from './commands/add-comment.command';
import { CommandResult } from './commands/command-result';
@@ -12,7 +13,7 @@ import { GetCommentsQuery } from './queries/get-comments.query';
@Injectable()
export class CommentsService {
constructor(
private readonly commandBus: CommandBus,
private readonly commands: CommandRunner,
private readonly queryBus: QueryBus,
) {}
@@ -22,7 +23,7 @@ export class CommentsService {
body: string,
replyTo: string | null = null,
): Promise<CommandResult> {
return this.commandBus.execute(
return this.commands.execute(
new AddCommentCommand(actorId, issueId, body, replyTo),
);
}
@@ -32,7 +33,7 @@ export class CommentsService {
commentId: string,
body: string,
): Promise<CommandResult> {
return this.commandBus.execute(
return this.commands.execute(
new EditCommentCommand(actorId, commentId, body),
);
}
@@ -44,15 +45,13 @@ export class CommentsService {
emoji: string,
on: boolean,
): Promise<CommandResult> {
return this.commandBus.execute(
return this.commands.execute(
new ReactToCommentCommand(actorId, commentId, emoji, on),
);
}
delete(actorId: string, commentId: string): Promise<CommandResult> {
return this.commandBus.execute(
new DeleteCommentCommand(actorId, commentId),
);
return this.commands.execute(new DeleteCommentCommand(actorId, commentId));
}
list(

View File

@@ -1,4 +1,5 @@
import { CommandBus, QueryBus } from '@nestjs/cqrs';
import { QueryBus } from '@nestjs/cqrs';
import { CommandRunner } from '@task-stream/api/shared/application';
import { ForbiddenError } from '@task-stream/api/shared/domain';
import { createHmac } from 'node:crypto';
import { RecordGitMentionsCommand } from './git/git';
@@ -28,7 +29,7 @@ describe('GitService', () => {
executed.push(command);
return 1;
},
} as unknown as CommandBus,
} as unknown as CommandRunner,
{} as QueryBus,
{
findById: async (id: string) => (id === 'p1' ? { id: 'p1' } : null),

View File

@@ -1,5 +1,6 @@
import { CommandRunner } from '@task-stream/api/shared/application';
import { Injectable } from '@nestjs/common';
import { CommandBus, QueryBus } from '@nestjs/cqrs';
import { QueryBus } from '@nestjs/cqrs';
import { ForbiddenError } from '@task-stream/api/shared/domain';
import {
AddGitRepositoryCommand,
@@ -30,7 +31,7 @@ export interface GitDelivery {
@Injectable()
export class GitService {
constructor(
private readonly commandBus: CommandBus,
private readonly commands: CommandRunner,
private readonly queryBus: QueryBus,
private readonly findProjects: FindProjectsRepository,
private readonly integrations: GitIntegrationsRepository,
@@ -47,7 +48,7 @@ export class GitService {
projectId: string,
name: string,
): Promise<GitRepository> {
return this.commandBus.execute(
return this.commands.execute(
new AddGitRepositoryCommand(actorId, projectId, name),
);
}
@@ -57,7 +58,7 @@ export class GitService {
projectId: string,
repositoryId: string,
): Promise<GitRepository> {
return this.commandBus.execute(
return this.commands.execute(
new RotateGitRepositorySecretCommand(actorId, projectId, repositoryId),
);
}
@@ -67,7 +68,7 @@ export class GitService {
projectId: string,
repositoryId: string,
): Promise<void> {
return this.commandBus.execute(
return this.commands.execute(
new RemoveGitRepositoryCommand(actorId, projectId, repositoryId),
);
}
@@ -104,7 +105,7 @@ export class GitService {
delivery.body,
);
if (!mentions.length) return { recorded: 0 };
const recorded: number = await this.commandBus.execute(
const recorded: number = await this.commands.execute(
new RecordGitMentionsCommand(project.id, repository.name, mentions),
);
return { recorded };

View File

@@ -1,5 +1,6 @@
import { CommandRunner } from '@task-stream/api/shared/application';
import { Injectable } from '@nestjs/common';
import { CommandBus, QueryBus } from '@nestjs/cqrs';
import { QueryBus } from '@nestjs/cqrs';
import { assertThat } from '@task-stream/api/shared/domain';
import {
ActivityReadModel,
@@ -79,7 +80,7 @@ export interface IssueLinkInput {
@Injectable()
export class IssuesService {
constructor(
private readonly commandBus: CommandBus,
private readonly commands: CommandRunner,
private readonly queryBus: QueryBus,
private readonly access: WriteAccess,
private readonly findIssues: FindIssuesRepository,
@@ -97,7 +98,7 @@ export class IssuesService {
useTemplate,
...details
} = input;
return this.commandBus.execute(
return this.commands.execute(
new CreateIssueCommand(
actorId,
projectId,
@@ -118,7 +119,7 @@ export class IssuesService {
issueId: string,
changes: IssueDetails,
): Promise<CommandResult> {
return this.commandBus.execute(
return this.commands.execute(
new UpdateIssueCommand(actorId, issueId, changes),
);
}
@@ -129,7 +130,7 @@ export class IssuesService {
status: string,
reason?: string | null,
): Promise<CommandResult> {
return this.commandBus.execute(
return this.commands.execute(
new ChangeIssueStatusCommand(actorId, issueId, status, reason),
);
}
@@ -140,13 +141,13 @@ export class IssuesService {
issueId: string,
assigneeId: string | null,
): Promise<CommandResult> {
return this.commandBus.execute(
return this.commands.execute(
new AssignIssueCommand(actorId, issueId, assigneeId),
);
}
link(actorId: string, input: IssueLinkInput): Promise<CommandResult> {
return this.commandBus.execute(
return this.commands.execute(
new LinkIssuesCommand(
actorId,
input.issueId,
@@ -157,7 +158,7 @@ export class IssuesService {
}
unlink(actorId: string, input: IssueLinkInput): Promise<CommandResult> {
return this.commandBus.execute(
return this.commands.execute(
new UnlinkIssuesCommand(
actorId,
input.issueId,
@@ -173,7 +174,7 @@ export class IssuesService {
issueId: string,
releaseId: string | null,
): Promise<CommandResult> {
return this.commandBus.execute(
return this.commands.execute(
new SetIssueReleaseCommand(actorId, issueId, releaseId),
);
}
@@ -185,7 +186,7 @@ export class IssuesService {
file: IncomingFile,
commentId: string | null = null,
): Promise<AttachmentResult> {
return this.commandBus.execute(
return this.commands.execute(
new AttachFileCommand(actorId, issueId, file, commentId),
);
}
@@ -195,7 +196,7 @@ export class IssuesService {
issueId: string,
attachmentId: string,
): Promise<CommandResult> {
return this.commandBus.execute(
return this.commands.execute(
new DetachFileCommand(actorId, issueId, attachmentId),
);
}
@@ -206,7 +207,7 @@ export class IssuesService {
issueId: string,
watching: boolean,
): Promise<CommandResult> {
return this.commandBus.execute(
return this.commands.execute(
new WatchIssueCommand(actorId, issueId, actorId, watching),
);
}
@@ -236,7 +237,7 @@ export class IssuesService {
issueId: string,
parentId: string | null,
): Promise<CommandResult> {
return this.commandBus.execute(
return this.commands.execute(
new SetIssueParentCommand(actorId, issueId, parentId),
);
}
@@ -248,7 +249,7 @@ export class IssuesService {
above: string | null,
below: string | null,
): Promise<CommandResult> {
return this.commandBus.execute(
return this.commands.execute(
new RankIssueCommand(actorId, issueId, above, below),
);
}
@@ -278,7 +279,7 @@ export class IssuesService {
if (change.assigneeId !== undefined)
await this.assign(actorId, issueId, change.assigneeId);
if (change.sprintId !== undefined)
await this.commandBus.execute(
await this.commands.execute(
new SetIssueSprintCommand(actorId, issueId, change.sprintId),
);
if (change.releaseId !== undefined)
@@ -306,12 +307,12 @@ export class IssuesService {
/** To the trash (whoever opened it, or a manager); back with `restore`. */
trash(actorId: string, issueId: string): Promise<CommandResult> {
return this.commandBus.execute(new TrashIssueCommand(actorId, issueId));
return this.commands.execute(new TrashIssueCommand(actorId, issueId));
}
/** Deletes it for good (the trash is emptied this way). */
delete(actorId: string, issueId: string): Promise<void> {
return this.commandBus.execute(new DeleteIssueCommand(actorId, issueId));
return this.commands.execute(new DeleteIssueCommand(actorId, issueId));
}
/**
@@ -326,12 +327,12 @@ export class IssuesService {
}
restore(actorId: string, issueId: string): Promise<CommandResult> {
return this.commandBus.execute(new RestoreIssueCommand(actorId, issueId));
return this.commands.execute(new RestoreIssueCommand(actorId, issueId));
}
/** Votes for the ticket (`on`) or takes the vote back. */
vote(actorId: string, issueId: string, on: boolean): Promise<CommandResult> {
return this.commandBus.execute(new VoteIssueCommand(actorId, issueId, on));
return this.commands.execute(new VoteIssueCommand(actorId, issueId, on));
}
/**
@@ -378,7 +379,7 @@ export class IssuesService {
issueId: string,
text: string,
): Promise<CommandResult> {
return this.commandBus.execute(
return this.commands.execute(
new AddChecklistItemCommand(actorId, issueId, text),
);
}
@@ -389,7 +390,7 @@ export class IssuesService {
itemId: string,
changes: { text?: string; done?: boolean },
): Promise<CommandResult> {
return this.commandBus.execute(
return this.commands.execute(
new UpdateChecklistItemCommand(actorId, issueId, itemId, changes),
);
}
@@ -399,7 +400,7 @@ export class IssuesService {
issueId: string,
itemId: string,
): Promise<CommandResult> {
return this.commandBus.execute(
return this.commands.execute(
new RemoveChecklistItemCommand(actorId, issueId, itemId),
);
}

View File

@@ -1,5 +1,6 @@
import { CommandRunner } from '@task-stream/api/shared/application';
import { Injectable } from '@nestjs/common';
import { CommandBus, QueryBus } from '@nestjs/cqrs';
import { QueryBus } from '@nestjs/cqrs';
import { NotificationReadModel } from '@task-stream/api/tracker/domain';
import { SendDueRemindersCommand } from './notifications/reminders';
import {
@@ -20,7 +21,7 @@ import { ChangeFeed } from './ports/change-feed';
@Injectable()
export class NotificationsService {
constructor(
private readonly commandBus: CommandBus,
private readonly commands: CommandRunner,
private readonly queryBus: QueryBus,
private readonly changeFeed: ChangeFeed,
) {}
@@ -45,25 +46,23 @@ export class NotificationsService {
endpoint: string,
keys: { p256dh: string; auth: string },
): Promise<void> {
return this.commandBus.execute(
return this.commands.execute(
new SubscribePushCommand(userId, endpoint, keys),
);
}
/** Push off on this device. */
unsubscribePush(userId: string, endpoint: string): Promise<void> {
return this.commandBus.execute(
new UnsubscribePushCommand(userId, endpoint),
);
return this.commands.execute(new UnsubscribePushCommand(userId, endpoint));
}
/** Tells of the tickets due around `day` (YYYY-MM-DD); how many it sent. */
sendDueReminders(day: string): Promise<number> {
return this.commandBus.execute(new SendDueRemindersCommand(day));
return this.commands.execute(new SendDueRemindersCommand(day));
}
markRead(userId: string, ids?: string[] | null): Promise<void> {
return this.commandBus.execute(
return this.commands.execute(
new MarkNotificationsReadCommand(userId, ids?.length ? ids : null),
);
}
@@ -76,7 +75,7 @@ export class NotificationsService {
userId: string,
changes: { email: boolean },
): Promise<MyNotificationSettings> {
await this.commandBus.execute(
await this.commands.execute(
new UpdateNotificationSettingsCommand(userId, changes.email),
);
return this.settings(userId);

View File

@@ -1,6 +1,7 @@
import { GetShowcaseQuery, Showcase } from './queries/get-showcase.query';
import { CommandRunner } from '@task-stream/api/shared/application';
import { Injectable } from '@nestjs/common';
import { CommandBus, QueryBus } from '@nestjs/cqrs';
import { QueryBus } from '@nestjs/cqrs';
import {
BusinessRuleError,
ForbiddenError,
@@ -50,7 +51,7 @@ import { Automation } from './ports/automations.repository';
@Injectable()
export class ProjectsService {
constructor(
private readonly commandBus: CommandBus,
private readonly commands: CommandRunner,
private readonly queryBus: QueryBus,
private readonly findProjects: FindProjectsRepository,
private readonly access: WriteAccess,
@@ -92,7 +93,7 @@ export class ProjectsService {
const signer = author?.trim().slice(0, 80);
const signature = `_Propuesta enviada desde el formulario público${signer ? ` por ${signer}` : ''}._`;
return this.commandBus.execute(
return this.commands.execute(
new CreateIssueCommand(
SYSTEM_ACTOR_ID,
project.id,
@@ -139,7 +140,7 @@ export class ProjectsService {
const template = input.templateId
? await this.template(actorId, input.templateId)
: null;
const created: CommandResult = await this.commandBus.execute(
const created: CommandResult = await this.commands.execute(
new CreateProjectCommand(
actorId,
input.key,
@@ -149,7 +150,7 @@ export class ProjectsService {
),
);
if (!template) return created;
const configured: CommandResult = await this.commandBus.execute(
const configured: CommandResult = await this.commands.execute(
new UpdateProjectSettingsCommand(
actorId,
created.id,
@@ -182,7 +183,7 @@ export class ProjectsService {
rule: Automation,
): Promise<void> {
try {
const copy: Automation = await this.commandBus.execute(
const copy: Automation = await this.commands.execute(
new CreateAutomationCommand(actorId, projectId, {
name: rule.name,
trigger: rule.trigger,
@@ -192,7 +193,7 @@ export class ProjectsService {
}),
);
if (!rule.active)
await this.commandBus.execute(
await this.commands.execute(
new UpdateAutomationCommand(actorId, copy.id, { active: false }),
);
} catch (error) {
@@ -215,7 +216,7 @@ export class ProjectsService {
acceptsProposals?: boolean;
},
): Promise<CommandResult> {
return this.commandBus.execute(
return this.commands.execute(
new UpdateProjectCommand(actorId, projectId, changes),
);
}
@@ -229,7 +230,7 @@ export class ProjectsService {
projectId: string,
confirmKey: string,
): Promise<CommandResult> {
return this.commandBus.execute(
return this.commands.execute(
new DeleteProjectCommand(actorId, projectId, confirmKey),
);
}
@@ -245,7 +246,7 @@ export class ProjectsService {
projectId: string,
settings: ProjectSettings,
): Promise<CommandResult> {
return this.commandBus.execute(
return this.commands.execute(
new UpdateProjectSettingsCommand(actorId, projectId, settings),
);
}
@@ -255,7 +256,7 @@ export class ProjectsService {
input: { projectId: string; userId: string; role: ProjectRole },
accessToken?: string,
): Promise<CommandResult> {
return this.commandBus.execute(
return this.commands.execute(
new AddProjectMemberCommand(
actorId,
input.projectId,
@@ -270,7 +271,7 @@ export class ProjectsService {
actorId: string,
input: { projectId: string; userId: string; role: ProjectRole },
): Promise<CommandResult> {
return this.commandBus.execute(
return this.commands.execute(
new ChangeProjectMemberRoleCommand(
actorId,
input.projectId,
@@ -285,7 +286,7 @@ export class ProjectsService {
projectId: string,
userId: string,
): Promise<CommandResult> {
return this.commandBus.execute(
return this.commands.execute(
new RemoveProjectMemberCommand(actorId, projectId, userId),
);
}

View File

@@ -1,5 +1,6 @@
import { CommandRunner } from '@task-stream/api/shared/application';
import { Injectable } from '@nestjs/common';
import { CommandBus, QueryBus } from '@nestjs/cqrs';
import { QueryBus } from '@nestjs/cqrs';
import {
ReleaseBurnupReadModel,
ReleaseProgressReadModel,
@@ -26,7 +27,7 @@ interface ReleaseDetails {
@Injectable()
export class ReleasesService {
constructor(
private readonly commandBus: CommandBus,
private readonly commands: CommandRunner,
private readonly queryBus: QueryBus,
) {}
@@ -34,7 +35,7 @@ export class ReleasesService {
actorId: string,
input: ReleaseDetails & { projectId: string; name: string },
): Promise<CommandResult> {
return this.commandBus.execute(
return this.commands.execute(
new CreateReleaseCommand(
actorId,
input.projectId,
@@ -50,7 +51,7 @@ export class ReleasesService {
releaseId: string,
changes: ReleaseDetails,
): Promise<CommandResult> {
return this.commandBus.execute(
return this.commands.execute(
new UpdateReleaseCommand(actorId, releaseId, changes),
);
}
@@ -61,7 +62,7 @@ export class ReleasesService {
releaseId: string,
force = false,
): Promise<CommandResult> {
return this.commandBus.execute(
return this.commands.execute(
new PublishReleaseCommand(actorId, releaseId, force),
);
}
@@ -72,7 +73,7 @@ export class ReleasesService {
releaseId: string,
moveTo: string | null = null,
): Promise<CommandResult> {
return this.commandBus.execute(
return this.commands.execute(
new ArchiveReleaseCommand(actorId, releaseId, moveTo),
);
}
@@ -95,7 +96,7 @@ export class ReleasesService {
releaseId: string,
notes: string | null,
): Promise<CommandResult> {
return this.commandBus.execute(
return this.commands.execute(
new WriteReleaseNotesCommand(actorId, releaseId, notes),
);
}

View File

@@ -1,5 +1,6 @@
import { CommandRunner } from '@task-stream/api/shared/application';
import { Injectable } from '@nestjs/common';
import { CommandBus, QueryBus } from '@nestjs/cqrs';
import { QueryBus } from '@nestjs/cqrs';
import {
SavedFilterCriteriaReadModel,
SavedFilterReadModel,
@@ -21,7 +22,7 @@ export interface SaveFilterInput {
@Injectable()
export class SavedFiltersService {
constructor(
private readonly commandBus: CommandBus,
private readonly commands: CommandRunner,
private readonly queryBus: QueryBus,
) {}
@@ -30,7 +31,7 @@ export class SavedFiltersService {
}
save(actorId: string, input: SaveFilterInput): Promise<{ id: string }> {
return this.commandBus.execute(
return this.commands.execute(
new SaveFilterCommand(
actorId,
input.projectId,
@@ -42,7 +43,7 @@ export class SavedFiltersService {
}
delete(actorId: string, filterId: string): Promise<void> {
return this.commandBus.execute(
return this.commands.execute(
new DeleteSavedFilterCommand(actorId, filterId),
);
}

View File

@@ -1,5 +1,6 @@
import { CommandRunner } from '@task-stream/api/shared/application';
import { Injectable } from '@nestjs/common';
import { CommandBus, QueryBus } from '@nestjs/cqrs';
import { QueryBus } from '@nestjs/cqrs';
import {
SprintBurndownReadModel,
SprintDetails,
@@ -34,7 +35,7 @@ export interface CreateSprintInput {
@Injectable()
export class SprintsService {
constructor(
private readonly commandBus: CommandBus,
private readonly commands: CommandRunner,
private readonly queryBus: QueryBus,
) {}
@@ -65,7 +66,7 @@ export class SprintsService {
}
create(actorId: string, input: CreateSprintInput): Promise<CommandResult> {
return this.commandBus.execute(
return this.commands.execute(
new CreateSprintCommand(
actorId,
input.projectId,
@@ -82,13 +83,13 @@ export class SprintsService {
sprintId: string,
changes: SprintDetails,
): Promise<CommandResult> {
return this.commandBus.execute(
return this.commands.execute(
new UpdateSprintCommand(actorId, sprintId, changes),
);
}
start(actorId: string, sprintId: string): Promise<CommandResult> {
return this.commandBus.execute(new StartSprintCommand(actorId, sprintId));
return this.commands.execute(new StartSprintCommand(actorId, sprintId));
}
/** Unfinished tickets go to `carryOverTo` (null: the backlog). */
@@ -97,14 +98,14 @@ export class SprintsService {
sprintId: string,
carryOverTo: string | null,
): Promise<CommandResult> {
return this.commandBus.execute(
return this.commands.execute(
new CloseSprintCommand(actorId, sprintId, carryOverTo),
);
}
/** Planned sprints only; its tickets go back to the backlog. */
delete(actorId: string, sprintId: string): Promise<CommandResult> {
return this.commandBus.execute(
return this.commands.execute(
new DeleteSprintCommand(actorId, sprintId),
) as Promise<CommandResult>;
}
@@ -115,7 +116,7 @@ export class SprintsService {
issueId: string,
sprintId: string | null,
): Promise<CommandResult> {
return this.commandBus.execute(
return this.commands.execute(
new SetIssueSprintCommand(actorId, issueId, sprintId),
);
}

View File

@@ -1,5 +1,6 @@
import { CommandRunner } from '@task-stream/api/shared/application';
import { Injectable } from '@nestjs/common';
import { CommandBus, QueryBus } from '@nestjs/cqrs';
import { QueryBus } from '@nestjs/cqrs';
import { WebhookDelivery } from './ports/outgoing-webhooks.repository';
import { WEBHOOK_EVENT_NAMES } from './webhooks/webhook-events';
import {
@@ -15,7 +16,7 @@ import {
@Injectable()
export class WebhooksService {
constructor(
private readonly commandBus: CommandBus,
private readonly commands: CommandRunner,
private readonly queryBus: QueryBus,
) {}
@@ -35,7 +36,7 @@ export class WebhooksService {
url: string,
events: string[],
): Promise<{ id: string; secret: string }> {
return this.commandBus.execute(
return this.commands.execute(
new CreateWebhookCommand(actorId, projectId, url, events),
);
}
@@ -45,18 +46,16 @@ export class WebhooksService {
webhookId: string,
changes: { url?: string; events?: string[]; active?: boolean },
): Promise<WebhookView> {
return this.commandBus.execute(
return this.commands.execute(
new UpdateWebhookCommand(actorId, webhookId, changes),
);
}
delete(actorId: string, webhookId: string): Promise<void> {
return this.commandBus.execute(
new DeleteWebhookCommand(actorId, webhookId),
);
return this.commands.execute(new DeleteWebhookCommand(actorId, webhookId));
}
test(actorId: string, webhookId: string): Promise<WebhookDelivery> {
return this.commandBus.execute(new TestWebhookCommand(actorId, webhookId));
return this.commands.execute(new TestWebhookCommand(actorId, webhookId));
}
}