Skip to content

Commit

Permalink
feat(api): publish events in command handlers (#623)
Browse files Browse the repository at this point in the history
* publish events in command handlers

* WIP publish events in command handlers

* initialize term view to have empty contributions list

* fix lint

* refactor based on PR #623

* update integration test setup to include an event publisher provider

* switch to state-based test setup for term by ID query test

* use state based test setup for term fetch many queries
  • Loading branch information
aaron-plahn authored Jan 16, 2025
1 parent 80e1430 commit 289939d
Show file tree
Hide file tree
Showing 36 changed files with 396 additions and 363 deletions.
35 changes: 33 additions & 2 deletions apps/api/src/app/controllers/__tests__/createTestModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ import { MockJwtAuthGuard } from '../../../authorization/mock-jwt-auth-guard';
import { MockJwtStrategy } from '../../../authorization/mock-jwt.strategy';
import { OptionalJwtAuthGuard } from '../../../authorization/optional-jwt-auth-guard';
import { ConsoleCoscradCliLogger } from '../../../coscrad-cli/logging';
import { CoscradEventFactory, CoscradEventUnion } from '../../../domain/common';
import { CoscradEventFactory, CoscradEventUnion, EventModule } from '../../../domain/common';
import { ObservableInMemoryEventPublisher } from '../../../domain/common/events/in-memory-event-publisher';
import { EVENT_PUBLISHER_TOKEN } from '../../../domain/common/events/interfaces';
import { SyncInMemoryEventPublisher } from '../../../domain/common/events/sync-in-memory-event-publisher';
import { ID_MANAGER_TOKEN } from '../../../domain/interfaces/id-manager.interface';
import { AudioItemController } from '../../../domain/models/audio-visual/application/audio-item.controller';
import { VideoController } from '../../../domain/models/audio-visual/application/video.controller';
Expand Down Expand Up @@ -143,8 +146,12 @@ import {
UnpublishResource,
UnpublishResourceCommandHandler,
} from '../../../domain/models/shared/common-commands';
import { ResourceReadAccessGrantedToUserEventHandler } from '../../../domain/models/shared/common-commands/grant-resource-read-access-to-user/resource-read-access-granted-to-user.event-handler';
import { ResourcePublished } from '../../../domain/models/shared/common-commands/publish-resource/resource-published.event';
import { IQueryRepositoryProvider } from '../../../domain/models/shared/common-commands/publish-resource/resource-published.event-handler';
import {
IQueryRepositoryProvider,
ResourcePublishedEventHandler,
} from '../../../domain/models/shared/common-commands/publish-resource/resource-published.event-handler';
import {
AddLyricsForSong,
AddLyricsForSongCommandHandler,
Expand Down Expand Up @@ -187,10 +194,15 @@ import {
PromptTermCreated,
TermCreated,
TermElicitedFromPrompt,
TermElicitedFromPromptEventHandler,
TermTranslated,
TranslateTerm,
TranslateTermCommandHandler,
} from '../../../domain/models/term/commands';
import { AudioAddedForTermEventHandler } from '../../../domain/models/term/commands/add-audio-for-term/audio-added-for-term.event-handler';
import { PromptTermCreatedEventHandler } from '../../../domain/models/term/commands/create-prompt-term/prompt-term-created.event-handler';
import { TermCreatedEventHandler } from '../../../domain/models/term/commands/create-term/term-created.event-handler';
import { TermTranslatedEventHandler } from '../../../domain/models/term/commands/translate-term/term-translated.event-handler';
import { Term } from '../../../domain/models/term/entities/term.entity';
import {
ITermQueryRepository,
Expand Down Expand Up @@ -380,6 +392,7 @@ export default async (
CommandModule,
PassportModule.register({ defaultStrategy: 'jwt' }),
DynamicDataTypeModule,
EventModule,
],
providers: [
CommandInfoService,
Expand All @@ -391,6 +404,10 @@ export default async (
buildConfigFilePath(Environment.test)
),
},
{
provide: EVENT_PUBLISHER_TOKEN,
useValue: new SyncInMemoryEventPublisher(new ConsoleCoscradCliLogger()),
},
{
provide: ArangoConnectionProvider,
useFactory: async (configService: ConfigService) => {
Expand Down Expand Up @@ -674,7 +691,13 @@ export default async (
DynamicDataTypeFinderService,
],
},
{
provide: 'COSCRAD_EVENT_PUBLISHER',
useFactory: () =>
new ObservableInMemoryEventPublisher(new ConsoleCoscradCliLogger()),
},
...dataClassProviders,

/**
* TODO [https://www.pivotaltracker.com/story/show/182576828]
*
Expand Down Expand Up @@ -799,6 +822,14 @@ export default async (
AddContentToDigitalTextPageCommandHandler,
CreatePhotograph,
CreatePhotographCommandHandler,
// Event Handlers
ResourcePublishedEventHandler,
ResourceReadAccessGrantedToUserEventHandler,
TermCreatedEventHandler,
TermTranslatedEventHandler,
PromptTermCreatedEventHandler,
TermElicitedFromPromptEventHandler,
AudioAddedForTermEventHandler,
],

controllers: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import { CommandHandlerService, FluxStandardAction } from '@coscrad/commands';
import { CoscradUserRole } from '@coscrad/data-types';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { ConsoleCoscradCliLogger } from '../../../coscrad-cli/logging';
import getValidAggregateInstanceForTest from '../../../domain/__tests__/utilities/getValidAggregateInstanceForTest';
import { ObservableInMemoryEventPublisher } from '../../../domain/common/events/in-memory-event-publisher';
import { IIdManager } from '../../../domain/interfaces/id-manager.interface';
import { buildFakeTimersConfig } from '../../../domain/models/__tests__/utilities/buildFakeTimersConfig';
import { CreateSong } from '../../../domain/models/song/commands/create-song.command';
Expand Down Expand Up @@ -80,7 +82,11 @@ describe('The Command Controller', () => {
*/
commandHandlerService.registerHandler(
'CREATE_SONG',
new CreateSongCommandHandler(testRepositoryProvider, idManager)
new CreateSongCommandHandler(
testRepositoryProvider,
idManager,
new ObservableInMemoryEventPublisher(new ConsoleCoscradCliLogger())
)
);

jest.useFakeTimers(buildFakeTimersConfig());
Expand Down
10 changes: 4 additions & 6 deletions apps/api/src/domain/common/events/event.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,6 @@ import { SyncInMemoryEventPublisher } from './sync-in-memory-event-publisher';
{
provide: EVENT_PUBLISHER_TOKEN,
useFactory: () => new SyncInMemoryEventPublisher(new ConsoleCoscradCliLogger()),
// new InMemoryEventPublisher(
// // TODO We need to work on our logging strategy
// new ConsoleCoscradCliLogger()
// ),
},
],
exports: [CoscradEventFactory, EVENT_PUBLISHER_TOKEN],
Expand All @@ -45,13 +41,15 @@ export class EventModule {
) {}

onApplicationBootstrap() {
const handlersAndTypes = this.discoveryService
const allInstancesAndMeta = this.discoveryService
.getProviders()
.filter((provider) => provider.instance)
.map((provider) => [
provider.instance,
getCoscradEventConsumerMeta(Object.getPrototypeOf(provider.instance).constructor),
])
]);

const handlersAndTypes = allInstancesAndMeta
.filter(
(
instanceAndMeta
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ export class ObservableInMemoryEventPublisher
super();
}

// todo make this async
publish(eventOrEvents: ICoscradEvent<unknown> | ICoscradEvent<unknown>[]): void {
const eventsToPublish = Array.isArray(eventOrEvents) ? eventOrEvents : [eventOrEvents];

Expand Down Expand Up @@ -56,7 +55,11 @@ export class ObservableInMemoryEventPublisher
}

private ofEventType(type: string) {
return this.subject$.pipe(filter((event) => event.isOfType(type)));
return this.subject$.pipe(
filter((event) => {
return event.isOfType(type);
})
);
}

onModuleDestroy() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { ICoscradEventHandler } from '../coscrad-event-handler.interface';
import { ICoscradEvent } from '../coscrad-event.interface';

export const EVENT_PUBLISHER_TOKEN = 'EVENT_PUBLISHER_TOKEN';

export interface ICoscradEventPublisher {
publish(eventsToPublish: ICoscradEvent | ICoscradEvent[]): void;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,19 @@ export class SyncInMemoryEventPublisher implements ICoscradEventPublisher {
this.eventTypeToConsumers.set(eventType, []);
}

this.eventTypeToConsumers.get(eventType).push(eventConsumer);
/**
* We want registration to be idempotent in case this module is somehow
* initialized multiple times.
*/
if (!this.has(eventType, eventConsumer))
this.eventTypeToConsumers.get(eventType).push(eventConsumer);
}

private has(eventType: string, eventConsumer: ICoscradEventHandler) {
return (
this.eventTypeToConsumers.has(eventType) &&
// Compare by reference
this.eventTypeToConsumers.get(eventType).some((handler) => handler === eventConsumer)
);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { CommandHandler } from '@coscrad/commands';
import { Inject } from '@nestjs/common';
import {
EVENT_PUBLISHER_TOKEN,
ICoscradEventPublisher,
} from '../../../../../../../domain/common/events/interfaces';
import { InternalError } from '../../../../../../../lib/errors/InternalError';
import { isNotFound } from '../../../../../../../lib/types/not-found';
import { REPOSITORY_PROVIDER_TOKEN } from '../../../../../../../persistence/constants/persistenceConstants';
Expand Down Expand Up @@ -31,9 +35,10 @@ export class AddLineItemtoTranscriptCommandHandler extends BaseCommandHandler<Tr
constructor(
@Inject(REPOSITORY_PROVIDER_TOKEN)
protected readonly repositoryProvider: IRepositoryProvider,
@Inject(ID_MANAGER_TOKEN) protected readonly idManager: IIdManager
@Inject(ID_MANAGER_TOKEN) protected readonly idManager: IIdManager,
@Inject(EVENT_PUBLISHER_TOKEN) protected readonly eventPublisher: ICoscradEventPublisher
) {
super(repositoryProvider, idManager);
super(repositoryProvider, idManager, eventPublisher);
}

protected fetchRequiredExternalState(): Promise<InMemorySnapshot> {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { CommandHandler } from '@coscrad/commands';
import { Inject } from '@nestjs/common';
import { EVENT_PUBLISHER_TOKEN } from '../../../../../../../domain/common';
import { ICoscradEventPublisher } from '../../../../../../../domain/common/events/interfaces';
import { InternalError } from '../../../../../../../lib/errors/InternalError';
import { isNotFound } from '../../../../../../../lib/types/not-found';
import { REPOSITORY_PROVIDER_TOKEN } from '../../../../../../../persistence/constants/persistenceConstants';
Expand Down Expand Up @@ -34,9 +36,10 @@ export class AddParticipantToTranscriptCommandHandler extends BaseCommandHandler
constructor(
@Inject(REPOSITORY_PROVIDER_TOKEN)
protected readonly repositoryProvider: IRepositoryProvider,
@Inject(ID_MANAGER_TOKEN) protected readonly idManager: IIdManager
@Inject(ID_MANAGER_TOKEN) protected readonly idManager: IIdManager,
@Inject(EVENT_PUBLISHER_TOKEN) protected readonly eventPublisher: ICoscradEventPublisher
) {
super(repositoryProvider, idManager);
super(repositoryProvider, idManager, eventPublisher);
}

protected async createOrFetchWriteContext({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { CommandHandler } from '@coscrad/commands';
import { Inject } from '@nestjs/common';
import { EVENT_PUBLISHER_TOKEN } from '../../../../../../../domain/common';
import { ICoscradEventPublisher } from '../../../../../../../domain/common/events/interfaces';
import { InternalError } from '../../../../../../../lib/errors/InternalError';
import { isNotFound } from '../../../../../../../lib/types/not-found';
import { REPOSITORY_PROVIDER_TOKEN } from '../../../../../../../persistence/constants/persistenceConstants';
Expand Down Expand Up @@ -28,9 +30,10 @@ export class CreateTranscriptCommandHandler extends BaseCommandHandler<AudioItem
constructor(
@Inject(REPOSITORY_PROVIDER_TOKEN)
protected readonly repositoryProvider: IRepositoryProvider,
@Inject(ID_MANAGER_TOKEN) protected readonly idManager: IIdManager
@Inject(ID_MANAGER_TOKEN) protected readonly idManager: IIdManager,
@Inject(EVENT_PUBLISHER_TOKEN) protected readonly eventPublisher: ICoscradEventPublisher
) {
super(repositoryProvider, idManager);
super(repositoryProvider, idManager, eventPublisher);
}

protected async createOrFetchWriteContext({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { Inject } from '@nestjs/common';
import { EVENT_PUBLISHER_TOKEN } from '../../../../../domain/common';
import { ICoscradEventPublisher } from '../../../../../domain/common/events/interfaces';
import { InternalError } from '../../../../../lib/errors/InternalError';
import { REPOSITORY_PROVIDER_TOKEN } from '../../../../../persistence/constants/persistenceConstants';
import { Valid } from '../../../../domainModelValidators/Valid';
Expand All @@ -23,9 +25,10 @@ export abstract class BaseCreateBibliographicCitation extends BaseCreateCommandH
constructor(
@Inject(REPOSITORY_PROVIDER_TOKEN)
protected readonly repositoryProvider: IRepositoryProvider,
@Inject('ID_MANAGER') protected readonly idManager: IIdManager
@Inject('ID_MANAGER') protected readonly idManager: IIdManager,
@Inject(EVENT_PUBLISHER_TOKEN) protected readonly eventPublisher: ICoscradEventPublisher
) {
super(repositoryProvider, idManager);
super(repositoryProvider, idManager, eventPublisher);

this.repositoryForCommandsTargetAggregate = this.repositoryProvider.forResource(
this.aggregateType
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { CommandHandler } from '@coscrad/commands';
import { Inject } from '@nestjs/common';
import { EVENT_PUBLISHER_TOKEN } from '../../../../../../domain/common';
import { ICoscradEventPublisher } from '../../../../../../domain/common/events/interfaces';
import { REPOSITORY_PROVIDER_TOKEN } from '../../../../../../persistence/constants/persistenceConstants';
import { DTO } from '../../../../../../types/DTO';
import { ResultOrError } from '../../../../../../types/ResultOrError';
Expand All @@ -23,9 +25,10 @@ export class CreateCourtCaseBibliographicCitationCommandHandler extends BaseCrea
constructor(
@Inject(REPOSITORY_PROVIDER_TOKEN)
protected readonly repositoryProvider: IRepositoryProvider,
@Inject('ID_MANAGER') protected readonly idManager: IIdManager
@Inject('ID_MANAGER') protected readonly idManager: IIdManager,
@Inject(EVENT_PUBLISHER_TOKEN) protected readonly eventPublisher: ICoscradEventPublisher
) {
super(repositoryProvider, idManager);
super(repositoryProvider, idManager, eventPublisher);

this.repositoryForCommandsTargetAggregate = this.repositoryProvider.forResource(
this.aggregateType
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { CommandHandler } from '@coscrad/commands';
import { Inject } from '@nestjs/common';
import { EVENT_PUBLISHER_TOKEN } from '../../../../../domain/common';
import { ICoscradEventPublisher } from '../../../../../domain/common/events/interfaces';
import { REPOSITORY_PROVIDER_TOKEN } from '../../../../../persistence/constants/persistenceConstants';
import { DTO } from '../../../../../types/DTO';
import { ResultOrError } from '../../../../../types/ResultOrError';
Expand All @@ -24,9 +26,10 @@ export class CreateJournalArticleBibliographicCitationCommandHandler extends Bas
constructor(
@Inject(REPOSITORY_PROVIDER_TOKEN)
protected readonly repositoryProvider: IRepositoryProvider,
@Inject('ID_MANAGER') protected readonly idManager: IIdManager
@Inject('ID_MANAGER') protected readonly idManager: IIdManager,
@Inject(EVENT_PUBLISHER_TOKEN) protected readonly eventPublisher: ICoscradEventPublisher
) {
super(repositoryProvider, idManager);
super(repositoryProvider, idManager, eventPublisher);

this.repositoryForCommandsTargetAggregate = this.repositoryProvider.forResource(
this.aggregateType
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { ResourceType } from '@coscrad/api-interfaces';
import { CommandHandler } from '@coscrad/commands';
import { Inject } from '@nestjs/common';
import { EVENT_PUBLISHER_TOKEN } from '../../../../../domain/common';
import { ICoscradEventPublisher } from '../../../../../domain/common/events/interfaces';
import { Valid } from '../../../../../domain/domainModelValidators/Valid';
import {
ID_MANAGER_TOKEN,
Expand All @@ -27,9 +29,10 @@ export class AddAudioItemToPlaylistCommandHandler extends BaseUpdateCommandHandl
constructor(
@Inject(REPOSITORY_PROVIDER_TOKEN)
protected readonly repositoryProvider: IRepositoryProvider,
@Inject(ID_MANAGER_TOKEN) protected readonly idManager: IIdManager
@Inject(ID_MANAGER_TOKEN) protected readonly idManager: IIdManager,
@Inject(EVENT_PUBLISHER_TOKEN) protected readonly eventPublisher: ICoscradEventPublisher
) {
super(repositoryProvider, idManager);
super(repositoryProvider, idManager, eventPublisher);
}

protected actOnInstance(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import { InternalError, isInternalError } from '../../../../lib/errors/InternalE
import { REPOSITORY_PROVIDER_TOKEN } from '../../../../persistence/constants/persistenceConstants';
import { DTO } from '../../../../types/DTO';
import { ResultOrError } from '../../../../types/ResultOrError';
import { EVENT_PUBLISHER_TOKEN } from '../../../common';
import { buildMultilingualTextWithSingleItem } from '../../../common/build-multilingual-text-with-single-item';
import { ICoscradEventPublisher } from '../../../common/events/interfaces';
import { Valid } from '../../../domainModelValidators/Valid';
import getInstanceFactoryForResource from '../../../factories/get-instance-factory-for-resource';
import { IIdManager } from '../../../interfaces/id-manager.interface';
Expand Down Expand Up @@ -36,9 +38,10 @@ export class CreatePlayListCommandHandler extends BaseCreateCommandHandler<Playl
@Inject(REPOSITORY_PROVIDER_TOKEN)
protected readonly repositoryProvider: IRepositoryProvider,
// TODO export a constant for ID manager token
@Inject('ID_MANAGER') protected readonly idManager: IIdManager
@Inject('ID_MANAGER') protected readonly idManager: IIdManager,
@Inject(EVENT_PUBLISHER_TOKEN) protected readonly eventPublisher: ICoscradEventPublisher
) {
super(repositoryProvider, idManager);
super(repositoryProvider, idManager, eventPublisher);

this.repositoryForCommandsTargetAggregate = this.repositoryProvider.forResource<Playlist>(
ResourceType.playlist
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { CommandHandler } from '@coscrad/commands';
import { Inject } from '@nestjs/common';
import { EVENT_PUBLISHER_TOKEN } from '../../../../../domain/common';
import {
MultilingualTextItem,
MultilingualTextItemRole,
} from '../../../../../domain/common/entities/multilingual-text';
import { ICoscradEventPublisher } from '../../../../../domain/common/events/interfaces';
import { Valid } from '../../../../../domain/domainModelValidators/Valid';
import {
ID_MANAGER_TOKEN,
Expand All @@ -27,9 +29,10 @@ export class TranslatePlaylistNameCommandHandler extends BaseUpdateCommandHandle
constructor(
@Inject(REPOSITORY_PROVIDER_TOKEN)
protected readonly repositoryProvider: IRepositoryProvider,
@Inject(ID_MANAGER_TOKEN) protected readonly idManager: IIdManager
@Inject(ID_MANAGER_TOKEN) protected readonly idManager: IIdManager,
@Inject(EVENT_PUBLISHER_TOKEN) protected readonly eventPublisher: ICoscradEventPublisher
) {
super(repositoryProvider, idManager);
super(repositoryProvider, idManager, eventPublisher);
}

// note that we don't prevent duplication in translation of a name, only the orignal
Expand Down
Loading

0 comments on commit 289939d

Please sign in to comment.