UNPKG

@serafin/pipeline

Version:

CRUD data access library with a functional approach

316 lines 14.4 kB
"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