src/Owner.ts
/* Copyright (c) 2018-2020 Voxgig and other contributors, MIT License */
/* $lab:coverage:off$ */
'use strict'
import { Gubu } from 'gubu'
import { refine_query } from './refine_query'
/* $lab:coverage:on$ */
const { Open, Any } = Gubu
const defaults = {
default_spec: {
active: true,
fields: [],
read: Open({
// default true
//usr: true,
//org: true
}),
write: Open({
// default true
//usr: true,
//org: true
}),
inject: Open({
// default true
//usr: true,
//org: true
}),
alter: Open({
// default false
//usr: false,
//org: false
}),
public: Open({
read: Open({
// field -> public boolean field
})
})
},
specprop: 'sys-owner-spec',
ownerprop: 'sysowner',
caseprop: 'case$',
entprop: 'ent',
queryprop: 'q',
annotate: [],
ignore: [],
fields: [],
owner_required: true,
explain: Any(),
include: {
custom: Open({})
}
}
function Owner(this: any, options: any) {
const seneca = this
const { deep } = seneca.util
intern.deepextend = seneca.util.deepextend
options.default_spec.fields = [
...new Set(options.default_spec.fields.concat(options.fields))
]
// intern.default_spec = intern.make_spec(options.default_spec)
const default_spec = intern.make_spec(options.default_spec, {})
const casemap: any = {}
this.fix('sys:owner').add('hook:case', hook_case)
// TODO: allow multiple ordered cases
function hook_case(msg: any, reply: any) {
var kase = msg.case
var modifiers = msg.modifiers
if ('string' === typeof kase && 'object' === typeof modifiers) {
casemap[kase] = modifiers
}
reply()
}
const specP = options.specprop
const ownerprop = options.ownerprop
const caseP = options.caseprop
const entprop = options.entprop
const queryprop = options.queryprop
const include = options.include
const hasInclude = 0 < Object.keys(include).length
const resolvedFieldNames: { [fieldName: string]: string[] } = {}
// By default, ownerprop needed to activate
include.custom = deep({ [ownerprop]: { owner$: 'exists' } }, include.custom)
const ignoreMatch = seneca.util.Patrun()
const ignore = options.ignore.map((p: any) => seneca.util.Jsonic(p))
ignore.map((pat: any) => ignoreMatch.add(pat, pat))
const annotate = options.annotate.map((p: any) => seneca.util.Jsonic(p))
annotate.forEach(function(msgpat: any) {
const checkOwner: any = function checkOwner(this: any, msg: any, reply: any, meta: any) {
const self = this
const explain = this.explain()
const expdata: any = explain && {
when: Date.now(),
msgpat: msgpat,
msgid: meta.id,
modifiers: {},
options: options
}
let ignorepat = ignoreMatch.find(msg)
if (null != ignorepat) {
explain && (expdata.ignored = true, expdata.ignorepat = ignorepat)
return intern.prior(self, msg, reply, explain, expdata)
}
let spec = self.util.deepextend(meta.custom[specP] || default_spec)
const owner = meta.custom[ownerprop] || getprop(meta.custom, ownerprop)
if (!owner && !options.owner_required) {
explain && ((expdata.owner_required = false), (expdata.pass = true))
return intern.prior(self, msg, reply, explain, expdata)
}
let modifiers: any = {}
if (owner && casemap[owner[caseP]]) {
modifiers = casemap[owner[caseP]]
}
if (modifiers.query) {
explain && (expdata.modifiers.query = true)
spec = modifiers.query.call(self, spec, owner, msg)
}
explain &&
((expdata.owner = owner), (expdata.spec = self.util.deepextend(spec)))
let active = spec.active
if (active && hasInclude) {
if (include.custom) {
let cip, cval, mval
for (cip in include.custom) {
active = active && (
// direct prop
(((cval = include.custom[cip]) === (mval = meta.custom[cip])) && null != cval) ||
((cval && ('exists' === cval.owner$ && null != mval)) && null != cval) ||
// path prop
((cval === (mval = getprop(meta.custom, cip))) && null != cval) ||
((cval && ('exists' === cval.owner$ && null != mval)) && null != cval)
)
// console.log('CIP', cip, active, include.custom, meta.custom)
if (!active) { break }
}
explain && ((expdata.include_custom = active),
(!active && (expdata.include_custom_prop = cip)))
}
}
// console.log('QQQ', active, hasInclude, include.custom, meta.custom, msg)
if (active) {
if ('list' === msg.cmd) {
explain && (expdata.path = 'list')
refine_query(self, msg, queryprop, spec, owner, intern, resolvedFieldNames)
explain && (expdata.query = msg[queryprop])
return self.prior(msg, function(err: any, list: any) {
if (err) return reply(err)
if (null == list) return reply()
if (modifiers.list) {
explain &&
((expdata.modifiers.list = true),
(expdata.orig_list_len = list ? list.length : 0))
list = modifiers.list.call(self, spec, owner, msg, list)
}
explain && (expdata.list_len = list ? list.length : 0)
return intern.reply(self, reply, list, explain, expdata)
})
}
// handle remove operation
else if ('remove' === msg.cmd) {
explain && (expdata.path = 'remove')
refine_query(self, msg, queryprop, spec, owner, intern, resolvedFieldNames)
explain && (expdata.query = msg[queryprop])
self.make(msg.ent.entity$).list$(msg.q, function(err: any, list: any) {
if (err) return self.fail(err)
if (modifiers.list) {
explain &&
((expdata.modifiers.list = true),
(expdata.orig_list_len = list ? list.length : 0))
list = modifiers.list.call(self, spec, owner, msg, list)
}
// TODO: should use list result ids!!!
if (0 < list.length) {
explain &&
((expdata.empty = false),
(expdata.list_len = list ? list.length : 0))
return intern.prior(self, msg, reply, explain, expdata)
}
// nothing to delete
else {
explain && (expdata.empty = true)
return intern.reply(self, reply, void 0, explain, expdata)
}
})
}
// handle load operation
else if ('load' === msg.cmd) {
explain && (expdata.path = 'load')
// only change query if not loading by id - preserves caching!
if (null == msg[queryprop].id) {
refine_query(self, msg, queryprop, spec, owner, intern, resolvedFieldNames)
explain && (expdata.query = msg[queryprop])
}
self.prior(msg, function(err: any, load_ent: any) {
if (err) return reply(err)
if (null == load_ent) return reply()
// was not an id-based query, so refinement already made
if (null == msg[queryprop].id) {
explain && ((expdata.query_load = true), (expdata.ent = load_ent))
return intern.reply(self, reply, load_ent, explain, expdata)
}
if (modifiers.entity) {
explain && (expdata.modifiers.entity = true)
spec = modifiers.entity.call(self, spec, owner, msg, load_ent)
explain && (expdata.modifiers.entity_spec = spec)
}
let pass = true
for (let fieldI = 0; fieldI < spec.fields.length; fieldI++) {
const fieldName = spec.fields[fieldI]
const [ownerFieldName, entityFieldName] =
(resolvedFieldNames[fieldName] ||
(resolvedFieldNames[fieldName] =
intern.resolveFieldNames(spec.fields[fieldI])))
// need this field to match owner for ent to be readable
// if (spec.read[f]) {
if (spec.read[fieldName]) {
pass = pass && intern.match(owner[ownerFieldName], load_ent[entityFieldName])
if (!pass) {
explain &&
(expdata.field_match_fail = {
field: spec.fields[fieldI],
ownerFieldName,
entityFieldName,
ent_val: load_ent[entityFieldName],
owner_val: owner[ownerFieldName]
})
break
}
}
}
explain && ((expdata.pass = pass), (expdata.ent = load_ent))
return intern.reply(
self,
reply,
pass ? load_ent : null,
explain,
expdata
)
})
}
// handle save operation
else if ('save' === msg.cmd) {
explain && (expdata.path = 'save')
const ent = msg[entprop]
// console.log('OWNER save A', ent, spec)
// only set fields props if not already set
for (let fieldI = 0; fieldI < spec.fields.length; fieldI++) {
const fieldName = spec.fields[fieldI]
const [ownerFieldName, entityFieldName] =
(resolvedFieldNames[fieldName] ||
(resolvedFieldNames[fieldName] =
intern.resolveFieldNames(spec.fields[fieldI])))
// const f = spec.fields[i]
if (
spec.inject[fieldName] &&
null == ent[entityFieldName] &&
null != owner[ownerFieldName]
) {
ent[entityFieldName] =
Array.isArray(owner[ownerFieldName]) ?
owner[ownerFieldName][0] : owner[ownerFieldName]
}
}
// creating
if (null == ent.id) {
explain && (expdata.path = 'save/create')
for (let fieldI = 0; fieldI < spec.fields.length; fieldI++) {
const fieldName = spec.fields[fieldI]
const [ownerFieldName, entityFieldName] =
(resolvedFieldNames[fieldName] ||
(resolvedFieldNames[fieldName] =
intern.resolveFieldNames(spec.fields[fieldI])))
// console.log('CREATE', fieldName, ownerFieldName, entityFieldName, ent, spec)
if (spec.write[fieldName] && null != ent[entityFieldName]) {
if (!intern.match(owner[ownerFieldName], ent[entityFieldName])) {
const fail = {
code: 'create-not-allowed',
details: {
why: 'field-mismatch-on-create',
field: fieldName,
ownerFieldName,
entityFieldName,
ent_val: ent[entityFieldName],
owner_val: owner[ownerFieldName]
}
}
explain && (expdata.fail = fail)
return intern.fail(self, reply, fail, explain, expdata)
}
}
}
return intern.prior(self, msg, reply, explain, expdata)
}
// updating
else {
explain && (expdata.path = 'save/update')
let fail: any
// TODO: seneca entity update would really help there!
self.make(ent.entity$).load$(ent.id, function(this: any, err: any, oldent: any) {
if (err) return this.fail(err)
if (null == oldent) {
fail = {
code: 'save-not-found',
details: { entity: ent.entity$, id: ent.id }
}
explain && (expdata.fail = fail)
return intern.fail(self, reply, fail, explain, expdata)
}
for (let fieldI = 0; fieldI < spec.fields.length; fieldI++) {
const fieldName = spec.fields[fieldI]
const [ownerFieldName, entityFieldName] =
(resolvedFieldNames[fieldName] ||
(resolvedFieldNames[fieldName] =
intern.resolveFieldNames(spec.fields[fieldI])))
if (
spec.write[fieldName] &&
!spec.alter[fieldName] &&
oldent[entityFieldName] !== ent[entityFieldName]
) {
fail = {
code: 'update-not-allowed',
details: {
why: 'field-mismatch-on-update',
field: fieldName,
ownerFieldName,
entityFieldName,
oldent_val: oldent[entityFieldName],
ent_val: ent[entityFieldName]
}
}
explain && (expdata.fail = fail)
return intern.fail(self, reply, fail, explain, expdata)
}
}
explain && (expdata.save = true)
return intern.prior(self, msg, reply, explain, expdata)
})
}
}
}
// not active, do nothing
else {
explain && (expdata.active = false)
return intern.prior(self, msg, reply, explain, expdata)
}
}
checkOwner.desc = 'Validate owner for ' + seneca.util.pattern(msgpat)
// seneca.add(msgpat, owner)
seneca.wrap(msgpat, checkOwner)
//if (!seneca.find(msgpat, { exact: true })) {
seneca.add(msgpat, checkOwner)
// }
})
return {
exports: {
make_spec: (inspec: any) => intern.make_spec(inspec, default_spec),
casemap: casemap,
config: {
spec: default_spec,
options: options
}
}
}
}
const intern = (Owner.intern = {
// default_spec: null,
deepextend: (a: any, b: any, c: any) => null,
make_spec: function(inspec: any, default_spec: any) {
const spec: any = intern.deepextend({}, default_spec, inspec)
spec.fields = [...new Set(spec.fields)]
;['write', 'read', 'inject', 'alter'].forEach(m => {
spec[m] = spec[m] || {}
})
spec.fields.forEach(function(f: any) {
spec.write[f] = null == spec.write[f] ? true : spec.write[f]
spec.read[f] = null == spec.read[f] ? true : spec.read[f]
spec.inject[f] = null == spec.inject[f] ? true : spec.inject[f]
})
;['write', 'read', 'inject', 'alter'].forEach(m => {
spec.fields = [...new Set(spec.fields.concat(Object.keys(spec[m])))]
})
spec.public = spec.public || {}
spec.public.read = spec.public.read || {}
return spec
},
match: function(matching_val: any, check_val: any) {
// match if check_val (from ent) is undefined (thus not considered), or
// if check_val (from ent) equals one of the valid matching vals
return (
void 0 === check_val ||
(Array.isArray(matching_val) && matching_val.includes(check_val)) ||
check_val === matching_val
)
},
prior: function(self: any, msg: any, reply: any, explain: any, expdata: any) {
explain && explain(expdata)
return self.prior(msg, reply)
},
reply: function(self: any, reply: any, result: any, explain: any, expdata: any) {
explain && explain(expdata)
return reply(result)
},
fail: function(self: any, reply: any, fail: any, explain: any, expdata: any) {
explain && explain(expdata)
return reply(self.error(fail.code, fail.details))
},
resolveFieldNames: (fieldName: string) => {
const parts = fieldName.split(':')
const resolvedNames = [parts[0], null == parts[1] ? parts[0] : parts[1]]
// console.log('resolvedNames', fieldName, resolvedNames)
return resolvedNames
}
})
// get dot path property
const getprop = (o: any, p: string, _?: any): any =>
(_ = ('' + p).match(/^([^\.]+)\.(.*)$/), ((null != o && null != _) ?
getprop(o[_[1]], _[2]) : (null == o ? o : o[p])))
Object.assign(Owner, { defaults, intern })
// Prevent name mangling
Object.defineProperty(Owner, 'name', { value: 'Owner' })
export default Owner
if ('undefined' !== typeof module) {
module.exports = Owner
}