UNPKG

@worldbrain/storex-sync

Version:

Offline-first syncing between multiple databases using Storex

178 lines (147 loc) 9.19 kB
import { StorageRegistry } from '@worldbrain/storex'; import * as expect from 'expect' import { ClientSyncLogEntry } from '../client-sync-log/types'; import { ExecutableOperation } from './types'; import { reconcileSyncLog } from './default'; function test({logEntries, expectedOperations} : {logEntries : ClientSyncLogEntry[], expectedOperations? : ExecutableOperation[]}) { const storageRegistry = new StorageRegistry() storageRegistry.registerCollections({ list: { version: new Date('2019-02-02'), fields: { title: {type: 'string'}, } }, listEntry: { version: new Date('2019-02-02'), fields: { title: {type: 'string'}, url: {type: 'string'}, } } }) const reconciled = reconcileSyncLog(logEntries, {storageRegistry}) if (expectedOperations) { expect(reconciled).toEqual(expectedOperations) } } describe('Reconciliation', () => { it('should choose the newest write when finding two entries for the same object field', () => { const logEntries : ClientSyncLogEntry[] = [ {operation: 'modify', createdOn: 2, sharedOn: null, needsIntegration: true, collection: 'list', pk: 'list-one', field: 'title', value: 'second'}, {operation: 'modify', createdOn: 1, sharedOn: null, needsIntegration: true, collection: 'list', pk: 'list-one', field: 'title', value: 'first'}, ] test({logEntries, expectedOperations: [ {operation: 'updateOneObject', collection: 'list', args: [{id: 'list-one'}, {title: 'second'}]} ]}) }) it('should choose the newest write when finding more than two entries for the same object field', () => { const logEntries : ClientSyncLogEntry[] = [ {operation: 'modify', createdOn: 2, sharedOn: null, needsIntegration: true, collection: 'list', pk: 'list-one', field: 'title', value: 'second'}, {operation: 'modify', createdOn: 3, sharedOn: null, needsIntegration: true, collection: 'list', pk: 'list-one', field: 'title', value: 'third'}, {operation: 'modify', createdOn: 1, sharedOn: null, needsIntegration: true, collection: 'list', pk: 'list-one', field: 'title', value: 'first'}, ] test({logEntries, expectedOperations: [ {operation: 'updateOneObject', collection: 'list', args: [{id: 'list-one'}, {title: 'third'}]} ]}) }) it('should ignore writes to an object that needs deletion', () => { const logEntries : ClientSyncLogEntry[] = [ {operation: 'modify', createdOn: 2, sharedOn: null, needsIntegration: true, collection: 'list', pk: 'list-one', field: 'title', value: 'second'}, {operation: 'delete', createdOn: 1, sharedOn: null, needsIntegration: true, collection: 'list', pk: 'list-one'}, ] test({logEntries, expectedOperations: [ {operation: 'deleteOneObject', collection: 'list', args: [{id: 'list-one'}]} ]}) }) it('should ignore writes to an already deleted object', () => { const logEntries : ClientSyncLogEntry[] = [ {operation: 'modify', createdOn: 2, sharedOn: null, needsIntegration: true, collection: 'list', pk: 'list-one', field: 'title', value: 'second'}, {operation: 'delete', createdOn: 4, sharedOn: 3, needsIntegration: true, collection: 'list', pk: 'list-one'}, {operation: 'delete', createdOn: 1, sharedOn: 3, needsIntegration: true, collection: 'list', pk: 'list-one'}, ] test({logEntries, expectedOperations: []}) }) it('should work with only one delete', () => { const logEntries : ClientSyncLogEntry[] = [ {operation: 'delete', createdOn: 1, sharedOn: null, needsIntegration: true, collection: 'list', pk: 'list-one'}, ] test({logEntries, expectedOperations: [ {operation: 'deleteOneObject', collection: 'list', args: [{id: 'list-one'}]} ]}) }) it('should ignore double deletes', () => { const logEntries : ClientSyncLogEntry[] = [ {operation: 'delete', createdOn: 4, sharedOn: null, needsIntegration: true, collection: 'list', pk: 'list-one'}, {operation: 'delete', createdOn: 1, sharedOn: null, needsIntegration: true, collection: 'list', pk: 'list-one'}, ] test({logEntries, expectedOperations: [ {operation: 'deleteOneObject', collection: 'list', args: [{id: 'list-one'}]} ]}) }) it('should work with deletes having compound keys', () => { const logEntries : ClientSyncLogEntry[] = [ {operation: 'delete', createdOn: 4, sharedOn: null, needsIntegration: true, collection: 'listEntry', pk: ['list-one', 3]}, ] test({logEntries, expectedOperations: [ {operation: 'deleteOneObject', collection: 'listEntry', args: [{id: ['list-one', 3]}]} ]}) }) it('should ignore writes that are already synced', () => { const logEntries : ClientSyncLogEntry[] = [ {operation: 'modify', createdOn: 1, sharedOn: null, needsIntegration: true, collection: 'list', pk: 'list-one', field: 'title', value: 'second'}, {operation: 'modify', createdOn: 2, sharedOn: 3, needsIntegration: true, collection: 'list', pk: 'list-one', field: 'title', value: 'second'}, ] test({logEntries, expectedOperations: []}) }) it('should create objects', () => { const logEntries : ClientSyncLogEntry[] = [ {operation: 'create', createdOn: 1, sharedOn: null, needsIntegration: true, collection: 'list', pk: 'list-one', value: {title: 'first'}} ] test({logEntries, expectedOperations: [ {operation: 'createObject', collection: 'list', args: {id: 'list-one', title: 'first'}} ]}) }) it('should consolidate object creation with object updates', () => { const logEntries : ClientSyncLogEntry[] = [ {operation: 'modify', createdOn: 2, sharedOn: null, needsIntegration: true, collection: 'list', pk: 'list-one', field: 'title', value: 'second'}, {operation: 'create', createdOn: 1, sharedOn: null, needsIntegration: true, collection: 'list', pk: 'list-one', value: {title: 'first', prio: 5}}, ] test({logEntries, expectedOperations: [ {operation: 'createObject', collection: 'list', args: {id: 'list-one', title: 'second', prio: 5}} ]}) }) it('should consolidate object creation with object deletion', () => { const logEntries : ClientSyncLogEntry[] = [ {operation: 'modify', createdOn: 2, sharedOn: null, needsIntegration: true, collection: 'list', pk: 'list-one', field: 'title', value: 'second'}, {operation: 'create', createdOn: 1, sharedOn: null, needsIntegration: true, collection: 'list', pk: 'list-one', value: {pk: 'list-one', title: 'first', prio: 5}}, {operation: 'delete', createdOn: 3, sharedOn: null, needsIntegration: true, collection: 'list', pk: 'list-one'}, ] test({logEntries, expectedOperations: []}) }) it('should complain about double creates', () => { const logEntries : ClientSyncLogEntry[] = [ {operation: 'create', createdOn: 1, sharedOn: 1, needsIntegration: true, collection: 'list', pk: 'list-one', value: {pk: 'list-one', title: 'first', prio: 5}}, {operation: 'create', createdOn: 2, sharedOn: null, needsIntegration: true, collection: 'list', pk: 'list-one', value: {pk: 'list-one', title: 'first', prio: 5}}, ] expect(() => test({logEntries})).toThrow(`Detected double create in collection 'list', pk '"list-one"'`) }) it('should complain about modifications made to an object before creation', () => { const logEntries : ClientSyncLogEntry[] = [ {operation: 'modify', createdOn: 1, sharedOn: null, needsIntegration: true, collection: 'list', pk: 'list-one', field: 'title', value: 'second'}, {operation: 'create', createdOn: 2, sharedOn: null, needsIntegration: true, collection: 'list', pk: 'list-one', value: {pk: 'list-one', title: 'first', prio: 5}}, ] expect(() => test({logEntries})).toThrow( `Detected modification to collection 'list', pk '"list-one"' before it was created (likely pk collision)` ) }) it('should complain about modifications made to an object before creation even if received in the right order', () => { const logEntries : ClientSyncLogEntry[] = [ {operation: 'create', createdOn: 2, sharedOn: null, needsIntegration: true, collection: 'list', pk: 'list-one', value: {pk: 'list-one', title: 'first', prio: 5}}, {operation: 'modify', createdOn: 1, sharedOn: null, needsIntegration: true, collection: 'list', pk: 'list-one', field: 'title', value: 'second'}, ] expect(() => test({logEntries})).toThrow( `Detected modification to collection 'list', pk '"list-one"' before it was created (likely pk collision)` ) }) })