feat(syncing-server): add creating item dumps for revision service
This commit is contained in:
parent
1a16d2e4f4
commit
8d152ddfcb
11 changed files with 117 additions and 63 deletions
|
@ -0,0 +1,7 @@
|
|||
import { DomainEventInterface } from './DomainEventInterface'
|
||||
import { ItemDumpedEventPayload } from './ItemDumpedEventPayload'
|
||||
|
||||
export interface ItemDumpedEvent extends DomainEventInterface {
|
||||
type: 'ITEM_DUMPED'
|
||||
payload: ItemDumpedEventPayload
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export interface ItemDumpedEventPayload {
|
||||
fileDumpPath: string
|
||||
}
|
|
@ -46,6 +46,8 @@ export * from './Event/GoogleDriveBackupFailedEvent'
|
|||
export * from './Event/GoogleDriveBackupFailedEventPayload'
|
||||
export * from './Event/InvoiceGeneratedEvent'
|
||||
export * from './Event/InvoiceGeneratedEventPayload'
|
||||
export * from './Event/ItemDumpedEvent'
|
||||
export * from './Event/ItemDumpedEventPayload'
|
||||
export * from './Event/ItemRevisionCreationRequestedEvent'
|
||||
export * from './Event/ItemRevisionCreationRequestedEventPayload'
|
||||
export * from './Event/ItemsSyncedEvent'
|
||||
|
|
|
@ -7,6 +7,6 @@ module.exports = {
|
|||
transform: {
|
||||
...tsjPreset.transform,
|
||||
},
|
||||
coveragePathIgnorePatterns: ['/Bootstrap/', 'HealthCheckController'],
|
||||
coveragePathIgnorePatterns: ['/Bootstrap/', 'HealthCheckController', '/Infra/'],
|
||||
setupFilesAfterEnv: ['./test-setup.ts'],
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import {
|
|||
EmailArchiveExtensionSyncedEvent,
|
||||
EmailBackupAttachmentCreatedEvent,
|
||||
GoogleDriveBackupFailedEvent,
|
||||
ItemDumpedEvent,
|
||||
ItemRevisionCreationRequestedEvent,
|
||||
ItemsSyncedEvent,
|
||||
OneDriveBackupFailedEvent,
|
||||
|
@ -20,6 +21,23 @@ import { DomainEventFactoryInterface } from './DomainEventFactoryInterface'
|
|||
export class DomainEventFactory implements DomainEventFactoryInterface {
|
||||
constructor(@inject(TYPES.Timer) private timer: TimerInterface) {}
|
||||
|
||||
createItemDumpedEvent(fileDumpPath: string, userUuid: string): ItemDumpedEvent {
|
||||
return {
|
||||
type: 'ITEM_DUMPED',
|
||||
createdAt: this.timer.getUTCDate(),
|
||||
meta: {
|
||||
correlation: {
|
||||
userIdentifier: userUuid,
|
||||
userIdentifierType: 'uuid',
|
||||
},
|
||||
origin: DomainEventService.SyncingServer,
|
||||
},
|
||||
payload: {
|
||||
fileDumpPath,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
createItemRevisionCreationRequested(itemUuid: string, userUuid: string): ItemRevisionCreationRequestedEvent {
|
||||
return {
|
||||
type: 'ITEM_REVISION_CREATION_REQUESTED',
|
||||
|
|
|
@ -4,6 +4,7 @@ import {
|
|||
EmailArchiveExtensionSyncedEvent,
|
||||
EmailBackupAttachmentCreatedEvent,
|
||||
GoogleDriveBackupFailedEvent,
|
||||
ItemDumpedEvent,
|
||||
ItemRevisionCreationRequestedEvent,
|
||||
ItemsSyncedEvent,
|
||||
OneDriveBackupFailedEvent,
|
||||
|
@ -33,4 +34,5 @@ export interface DomainEventFactoryInterface {
|
|||
}): EmailBackupAttachmentCreatedEvent
|
||||
createDuplicateItemSyncedEvent(itemUuid: string, userUuid: string): DuplicateItemSyncedEvent
|
||||
createItemRevisionCreationRequested(itemUuid: string, userUuid: string): ItemRevisionCreationRequestedEvent
|
||||
createItemDumpedEvent(fileDumpPath: string, userUuid: string): ItemDumpedEvent
|
||||
}
|
||||
|
|
|
@ -1,18 +1,34 @@
|
|||
import 'reflect-metadata'
|
||||
|
||||
import { ItemRevisionCreationRequestedEvent } from '@standardnotes/domain-events'
|
||||
import {
|
||||
DomainEventPublisherInterface,
|
||||
DomainEventService,
|
||||
ItemRevisionCreationRequestedEvent,
|
||||
} from '@standardnotes/domain-events'
|
||||
import { Item } from '../Item/Item'
|
||||
import { ItemRepositoryInterface } from '../Item/ItemRepositoryInterface'
|
||||
import { ItemRevisionCreationRequestedEventHandler } from './ItemRevisionCreationRequestedEventHandler'
|
||||
import { RevisionServiceInterface } from '../Revision/RevisionServiceInterface'
|
||||
import { ItemBackupServiceInterface } from '../Item/ItemBackupServiceInterface'
|
||||
import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
|
||||
|
||||
describe('ItemRevisionCreationRequestedEventHandler', () => {
|
||||
let itemRepository: ItemRepositoryInterface
|
||||
let revisionService: RevisionServiceInterface
|
||||
let event: ItemRevisionCreationRequestedEvent
|
||||
let item: Item
|
||||
let itemBackupService: ItemBackupServiceInterface
|
||||
let domainEventFactory: DomainEventFactoryInterface
|
||||
let domainEventPublisher: DomainEventPublisherInterface
|
||||
|
||||
const createHandler = () => new ItemRevisionCreationRequestedEventHandler(itemRepository, revisionService)
|
||||
const createHandler = () =>
|
||||
new ItemRevisionCreationRequestedEventHandler(
|
||||
itemRepository,
|
||||
revisionService,
|
||||
itemBackupService,
|
||||
domainEventFactory,
|
||||
domainEventPublisher,
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
item = {
|
||||
|
@ -31,12 +47,30 @@ describe('ItemRevisionCreationRequestedEventHandler', () => {
|
|||
event.payload = {
|
||||
itemUuid: '2-3-4',
|
||||
}
|
||||
event.meta = {
|
||||
correlation: {
|
||||
userIdentifier: '1-2-3',
|
||||
userIdentifierType: 'uuid',
|
||||
},
|
||||
origin: DomainEventService.SyncingServer,
|
||||
}
|
||||
|
||||
itemBackupService = {} as jest.Mocked<ItemBackupServiceInterface>
|
||||
itemBackupService.dump = jest.fn().mockReturnValue('foo://bar')
|
||||
|
||||
domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
|
||||
domainEventFactory.createItemDumpedEvent = jest.fn()
|
||||
|
||||
domainEventPublisher = {} as jest.Mocked<DomainEventPublisherInterface>
|
||||
domainEventPublisher.publish = jest.fn()
|
||||
})
|
||||
|
||||
it('should create a revision for an item', async () => {
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(revisionService.createRevision).toHaveBeenCalled()
|
||||
expect(domainEventPublisher.publish).toHaveBeenCalled()
|
||||
expect(domainEventFactory.createItemDumpedEvent).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not create a revision for an item that does not exist', async () => {
|
||||
|
@ -46,4 +80,13 @@ describe('ItemRevisionCreationRequestedEventHandler', () => {
|
|||
|
||||
expect(revisionService.createRevision).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not create a revision for an item if the dump was not created', async () => {
|
||||
itemBackupService.dump = jest.fn().mockReturnValue('')
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
|
||||
expect(domainEventFactory.createItemDumpedEvent).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,7 +1,13 @@
|
|||
import { ItemRevisionCreationRequestedEvent, DomainEventHandlerInterface } from '@standardnotes/domain-events'
|
||||
import {
|
||||
ItemRevisionCreationRequestedEvent,
|
||||
DomainEventHandlerInterface,
|
||||
DomainEventPublisherInterface,
|
||||
} from '@standardnotes/domain-events'
|
||||
import { inject, injectable } from 'inversify'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
|
||||
import { ItemBackupServiceInterface } from '../Item/ItemBackupServiceInterface'
|
||||
import { ItemRepositoryInterface } from '../Item/ItemRepositoryInterface'
|
||||
import { RevisionServiceInterface } from '../Revision/RevisionServiceInterface'
|
||||
|
||||
|
@ -10,6 +16,9 @@ export class ItemRevisionCreationRequestedEventHandler implements DomainEventHan
|
|||
constructor(
|
||||
@inject(TYPES.ItemRepository) private itemRepository: ItemRepositoryInterface,
|
||||
@inject(TYPES.RevisionService) private revisionService: RevisionServiceInterface,
|
||||
@inject(TYPES.ItemBackupService) private itemBackupService: ItemBackupServiceInterface,
|
||||
@inject(TYPES.DomainEventFactory) private domainEventFactory: DomainEventFactoryInterface,
|
||||
@inject(TYPES.DomainEventPublisher) private domainEventPublisher: DomainEventPublisherInterface,
|
||||
) {}
|
||||
|
||||
async handle(event: ItemRevisionCreationRequestedEvent): Promise<void> {
|
||||
|
@ -18,6 +27,13 @@ export class ItemRevisionCreationRequestedEventHandler implements DomainEventHan
|
|||
return
|
||||
}
|
||||
|
||||
const fileDumpPath = await this.itemBackupService.dump(item)
|
||||
if (fileDumpPath) {
|
||||
await this.domainEventPublisher.publish(
|
||||
this.domainEventFactory.createItemDumpedEvent(fileDumpPath, event.meta.correlation.userIdentifier),
|
||||
)
|
||||
}
|
||||
|
||||
await this.revisionService.createRevision(item)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,4 +3,5 @@ import { Item } from './Item'
|
|||
|
||||
export interface ItemBackupServiceInterface {
|
||||
backup(items: Array<Item>, authParams: KeyParamsData): Promise<string>
|
||||
dump(item: Item): Promise<string>
|
||||
}
|
||||
|
|
|
@ -1,59 +0,0 @@
|
|||
import 'reflect-metadata'
|
||||
|
||||
import { KeyParamsData } from '@standardnotes/responses'
|
||||
import { S3 } from 'aws-sdk'
|
||||
import { Logger } from 'winston'
|
||||
import { Item } from '../../Domain/Item/Item'
|
||||
import { S3ItemBackupService } from './S3ItemBackupService'
|
||||
import { ProjectorInterface } from '../../Projection/ProjectorInterface'
|
||||
import { ItemProjection } from '../../Projection/ItemProjection'
|
||||
|
||||
describe('S3ItemBackupService', () => {
|
||||
let s3Client: S3 | undefined
|
||||
let itemProjector: ProjectorInterface<Item, ItemProjection>
|
||||
let s3BackupBucketName = 'backup-bucket'
|
||||
let logger: Logger
|
||||
let item: Item
|
||||
let keyParams: KeyParamsData
|
||||
|
||||
const createService = () => new S3ItemBackupService(s3BackupBucketName, itemProjector, logger, s3Client)
|
||||
|
||||
beforeEach(() => {
|
||||
s3Client = {} as jest.Mocked<S3>
|
||||
s3Client.upload = jest.fn().mockReturnValue({
|
||||
promise: jest.fn().mockReturnValue(Promise.resolve({ Key: 'test' })),
|
||||
})
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.warn = jest.fn()
|
||||
|
||||
item = {} as jest.Mocked<Item>
|
||||
|
||||
keyParams = {} as jest.Mocked<KeyParamsData>
|
||||
|
||||
itemProjector = {} as jest.Mocked<ProjectorInterface<Item, ItemProjection>>
|
||||
itemProjector.projectFull = jest.fn().mockReturnValue({ foo: 'bar' })
|
||||
})
|
||||
|
||||
it('should upload items to S3 as a backup file', async () => {
|
||||
await createService().backup([item], keyParams)
|
||||
|
||||
expect((<S3>s3Client).upload).toHaveBeenCalledWith({
|
||||
Body: '{"items":[{"foo":"bar"}],"auth_params":{}}',
|
||||
Bucket: 'backup-bucket',
|
||||
Key: expect.any(String),
|
||||
})
|
||||
})
|
||||
|
||||
it('should not upload items to S3 if bucket name is not configured', async () => {
|
||||
s3BackupBucketName = ''
|
||||
await createService().backup([item], keyParams)
|
||||
|
||||
expect((<S3>s3Client).upload).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not upload items to S3 if S3 client is not configured', async () => {
|
||||
s3Client = undefined
|
||||
expect(await createService().backup([item], keyParams)).toEqual('')
|
||||
})
|
||||
})
|
|
@ -3,6 +3,7 @@ import { KeyParamsData } from '@standardnotes/responses'
|
|||
import { S3 } from 'aws-sdk'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { Item } from '../../Domain/Item/Item'
|
||||
import { ItemBackupServiceInterface } from '../../Domain/Item/ItemBackupServiceInterface'
|
||||
|
@ -18,6 +19,26 @@ export class S3ItemBackupService implements ItemBackupServiceInterface {
|
|||
@inject(TYPES.S3) private s3Client?: S3,
|
||||
) {}
|
||||
|
||||
async dump(item: Item): Promise<string> {
|
||||
if (!this.s3BackupBucketName || this.s3Client === undefined) {
|
||||
this.logger.warn('S3 backup not configured')
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
const uploadResult = await this.s3Client
|
||||
.upload({
|
||||
Bucket: this.s3BackupBucketName,
|
||||
Key: uuid.v4(),
|
||||
Body: JSON.stringify({
|
||||
item: await this.itemProjector.projectFull(item),
|
||||
}),
|
||||
})
|
||||
.promise()
|
||||
|
||||
return uploadResult.Key
|
||||
}
|
||||
|
||||
async backup(items: Item[], authParams: KeyParamsData): Promise<string> {
|
||||
if (!this.s3BackupBucketName || this.s3Client === undefined) {
|
||||
this.logger.warn('S3 backup not configured')
|
||||
|
|
Loading…
Reference in a new issue