@serafin/pipeline
Version:
CRUD data access library with a functional approach
316 lines • 14.4 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.PipelineAbstract = exports.defaultPipelineAbstractOptions = void 0;
const _ = __importStar(require("lodash"));
const schema_builder_1 = require("@serafin/schema-builder");
const error_1 = require("./error");
const SchemaBuildersInterface_1 = require("./SchemaBuildersInterface");
const Relation_1 = require("./Relation");
const PipelineInterface_1 = require("./PipelineInterface");
const RelationType_1 = require("./RelationType");
exports.defaultPipelineAbstractOptions = { validationEnabled: true };
class PipelineAbstract {
constructor(schemaBuilders, options) {
this.schemaBuilders = schemaBuilders;
this.relations = {};
this.pipes = [];
this.options = { ...exports.defaultPipelineAbstractOptions, ...options };
}
get modelSchemaBuilder() {
return this.schemaBuilders.model;
}
pipe(pipe) {
// run the pipe
const result = typeof pipe === "function"
? pipe({
...this.schemaBuilders,
})
: pipe.transform({
...this.schemaBuilders,
});
// combine schema modifications with the current schemas
const newPipeline = this.clone();
const modifiedSchemas = _.pick(result, SchemaBuildersInterface_1.schemaBuildersInterfaceKeys);
if (Object.keys(modifiedSchemas).length > 0) {
newPipeline.schemaBuilders = {
...this.schemaBuilders,
...modifiedSchemas,
};
}
// add pipe methods modifications to the pipeline if it implements at least one of the CRUD methods
const modifiedMethods = _.pickBy(result, (v, k) => !!v && PipelineInterface_1.pipelineMethods.includes(k));
if (Object.keys(modifiedMethods).length > 0) {
newPipeline.pipes = [
_.mapValues(modifiedMethods, (value, key) => (...props) => value(...props, this)),
...this.pipes,
];
}
return newPipeline;
}
/**
* Build a recursive function that will call all the pipes for a CRUD method
*/
pipeChain(method) {
const callChain = async (i, ...args) => {
while (i < this.pipes.length && !this.pipes[i][method]) {
++i;
}
if (i >= this.pipes.length) {
return this[`_${method}`](...args);
}
else {
return this.pipes[i++][method]((...args) => callChain(i, ...args), ...args);
}
};
return async (...args) => callChain(0, ...args);
}
/**
* Add a many relation to the pipeline.
* A query parameter is added to the all actions to conditionally fetch the related entities.
*/
addRelationWithMany(name, pipeline, query) {
const relationSchema = schema_builder_1.SchemaBuilder.arraySchema(pipeline.schemaBuilders.model);
const relationOption = `with${_.upperFirst(name)}`;
const relationOptionSchema = schema_builder_1.SchemaBuilder.booleanSchema({ description: `If set to 'true', the result will include the property '${name}'` });
const relation = new Relation_1.Relation(this, name, pipeline, query, RelationType_1.RelationType.many);
const newPipeline = this.pipe((p) => {
const model = p.model.addProperty(name, relationSchema, false);
const readQuery = p.readQuery.addProperty(relationOption, relationOptionSchema, false);
const createOptions = p.createOptions.addProperty(relationOption, relationOptionSchema, false);
const patchQuery = p.patchQuery.addProperty(relationOption, relationOptionSchema, false);
const deleteQuery = p.deleteQuery.addProperty(relationOption, relationOptionSchema, false);
return {
model,
readQuery,
createOptions,
patchQuery,
deleteQuery,
create: async (next, resources, options, context) => {
const result = await next(resources, options, context);
return { ...result, data: await relation.assignToResources(result.data) };
},
read: async (next, query, context) => {
const result = await next(query, context);
return { ...result, data: await relation.assignToResources(result.data) };
},
patch: async (next, query, values, context) => {
const result = await next(query, values, context);
return { ...result, data: await relation.assignToResources(result.data) };
},
delete: async (next, query, context) => {
const result = await next(query, context);
return { ...result, data: await relation.assignToResources(result.data) };
},
};
});
newPipeline.relations[name] = relation;
return newPipeline;
}
/**
* Add a one relation to the pipeline.
* A query parameter is added to the all actions to conditionally fetch the related entity.
*/
addRelationWithOne(name, pipeline, query) {
const relationSchema = pipeline.schemaBuilders.model;
const relationOption = `with${_.upperFirst(name)}`;
const relationOptionSchema = schema_builder_1.SchemaBuilder.booleanSchema({ description: `If set to 'true', the result will include the property '${name}'` });
const relation = new Relation_1.Relation(this, name, pipeline, query, RelationType_1.RelationType.one);
const newPipeline = this.pipe((p) => {
const model = p.model.addProperty(name, relationSchema, false);
const readQuery = p.readQuery.addProperty(relationOption, relationOptionSchema, false);
const createOptions = p.createOptions.addProperty(relationOption, relationOptionSchema, false);
const patchQuery = p.patchQuery.addProperty(relationOption, relationOptionSchema, false);
const deleteQuery = p.deleteQuery.addProperty(relationOption, relationOptionSchema, false);
return {
model,
readQuery,
createOptions,
patchQuery,
deleteQuery,
create: async (next, resources, options, context) => {
const result = await next(resources, options, context);
return { ...result, data: await relation.assignToResources(result.data) };
},
read: async (next, query, context) => {
const result = await next(query, context);
return { ...result, data: await relation.assignToResources(result.data) };
},
patch: async (next, query, values, context) => {
const result = await next(query, values, context);
return { ...result, data: await relation.assignToResources(result.data) };
},
delete: async (next, query, context) => {
const result = await next(query, context);
return { ...result, data: await relation.assignToResources(result.data) };
},
};
});
newPipeline.relations[name] = relation;
return newPipeline;
}
/**
* Create new resources based on `resources` input array.
*
* @param resources An array of partial resources to be created
* @param options Options that can alter the action behavior
* @param context Context object
*/
async create(resources, options, context) {
resources = _.cloneDeep(resources);
options = _.cloneDeep(options ?? {});
context = _.cloneDeep(context ?? {});
this.handleValidationOfData("create", "resources", this.schemaBuilders.createValues, resources, true);
this.handleValidationOfData("create", "options", this.schemaBuilders.createOptions, options);
this.handleValidationOfData("create", "context", this.schemaBuilders.context, context);
return this.pipeChain("create")(resources, options, context);
}
/**
* Create action placeholder
* It should be overridden by child pipeline class
*/
_create(resources, options, context) {
throw (0, error_1.notImplementedError)("create", Object.getPrototypeOf(this).constructor.name);
}
/**
* Extract a standalone create function.
* It can be used as a parameter for pipes to isolate dependencies and ease constraints definition
*/
getCreateFunction() {
return this.create.bind(this);
}
/**
* Read resources from the underlying source according to the given `query`.
*
* @param query The query filter to be used for fetching the data
* @param context Context object
*/
async read(query, context) {
query = _.cloneDeep(query);
context = _.cloneDeep(context ?? {});
this.handleValidationOfData("read", "query", this.schemaBuilders.readQuery, query);
this.handleValidationOfData("read", "context", this.schemaBuilders.context, context);
return this.pipeChain("read")(query, context);
}
/**
* Read action placeholder
* It should be overridden by child pipeline class
*/
_read(query, context) {
throw (0, error_1.notImplementedError)("read", Object.getPrototypeOf(this).constructor.name);
}
/**
* Extract a standalone read function.
* It can be used as a parameter for pipes to isolate dependencies and ease constraints definition
*/
getReadFunction() {
return this.read.bind(this);
}
/**
* Patch resources according to the given query and values.
* The `query` will select a subset of the underlying data source and `values` are updated on it.
* This method should follow the JSON merge patch standard if possible. @see https://tools.ietf.org/html/rfc7396
*
* @param query
* @param values
* @param context Context object
*/
async patch(query, values, context) {
query = _.cloneDeep(query);
values = _.cloneDeep(values);
context = _.cloneDeep(context ?? {});
this.handleValidationOfData("patch", "query", this.schemaBuilders.patchQuery, query);
this.handleValidationOfData("patch", "values", this.schemaBuilders.patchValues, values);
this.handleValidationOfData("patch", "context", this.schemaBuilders.context, context);
return this.pipeChain("patch")(query, values, context);
}
/**
* Patch action placeholder
* It should be overridden by child pipeline class
*/
_patch(query, values, context) {
throw (0, error_1.notImplementedError)("patch", Object.getPrototypeOf(this).constructor.name);
}
/**
* Extract a standalone patch function.
* It can be used as a parameter for pipes to isolate dependencies and ease constraints definition
*/
getPatchFunction() {
return this.patch.bind(this);
}
/**
* Delete resources that match th given Query.
* @param query The query filter to be used for selecting resources to delete
* @param context Context object
*/
async delete(query, context) {
query = _.cloneDeep(query);
context = _.cloneDeep(context ?? {});
this.handleValidationOfData("delete", "query", this.schemaBuilders.deleteQuery, query);
this.handleValidationOfData("delete", "context", this.schemaBuilders.context, context);
return this.pipeChain("delete")(query, context);
}
/**
* Delete action placeholder
* It should be overridden by child pipeline class
*/
_delete(query, context) {
throw (0, error_1.notImplementedError)("delete", Object.getPrototypeOf(this).constructor.name);
}
/**
* Extract a standalone delete function.
* It can be used as a parameter for pipes to isolate dependencies and ease constraints definition
*/
getDeleteFunction() {
return this.delete.bind(this);
}
handleValidationOfData(method, valueName, schema, data, isArray = false) {
if (!this.options.validationEnabled) {
return;
}
try {
if (isArray) {
schema.validateList(data);
}
else {
schema.validate(data);
}
}
catch (e) {
throw (0, error_1.error)("SerafinValidationError", `Validation failed in ${Object.getPrototypeOf(this).constructor.name}${this.options.name ? `::${this.options.name}` : ""}::${method}(${valueName})`, { constructor: Object.getPrototypeOf(this).constructor.name, method: method, schema: schema, data: data }, e);
}
}
clone() {
let clonedPipeline = _.cloneDeepWith(this, (value, key) => {
if (key === "relations" || key === "schemaBuilders" || key === "pipes") {
// shallow clone
return _.clone(value);
}
});
return clonedPipeline;
}
}
exports.PipelineAbstract = PipelineAbstract;
//# sourceMappingURL=PipelineAbstract.js.map