From f4507e1cb0ffc19cdc920d4041360ba32dcc55c8 Mon Sep 17 00:00:00 2001 From: Alexandre Bodin Date: Mon, 8 Nov 2021 16:05:19 +0100 Subject: [PATCH] Init factories for controllers --- .../src/api/address/controllers/address.js | 17 ++++ .../src/api/address/services/address.js | 12 +++ .../src/api/temp/controllers/temp.js | 15 ---- .../core-api/controller/collection-type.js | 31 ++++---- .../strapi/lib/core-api/controller/index.js | 22 ++++-- .../lib/core-api/controller/single-type.js | 21 ++--- packages/core/strapi/lib/core-api/index.js | 15 ++-- .../lib/core-api/service/collection-type.js | 12 ++- .../core/strapi/lib/core-api/service/index.js | 21 +++-- .../lib/core-api/service/single-type.js | 7 +- .../core/strapi/lib/core/registries/apis.js | 30 +++++--- .../lib/core/registries/controllers.d.ts | 7 ++ .../strapi/lib/core/registries/controllers.js | 77 ++++++++++++++++++- .../strapi/lib/core/registries/services.js | 21 +++-- packages/core/strapi/lib/factories.js | 48 ++++++++++++ packages/core/strapi/lib/index.js | 6 +- .../plugins/i18n/server/services/core-api.js | 5 +- .../server/services/users-permissions.js | 4 + 18 files changed, 260 insertions(+), 111 deletions(-) create mode 100644 examples/getstarted/src/api/address/controllers/address.js create mode 100644 examples/getstarted/src/api/address/services/address.js delete mode 100644 examples/getstarted/src/api/temp/controllers/temp.js create mode 100644 packages/core/strapi/lib/core/registries/controllers.d.ts create mode 100644 packages/core/strapi/lib/factories.js diff --git a/examples/getstarted/src/api/address/controllers/address.js b/examples/getstarted/src/api/address/controllers/address.js new file mode 100644 index 0000000000..1732f1e13d --- /dev/null +++ b/examples/getstarted/src/api/address/controllers/address.js @@ -0,0 +1,17 @@ +const { createCoreController } = require('@strapi/strapi').factories; + +module.exports = createCoreController('api::address.address', ({ strapi }) => ({ + async find(ctx) { + const { query } = ctx; + + const { results, pagination } = await strapi.service(uid).find(query); + const sanitizedResults = await this.sanitizeOutput(results, ctx); + + return this.transformResponse(sanitizedResults, { pagination }); + }, + + async findOne(ctx) { + // use the parent controller + return Object.getPrototypeOf(this).findOne(ctx); + }, +})); diff --git a/examples/getstarted/src/api/address/services/address.js b/examples/getstarted/src/api/address/services/address.js new file mode 100644 index 0000000000..e323efcbfe --- /dev/null +++ b/examples/getstarted/src/api/address/services/address.js @@ -0,0 +1,12 @@ +const { createCoreService } = require('@strapi/strapi').factories; + +module.exports = createCoreService('api::address.address', { + find() { + return { + results: [], + pagination: { + foo: 'bar', + }, + }; + }, +}); diff --git a/examples/getstarted/src/api/temp/controllers/temp.js b/examples/getstarted/src/api/temp/controllers/temp.js deleted file mode 100644 index 0fcb4071e7..0000000000 --- a/examples/getstarted/src/api/temp/controllers/temp.js +++ /dev/null @@ -1,15 +0,0 @@ -'use strict'; - -/** - * A set of functions called "actions" for `temp` - */ - -module.exports = { - // exampleAction: async (ctx, next) => { - // try { - // ctx.body = 'ok'; - // } catch (err) { - // ctx.body = err; - // } - // } -}; diff --git a/packages/core/strapi/lib/core-api/controller/collection-type.js b/packages/core/strapi/lib/core-api/controller/collection-type.js index 8f6aac1157..13fd91e24b 100644 --- a/packages/core/strapi/lib/core-api/controller/collection-type.js +++ b/packages/core/strapi/lib/core-api/controller/collection-type.js @@ -9,12 +9,7 @@ const { parseBody } = require('./transform'); * * Returns a collection type controller to handle default core-api actions */ -const createCollectionTypeController = ({ - service, - sanitizeInput, - sanitizeOutput, - transformResponse, -}) => { +const createCollectionTypeController = ({ service }) => { return { /** * Retrieve records. @@ -25,9 +20,9 @@ const createCollectionTypeController = ({ const { query } = ctx; const { results, pagination } = await service.find(query); - const sanitizedResults = await sanitizeOutput(results, ctx); + const sanitizedResults = await this.sanitizeOutput(results, ctx); - return transformResponse(sanitizedResults, { pagination }); + return this.transformResponse(sanitizedResults, { pagination }); }, /** @@ -40,9 +35,9 @@ const createCollectionTypeController = ({ const { query } = ctx; const entity = await service.findOne(id, query); - const sanitizedEntity = await sanitizeOutput(entity, ctx); + const sanitizedEntity = await this.sanitizeOutput(entity, ctx); - return transformResponse(sanitizedEntity); + return this.transformResponse(sanitizedEntity); }, /** @@ -59,12 +54,12 @@ const createCollectionTypeController = ({ throw new ValidationError('Missing "data" payload in the request body'); } - const sanitizedInputData = await sanitizeInput(data, ctx); + const sanitizedInputData = await this.sanitizeInput(data, ctx); const entity = await service.create({ ...query, data: sanitizedInputData, files }); - const sanitizedEntity = await sanitizeOutput(entity, ctx); + const sanitizedEntity = await this.sanitizeOutput(entity, ctx); - return transformResponse(sanitizedEntity); + return this.transformResponse(sanitizedEntity); }, /** @@ -82,12 +77,12 @@ const createCollectionTypeController = ({ throw new ValidationError('Missing "data" payload in the request body'); } - const sanitizedInputData = await sanitizeInput(data, ctx); + const sanitizedInputData = await this.sanitizeInput(data, ctx); const entity = await service.update(id, { ...query, data: sanitizedInputData, files }); - const sanitizedEntity = await sanitizeOutput(entity, ctx); + const sanitizedEntity = await this.sanitizeOutput(entity, ctx); - return transformResponse(sanitizedEntity); + return this.transformResponse(sanitizedEntity); }, /** @@ -100,9 +95,9 @@ const createCollectionTypeController = ({ const { query } = ctx; const entity = await service.delete(id, query); - const sanitizedEntity = await sanitizeOutput(entity, ctx); + const sanitizedEntity = await this.sanitizeOutput(entity, ctx); - return transformResponse(sanitizedEntity); + return this.transformResponse(sanitizedEntity); }, }; }; diff --git a/packages/core/strapi/lib/core-api/controller/index.js b/packages/core/strapi/lib/core-api/controller/index.js index fcc63c67ff..ee0de89219 100644 --- a/packages/core/strapi/lib/core-api/controller/index.js +++ b/packages/core/strapi/lib/core-api/controller/index.js @@ -10,31 +10,37 @@ const createCollectionTypeController = require('./collection-type'); const getAuthFromKoaContext = getOr({}, 'state.auth'); -module.exports = ({ service, model }) => { +module.exports = ({ service, contentType }) => { const ctx = { - model, + contentType, service, + }; + const proto = { transformResponse(data, meta) { - return transformResponse(data, meta, { contentType: model }); + return transformResponse(data, meta, { contentType }); }, sanitizeOutput(data, ctx) { const auth = getAuthFromKoaContext(ctx); - return sanitize.contentAPI.output(data, strapi.getModel(model.uid), { auth }); + return sanitize.contentAPI.output(data, contentType, { auth }); }, sanitizeInput(data, ctx) { const auth = getAuthFromKoaContext(ctx); - return sanitize.contentAPI.input(data, strapi.getModel(model.uid), { auth }); + return sanitize.contentAPI.input(data, contentType, { auth }); }, }; - if (contentTypes.isSingleType(model)) { - return createSingleTypeController(ctx); + let ctrl; + + if (contentTypes.isSingleType(contentType)) { + ctrl = createSingleTypeController(ctx); + } else { + ctrl = createCollectionTypeController(ctx); } - return createCollectionTypeController(ctx); + return Object.assign(Object.create(proto), ctrl); }; diff --git a/packages/core/strapi/lib/core-api/controller/single-type.js b/packages/core/strapi/lib/core-api/controller/single-type.js index 9e6bdf9597..77f3a37ca2 100644 --- a/packages/core/strapi/lib/core-api/controller/single-type.js +++ b/packages/core/strapi/lib/core-api/controller/single-type.js @@ -8,12 +8,7 @@ const { parseBody } = require('./transform'); /** * Returns a single type controller to handle default core-api actions */ -const createSingleTypeController = ({ - service, - sanitizeInput, - sanitizeOutput, - transformResponse, -}) => { +const createSingleTypeController = ({ service }) => { return { /** * Retrieve single type content @@ -24,9 +19,9 @@ const createSingleTypeController = ({ const { query } = ctx; const entity = await service.find(query); - const sanitizedEntity = await sanitizeOutput(entity, ctx); + const sanitizedEntity = await this.sanitizeOutput(entity, ctx); - return transformResponse(sanitizedEntity); + return this.transformResponse(sanitizedEntity); }, /** @@ -42,21 +37,21 @@ const createSingleTypeController = ({ throw new ValidationError('Missing "data" payload in the request body'); } - const sanitizedInputData = await sanitizeInput(data, ctx); + const sanitizedInputData = await this.sanitizeInput(data, ctx); const entity = await service.createOrUpdate({ ...query, data: sanitizedInputData, files }); - const sanitizedEntity = await sanitizeOutput(entity, ctx); + const sanitizedEntity = await this.sanitizeOutput(entity, ctx); - return transformResponse(sanitizedEntity); + return this.transformResponse(sanitizedEntity); }, async delete(ctx) { const { query } = ctx; const entity = await service.delete(query); - const sanitizedEntity = await sanitizeOutput(entity, ctx); + const sanitizedEntity = await this.sanitizeOutput(entity, ctx); - return transformResponse(sanitizedEntity); + return this.transformResponse(sanitizedEntity); }, }; }; diff --git a/packages/core/strapi/lib/core-api/index.js b/packages/core/strapi/lib/core-api/index.js index 9413bbb43a..807116e14b 100644 --- a/packages/core/strapi/lib/core-api/index.js +++ b/packages/core/strapi/lib/core-api/index.js @@ -3,8 +3,6 @@ */ 'use strict'; -const _ = require('lodash'); - const createController = require('./controller'); const { createService } = require('./service'); @@ -17,16 +15,15 @@ const { createService } = require('./service'); * @param {object} opts.strapi strapi * @returns {object} controller & service */ -function createCoreApi({ api, model, strapi }) { - const { modelName } = model; +function createCoreApi({ api, contentType, strapi }) { + const { modelName } = contentType; // find corresponding service and controller - const userService = _.get(api, ['services', modelName], {}); - const userController = _.get(api, ['controllers', modelName], {}); + const userService = api.service(modelName); + const userController = api.controller(modelName); - const service = Object.assign(createService({ model, strapi }), userService); - - const controller = Object.assign(createController({ service, model }), userController); + const service = Object.assign(createService({ contentType, strapi }), userService); + const controller = Object.assign(createController({ service, contentType }), userController); return { service, diff --git a/packages/core/strapi/lib/core-api/service/collection-type.js b/packages/core/strapi/lib/core-api/service/collection-type.js index e2d478d56f..5ddca854cf 100644 --- a/packages/core/strapi/lib/core-api/service/collection-type.js +++ b/packages/core/strapi/lib/core-api/service/collection-type.js @@ -22,14 +22,12 @@ const setPublishedAt = data => { * * Returns a collection type service to handle default core-api actions */ -const createCollectionTypeService = ({ model, strapi, utils }) => { - const { uid } = model; - - const { getFetchParams } = utils; +const createCollectionTypeService = ({ contentType, strapi }) => { + const { uid } = contentType; return { async find(params = {}) { - const fetchParams = getFetchParams(params); + const fetchParams = this.getFetchParams(params); const paginationInfo = getPaginationInfo(fetchParams); @@ -54,13 +52,13 @@ const createCollectionTypeService = ({ model, strapi, utils }) => { }, findOne(entityId, params = {}) { - return strapi.entityService.findOne(uid, entityId, getFetchParams(params)); + return strapi.entityService.findOne(uid, entityId, this.getFetchParams(params)); }, create(params = {}) { const { data } = params; - if (hasDraftAndPublish(model)) { + if (hasDraftAndPublish(contentType)) { setPublishedAt(data); } diff --git a/packages/core/strapi/lib/core-api/service/index.js b/packages/core/strapi/lib/core-api/service/index.js index f0dbbe2463..170c958394 100644 --- a/packages/core/strapi/lib/core-api/service/index.js +++ b/packages/core/strapi/lib/core-api/service/index.js @@ -13,14 +13,18 @@ const createCollectionTypeService = require('./collection-type'); * @param {{ model: object, strapi: object }} context * @returns {object} */ -const createService = ({ model, strapi }) => { - const utils = createUtils({ model }); +const createService = ({ contentType, strapi }) => { + const proto = { getFetchParams }; - if (isSingleType(model)) { - return createSingleTypeService({ model, strapi, utils }); + let service; + + if (isSingleType(contentType)) { + service = createSingleTypeService({ contentType, strapi }); + } else { + service = createCollectionTypeService({ contentType, strapi }); } - return createCollectionTypeService({ model, strapi, utils }); + return Object.assign(Object.create(proto), service); }; /** @@ -35,13 +39,6 @@ const getFetchParams = (params = {}) => { }; }; -/** - * Mixins - */ -const createUtils = () => ({ - getFetchParams, -}); - module.exports = { createService, getFetchParams, diff --git a/packages/core/strapi/lib/core-api/service/single-type.js b/packages/core/strapi/lib/core-api/service/single-type.js index 92e44ffe81..56c9f6bb48 100644 --- a/packages/core/strapi/lib/core-api/service/single-type.js +++ b/packages/core/strapi/lib/core-api/service/single-type.js @@ -5,9 +5,8 @@ const { ValidationError } = require('@strapi/utils').errors; /** * Returns a single type service to handle default core-api actions */ -const createSingleTypeService = ({ model, strapi, utils }) => { - const { uid } = model; - const { getFetchParams } = utils; +const createSingleTypeService = ({ contentType, strapi }) => { + const { uid } = contentType; return { /** @@ -16,7 +15,7 @@ const createSingleTypeService = ({ model, strapi, utils }) => { * @return {Promise} */ find(params = {}) { - return strapi.entityService.findMany(uid, getFetchParams(params)); + return strapi.entityService.findMany(uid, this.getFetchParams(params)); }, /** diff --git a/packages/core/strapi/lib/core/registries/apis.js b/packages/core/strapi/lib/core/registries/apis.js index 806c5a64aa..9de2488508 100644 --- a/packages/core/strapi/lib/core/registries/apis.js +++ b/packages/core/strapi/lib/core/registries/apis.js @@ -1,7 +1,8 @@ 'use strict'; const { has } = require('lodash/fp'); -const { createCoreApi } = require('../../core-api'); +const { createService } = require('../../core-api/service'); +const createController = require('../../core-api/controller'); const apisRegistry = strapi => { const apis = {}; @@ -18,22 +19,27 @@ const apisRegistry = strapi => { throw new Error(`API ${apiName} has already been registered.`); } - const apiInstance = strapi.container.get('modules').add(`api::${apiName}`, apiConfig); + const api = strapi.container.get('modules').add(`api::${apiName}`, apiConfig); - for (const ctName in apiInstance.contentTypes || {}) { - const contentType = apiInstance.contentTypes[ctName]; + for (const ctName in api.contentTypes || {}) { + const contentType = api.contentTypes[ctName]; - const { service, controller } = createCoreApi({ - model: contentType, - api: apiInstance, - strapi, - }); + const uid = `api::${apiName}.${ctName}`; - strapi.container.get('services').set(`api::${apiName}.${ctName}`, service); - strapi.container.get('controllers').set(`api::${apiName}.${ctName}`, controller); + if (!has(contentType.modelName, api.services)) { + const service = createService({ contentType, strapi }); + strapi.container.get('services').set(uid, service); + } + + if (!has(contentType.modelName, api.controllers)) { + const service = strapi.container.get('services').get(uid); + + const controller = createController({ contentType, service }); + strapi.container.get('controllers').set(uid, controller); + } } - apis[apiName] = apiInstance; + apis[apiName] = api; return apis[apiName]; }, diff --git a/packages/core/strapi/lib/core/registries/controllers.d.ts b/packages/core/strapi/lib/core/registries/controllers.d.ts new file mode 100644 index 0000000000..cb28faf128 --- /dev/null +++ b/packages/core/strapi/lib/core/registries/controllers.d.ts @@ -0,0 +1,7 @@ +import { Strapi } from '../../'; + +export type Controller = { + [key: string]: (...args: any) => any; +}; + +export type ControllerFactory = ({ strapi: Strapi }) => Controller; diff --git a/packages/core/strapi/lib/core/registries/controllers.js b/packages/core/strapi/lib/core/registries/controllers.js index 33ea9b99d8..47d965c1ae 100644 --- a/packages/core/strapi/lib/core/registries/controllers.js +++ b/packages/core/strapi/lib/core/registries/controllers.js @@ -3,20 +3,80 @@ const { pickBy, has } = require('lodash/fp'); const { addNamespace, hasNamespace } = require('../utils'); +/** + * @typedef {import('./controllers').Controller} Controller + * @typedef {import('./controllers').ControllerFactory} ControllerFactory + */ + const controllersRegistry = () => { const controllers = {}; + const instances = {}; return { + /** + * Returns this list of registered controllers uids + * @returns {string[]} + */ + keys() { + return Object.keys(controllers); + }, + + /** + * Returns the instance of a controller. Instantiate the controller if not already done + * @param {string} uid + * @returns {Controller} + */ get(uid) { - return controllers[uid]; + if (instances[uid]) { + return instances[uid]; + } + + const controller = controllers[uid]; + + if (controller) { + instances[uid] = typeof controller === 'function' ? controller({ strapi }) : controller; + return instances[uid]; + } }, + + /** + * Returns a map with all the controller in a namespace + * @param {string} namespace + * @returns {{ [key: string]: Controller }} + */ getAll(namespace) { - return pickBy((_, uid) => hasNamespace(uid, namespace))(controllers); + const filteredControllers = pickBy((_, uid) => hasNamespace(uid, namespace))(controllers); + + const map = {}; + for (const uid in filteredControllers) { + Object.defineProperty(map, uid, { + enumerable: true, + get: () => { + return this.get(uid); + }, + }); + } + + return map; }, + + /** + * Registers a controller + * @param {string} uid + * @param {Controller} controller + */ set(uid, value) { controllers[uid] = value; + delete instances[uid]; return this; }, + + /** + * Registers a map of controllers for a specific namespace + * @param {string} namespace + * @param {{ [key: string]: Controller|ControllerFactory }} newControllers + * @returns + */ add(namespace, newControllers) { for (const controllerName in newControllers) { const controller = newControllers[controllerName]; @@ -27,15 +87,26 @@ const controllersRegistry = () => { } controllers[uid] = controller; } + return this; }, + + /** + * Wraps a controller to extend it + * @param {string} uid + * @param {(controller: Controller) => Controller} extendFn + */ extend(controllerUID, extendFn) { const currentController = this.get(controllerUID); + if (!currentController) { throw new Error(`Controller ${controllerUID} doesn't exist`); } + const newController = extendFn(currentController); - controllers[controllerUID] = newController; + instances[controllerUID] = newController; + + return this; }, }; }; diff --git a/packages/core/strapi/lib/core/registries/services.js b/packages/core/strapi/lib/core/registries/services.js index 6721b2ec9f..d3427d3c54 100644 --- a/packages/core/strapi/lib/core/registries/services.js +++ b/packages/core/strapi/lib/core/registries/services.js @@ -1,6 +1,5 @@ 'use strict'; -const _ = require('lodash'); const { pickBy, has } = require('lodash/fp'); const { addNamespace, hasNamespace } = require('../utils'); @@ -37,8 +36,6 @@ const servicesRegistry = strapi => { instantiatedServices[uid] = typeof service === 'function' ? service({ strapi }) : service; return instantiatedServices[uid]; } - - return undefined; }, /** @@ -49,16 +46,28 @@ const servicesRegistry = strapi => { getAll(namespace) { const filteredServices = pickBy((_, uid) => hasNamespace(uid, namespace))(services); - return _.mapValues(filteredServices, (service, serviceUID) => this.get(serviceUID)); + // create lazy accessor to avoid instantiating the services; + const map = {}; + for (const uid in filteredServices) { + Object.defineProperty(map, uid, { + enumerable: true, + get: () => { + return this.get(uid); + }, + }); + } + + return map; }, /** * Registers a service * @param {string} uid - * @param {Service|ServiceFactory} service + * @param {Service} service */ set(uid, service) { - instantiatedServices[uid] = service; + services[uid] = service; + delete instantiatedServices[uid]; return this; }, diff --git a/packages/core/strapi/lib/factories.js b/packages/core/strapi/lib/factories.js new file mode 100644 index 0000000000..47a7a3368e --- /dev/null +++ b/packages/core/strapi/lib/factories.js @@ -0,0 +1,48 @@ +'use strict'; + +const createController = require('./core-api/controller'); +const { createService } = require('./core-api/service'); + +const createCoreController = (uid, cfg) => { + return ({ strapi }) => { + const deps = { + service: strapi.service(uid), + contentType: strapi.contentType(uid), + }; + + const baseController = createController(deps); + + let userCtrl = typeof cfg === 'function' ? cfg({ strapi }) : cfg; + + // TODO: can only extend the defined action so we can add some without creating breaking + + return Object.assign( + Object.create(baseController), + { + get coreController() { + return baseController; + }, + }, + userCtrl + ); + }; +}; + +const createCoreService = (uid, cfg) => { + return ({ strapi }) => { + const deps = { + contentType: strapi.contentType(uid), + }; + + const baseService = createService(deps); + + let userCtrl = typeof cfg === 'function' ? cfg({ strapi }) : cfg; + + return Object.assign(baseService, userCtrl); + }; +}; + +module.exports = { + createCoreController, + createCoreService, +}; diff --git a/packages/core/strapi/lib/index.js b/packages/core/strapi/lib/index.js index 6d63701c3c..05a8d860d6 100644 --- a/packages/core/strapi/lib/index.js +++ b/packages/core/strapi/lib/index.js @@ -1,3 +1,7 @@ 'use strict'; -module.exports = require('./Strapi'); +const Strapi = require('./Strapi'); + +Strapi.factories = require('./factories'); + +module.exports = Strapi; diff --git a/packages/plugins/i18n/server/services/core-api.js b/packages/plugins/i18n/server/services/core-api.js index 682156af32..831a331e4c 100644 --- a/packages/plugins/i18n/server/services/core-api.js +++ b/packages/plugins/i18n/server/services/core-api.js @@ -182,10 +182,9 @@ const addCreateLocalizationAction = contentType => { strapi.api[apiName].routes[modelName].routes.push(localizationRoute); strapi.container.get('controllers').extend(`api::${apiName}.${modelName}`, controller => { - return { - ...controller, + return Object.assign(controller, { createLocalization: createLocalizationHandler(contentType), - }; + }); }); }; diff --git a/packages/plugins/users-permissions/server/services/users-permissions.js b/packages/plugins/users-permissions/server/services/users-permissions.js index 3a82d9f58b..e97b614366 100644 --- a/packages/plugins/users-permissions/server/services/users-permissions.js +++ b/packages/plugins/users-permissions/server/services/users-permissions.js @@ -40,9 +40,13 @@ module.exports = ({ strapi }) => ({ }; _.forEach(strapi.api, (api, apiName) => { + console.log(api.controllers); + const controllers = _.reduce( api.controllers, (acc, controller, controllerName) => { + console.log(controllerName, controller); + const contentApiActions = _.pickBy(controller, isContentApi); if (_.isEmpty(contentApiActions)) {