UNPKG

@digicms/cms

Version:

An open source headless CMS solution to create and manage your own API. It provides a powerful dashboard and features to make your life easier. Databases supported: MySQL, MariaDB, PostgreSQL, SQLite

346 lines (261 loc) 10.1 kB
'use strict'; const _ = require('lodash'); const delegate = require('delegates'); const { InvalidTimeError, InvalidDateError, InvalidDateTimeError, InvalidRelationError } = require('@strapi/database').errors; const { webhook: webhookUtils, contentTypes: contentTypesUtils, sanitize, } = require('@strapi/utils'); const { ValidationError } = require('@strapi/utils').errors; const { isAnyToMany } = require('@strapi/utils').relations; const { transformParamsToQuery } = require('@strapi/utils').convertQueryParams; const uploadFiles = require('../utils/upload-files'); const { omitComponentData, getComponents, createComponents, updateComponents, deleteComponents, } = require('./components'); const { pickSelectionParams } = require('./params'); const { applyTransforms } = require('./attributes'); const transformLoadParamsToQuery = (uid, field, params = {}, pagination = {}) => { return { ...transformParamsToQuery(uid, { populate: { [field]: params } }).populate[field], ...pagination, }; }; // TODO: those should be strapi events used by the webhooks not the other way arround const { ENTRY_CREATE, ENTRY_UPDATE, ENTRY_DELETE } = webhookUtils.webhookEvents; const databaseErrorsToTransform = [ InvalidTimeError, InvalidDateTimeError, InvalidDateError, InvalidRelationError, ]; const creationPipeline = (data, context) => { return applyTransforms(data, context); }; const updatePipeline = (data, context) => { return applyTransforms(data, context); }; /** * @type {import('.').default} */ const createDefaultImplementation = ({ strapi, db, eventHub, entityValidator }) => ({ uploadFiles, async wrapParams(options = {}) { return options; }, async emitEvent(uid, event, entity) { // Ignore audit log events to prevent infinite loops if (uid === 'admin::audit-log') { return; } const model = strapi.getModel(uid); const sanitizedEntity = await sanitize.sanitizers.defaultSanitizeOutput(model, entity); eventHub.emit(event, { model: model.modelName, uid: model.uid, entry: sanitizedEntity, }); }, async findMany(uid, opts) { const { kind } = strapi.getModel(uid); const wrappedParams = await this.wrapParams(opts, { uid, action: 'findMany' }); const query = transformParamsToQuery(uid, wrappedParams); if (kind === 'singleType') { return db.query(uid).findOne(query); } return db.query(uid).findMany(query); }, async findPage(uid, opts) { const wrappedParams = await this.wrapParams(opts, { uid, action: 'findPage' }); const query = transformParamsToQuery(uid, wrappedParams); return db.query(uid).findPage(query); }, // TODO: streamline the logic based on the populate option async findWithRelationCountsPage(uid, opts) { const wrappedParams = await this.wrapParams(opts, { uid, action: 'findWithRelationCounts' }); const query = transformParamsToQuery(uid, wrappedParams); return db.query(uid).findPage(query); }, async findWithRelationCounts(uid, opts) { const wrappedParams = await this.wrapParams(opts, { uid, action: 'findWithRelationCounts' }); const query = transformParamsToQuery(uid, wrappedParams); return db.query(uid).findMany(query); }, async findOne(uid, entityId, opts) { const wrappedParams = await this.wrapParams(opts, { uid, action: 'findOne' }); const query = transformParamsToQuery(uid, pickSelectionParams(wrappedParams)); return db.query(uid).findOne({ ...query, where: { id: entityId } }); }, async count(uid, opts) { const wrappedParams = await this.wrapParams(opts, { uid, action: 'count' }); const query = transformParamsToQuery(uid, wrappedParams); return db.query(uid).count(query); }, async create(uid, opts) { const wrappedParams = await this.wrapParams(opts, { uid, action: 'create' }); const { data, files } = wrappedParams; const model = strapi.getModel(uid); const isDraft = contentTypesUtils.isDraft(data, model); const validData = await entityValidator.validateEntityCreation(model, data, { isDraft }); // select / populate const query = transformParamsToQuery(uid, pickSelectionParams(wrappedParams)); // TODO: wrap into transaction const componentData = await createComponents(uid, validData); const entityData = creationPipeline( Object.assign(omitComponentData(model, validData), componentData), { contentType: model, } ); let entity = await db.query(uid).create({ ...query, data: entityData, }); // TODO: upload the files then set the links in the entity like with compo to avoid making too many queries if (files && Object.keys(files).length > 0) { await this.uploadFiles(uid, Object.assign(entityData, entity), files); entity = await this.findOne(uid, entity.id, wrappedParams); } await this.emitEvent(uid, ENTRY_CREATE, entity); return entity; }, async update(uid, entityId, opts) { const wrappedParams = await this.wrapParams(opts, { uid, action: 'update' }); const { data, files } = wrappedParams; const model = strapi.getModel(uid); const entityToUpdate = await db.query(uid).findOne({ where: { id: entityId } }); if (!entityToUpdate) { return null; } const isDraft = contentTypesUtils.isDraft(entityToUpdate, model); const validData = await entityValidator.validateEntityUpdate( model, data, { isDraft, }, entityToUpdate ); const query = transformParamsToQuery(uid, pickSelectionParams(wrappedParams)); // TODO: wrap in transaction const componentData = await updateComponents(uid, entityToUpdate, validData); const entityData = updatePipeline( Object.assign(omitComponentData(model, validData), componentData), { contentType: model, } ); let entity = await db.query(uid).update({ ...query, where: { id: entityId }, data: entityData, }); // TODO: upload the files then set the links in the entity like with compo to avoid making too many queries if (files && Object.keys(files).length > 0) { await this.uploadFiles(uid, Object.assign(entityData, entity), files); entity = await this.findOne(uid, entity.id, wrappedParams); } await this.emitEvent(uid, ENTRY_UPDATE, entity); return entity; }, async delete(uid, entityId, opts) { const wrappedParams = await this.wrapParams(opts, { uid, action: 'delete' }); // select / populate const query = transformParamsToQuery(uid, pickSelectionParams(wrappedParams)); const entityToDelete = await db.query(uid).findOne({ ...query, where: { id: entityId }, }); if (!entityToDelete) { return null; } const componentsToDelete = await getComponents(uid, entityToDelete); await db.query(uid).delete({ where: { id: entityToDelete.id } }); await deleteComponents(uid, componentsToDelete, { loadComponents: false }); await this.emitEvent(uid, ENTRY_DELETE, entityToDelete); return entityToDelete; }, // FIXME: used only for the CM to be removed async deleteMany(uid, opts) { const wrappedParams = await this.wrapParams(opts, { uid, action: 'delete' }); // select / populate const query = transformParamsToQuery(uid, wrappedParams); const entitiesToDelete = await db.query(uid).findMany(query); if (!entitiesToDelete.length) { return null; } const componentsToDelete = await Promise.all( entitiesToDelete.map((entityToDelete) => getComponents(uid, entityToDelete)) ); const deletedEntities = await db.query(uid).deleteMany(query); await Promise.all( componentsToDelete.map((compos) => deleteComponents(uid, compos, { loadComponents: false })) ); // Trigger webhooks. One for each entity await Promise.all(entitiesToDelete.map((entity) => this.emitEvent(uid, ENTRY_DELETE, entity))); return deletedEntities; }, load(uid, entity, field, params = {}) { if (!_.isString(field)) { throw new Error(`Invalid load. Expected "${field}" to be a string`); } return db.query(uid).load(entity, field, transformLoadParamsToQuery(uid, field, params)); }, loadPages(uid, entity, field, params = {}, pagination = {}) { if (!_.isString(field)) { throw new Error(`Invalid load. Expected "${field}" to be a string`); } const { attributes } = strapi.getModel(uid); const attribute = attributes[field]; if (!isAnyToMany(attribute)) { throw new Error(`Invalid load. Expected "${field}" to be an anyToMany relational attribute`); } const query = transformLoadParamsToQuery(uid, field, params, pagination); return db.query(uid).loadPages(entity, field, query); }, }); module.exports = (ctx) => { const implementation = createDefaultImplementation(ctx); const service = { implementation, decorate(decorator) { if (typeof decorator !== 'function') { throw new Error(`Decorator must be a function, received ${typeof decorator}`); } this.implementation = { ...this.implementation, ...decorator(this.implementation) }; return this; }, }; const delegator = delegate(service, 'implementation'); // delegate every method in implementation Object.keys(service.implementation).forEach((key) => delegator.method(key)); // wrap methods to handle Database Errors service.decorate((oldService) => { const newService = _.mapValues( oldService, (method, methodName) => async function (...args) { try { return await oldService[methodName].call(this, ...args); } catch (error) { if ( databaseErrorsToTransform.some( (errorToTransform) => error instanceof errorToTransform ) ) { throw new ValidationError(error.message); } throw error; } } ); return newService; }); return service; };