diff --git a/packages/core/admin/server/utils/index.js b/packages/core/admin/server/utils/index.js index 9281650d36..9c44864d0c 100644 --- a/packages/core/admin/server/utils/index.js +++ b/packages/core/admin/server/utils/index.js @@ -1,9 +1,7 @@ 'use strict'; -const { prop } = require('lodash/fp'); - const getService = name => { - return prop(`admin.services.${name}`, strapi); + return strapi.service(`admin::${name}`); }; module.exports = { diff --git a/packages/core/strapi/bin/strapi.js b/packages/core/strapi/bin/strapi.js index 7ba32b39d6..f7752440e7 100755 --- a/packages/core/strapi/bin/strapi.js +++ b/packages/core/strapi/bin/strapi.js @@ -208,4 +208,9 @@ program .description('List all the application hooks') .action(getLocalScript('hooks/list')); +program + .command('services:list') + .description('List all the application services') + .action(getLocalScript('services/list')); + program.parseAsync(process.argv); diff --git a/packages/core/strapi/lib/Strapi.js b/packages/core/strapi/lib/Strapi.js index 8a1060377a..0687baf267 100644 --- a/packages/core/strapi/lib/Strapi.js +++ b/packages/core/strapi/lib/Strapi.js @@ -70,6 +70,7 @@ class Strapi { this.startupLogger = createStartupLogger(this); this.log = createLogger(this.config.get('logger', {})); this.cron = createCronService(); + this.telemetry = createTelemetry(this); createUpdateNotifier(this).notify(); } @@ -310,7 +311,7 @@ class Strapi { this.hook('strapi::content-types.afterSync').register(draftAndPublishSync.enable); } - async load() { + async register() { await Promise.all([ this.loadApp(), this.loadPlugins(), @@ -332,8 +333,14 @@ class Strapi { this.registerInternalHooks(); + this.telemetry.register(); + await this.runLifecyclesFunctions(LIFECYCLES.REGISTER); + return this; + } + + async bootstrap() { const contentTypes = [ coreStoreModel, webhookModel, @@ -360,7 +367,7 @@ class Strapi { const cronTasks = this.config.get('server.cron.tasks', {}); this.cron.add(cronTasks); - this.telemetry = createTelemetry(this); + this.telemetry.bootstrap(); let oldContentTypes; if (await this.db.connection.schema.hasTable(coreStoreModel.collectionName)) { @@ -399,6 +406,13 @@ class Strapi { this.cron.start(); + return this; + } + + async load() { + await this.register(); + await this.bootstrap(); + this.isLoaded = true; return this; diff --git a/packages/core/strapi/lib/commands/content-types/list.js b/packages/core/strapi/lib/commands/content-types/list.js index 91ddd99805..33234775f6 100644 --- a/packages/core/strapi/lib/commands/content-types/list.js +++ b/packages/core/strapi/lib/commands/content-types/list.js @@ -6,17 +6,15 @@ const chalk = require('chalk'); const strapi = require('../../index'); module.exports = async function() { - const app = await strapi().load(); + const app = await strapi().register(); - const list = app.contentTypes; + const list = app.container.get('content-types').keys(); const infoTable = new CLITable({ head: [chalk.blue('Name')], }); - Object.keys(list).forEach(name => { - infoTable.push([name]); - }); + list.forEach(name => infoTable.push([name])); console.log(infoTable.toString()); diff --git a/packages/core/strapi/lib/commands/hooks/list.js b/packages/core/strapi/lib/commands/hooks/list.js index bbb9f6886d..7ce0e570e6 100644 --- a/packages/core/strapi/lib/commands/hooks/list.js +++ b/packages/core/strapi/lib/commands/hooks/list.js @@ -6,17 +6,15 @@ const chalk = require('chalk'); const strapi = require('../../index'); module.exports = async function() { - const app = await strapi().load(); + const app = await strapi().register(); - const list = app.hooks; + const list = app.container.get('hooks').keys(); const infoTable = new CLITable({ head: [chalk.blue('Name')], }); - Object.keys(list).forEach(name => { - infoTable.push([name]); - }); + list.forEach(name => infoTable.push([name])); console.log(infoTable.toString()); diff --git a/packages/core/strapi/lib/commands/middlewares/list.js b/packages/core/strapi/lib/commands/middlewares/list.js index ec01fe5922..7c4bd40f32 100644 --- a/packages/core/strapi/lib/commands/middlewares/list.js +++ b/packages/core/strapi/lib/commands/middlewares/list.js @@ -6,17 +6,15 @@ const chalk = require('chalk'); const strapi = require('../../index'); module.exports = async function() { - const app = await strapi().load(); + const app = await strapi().register(); - const list = app.middlewares; + const list = app.container.get('middlewares').keys(); const infoTable = new CLITable({ head: [chalk.blue('Name')], }); - Object.keys(list).forEach(name => { - infoTable.push([name]); - }); + list.forEach(name => infoTable.push([name])); console.log(infoTable.toString()); diff --git a/packages/core/strapi/lib/commands/policies/list.js b/packages/core/strapi/lib/commands/policies/list.js index 93197cc794..01e29e6e6a 100644 --- a/packages/core/strapi/lib/commands/policies/list.js +++ b/packages/core/strapi/lib/commands/policies/list.js @@ -6,17 +6,15 @@ const chalk = require('chalk'); const strapi = require('../../index'); module.exports = async function() { - const app = await strapi().load(); + const app = await strapi().register(); - const list = app.policies; + const list = app.container.get('policies').keys(); const infoTable = new CLITable({ head: [chalk.blue('Name')], }); - Object.keys(list).forEach(name => { - infoTable.push([name]); - }); + list.forEach(name => infoTable.push([name])); console.log(infoTable.toString()); diff --git a/packages/core/strapi/lib/commands/services/list.js b/packages/core/strapi/lib/commands/services/list.js new file mode 100644 index 0000000000..a5e262fa82 --- /dev/null +++ b/packages/core/strapi/lib/commands/services/list.js @@ -0,0 +1,22 @@ +'use strict'; + +const CLITable = require('cli-table3'); +const chalk = require('chalk'); + +const strapi = require('../../index'); + +module.exports = async function() { + const app = await strapi().register(); + + const list = app.container.get('services').keys(); + + const infoTable = new CLITable({ + head: [chalk.blue('Name')], + }); + + list.forEach(name => infoTable.push([name])); + + console.log(infoTable.toString()); + + await app.destroy(); +}; diff --git a/packages/core/strapi/lib/core/registries/content-types.js b/packages/core/strapi/lib/core/registries/content-types.js index 3079c67372..b24ff19db4 100644 --- a/packages/core/strapi/lib/core/registries/content-types.js +++ b/packages/core/strapi/lib/core/registries/content-types.js @@ -20,32 +20,76 @@ const contentTypesRegistry = () => { const contentTypes = {}; return { - get(ctUID) { - return contentTypes[ctUID]; + /** + * Returns this list of registered contentTypes uids + * @returns {string[]} + */ + keys() { + return Object.keys(contentTypes); }, + + /** + * Returns the instance of a contentType. Instantiate the contentType if not already done + * @param {string} uid + * @returns + */ + get(uid) { + return contentTypes[uid]; + }, + + /** + * Returns a map with all the contentTypes in a namespace + * @param {string} namespace + */ getAll(namespace) { return pickBy((_, uid) => hasNamespace(uid, namespace))(contentTypes); }, - add(namespace, rawContentTypes) { - validateKeySameToSingularName(rawContentTypes); - for (const rawCtName in rawContentTypes) { + /** + * Registers a contentType + * @param {string} uid + * @param {Object} contentType + */ + set(uid, contentType) { + contentTypes[uid] = contentType; + return this; + }, + + /** + * Registers a map of contentTypes for a specific namespace + * @param {string} namespace + * @param {{ [key: string]: Object }} newContentTypes + */ + add(namespace, newContentTypes) { + validateKeySameToSingularName(newContentTypes); + + for (const rawCtName in newContentTypes) { const uid = addNamespace(rawCtName, namespace); if (has(uid, contentTypes)) { throw new Error(`Content-type ${uid} has already been registered.`); } - contentTypes[uid] = createContentType(uid, rawContentTypes[rawCtName]); + contentTypes[uid] = createContentType(uid, newContentTypes[rawCtName]); } }, + + /** + * Wraps a contentType to extend it + * @param {string} uid + * @param {(contentType: Object) => Object} extendFn + */ extend(ctUID, extendFn) { const currentContentType = this.get(ctUID); + if (!currentContentType) { throw new Error(`Content-Type ${ctUID} doesn't exist`); } + const newContentType = extendFn(currentContentType); contentTypes[ctUID] = newContentType; + + return this; }, }; }; diff --git a/packages/core/strapi/lib/core/registries/hooks.d.ts b/packages/core/strapi/lib/core/registries/hooks.d.ts new file mode 100644 index 0000000000..db296c7e2e --- /dev/null +++ b/packages/core/strapi/lib/core/registries/hooks.d.ts @@ -0,0 +1,20 @@ + +type Handler = (context: any) => any; + +type AsyncHook = { + handlers: Handler[]; + register(handler: Handler): this; + delete(handler: Handler): this; + call(): Promise; +}; + + +type SyncHook = { + get handlers(): Handler[]; + register(handler: Handler): this; + delete(handler: Handler): this; + call(): void; +}; + + +export type Hook = AsyncHook|SyncHook diff --git a/packages/core/strapi/lib/core/registries/hooks.js b/packages/core/strapi/lib/core/registries/hooks.js index bb1bc5c185..413e431b73 100644 --- a/packages/core/strapi/lib/core/registries/hooks.js +++ b/packages/core/strapi/lib/core/registries/hooks.js @@ -1,26 +1,58 @@ 'use strict'; -const { pickBy, has } = require('lodash/fp'); +const { pickBy } = require('lodash/fp'); const { addNamespace, hasNamespace } = require('../utils'); +/** + * @typedef {import('./hooks').Hook} Hook + */ + const hooksRegistry = () => { const hooks = {}; return { - get(hookUID) { - return hooks[hookUID]; + /** + * Returns this list of registered hooks uids + * @returns {string[]} + */ + keys() { + return Object.keys(hooks); }, + + /** + * Returns the instance of a hook. + * @param {string} uid + * @returns {Hook} + */ + get(uid) { + return hooks[uid]; + }, + + /** + * Returns a map with all the hooks in a namespace + * @param {string} namespace + * @returns {{ [key: string]: Hook }} + */ getAll(namespace) { return pickBy((_, uid) => hasNamespace(uid, namespace))(hooks); }, - set(uid, hook) { - if (has(uid, hooks)) { - throw new Error(`hook ${uid} has already been registered.`); - } + /** + * Registers a hook + * @param {string} uid + * @param {Hook} hook + */ + set(uid, hook) { hooks[uid] = hook; return this; }, + + /** + * Registers a map of hooks for a specific namespace + * @param {string} namespace + * @param {{ [key: string]: Hook }} newHooks + * @returns + */ add(namespace, hooks) { for (const hookName in hooks) { const hook = hooks[hookName]; @@ -31,6 +63,24 @@ const hooksRegistry = () => { return this; }, + + /** + * Wraps a hook to extend it + * @param {string} uid + * @param {(hook: Hook) => Hook} extendFn + */ + extend(uid, extendFn) { + const currentHook = this.get(uid); + + if (!currentHook) { + throw new Error(`Hook ${uid} doesn't exist`); + } + + const newHook = extendFn(currentHook); + hooks[uid] = newHook; + + return this; + }, }; }; diff --git a/packages/core/strapi/lib/core/registries/middlewares.d.ts b/packages/core/strapi/lib/core/registries/middlewares.d.ts new file mode 100644 index 0000000000..39e0d64c2b --- /dev/null +++ b/packages/core/strapi/lib/core/registries/middlewares.d.ts @@ -0,0 +1,5 @@ +import { BaseContext, Middleware as KoaMiddleware } from 'koa'; +import { Strapi } from '../../'; +import { MiddlewareFactory } from '../../middlewares'; + +export type Middleware = KoaMiddleware | MiddlewareFactory; diff --git a/packages/core/strapi/lib/core/registries/middlewares.js b/packages/core/strapi/lib/core/registries/middlewares.js index c71056797d..584dc57f4c 100644 --- a/packages/core/strapi/lib/core/registries/middlewares.js +++ b/packages/core/strapi/lib/core/registries/middlewares.js @@ -3,16 +3,57 @@ const { pickBy, has } = require('lodash/fp'); const { addNamespace, hasNamespace } = require('../utils'); +/** + * @typedef {import('./middlewares').Middleware} Middleware + */ + +// TODO: move instantiation part here instead of in the server service const middlewaresRegistry = () => { const middlewares = {}; return { - get(middlewareUID) { - return middlewares[middlewareUID]; + /** + * Returns this list of registered middlewares uids + * @returns {string[]} + */ + keys() { + return Object.keys(middlewares); }, + + /** + * Returns the instance of a middleware. Instantiate the middleware if not already done + * @param {string} uid + * @returns {Middleware} + */ + get(uid) { + return middlewares[uid]; + }, + + /** + * Returns a map with all the middlewares in a namespace + * @param {string} namespace + * @returns {{ [key: string]: Middleware }} + */ getAll(namespace) { return pickBy((_, uid) => hasNamespace(uid, namespace))(middlewares); }, + + /** + * Registers a middleware + * @param {string} uid + * @param {Middleware} middleware + */ + set(uid, middleware) { + middlewares[uid] = middleware; + return this; + }, + + /** + * Registers a map of middlewares for a specific namespace + * @param {string} namespace + * @param {{ [key: string]: Middleware }} newMiddlewares + * @returns + */ add(namespace, rawMiddlewares) { for (const middlewareName in rawMiddlewares) { const middleware = rawMiddlewares[middlewareName]; @@ -24,6 +65,24 @@ const middlewaresRegistry = () => { middlewares[uid] = middleware; } }, + + /** + * Wraps a middleware to extend it + * @param {string} uid + * @param {(middleware: Middleware) => Middleware} extendFn + */ + extend(uid, extendFn) { + const currentMiddleware = this.get(uid); + + if (!currentMiddleware) { + throw new Error(`Middleware ${uid} doesn't exist`); + } + + const newMiddleware = extendFn(currentMiddleware); + middlewares[uid] = newMiddleware; + + return this; + }, }; }; diff --git a/packages/core/strapi/lib/core/registries/policies.d.ts b/packages/core/strapi/lib/core/registries/policies.d.ts new file mode 100644 index 0000000000..b4012592dc --- /dev/null +++ b/packages/core/strapi/lib/core/registries/policies.d.ts @@ -0,0 +1,9 @@ +import { BaseContext } from 'koa'; +import { Strapi } from '../../'; + +interface PolicyContext extends BaseContext { + type: string; + is(name): boolean; +} + +export type Policy = (ctx: PolicyContext, { strapi: Strapi }) => boolean | undefined; diff --git a/packages/core/strapi/lib/core/registries/policies.js b/packages/core/strapi/lib/core/registries/policies.js index 8e36b54a6b..bd7dc840a8 100644 --- a/packages/core/strapi/lib/core/registries/policies.js +++ b/packages/core/strapi/lib/core/registries/policies.js @@ -3,16 +3,57 @@ const { pickBy, has } = require('lodash/fp'); const { addNamespace, hasNamespace } = require('../utils'); +/** + * @typedef {import('./policies').Policy} Policy + */ + +// TODO: move instantiation part here instead of in the policy utils const policiesRegistry = () => { const policies = {}; return { - get(policyUID) { - return policies[policyUID]; + /** + * Returns this list of registered policies uids + * @returns {string[]} + */ + keys() { + return Object.keys(policies); }, + + /** + * Returns the instance of a policy. Instantiate the policy if not already done + * @param {string} uid + * @returns {Policy} + */ + get(uid) { + return policies[uid]; + }, + + /** + * Returns a map with all the policies in a namespace + * @param {string} namespace + * @returns {{ [key: string]: Policy }} + */ getAll(namespace) { return pickBy((_, uid) => hasNamespace(uid, namespace))(policies); }, + + /** + * Registers a policy + * @param {string} uid + * @param {Policy} policy + */ + set(uid, policy) { + policies[uid] = policy; + return this; + }, + + /** + * Registers a map of policies for a specific namespace + * @param {string} namespace + * @param {{ [key: string]: Policy }} newPolicies + * @returns + */ add(namespace, newPolicies) { for (const policyName in newPolicies) { const policy = newPolicies[policyName]; @@ -24,13 +65,23 @@ const policiesRegistry = () => { policies[uid] = policy; } }, - extend(policyUID, extendFn) { - const currentPolicy = this.get(policyUID); + + /** + * Wraps a policy to extend it + * @param {string} uid + * @param {(policy: Policy) => Policy} extendFn + */ + extend(uid, extendFn) { + const currentPolicy = this.get(uid); + if (!currentPolicy) { - throw new Error(`Policy ${policyUID} doesn't exist`); + throw new Error(`Policy ${uid} doesn't exist`); } + const newPolicy = extendFn(currentPolicy); - policies[policyUID] = newPolicy; + policies[uid] = newPolicy; + + return this; }, }; }; diff --git a/packages/core/strapi/lib/core/registries/services.d.ts b/packages/core/strapi/lib/core/registries/services.d.ts new file mode 100644 index 0000000000..7a172f927f --- /dev/null +++ b/packages/core/strapi/lib/core/registries/services.d.ts @@ -0,0 +1,7 @@ +import { Strapi } from '../../'; + +export type Service = { + [key: string]: (...args: any) => any; +}; + +export type ServiceFactory = ({ strapi: Strapi }) => Service; diff --git a/packages/core/strapi/lib/core/registries/services.js b/packages/core/strapi/lib/core/registries/services.js index e4635c7351..6951893830 100644 --- a/packages/core/strapi/lib/core/registries/services.js +++ b/packages/core/strapi/lib/core/registries/services.js @@ -4,11 +4,29 @@ const _ = require('lodash'); const { pickBy, has } = require('lodash/fp'); const { addNamespace, hasNamespace } = require('../utils'); +/** + * @typedef {import('./services').Service} Service + * @typedef {import('./services').ServiceFactory} ServiceFactory + */ + const servicesRegistry = strapi => { const services = {}; const instantiatedServices = {}; return { + /** + * Returns this list of registered services uids + * @returns {string[]} + */ + keys() { + return Object.keys(services); + }, + + /** + * Returns the instance of a service. Instantiate the service if not already done + * @param {string} uid + * @returns {Service} + */ get(uid) { if (instantiatedServices[uid]) { return instantiatedServices[uid]; @@ -16,21 +34,40 @@ const servicesRegistry = strapi => { const service = services[uid]; if (service) { - instantiatedServices[uid] = service({ strapi }); + instantiatedServices[uid] = typeof service === 'function' ? service({ strapi }) : service; return instantiatedServices[uid]; } return undefined; }, + + /** + * Returns a map with all the services in a namespace + * @param {string} namespace + * @returns {{ [key: string]: Service }} + */ getAll(namespace) { const filteredServices = pickBy((_, uid) => hasNamespace(uid, namespace))(services); return _.mapValues(filteredServices, (service, serviceUID) => this.get(serviceUID)); }, - set(uid, value) { - instantiatedServices[uid] = value; + + /** + * Registers a service + * @param {string} uid + * @param {Service|ServiceFactory} service + */ + set(uid, service) { + services[uid] = service; return this; }, + + /** + * Registers a map of services for a specific namespace + * @param {string} namespace + * @param {{ [key: string]: Service|ServiceFactory }} newServices + * @returns + */ add(namespace, newServices) { for (const serviceName in newServices) { const service = newServices[serviceName]; @@ -44,13 +81,23 @@ const servicesRegistry = strapi => { return this; }, - extend(serviceUID, extendFn) { - const currentService = this.get(serviceUID); + + /** + * Wraps a service to extend it + * @param {string} uid + * @param {(service: Service) => Service} extendFn + */ + extend(uid, extendFn) { + const currentService = this.get(uid); + if (!currentService) { - throw new Error(`Service ${serviceUID} doesn't exist`); + throw new Error(`Service ${uid} doesn't exist`); } + const newService = extendFn(currentService); - instantiatedServices[serviceUID] = newService; + instantiatedServices[uid] = newService; + + return this; }, }; }; diff --git a/packages/core/strapi/lib/middlewares/index.d.ts b/packages/core/strapi/lib/middlewares/index.d.ts index 7f46f52fdd..24f9144050 100644 --- a/packages/core/strapi/lib/middlewares/index.d.ts +++ b/packages/core/strapi/lib/middlewares/index.d.ts @@ -1,4 +1,4 @@ import { Strapi } from '../'; import { Middleware } from 'koa'; -export type MiddlewareFactory = (options: any, ctx: { strapi: Strapi }) => Middleware | null; +export type MiddlewareFactory = (config: any, ctx: { strapi: Strapi }) => Middleware | null; diff --git a/packages/core/strapi/lib/services/metrics/__tests__/index.test.js b/packages/core/strapi/lib/services/metrics/__tests__/index.test.js index c94803878c..db4736d504 100644 --- a/packages/core/strapi/lib/services/metrics/__tests__/index.test.js +++ b/packages/core/strapi/lib/services/metrics/__tests__/index.test.js @@ -24,7 +24,7 @@ describe('metrics', () => { server: { use, }, - }); + }).register(); expect(use).toHaveBeenCalled(); }); @@ -46,7 +46,7 @@ describe('metrics', () => { server: { use, }, - }); + }).register(); expect(use).not.toHaveBeenCalled(); }); @@ -80,6 +80,7 @@ describe('metrics', () => { projectType: 'Community', }, }); + fetch.mockClear(); }); diff --git a/packages/core/strapi/lib/services/metrics/index.js b/packages/core/strapi/lib/services/metrics/index.js index 8b0402e0b5..72024f7ca3 100644 --- a/packages/core/strapi/lib/services/metrics/index.js +++ b/packages/core/strapi/lib/services/metrics/index.js @@ -30,42 +30,44 @@ const createTelemetryInstance = strapi => { const sender = createSender(strapi); const sendEvent = wrapWithRateLimit(sender, { limitedEvents: LIMITED_EVENTS }); - if (!isDisabled) { - const pingCron = scheduleJob('0 0 12 * * *', () => sendEvent('ping')); - crons.push(pingCron); - - strapi.server.use(createMiddleware({ sendEvent })); - } - - if (strapi.EE === true && ee.isEE === true) { - const pingDisabled = - isTruthy(process.env.STRAPI_LICENSE_PING_DISABLED) && ee.licenseInfo.type === 'gold'; - - const sendLicenseCheck = () => { - return sendEvent( - 'didCheckLicense', - { - licenseInfo: { - ...ee.licenseInfo, - projectHash: hashProject(strapi), - dependencyHash: hashDep(strapi), - }, - }, - { - headers: { 'x-strapi-project': 'enterprise' }, - } - ); - }; - - if (!pingDisabled) { - const licenseCron = scheduleJob('0 0 0 * * 7', () => sendLicenseCheck()); - crons.push(licenseCron); - - sendLicenseCheck(); - } - } - return { + register() { + if (!isDisabled) { + const pingCron = scheduleJob('0 0 12 * * *', () => sendEvent('ping')); + crons.push(pingCron); + + strapi.server.use(createMiddleware({ sendEvent })); + } + }, + bootstrap() { + if (strapi.EE === true && ee.isEE === true) { + const pingDisabled = + isTruthy(process.env.STRAPI_LICENSE_PING_DISABLED) && ee.licenseInfo.type === 'gold'; + + const sendLicenseCheck = () => { + return sendEvent( + 'didCheckLicense', + { + licenseInfo: { + ...ee.licenseInfo, + projectHash: hashProject(strapi), + dependencyHash: hashDep(strapi), + }, + }, + { + headers: { 'x-strapi-project': 'enterprise' }, + } + ); + }; + + if (!pingDisabled) { + const licenseCron = scheduleJob('0 0 0 * * 7', () => sendLicenseCheck()); + crons.push(licenseCron); + + sendLicenseCheck(); + } + } + }, destroy() { // clear open handles crons.forEach(cron => cron.cancel()); diff --git a/packages/core/utils/lib/hooks.js b/packages/core/utils/lib/hooks.js index 4e9ee0cc71..b4298bf530 100644 --- a/packages/core/utils/lib/hooks.js +++ b/packages/core/utils/lib/hooks.js @@ -82,7 +82,7 @@ const createAsyncSeriesWaterfallHook = () => ({ const createAsyncParallelHook = () => ({ ...createHook(), - call(context) { + async call(context) { const promises = this.handlers.map(handler => handler(cloneDeep(context))); return Promise.all(promises); diff --git a/test/unit.setup.js b/test/unit.setup.js index 597f821b3f..c507acb241 100644 --- a/test/unit.setup.js +++ b/test/unit.setup.js @@ -2,6 +2,7 @@ const _ = require('lodash'); +// TODO: remove this tmp fix and migrate tests let strapiInstance; Object.defineProperty(global, 'strapi', { get() { @@ -17,5 +18,11 @@ Object.defineProperty(global, 'strapi', { plugin.contentType = name => plugin.contentTypes[name]; plugin.policy = name => plugin.policies[name]; }); + + strapiInstance.service = name => { + if (name.startsWith('admin::')) { + return strapiInstance.admin.services[name.split('admin::')[1]]; + } + }; }, });