feat: add services package
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
import { CheckIntegrityResponse, IntegrityPayload } from '@standardnotes/responses'
|
||||
|
||||
export interface IntegrityApiInterface {
|
||||
checkIntegrity(integrityPayloads: IntegrityPayload[]): Promise<CheckIntegrityResponse>
|
||||
}
|
||||
4
packages/services/src/Domain/Integrity/IntegrityEvent.ts
Normal file
4
packages/services/src/Domain/Integrity/IntegrityEvent.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
/* istanbul ignore file */
|
||||
export enum IntegrityEvent {
|
||||
IntegrityCheckCompleted = 'IntegrityCheckCompleted',
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { ServerItemResponse } from '@standardnotes/responses'
|
||||
import { SyncSource } from '../Sync/SyncSource'
|
||||
|
||||
export type IntegrityEventPayload = {
|
||||
rawPayloads: ServerItemResponse[]
|
||||
source: SyncSource
|
||||
}
|
||||
160
packages/services/src/Domain/Integrity/IntegrityService.spec.ts
Normal file
160
packages/services/src/Domain/Integrity/IntegrityService.spec.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
import { TransferPayload } from '@standardnotes/models'
|
||||
import { SyncEvent } from '../Event/SyncEvent'
|
||||
|
||||
import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface'
|
||||
import { ItemsServerInterface } from '../Item/ItemsServerInterface'
|
||||
import { SyncSource } from '../Sync/SyncSource'
|
||||
import { IntegrityApiInterface } from './IntegrityApiInterface'
|
||||
import { IntegrityService } from './IntegrityService'
|
||||
import { PayloadManagerInterface } from '../Payloads/PayloadManagerInterface'
|
||||
import { IntegrityPayload } from '@standardnotes/responses'
|
||||
|
||||
describe('IntegrityService', () => {
|
||||
let integrityApi: IntegrityApiInterface
|
||||
let itemApi: ItemsServerInterface
|
||||
let payloadManager: PayloadManagerInterface
|
||||
let internalEventBus: InternalEventBusInterface
|
||||
|
||||
const createService = () => new IntegrityService(integrityApi, itemApi, payloadManager, internalEventBus)
|
||||
|
||||
beforeEach(() => {
|
||||
integrityApi = {} as jest.Mocked<IntegrityApiInterface>
|
||||
integrityApi.checkIntegrity = jest.fn()
|
||||
|
||||
itemApi = {} as jest.Mocked<ItemsServerInterface>
|
||||
itemApi.getSingleItem = jest.fn()
|
||||
|
||||
payloadManager = {} as jest.Mocked<PayloadManagerInterface>
|
||||
payloadManager.integrityPayloads = []
|
||||
|
||||
internalEventBus = {} as jest.Mocked<InternalEventBusInterface>
|
||||
internalEventBus.publishSync = jest.fn()
|
||||
})
|
||||
|
||||
it('should check integrity of payloads and publish mismatches', async () => {
|
||||
integrityApi.checkIntegrity = jest.fn().mockReturnValue({
|
||||
data: {
|
||||
mismatches: [{ uuid: '1-2-3', updated_at_timestamp: 234 } as IntegrityPayload],
|
||||
},
|
||||
})
|
||||
itemApi.getSingleItem = jest.fn().mockReturnValue({
|
||||
data: {
|
||||
item: {
|
||||
uuid: '1-2-3',
|
||||
content: 'foobar',
|
||||
} as Partial<TransferPayload>,
|
||||
},
|
||||
})
|
||||
|
||||
await createService().handleEvent({
|
||||
type: SyncEvent.SyncRequestsIntegrityCheck,
|
||||
payload: {
|
||||
integrityPayloads: [{ uuid: '1-2-3', updated_at_timestamp: 123 } as IntegrityPayload],
|
||||
source: SyncSource.AfterDownloadFirst,
|
||||
},
|
||||
})
|
||||
|
||||
expect(internalEventBus.publishSync).toHaveBeenCalledWith(
|
||||
{
|
||||
payload: {
|
||||
rawPayloads: [
|
||||
{
|
||||
content: 'foobar',
|
||||
uuid: '1-2-3',
|
||||
},
|
||||
],
|
||||
source: 5,
|
||||
},
|
||||
type: 'IntegrityCheckCompleted',
|
||||
},
|
||||
'SEQUENCE',
|
||||
)
|
||||
})
|
||||
|
||||
it('should publish empty mismatches if everything is in sync', async () => {
|
||||
integrityApi.checkIntegrity = jest.fn().mockReturnValue({
|
||||
data: {
|
||||
mismatches: [],
|
||||
},
|
||||
})
|
||||
|
||||
await createService().handleEvent({
|
||||
type: SyncEvent.SyncRequestsIntegrityCheck,
|
||||
payload: {
|
||||
integrityPayloads: [{ uuid: '1-2-3', updated_at_timestamp: 123 } as IntegrityPayload],
|
||||
source: SyncSource.AfterDownloadFirst,
|
||||
},
|
||||
})
|
||||
|
||||
expect(internalEventBus.publishSync).toHaveBeenCalledWith(
|
||||
{
|
||||
payload: {
|
||||
rawPayloads: [],
|
||||
source: 5,
|
||||
},
|
||||
type: 'IntegrityCheckCompleted',
|
||||
},
|
||||
'SEQUENCE',
|
||||
)
|
||||
})
|
||||
|
||||
it('should not publish mismatches if checking integrity fails', async () => {
|
||||
integrityApi.checkIntegrity = jest.fn().mockReturnValue({
|
||||
error: 'Ooops',
|
||||
})
|
||||
|
||||
await createService().handleEvent({
|
||||
type: SyncEvent.SyncRequestsIntegrityCheck,
|
||||
payload: {
|
||||
integrityPayloads: [{ uuid: '1-2-3', updated_at_timestamp: 123 } as IntegrityPayload],
|
||||
source: SyncSource.AfterDownloadFirst,
|
||||
},
|
||||
})
|
||||
|
||||
expect(internalEventBus.publishSync).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should publish empty mismatches if fetching items fails', async () => {
|
||||
integrityApi.checkIntegrity = jest.fn().mockReturnValue({
|
||||
data: {
|
||||
mismatches: [{ uuid: '1-2-3', updated_at_timestamp: 234 } as IntegrityPayload],
|
||||
},
|
||||
})
|
||||
itemApi.getSingleItem = jest.fn().mockReturnValue({
|
||||
error: 'Ooops',
|
||||
})
|
||||
|
||||
await createService().handleEvent({
|
||||
type: SyncEvent.SyncRequestsIntegrityCheck,
|
||||
payload: {
|
||||
integrityPayloads: [{ uuid: '1-2-3', updated_at_timestamp: 123 } as IntegrityPayload],
|
||||
source: SyncSource.AfterDownloadFirst,
|
||||
},
|
||||
})
|
||||
|
||||
expect(internalEventBus.publishSync).toHaveBeenCalledWith(
|
||||
{
|
||||
payload: {
|
||||
rawPayloads: [],
|
||||
source: 5,
|
||||
},
|
||||
type: 'IntegrityCheckCompleted',
|
||||
},
|
||||
'SEQUENCE',
|
||||
)
|
||||
})
|
||||
|
||||
it('should not handle different event types', async () => {
|
||||
await createService().handleEvent({
|
||||
type: SyncEvent.SyncCompletedWithAllItemsUploaded,
|
||||
payload: {
|
||||
integrityPayloads: [{ uuid: '1-2-3', updated_at_timestamp: 123 } as IntegrityPayload],
|
||||
source: SyncSource.AfterDownloadFirst,
|
||||
},
|
||||
})
|
||||
|
||||
expect(integrityApi.checkIntegrity).not.toHaveBeenCalled()
|
||||
expect(itemApi.getSingleItem).not.toHaveBeenCalled()
|
||||
expect(internalEventBus.publishSync).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
62
packages/services/src/Domain/Integrity/IntegrityService.ts
Normal file
62
packages/services/src/Domain/Integrity/IntegrityService.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { IntegrityEvent } from './IntegrityEvent'
|
||||
import { AbstractService } from '../Service/AbstractService'
|
||||
import { ItemsServerInterface } from '../Item/ItemsServerInterface'
|
||||
import { IntegrityApiInterface } from './IntegrityApiInterface'
|
||||
import { GetSingleItemResponse } from '@standardnotes/responses'
|
||||
import { InternalEventHandlerInterface } from '../Internal/InternalEventHandlerInterface'
|
||||
import { InternalEventInterface } from '../Internal/InternalEventInterface'
|
||||
import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface'
|
||||
import { SyncEvent } from '../Event/SyncEvent'
|
||||
import { IntegrityEventPayload } from './IntegrityEventPayload'
|
||||
import { SyncSource } from '../Sync/SyncSource'
|
||||
import { PayloadManagerInterface } from '../Payloads/PayloadManagerInterface'
|
||||
|
||||
export class IntegrityService
|
||||
extends AbstractService<IntegrityEvent, IntegrityEventPayload>
|
||||
implements InternalEventHandlerInterface
|
||||
{
|
||||
constructor(
|
||||
private integrityApi: IntegrityApiInterface,
|
||||
private itemApi: ItemsServerInterface,
|
||||
private payloadManager: PayloadManagerInterface,
|
||||
protected override internalEventBus: InternalEventBusInterface,
|
||||
) {
|
||||
super(internalEventBus)
|
||||
}
|
||||
|
||||
async handleEvent(event: InternalEventInterface): Promise<void> {
|
||||
if (event.type !== SyncEvent.SyncRequestsIntegrityCheck) {
|
||||
return
|
||||
}
|
||||
|
||||
const integrityCheckResponse = await this.integrityApi.checkIntegrity(this.payloadManager.integrityPayloads)
|
||||
if (integrityCheckResponse.error !== undefined) {
|
||||
this.log(`Could not obtain integrity check: ${integrityCheckResponse.error}`)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const serverItemResponsePromises: Promise<GetSingleItemResponse>[] = []
|
||||
for (const mismatch of integrityCheckResponse.data.mismatches) {
|
||||
serverItemResponsePromises.push(this.itemApi.getSingleItem(mismatch.uuid))
|
||||
}
|
||||
|
||||
const serverItemResponses = await Promise.all(serverItemResponsePromises)
|
||||
|
||||
const rawPayloads = []
|
||||
for (const serverItemResponse of serverItemResponses) {
|
||||
if (serverItemResponse.data === undefined || serverItemResponse.error || !('item' in serverItemResponse.data)) {
|
||||
this.log(`Could not obtain item for integrity adjustments: ${serverItemResponse.error}`)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
rawPayloads.push(serverItemResponse.data.item)
|
||||
}
|
||||
|
||||
await this.notifyEventSync(IntegrityEvent.IntegrityCheckCompleted, {
|
||||
rawPayloads: rawPayloads,
|
||||
source: (event.payload as { source: SyncSource }).source,
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user