// NOTE: The purpose of this module is to implement all the database API logic that's common across database backends; that mainly means initialization, settings merging, and input validation/normalization. Each individual database backend can then assume that it will always be called with valid input.
// FIXME: Verify that all internal method calls in the PostgreSQL backend are still valid after moving argument validation/normalization into this module
// NOTE: The backend.getDefaultTransaction method MUST return synchronously.
// TODO: Only accept an explicit `null` when defaulting, not an `undefined` which may be implicit? To ensure that the caller didn't just forget to provide one. Though most/all queries have arguments coming after the TX, so this might not be necessary.
// FIXME: Other than the missing readOperation wrapper and the tx argument, this is *basically* the same logic as under forItem... this should be simplified somehow.
getItem:function(_tx,_options){
let[tx,options]=validateArguments(arguments,{
tx:maybeTX,
options:[required,wrapValueAsOption("id"),{
id:[required,isString],
optional:[defaultTo(false),isBoolean]// FIXME: Can this handling be moved to the wrapper?
// We create a new instance of the actual API for every item being processed. This is necessary because some of the input arguments will default to item-specific values, and some of the logic is dependent on task-specific metadata. This is a more efficient (and understandable) approach than pretending the API is stateless and then separately wrapping the API *again* for every individual item with a whole separate layer of input validation rules.
// FIXME: Is this still correct, with the new task (graph) format?
// Tags are required to be specified (even if an empty array) because it's easily forgotten
tags:[required,arrayOf(isString)],
aliases:[defaultTo([]),arrayOf(isString)],
data:[anything],// FIXME: Check for object
update:[isFunction],
failIfExists:[defaultTo(false),isBoolean],
allowUpsert:[defaultTo(true),isBoolean],
parentID:[defaultTo(item.id),isString]
},requireEither(["data","update"])]
});
let{data,...rest}=options;
returnmutableOperation((tx)=>{
returnbackend.storeItem(tx,{
...rest,
// We normalize `data` and `update` (which are mutually-exclusive) into a single option here, so that the backend only needs to deal with the `update` case
// TODO: Can this be folded into the validation rules in a reasonable and readable way?
update:(data!=null)
?(existingData)=>({...existingData,...data})
:rest.update
});
});
},
moveItem:function(_options){
let[options]=validateArguments(arguments,{
options:[required,wrapValueAsOption("into"),{
from:[defaultTo(item.id),isString],
into:[required,isString],
// NOTE: If no `merge` function is specified, that indicates that merging is not allowed (ie. this is strictly a rename), and mergeMetadata is ignored too
failIfExists:[defaultTo(false),isBoolean]// TODO: Shouldn't this default to true, for any occurrence outside of a merge/rename?
}]
});
returnmutableOperation((tx)=>{
returnbackend.createAlias(tx,options);
});
},
deleteAlias:function(_options){
let[options]=validateArguments(arguments,{
options:[required,wrapValueAsOption("from"),{
from:[required,isString]
}]
});
returnmutableOperation((tx)=>{
returnbackend.deleteAlias(tx,options);
});
},
updateData:function(_options){
// NOTE: This is a semantically self-describing convenience wrapper for `storeItem` that updates the currently-being-processed item
let[options]=validateArguments(arguments,{
options:[required,wrapValueAsOption("update"),{
id:[defaultTo(item.id),isString],
update:[required,isFunction]
}]
});
returnexposedAPI.storeItem({
...options,
tags:[]
});
},
updateMetadata:function(_options){
let[options]=validateArguments(arguments,{
options:[required,wrapValueAsOption("update"),{
id:[defaultTo(item.id),isString],
update:[required,isFunction],
task:[required,isTask]
}]
});
returnmutableOperation((tx)=>{
returnbackend.updateMetadata(tx,options);
});
},
expire:function(_options){
// TODO: It probably doesn't make any semantic sense to leave *both* arguments unspecified. Maybe that should be prohibited via eg. a non-exclusive requireEither? Otherwise the user might expect to immediately expire the *current* task, but since the task is only updated *after* the task logic runs, that is not currently possible to express.
let[options]=validateArguments(arguments,{
options:[required,{
id:[defaultTo(item.id),isString],
isTask:[defaultTo(task),isTask]
}]
});
returnmutableOperation((tx)=>{
returnbackend.expire(tx,options);
});
},
expireDependents:function(_options){
// NOTE: This method does not have a counterpart in the database backend; it's a convenience abstraction over regular `backend.expire` calls