-
Notifications
You must be signed in to change notification settings - Fork 5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement AMQP publisherManager #144
Changes from 7 commits
e95a62a
9111000
b461b65
ae5a6ce
bf3d2f0
a51f188
542b672
65b2a75
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
import type * as Buffer from 'node:buffer' | ||
|
||
import { objectToBuffer } from '@message-queue-toolkit/core' | ||
import type { Options } from 'amqplib/properties' | ||
|
||
import type { AMQPPublisherOptions } from './AbstractAmqpPublisher' | ||
import { AbstractAmqpPublisher } from './AbstractAmqpPublisher' | ||
import type { AMQPDependencies } from './AbstractAmqpService' | ||
|
||
export type AMQPExchangePublisherOptions<MessagePayloadType extends object> = Omit< | ||
AMQPPublisherOptions<MessagePayloadType>, | ||
'creationConfig' | ||
> & { | ||
exchange: string | ||
} | ||
|
||
export type AmqpExchangeMessageOptions = { | ||
routingKey: string | ||
publishOptions: Options.Publish | ||
} | ||
|
||
export abstract class AbstractAmqpExchangePublisher< | ||
MessagePayloadType extends object, | ||
> extends AbstractAmqpPublisher<MessagePayloadType, AmqpExchangeMessageOptions> { | ||
constructor( | ||
dependencies: AMQPDependencies, | ||
options: AMQPExchangePublisherOptions<MessagePayloadType>, | ||
) { | ||
super(dependencies, { | ||
...options, | ||
// FixMe exchange publisher doesn't need queue at all | ||
creationConfig: { | ||
queueName: 'dummy', | ||
queueOptions: {}, | ||
updateAttributesIfExists: false, | ||
}, | ||
exchange: options.exchange, | ||
locatorConfig: undefined, | ||
}) | ||
} | ||
|
||
protected publishInternal(message: Buffer, options: AmqpExchangeMessageOptions): void { | ||
this.channel.publish( | ||
this.exchange!, | ||
options.routingKey, | ||
objectToBuffer(message), | ||
options.publishOptions, | ||
) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import type { Options } from 'amqplib/properties' | ||
|
||
import { AbstractAmqpPublisher } from './AbstractAmqpPublisher' | ||
|
||
export type AmqpQueueMessageOptions = { | ||
publishOptions: Options.Publish | ||
} | ||
|
||
const NO_PARAMS: AmqpQueueMessageOptions = { | ||
publishOptions: {}, | ||
} | ||
|
||
export abstract class AbstractAmqpQueuePublisher< | ||
MessagePayloadType extends object, | ||
> extends AbstractAmqpPublisher<MessagePayloadType, AmqpQueueMessageOptions> { | ||
protected publishInternal(message: Buffer, options: AmqpQueueMessageOptions): void { | ||
this.channel.sendToQueue(this.queueName, message, options.publishOptions) | ||
} | ||
|
||
publish(message: MessagePayloadType, options: AmqpQueueMessageOptions = NO_PARAMS) { | ||
super.publish(message, options) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
import { AbstractPublisherManager } from '@message-queue-toolkit/core' | ||
import type { MessageMetadataType } from '@message-queue-toolkit/core/lib/messages/baseMessageSchemas' | ||
import type { TypeOf } from 'zod' | ||
import type z from 'zod' | ||
import { util } from 'zod' | ||
|
||
import type { | ||
AbstractAmqpExchangePublisher, | ||
AMQPExchangePublisherOptions, | ||
} from './AbstractAmqpExchangePublisher' | ||
import type { AmqpQueueMessageOptions } from './AbstractAmqpQueuePublisher' | ||
import type { AMQPCreationConfig, AMQPDependencies, AMQPLocator } from './AbstractAmqpService' | ||
import type { | ||
AmqpAwareEventDefinition, | ||
AmqpMessageSchemaType, | ||
AmqpPublisherManagerDependencies, | ||
AmqpPublisherManagerOptions, | ||
} from './AmqpQueuePublisherManager' | ||
import { CommonAmqpExchangePublisherFactory } from './CommonAmqpPublisherFactory' | ||
|
||
import Omit = util.Omit | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟢 nit: Importing omit could cause confusing with TS Omit, I would prefer using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. good catch, this was autoresolve by WebStorm, not intentional. I wanted to use TS Omit |
||
|
||
export class AmqpExchangePublisherManager< | ||
T extends AbstractAmqpExchangePublisher<z.infer<SupportedEventDefinitions[number]['schema']>>, | ||
SupportedEventDefinitions extends AmqpAwareEventDefinition[], | ||
MetadataType = MessageMetadataType, | ||
> extends AbstractPublisherManager< | ||
AmqpAwareEventDefinition, | ||
NonNullable<SupportedEventDefinitions[number]['exchange']>, | ||
AbstractAmqpExchangePublisher<z.infer<SupportedEventDefinitions[number]['schema']>>, | ||
AMQPDependencies, | ||
AMQPCreationConfig, | ||
AMQPLocator, | ||
AmqpMessageSchemaType<AmqpAwareEventDefinition>, | ||
Omit< | ||
AMQPExchangePublisherOptions<z.infer<SupportedEventDefinitions[number]['schema']>>, | ||
'messageSchemas' | 'locatorConfig' | 'exchange' | ||
>, | ||
SupportedEventDefinitions, | ||
MetadataType, | ||
z.infer<SupportedEventDefinitions[number]['schema']> | ||
> { | ||
constructor( | ||
dependencies: AmqpPublisherManagerDependencies<SupportedEventDefinitions>, | ||
options: AmqpPublisherManagerOptions< | ||
T, | ||
AmqpQueueMessageOptions, | ||
AMQPExchangePublisherOptions<z.infer<SupportedEventDefinitions[number]['schema']>>, | ||
z.infer<SupportedEventDefinitions[number]['schema']>, | ||
MetadataType | ||
>, | ||
) { | ||
super({ | ||
isAsync: false, | ||
eventRegistry: dependencies.eventRegistry, | ||
metadataField: options.metadataField ?? 'metadata', | ||
metadataFiller: options.metadataFiller, | ||
newPublisherOptions: options.newPublisherOptions, | ||
publisherDependencies: { | ||
amqpConnectionManager: dependencies.amqpConnectionManager, | ||
logger: dependencies.logger, | ||
errorReporter: dependencies.errorReporter, | ||
}, | ||
publisherFactory: options.publisherFactory ?? new CommonAmqpExchangePublisherFactory(), | ||
}) | ||
} | ||
|
||
protected resolvePublisherConfigOverrides( | ||
exchange: string, | ||
): Partial< | ||
util.Omit< | ||
AMQPExchangePublisherOptions<TypeOf<SupportedEventDefinitions[number]['schema']>>, | ||
'messageSchemas' | 'locatorConfig' | ||
> | ||
> { | ||
return { | ||
exchange, | ||
} | ||
} | ||
|
||
protected override resolveCreationConfig( | ||
queueName: NonNullable<SupportedEventDefinitions[number]['exchange']>, | ||
): AMQPCreationConfig { | ||
return { | ||
...this.newPublisherOptions, | ||
queueOptions: {}, | ||
queueName, | ||
} | ||
} | ||
|
||
protected override resolveEventTarget( | ||
event: AmqpAwareEventDefinition, | ||
): NonNullable<SupportedEventDefinitions[number]['exchange']> | undefined { | ||
return event.queueName | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
import type { AwilixContainer } from 'awilix' | ||
import { beforeAll } from 'vitest' | ||
|
||
import { FakeConsumer } from '../test/fakes/FakeConsumer' | ||
import { TEST_AMQP_CONFIG } from '../test/utils/testAmqpConfig' | ||
import { registerDependencies, TestEvents } from '../test/utils/testContext' | ||
import type { Dependencies } from '../test/utils/testContext' | ||
|
||
describe('AmqpQueuePublisherManager', () => { | ||
describe('publish', () => { | ||
let diContainer: AwilixContainer<Dependencies> | ||
beforeAll(async () => { | ||
diContainer = await registerDependencies(TEST_AMQP_CONFIG) | ||
}) | ||
|
||
it('publishes to the correct queue', async () => { | ||
const { queuePublisherManager } = diContainer.cradle | ||
const fakeConsumer = new FakeConsumer(diContainer.cradle, TestEvents.updated) | ||
await fakeConsumer.start() | ||
|
||
const publishedMessage = await queuePublisherManager.publish(FakeConsumer.QUEUE_NAME, { | ||
...queuePublisherManager.resolveBaseFields(), | ||
type: 'entity.updated', | ||
payload: { | ||
updatedData: 'msg', | ||
}, | ||
}) | ||
|
||
const result = await fakeConsumer.handlerSpy.waitForMessageWithId(publishedMessage.id) | ||
|
||
expect(result.processingResult).toBe('consumed') | ||
}) | ||
}) | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
will be addressed during auto-init rework, as we need to implement exchange and subscription support