All files / json-ot/types/ot-json apply.ts

94.8% Statements 73/77
84.84% Branches 28/33
100% Functions 3/3
100% Lines 63/63

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 838x   8x 8x 8x 8x 8x 8x   8x 632x 632x 632x 187x 187x 193x 193x 193x     622x 622x 622x 272x 272x 102x 102x 102x 170x 130x 130x 130x   40x 40x     622x 622x 622x 615x 615x 40x 40x   575x 575x 575x 107x 107x 107x 107x 107x 105x 81x 468x 466x 466x   2x     618x 7x   7x   4x 4x 4x     3x 3x 3x     7x 7x   618x    
import {find, isArrayReference, isObjectReference} from '@jsonjoy.com/json-pointer';
import type {JsonOp} from './types';
import {evaluate as evalExpression} from '@jsonjoy.com/json-expression/lib/evaluate';
import {comparePath} from './util';
import {EDIT_TYPE} from './constants';
import {apply as applyStr} from '../ot-string-irreversible/apply';
import {apply as applyBin} from '../ot-binary-irreversible/apply';
import {Vars} from '@jsonjoy.com/json-expression/lib/Vars';
 
export const apply = (doc: unknown, op: JsonOp): unknown => {
  const [test, pick = [], data = [], drop = [], edit = []] = op;
  const testLength = test.length;
  if (testLength) {
    const expressionContext = {vars: new Vars(doc)};
    for (let i = 0; i < testLength; i++) {
      const testExpr = test[i];
      const testValue = evalExpression(testExpr, expressionContext);
      if (!testValue) throw new Error('TEST');
    }
  }
  const registers = new Map<number, unknown>();
  const picksSorted = pick.sort((a, b) => comparePath(a[1], b[1]));
  for (const [regId, path] of picksSorted) {
    const ref = find(doc, path);
    if (isArrayReference(ref)) {
      const {obj, key, val} = ref;
      obj.splice(key, 1);
      registers.set(regId, val);
    } else if (isObjectReference(ref)) {
      const {obj, key, val} = ref;
      delete obj[key];
      registers.set(regId, val);
    } else {
      doc = undefined;
      registers.set(regId, ref.val);
    }
  }
  for (const [regId, value] of data) registers.set(regId, value);
  const dropsSorted = drop.sort((a, b) => comparePath(b[1], a[1]));
  for (const [regId, where] of dropsSorted) {
    const value = registers.get(regId);
    if (!where.length) {
      doc = value;
      continue;
    }
    const path = where.slice(0, -1);
    const {val} = find(doc, path);
    if (val instanceof Array) {
      let index = where[where.length - 1];
      if (index === '-') index = val.length;
      const index2 = ~~index;
      if (typeof index === 'string') Iif ('' + index2 !== index) throw new Error('INVALID_INDEX');
      if (index2 > val.length || index2 < -1) throw new Error('INVALID_INDEX');
      if (index2 === -1 || index2 === val.length) val.push(value);
      else val.splice(index2, 0, value);
    } else if (val && typeof val === 'object') {
      const key = where[where.length - 1];
      (val as any)[key] = value;
    } else {
      throw new Error('NOT_FOUND');
    }
  }
  for (const [type, path, operation] of edit) {
    const {val, obj, key} = find(doc, path);
    let newVal: unknown;
    switch (type) {
      case EDIT_TYPE.OT_STRING: {
        Iif (typeof val !== 'string') throw new Error('NOT_STR');
        newVal = applyStr(val, operation);
        break;
      }
      case EDIT_TYPE.OT_BINARY: {
        Iif (!(val instanceof Uint8Array)) throw new Error('NOT_BIN');
        newVal = applyBin(val, operation);
        break;
      }
    }
    Iif (!obj) doc = newVal;
    else (obj as any)[key as any] = newVal;
  }
  return doc;
};