/**
 * Dexie add-on that provides DB-level change tracking.
 * https://gist.github.com/medmunds/17c331c694ae00ce072ff642619f473b
 */ import Dexie , { DBCore , DBCoreAddRequest , DBCoreDeleteRequest , DBCoreIndex , DBCoreMutateRequest , DBCoreMutateResponse , DBCorePutRequest , DBCoreTable , Middleware , } from 'dexie' ; import jsonpatch , { Operation } from 'fast-json-patch' ; import { initOverrideCreateTransaction , initOverrideParseStoreSpec , } from '../overrides' ; export const ChangeTableName = '_changes' ; export const StashTableName = '_stash' ; export const MetaTableName = '_meta' ; export interface Change < T = any > { name : string ; key : string ; type : 'put' | 'delete' | 'add' ; ops : Operation [ ] ; before : T | undefined ; after : T | undefined ; } const isEmpty = ( obj ) : boolean => Object . keys ( obj ) . length === 0 && obj . constructor === Object ; const zip = < T = any , R = T > ( arr1 , arr2 , fill = undefined ) : any [ ] [ ] => { return arr1 . map ( ( k , i ) => [ k , arr2 [ i ] ?? fill ] ) ; } ; export function createTrackedChangesMiddleware ( core ) { return { ... core , table ( name ) { const table = core . table ( name ) ; if ( name . startsWith ( '_' ) ) return table ; return { ... table , async mutate ( req ) { if ( name . startsWith ( '_' ) ) return table . mutate ( req ) ; if ( req . type === 'deleteRange' ) { return table . mutate ( req ) ; } const { primaryKey } = table . schema ; const autoIncrement = Boolean ( primaryKey . autoIncrement ) ; let keys = req . keys || getEffectiveKeys ( primaryKey , req ) ; let before = await getExistingValues ( table , req , keys ) ; if ( autoIncrement && ( req . type === 'add' || req . type === 'put' ) ) { req = { ... req , wantResults : true } ; } const response = await table . mutate ( req ) ; if ( autoIncrement && ( req . type === 'add' || req . type === 'put' ) ) { if ( response . results === undefined ) { throw new Error ( `autoIncrement keys not returned from ${ req . type } mutation` ) ; } keys = response . results ; } let after = await table . getMany ( { trans : req . trans , keys } ) ; let ops = zip ( before , req . type === 'delete' ? [ ] : req . values , { } ) . map ( ( [ prev , next ] ) => jsonpatch . compare ( prev , next ) ) ; const { failures , numFailures } = response ; if ( numFailures > 0 ) { before = before . filter ( ( _obj , i ) => ! failures [ i ] ) ; keys = keys . filter ( ( _key , i ) => ! failures [ i ] ) ; ops = ops . filter ( ( _obj , i ) => ! failures [ i ] ) ; after = after . filter ( ( _obj , i ) => ! failures [ i ] ) ; } if ( keys . length > 0 ) { const values = [ ] ; for ( let i = 0 ; i < keys . length ; i ++ ) { const b = before [ 0 ] ; const type = req . type === 'put' && isEmpty ( b ) ? 'add' : req . type ; values . push ( { name , type , key : keys [ i ] , before : b , ops : ops [ i ] , after : after [ i ] , } ) ; } const changes = core . table ( ChangeTableName ) ; const update = { type : 'add' , trans : req . trans , values , } ; await changes . mutate ( update ) ; } return response ; } , } ; } , } ; } function getEffectiveKeys ( primaryKey , req ) { if ( req . type === 'delete' ) return req . keys ; return req . keys || req . values . map ( primaryKey . extractKey ) ; } function getExistingValues ( table , req , effectiveKeys ) { return req . type === 'add' ? Promise . resolve ( new Array ( req . values . length ) . fill ( { } ) ) : table . getMany ( { trans : req . trans , keys : effectiveKeys } ) . then ( ( values ) => values . map ( ( obj ) => ( obj === undefined ? { } : obj ) ) ) ; } /**
 * ChangesMiddleware object
 */ export const changesMiddleware = { stack : 'dbcore' , name : 'ChangesMiddleware' , create : createTrackedChangesMiddleware , } ; /**
 * ChangesAddon function
 */ export function changesAddon ( db ) { const overrideCreateTransaction = initOverrideCreateTransaction ( db , ChangeTableName ) ; db . _createTransaction = Dexie . override ( db . _createTransaction , overrideCreateTransaction ) ; const overrideParseStoresSpec = initOverrideParseStoreSpec ( { [ ChangeTableName ] : '++id,name' , [ StashTableName ] : '++id,name' , [ MetaTableName ] : '&key' , } ) ; ( db . Version . prototype as any ) . _parseStoresSpec = Dexie . override ( ( db . Version . prototype as any ) . _parseStoresSpec , overrideParseStoresSpec ) ; db . use ( changesMiddleware ) ; }