Closed FossPrime closed 1 year ago
Proposed code: Setup in application hooks for get before and before patch. Also setup in all service after 'get' hooks. https://github.com/feathersjs/feathers/issues/1548
/*
Requirements:
- Either: For App Hooks, with reverse hook support
1. to be installed on the before:all, app hook
2. For race condition detection (optional)
- Install to the last before:update reverse app hook
- add and update updatedAt records on your services
3. To be installed in after:all reverse app hook (optional)
- Cleans up get request to play well with other hooks
- Skips after hooks to recieve a clean get request for Feathers 3
- Or: for individual services
1. Add to before:all
2. Add as the last before:update hook
3. Add as an after:all hook
A hook that
1. converts single patch calls to update calls
2. for multi patch, runs update before/after hooks, once for every record
- while skipping the running of the service method/adapter
- if update hooks run without error, will continue running the _patch call
ToDo:
- Convert to Feathers 4,
- This allows cleaner installation as beforeAfter App hook would not be neeeded
- Known issues...
- sending two patch calls at the same time may lead to a race condition
- Only one will save. Behavior differs based on latency.
- Some protections are in place... this is unlikely
- Do not run this as both an App Hook and a service hook, they will clash
- Use exceptPath to disable the app hook on a service that has this hook
*/
import { skipRemainingHooks } from 'feathers-hooks-common'
import { merge } from 'lodash'
// const NS = 'patch2Update'
const cm = {
exceptPath: []
}
// Ex: const blah = expand('a.b.c.d', 'last value')
function expand (str, val = {}) {
return str.split('.').reduceRight((acc, currentValue) => {
return { [currentValue]: acc }
}, val)
}
function isGoodObj (o) {
return typeof o === 'object' && (!Array.isArray(o)) && o !== null && Object.keys(o).length
}
// Takes dotted keys from an object and returns an object with real keys
// Only does first level
function objPatch (stale, fresh) { // {} || [{}, {}]
const result = Array.isArray(fresh) ? [] : {}
for (const k of Object.keys(fresh)) { // empty objects get skipped
if (k.indexOf('.') > -1) {
const firstKey = k.slice(0, k.indexOf('.'))
const res = expand(k, fresh[k])[firstKey]
result[firstKey] = result[firstKey] === undefined
? res
: isGoodObj(result[firstKey])
? merge(result[firstKey], res)
: res
} else {
result[k] = fresh[k]
}
}
return merge(stale, result)
}
const patch2Update = async ctx => {
ctx.params = ctx.params || {}
if (cm.exceptPath.includes(ctx.path)) {
return ctx
}
if (ctx.method === 'patch') {
if (ctx.id === null || ctx.id === undefined) {
return ctx // TODO: patch2Update has no support for null ID's... yet
} else if (ctx.type === 'after') {
return skipRemainingHooks()(ctx) // finish disabling the patch call. Disables softdelete.
} else {
return patch2UpdateSolo(ctx)
}
}
// we want data as raw as possible, no hooks for our get
// clean up, after get
if (ctx.method === 'get' && ctx.params.$patch2UpdateHooklessGet) {
if (ctx.type === 'after') {
delete ctx.params.$patch2UpdateHooklessGet
}
return skipRemainingHooks()(ctx) // skip hooks before and after get (F3)
}
// to be done on the final, post service, `before` app hook
if (ctx.method === 'update' && ctx.type === 'before') {
return addSafetyCheck(ctx)
}
}
const addSafetyCheck = async (ctx) => {
ctx.params = ctx.params || {}
ctx.params.query = ctx.params.query || {}
if (!ctx.params.$patch2UpdateConverted) {
return ctx
} else if (!ctx.params.$patch2UpdateInitialBeforePassed) {
ctx.params.$patch2UpdateInitialBeforePassed = true
return ctx // don't run on initial before app hook
} else if (
ctx.params.$patch2UpdateConverted &&
ctx.params.$patch2UpdateInitialBeforePassed
) {
delete ctx.params.$patch2UpdateConverted
delete ctx.params.$patch2UpdateInitialBeforePassed
}
// Apply safety checks
// Checks for current or updated `updatedAt` by the millisecond
if (ctx.data.updatedAt) {
ctx.params.query.updatedAt = {
$in: [ ctx.data.updatedAt, ctx.params.before.updatedAt ]
}
}
// Todo: for extra collision detection, add sha1/256 support
return ctx
}
/*
- 1. makes a get call
- 2. merges the result and makes a update call
- 3. sets result to disable the patch adapter call
*/
const patch2UpdateSolo = async (ctx) => {
const myParams = {
$patch2UpdateHooklessGet: true
}
const res = await ctx.service.get(ctx.id, myParams)
const data = objPatch(res, ctx.data)
// Setting result should bypass call to adapter
// for safety checks
ctx.params.$patch2UpdateConverted = true
ctx.params.before = JSON.parse(JSON.stringify(res))
ctx.result = await ctx.service.update(ctx.id, data, ctx.params)
return skipRemainingHooks()(ctx)
}
export default function (opts) {
const args = opts || {}
cm.exceptPath = args.exceptPath || cm.exceptPath
return patch2Update
}
/*
const testOb = {
'a.b.c': 'first',
d: {
'e.f': 'second',
g: 'third'
},
'h.i': {
'j.k': 'fourth'
},
'l': {},
'm': [],
n: {
o: 'fifth',
p: {},
q: {
lostcount: 1,
r: {
lostcount: 2
}
}
},
'n.p': 'sixth',
'n.q': {
'r.s': 'seventh'
}
}
const x = objExpand(testOb)
console.log(x) */
Code no longer works on dove. mongoKeys helps.
Motivation
Patch calls are an endless pain when it comes to validation
'a.b.c': 1
create
hooksSolution
Allow us to set an application level hook that would convert patch calls to update calls before any service hooks notice.
Status
Update 1
While it's easy to do this as a service hook, it's much harder as an App hook, as it needs to run after all before:update for some collision protection. On feathers 3, the work around for hookless services also requires exotic hook placement with an after:all reverse app hook. On feathers 5, with koa hooks and hookless services, this will all be cleaner.
Update 2
The name is probably bad. The point is not to kill patch, but to improve it so that it doesn't have all of it's pitfalls. Patch is essential to the NoSQL way, specially for batch jobs.