-
Notifications
You must be signed in to change notification settings - Fork 0
Additional backends #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
Merged
Merged
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
25b6cb1
Implement backend identifiers for multi-backend support
hugobessa a86b7eb
Update notification input types and backend methods to support option…
hugobessa e5ee1b8
Implement multi-backend support in VintaSend service with initializat…
hugobessa 1dec8dd
Implement multi-backend write operations in VintaSend service with er…
hugobessa a22e128
Implement multi-backend read operations in VintaSend service with bac…
hugobessa a545ee8
Enhance multi-backend management with sync verification, replication,…
hugobessa 787a87e
Update documentation and tests for multi-backend support; enhance REA…
hugobessa ddbea1d
Remove AI plan
hugobessa e45867a
Remove multi-backend example test file
hugobessa 24dadfb
Release v0.8.0
hugobessa 1c8fb68
Release v0.8.1
hugobessa File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,6 @@ | ||
| { | ||
| "name": "vintasend", | ||
| "version": "0.7.1", | ||
| "version": "0.8.1", | ||
| "main": "dist/index.js", | ||
| "files": [ | ||
| "dist" | ||
|
|
||
Submodule vintasend-medplum
updated
5 files
Submodule vintasend-prisma
updated
2 files
| +36 −0 | src/__tests__/prisma-notification-backend.test.ts | |
| +31 −14 | src/prisma-notification-backend.ts |
219 changes: 219 additions & 0 deletions
219
src/services/__tests__/multi-backend-error-handling.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,219 @@ | ||
| import { VintaSendFactory } from '../../index'; | ||
| import type { DatabaseNotification, NotificationInput } from '../../types/notification'; | ||
| import type { BaseLogger } from '../loggers/base-logger'; | ||
| import type { BaseNotificationAdapter } from '../notification-adapters/base-notification-adapter'; | ||
| import type { BaseNotificationBackend } from '../notification-backends/base-notification-backend'; | ||
| import type { BaseEmailTemplateRenderer } from '../notification-template-renderers/base-email-template-renderer'; | ||
|
|
||
| type ContextGenerators = { | ||
| testContext: { | ||
| generate: (params: { userId: string }) => Promise<{ userId: string }>; | ||
| }; | ||
| }; | ||
|
|
||
| type Config = { | ||
| ContextMap: ContextGenerators; | ||
| NotificationIdType: string; | ||
| UserIdType: string; | ||
| }; | ||
|
|
||
| type MockBackend = jest.Mocked<BaseNotificationBackend<Config>> & { | ||
| injectLogger: jest.Mock; | ||
| injectAttachmentManager: jest.Mock; | ||
| getBackendIdentifier: jest.Mock<string, []>; | ||
| }; | ||
|
|
||
| const templateRenderer: jest.Mocked<BaseEmailTemplateRenderer<Config>> = { | ||
| render: jest.fn(), | ||
| renderFromTemplateContent: jest.fn(), | ||
| }; | ||
|
|
||
| const logger: jest.Mocked<BaseLogger> = { | ||
| info: jest.fn(), | ||
| error: jest.fn(), | ||
| warn: jest.fn(), | ||
| }; | ||
|
|
||
| // biome-ignore lint/suspicious/noExplicitAny: test adapter mock | ||
| const adapter: jest.Mocked<BaseNotificationAdapter<any, Config>> = { | ||
| notificationType: 'EMAIL', | ||
| key: 'adapter-1', | ||
| enqueueNotifications: false, | ||
| send: jest.fn(), | ||
| injectBackend: jest.fn(), | ||
| injectLogger: jest.fn(), | ||
| backend: null, | ||
| templateRenderer, | ||
| logger, | ||
| supportsAttachments: false, | ||
| // biome-ignore lint/suspicious/noExplicitAny: test-only cast | ||
| } as any; | ||
|
|
||
| const contextGenerators: ContextGenerators = { | ||
| testContext: { | ||
| generate: jest.fn(), | ||
| }, | ||
| }; | ||
|
|
||
| function createMockBackend(identifier: string): MockBackend { | ||
| return { | ||
| persistNotification: jest.fn(), | ||
| persistNotificationUpdate: jest.fn(), | ||
| getAllFutureNotifications: jest.fn(), | ||
| getAllFutureNotificationsFromUser: jest.fn(), | ||
| getFutureNotificationsFromUser: jest.fn(), | ||
| getFutureNotifications: jest.fn(), | ||
| getAllPendingNotifications: jest.fn(), | ||
| getPendingNotifications: jest.fn(), | ||
| getNotification: jest.fn(), | ||
| markAsRead: jest.fn(), | ||
| filterAllInAppUnreadNotifications: jest.fn(), | ||
| cancelNotification: jest.fn(), | ||
| markAsSent: jest.fn(), | ||
| markAsFailed: jest.fn(), | ||
| storeAdapterAndContextUsed: jest.fn(), | ||
| getUserEmailFromNotification: jest.fn(), | ||
| filterInAppUnreadNotifications: jest.fn(), | ||
| filterNotifications: jest.fn(), | ||
| bulkPersistNotifications: jest.fn(), | ||
| getAllNotifications: jest.fn(), | ||
| getNotifications: jest.fn(), | ||
| persistOneOffNotification: jest.fn(), | ||
| persistOneOffNotificationUpdate: jest.fn(), | ||
| getOneOffNotification: jest.fn(), | ||
| getAllOneOffNotifications: jest.fn(), | ||
| getOneOffNotifications: jest.fn(), | ||
| storeAttachmentFileRecord: jest.fn(), | ||
| getAttachmentFileRecord: jest.fn(), | ||
| getAttachmentFile: jest.fn(), | ||
| findAttachmentFileByChecksum: jest.fn(), | ||
| deleteAttachmentFile: jest.fn(), | ||
| getOrphanedAttachmentFiles: jest.fn(), | ||
| getAttachments: jest.fn(), | ||
| deleteNotificationAttachment: jest.fn(), | ||
| injectLogger: jest.fn(), | ||
| injectAttachmentManager: jest.fn(), | ||
| getBackendIdentifier: jest.fn(() => identifier), | ||
| } as unknown as MockBackend; | ||
| } | ||
|
|
||
| describe('VintaSend multi-backend error handling (Phase 4)', () => { | ||
| const createNotificationInput: Omit<NotificationInput<Config>, 'id'> = { | ||
| userId: 'user-1', | ||
| notificationType: 'EMAIL', | ||
| title: 'Title', | ||
| bodyTemplate: 'body', | ||
| contextName: 'testContext', | ||
| contextParameters: { userId: 'user-1' }, | ||
| sendAfter: new Date(Date.now() + 60_000), | ||
| subjectTemplate: 'subject', | ||
| extraParams: null, | ||
| }; | ||
|
|
||
| const databaseNotification = { | ||
| ...createNotificationInput, | ||
| id: 'notif-1', | ||
| status: 'PENDING_SEND', | ||
| contextUsed: null, | ||
| adapterUsed: null, | ||
| sentAt: null, | ||
| readAt: null, | ||
| gitCommitSha: null, | ||
| } as DatabaseNotification<Config>; | ||
|
|
||
| beforeEach(() => { | ||
| jest.clearAllMocks(); | ||
| contextGenerators.testContext.generate = jest.fn().mockResolvedValue({ userId: 'user-1' }); | ||
| }); | ||
|
|
||
| it('continues when one additional backend replication fails', async () => { | ||
| const primaryBackend = createMockBackend('primary'); | ||
| const failingReplica = createMockBackend('replica-a'); | ||
| const healthyReplica = createMockBackend('replica-b'); | ||
|
|
||
| primaryBackend.persistNotification.mockResolvedValue(databaseNotification); | ||
| failingReplica.persistNotification.mockRejectedValue(new Error('replication failed')); | ||
| healthyReplica.persistNotification.mockResolvedValue(databaseNotification); | ||
|
|
||
| const service = new VintaSendFactory<Config>().create({ | ||
| adapters: [adapter], | ||
| backend: primaryBackend, | ||
| additionalBackends: [failingReplica, healthyReplica], | ||
| logger, | ||
| contextGeneratorsMap: contextGenerators, | ||
| }); | ||
|
|
||
| const result = await service.createNotification(createNotificationInput); | ||
|
|
||
| expect(result.id).toBe('notif-1'); | ||
| expect(healthyReplica.persistNotification).toHaveBeenCalledWith({ | ||
| ...createNotificationInput, | ||
| id: 'notif-1', | ||
| }); | ||
| expect(logger.error).toHaveBeenCalledWith( | ||
| expect.stringContaining('Failed to replicate createNotification to backend replica-a'), | ||
| ); | ||
| }); | ||
|
|
||
| it('continues processing send path when additional backend markAsSent fails', async () => { | ||
| const primaryBackend = createMockBackend('primary'); | ||
| const failingReplica = createMockBackend('replica-a'); | ||
|
|
||
| adapter.send.mockResolvedValue(); | ||
|
|
||
| primaryBackend.markAsSent.mockResolvedValue(databaseNotification); | ||
| failingReplica.markAsSent.mockRejectedValue(new Error('markAsSent failed')); | ||
|
|
||
| primaryBackend.storeAdapterAndContextUsed.mockResolvedValue(); | ||
| failingReplica.storeAdapterAndContextUsed.mockResolvedValue(); | ||
|
|
||
| const service = new VintaSendFactory<Config>().create({ | ||
| adapters: [adapter], | ||
| backend: primaryBackend, | ||
| additionalBackends: [failingReplica], | ||
| logger, | ||
| contextGeneratorsMap: contextGenerators, | ||
| }); | ||
|
|
||
| await expect(service.send(databaseNotification)).resolves.toBeUndefined(); | ||
|
|
||
| expect(primaryBackend.markAsSent).toHaveBeenCalledWith('notif-1', true); | ||
| expect(failingReplica.markAsSent).toHaveBeenCalledWith('notif-1', true); | ||
| expect(primaryBackend.storeAdapterAndContextUsed).toHaveBeenCalled(); | ||
| expect(failingReplica.storeAdapterAndContextUsed).toHaveBeenCalled(); | ||
| expect(logger.error).toHaveBeenCalledWith( | ||
| expect.stringContaining('Failed to replicate markAsSent to backend replica-a'), | ||
| ); | ||
| }); | ||
|
|
||
| it('logs all additional backend failures without failing operation', async () => { | ||
| const primaryBackend = createMockBackend('primary'); | ||
| const replicaA = createMockBackend('replica-a'); | ||
| const replicaB = createMockBackend('replica-b'); | ||
|
|
||
| primaryBackend.markAsRead.mockResolvedValue({ | ||
| ...databaseNotification, | ||
| status: 'SENT', | ||
| readAt: new Date(), | ||
| }); | ||
| replicaA.markAsRead.mockRejectedValue(new Error('replica A fail')); | ||
| replicaB.markAsRead.mockRejectedValue(new Error('replica B fail')); | ||
|
|
||
| const service = new VintaSendFactory<Config>().create({ | ||
| adapters: [adapter], | ||
| backend: primaryBackend, | ||
| additionalBackends: [replicaA, replicaB], | ||
| logger, | ||
| contextGeneratorsMap: contextGenerators, | ||
| }); | ||
|
|
||
| await expect(service.markRead('notif-1')).resolves.toMatchObject({ id: 'notif-1' }); | ||
|
|
||
| expect(logger.error).toHaveBeenCalledWith( | ||
| expect.stringContaining('Failed to replicate markRead to backend replica-a'), | ||
| ); | ||
| expect(logger.error).toHaveBeenCalledWith( | ||
| expect.stringContaining('Failed to replicate markRead to backend replica-b'), | ||
| ); | ||
| }); | ||
| }); | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
suggestion: Consider deduplicating createMockBackend and shared test setup across multi-backend test suites.
This helper (and the associated logger/templateRenderer/adapter setup) is duplicated across several suites (
multi-backend-error-handling.test.ts,multi-backend-writes.test.ts,multi-backend-reads.test.ts,multi-backend-management.test.ts, plus the example tests). Extracting these into a shared test utility (e.g.,__tests__/helpers/multi-backend-mocks.ts) would centralize backend/mock configuration, clarify each suite’s intent, and reduce divergence risk as the multi-backend API evolves. Non-blocking, but a worthwhile cleanup as the surface area grows.