senecajs/seneca-entity-history

View on GitHub
lib/make_cmd_save_history_msg.ts

Summary

Maintainability
C
7 hrs
Test Coverage
/* Copyright (c) 2020 voxgig and other contributors, MIT License */
/* $lab:coverage:off$ */
'use strict'

import intern from './intern'

/* $lab:coverage:on$ */


// TODO: this should not be necessary
// https://github.com/senecajs/seneca/issues/873
export function make_cmd_save_history_msg(options: any) {
  return async function cmd_save_history(
    msg: {
      ent: {
        id: null | string
        entity$: string
        load$: any
        custom$: any
        history$: {
          wait: boolean
        }
      }
    },
    meta: any
  ) {
    let seneca = this
    let entity$ = msg.ent.entity$ = (msg.ent.entity$ || intern.canon(msg))

    // Avoid infinite loops
    if (entity$.endsWith('sys/entver')) {
      return this.prior(msg, meta)
    }

    let ent = seneca.entity(msg.ent)

    // TODO seneca-entity should return null, thus removing need for ?:
    // let entprev = null == ent.id ? null : await ent.load$(ent.id)
    let entprev = await ent.load$(ent.id)

    // Return this entity.
    let orig_entout = await this.prior(msg, meta)

    // Snapshot: use this to generate version.
    let entout = orig_entout.clone$()

    
    let changed: string[] = [] // changed fields

    if (entprev) {
      let od = entout.data$(false)
      let pd = entprev.data$(false)
      let allkeysuniq = [...new Set([...Object.keys(od), ...Object.keys(pd)])]

        // Do not include resver_id in changed fields as this would be spurious
        .filter((k: string) => k != 'resver_id')

      allkeysuniq.forEach((fn) => {
        let ov = od[fn]
        let pv = pd[fn]
        let ot = typeof ov
        let pt = typeof pv

        if (null != ov || null != pv) {
          if ('object' === ot && 'object' === pt) {
            changed.push(fn) // TODO: proper object data equiv test
          } else if (ov !== pv) {
            changed.push(fn)
          }
        }
      })
    }

    let who: any =
      null == options.build_who
        ? {}
        : options.build_who.call(this, entprev, changed, entout, ...arguments)

    let what: any =
      null == options.build_who
        ? {}
        : options.build_what.call(this, entprev, changed, entout, ...arguments)


    let histspec = {
      seneca,
      entmsg: msg.ent,
      entout,
      entprev,
      changed,
      who,
      what,
    }


    let wait = options.wait ||
      (ent.history$ && ent.history$.wait) ||
      (ent.custom$ && ent.custom$.history && ent.custom$.history.wait)

    // don't wait for version handling to complete, unless options.wait
    if (wait) {
      await intern.history(histspec)
    }
    else {
      intern.history(histspec)
    }


    // NOTE: return the original, which may be safely changed by calling code, since
    // it is not used to create the version later (if not waiting).
    return orig_entout

    /*
      
      # sys:entity,cmd:save
      
      // msg$ is a special name - will attempt to unify with seneca inbound message
      msg$:
      ent:
      id: null | string
      
      out$: Entity // Entity is a type declatation, external provided
      
      prev: load$ msg$.ent.entity$ msg$.ent.id
      
      // null result will fail as cannot unify with Entity
      out$: prior$
      
      
      // conditionals
      result: if$ expr0 expr1
      
      // throwaway
      : if$ expr0 expr1
      
      // implicit throwaway
      if$ expr0 expr1
      
      // expr0 is truthy: true is non-nil
      
      // expr1 can't have side effects!!!
      // but does get it's own local context with access to top
      // you can only change top level at the top level
      if$ expr0 expr1
      
      
      if$ prev
      // indent is an implicit ()
      
      // equivs, generates: {base:string|null,name:string,}
      canon: /((?<base>\w+)/)?(?<name>\w+)$/ out$.entity$  // apply a regexp 
      canon: out$.canon$  // recognize function, call it!
      
      fields: string[]  // types are values! unify!
      
      // get out of jail
      // lazy eval, unify passes if return val unifies
      fields => {
      if (prev) {
      ...js as above, $ is implicit context
      }
      }
      // NOTE: => is lazy, : is not - as you need well-defined order of persistence ops and msg calls
      
      
      field-keys: \ // multi line value, - in names as just valid JSON "field-keys"
      keys$ data$ out$ data$ prev  // keys$ list uniq keys of objects   
      // data$ is entity.data$, handles null gracefully
      // (keys$ (data$ out$) (data$ prev)) // eager function calls
      // RHS is LISPish :)
      
      // push$ does not push nils, eq$/3 return /3 or nil (eq$ lhs rhs yesval?true$ noval?nil)
      // eq$ is intelligent and deep - unifies?!
      // prev[field] is nil if prev is nil
      fields: $reduce field-keys [] (changed,field)=>push$ changed eq$ out$[field] prev[field] field
      
      // save$ implicitly async
      entver: save$ sys/entver {
      ent_id: out$.id // RHS also an s-exp
      fields: fields
      base: canon.base
      name: canon.name
      when: now$
      d: data$ out$
      }
      
      // throw away result
      : save$ sys/enthist {
      ver_id: entver.id
      ent_id: out$.id
      fields: fields
      base: canon.base
      name: canon.name
      when: entver.when
      }
      
      // even top level is LISP really
      
      foo: bar
      (set$ 'foo' 'bar')  // where set operates on current context
      (set$ path expr)  // where set operates on current context
      
      // NOTE: set$ performs a unify at path point
      
      // possible engines:
      // https://github.com/maryrosecook/littlelisp/blob/master/littlelisp.js
      // https://jcubic.github.io/lips/
      // https://github.com/mishoo/SLip
      // http://synapticfailure.com/ai/lisp_js/
      // http://www.joeganley.com/code/jslisp.html
      // https://calormen.com/jisp/
      */
  }
}