Harmonize registries and simplify listing commands

This commit is contained in:
Alexandre Bodin 2021-10-07 13:06:40 +02:00
parent fdc0fa82fc
commit c1c6dd4c11
20 changed files with 415 additions and 90 deletions

View File

@ -1,9 +1,7 @@
'use strict'; 'use strict';
const { prop } = require('lodash/fp');
const getService = name => { const getService = name => {
return prop(`admin.services.${name}`, strapi); return strapi.service(`admin::${name}`);
}; };
module.exports = { module.exports = {

View File

@ -208,4 +208,9 @@ program
.description('List all the application hooks') .description('List all the application hooks')
.action(getLocalScript('hooks/list')); .action(getLocalScript('hooks/list'));
program
.command('services:list')
.description('List all the application services')
.action(getLocalScript('services/list'));
program.parseAsync(process.argv); program.parseAsync(process.argv);

View File

@ -70,6 +70,7 @@ class Strapi {
this.startupLogger = createStartupLogger(this); this.startupLogger = createStartupLogger(this);
this.log = createLogger(this.config.get('logger', {})); this.log = createLogger(this.config.get('logger', {}));
this.cron = createCronService(); this.cron = createCronService();
this.telemetry = createTelemetry(this);
createUpdateNotifier(this).notify(); createUpdateNotifier(this).notify();
} }
@ -310,7 +311,7 @@ class Strapi {
this.hook('strapi::content-types.afterSync').register(draftAndPublishSync.enable); this.hook('strapi::content-types.afterSync').register(draftAndPublishSync.enable);
} }
async load() { async register() {
await Promise.all([ await Promise.all([
this.loadApp(), this.loadApp(),
this.loadPlugins(), this.loadPlugins(),
@ -332,8 +333,14 @@ class Strapi {
this.registerInternalHooks(); this.registerInternalHooks();
this.telemetry.register();
await this.runLifecyclesFunctions(LIFECYCLES.REGISTER); await this.runLifecyclesFunctions(LIFECYCLES.REGISTER);
return this;
}
async bootstrap() {
const contentTypes = [ const contentTypes = [
coreStoreModel, coreStoreModel,
webhookModel, webhookModel,
@ -360,7 +367,7 @@ class Strapi {
const cronTasks = this.config.get('server.cron.tasks', {}); const cronTasks = this.config.get('server.cron.tasks', {});
this.cron.add(cronTasks); this.cron.add(cronTasks);
this.telemetry = createTelemetry(this); this.telemetry.bootstrap();
let oldContentTypes; let oldContentTypes;
if (await this.db.connection.schema.hasTable(coreStoreModel.collectionName)) { if (await this.db.connection.schema.hasTable(coreStoreModel.collectionName)) {
@ -399,6 +406,13 @@ class Strapi {
this.cron.start(); this.cron.start();
return this;
}
async load() {
await this.register();
await this.bootstrap();
this.isLoaded = true; this.isLoaded = true;
return this; return this;

View File

@ -6,17 +6,15 @@ const chalk = require('chalk');
const strapi = require('../../index'); const strapi = require('../../index');
module.exports = async function() { 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({ const infoTable = new CLITable({
head: [chalk.blue('Name')], head: [chalk.blue('Name')],
}); });
Object.keys(list).forEach(name => { list.forEach(name => infoTable.push([name]));
infoTable.push([name]);
});
console.log(infoTable.toString()); console.log(infoTable.toString());

View File

@ -6,17 +6,15 @@ const chalk = require('chalk');
const strapi = require('../../index'); const strapi = require('../../index');
module.exports = async function() { 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({ const infoTable = new CLITable({
head: [chalk.blue('Name')], head: [chalk.blue('Name')],
}); });
Object.keys(list).forEach(name => { list.forEach(name => infoTable.push([name]));
infoTable.push([name]);
});
console.log(infoTable.toString()); console.log(infoTable.toString());

View File

@ -6,17 +6,15 @@ const chalk = require('chalk');
const strapi = require('../../index'); const strapi = require('../../index');
module.exports = async function() { 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({ const infoTable = new CLITable({
head: [chalk.blue('Name')], head: [chalk.blue('Name')],
}); });
Object.keys(list).forEach(name => { list.forEach(name => infoTable.push([name]));
infoTable.push([name]);
});
console.log(infoTable.toString()); console.log(infoTable.toString());

View File

@ -6,17 +6,15 @@ const chalk = require('chalk');
const strapi = require('../../index'); const strapi = require('../../index');
module.exports = async function() { 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({ const infoTable = new CLITable({
head: [chalk.blue('Name')], head: [chalk.blue('Name')],
}); });
Object.keys(list).forEach(name => { list.forEach(name => infoTable.push([name]));
infoTable.push([name]);
});
console.log(infoTable.toString()); console.log(infoTable.toString());

View File

@ -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();
};

View File

@ -20,32 +20,76 @@ const contentTypesRegistry = () => {
const contentTypes = {}; const contentTypes = {};
return { 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 ma with all the contentTypes in a namespace
* @param {string} namespace
*/
getAll(namespace) { getAll(namespace) {
return pickBy((_, uid) => hasNamespace(uid, namespace))(contentTypes); 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); const uid = addNamespace(rawCtName, namespace);
if (has(uid, contentTypes)) { if (has(uid, contentTypes)) {
throw new Error(`Content-type ${uid} has already been registered.`); 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) { extend(ctUID, extendFn) {
const currentContentType = this.get(ctUID); const currentContentType = this.get(ctUID);
if (!currentContentType) { if (!currentContentType) {
throw new Error(`Content-Type ${ctUID} doesn't exist`); throw new Error(`Content-Type ${ctUID} doesn't exist`);
} }
const newContentType = extendFn(currentContentType); const newContentType = extendFn(currentContentType);
contentTypes[ctUID] = newContentType; contentTypes[ctUID] = newContentType;
return this;
}, },
}; };
}; };

View File

@ -0,0 +1,20 @@
type Handler = (context: any) => any;
type AsyncHook = {
handlers: Handler[];
register(handler: Handler): this;
delete(handler: Handler): this;
call(): Promise<void>;
};
type SyncHook = {
get handlers(): Handler[];
register(handler: Handler): this;
delete(handler: Handler): this;
call(): void;
};
export type Hook = AsyncHook|SyncHook

View File

@ -1,26 +1,58 @@
'use strict'; 'use strict';
const { pickBy, has } = require('lodash/fp'); const { pickBy } = require('lodash/fp');
const { addNamespace, hasNamespace } = require('../utils'); const { addNamespace, hasNamespace } = require('../utils');
/**
* @typedef {import('./hooks').Hook} Hook
*/
const hooksRegistry = () => { const hooksRegistry = () => {
const hooks = {}; const hooks = {};
return { 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. Instantiate the hook if not already done
* @param {string} uid
* @returns {Hook}
*/
get(uid) {
return hooks[uid];
},
/**
* Returns a ma with all the hooks in a namespace
* @param {string} namespace
* @returns {{ [key: string]: Hook }}
*/
getAll(namespace) { getAll(namespace) {
return pickBy((_, uid) => hasNamespace(uid, namespace))(hooks); 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; hooks[uid] = hook;
return this; return this;
}, },
/**
* Registers a map of hooks for a specific namespace
* @param {string} namespace
* @param {{ [key: string]: Hook }} newHooks
* @returns
*/
add(namespace, hooks) { add(namespace, hooks) {
for (const hookName in hooks) { for (const hookName in hooks) {
const hook = hooks[hookName]; const hook = hooks[hookName];
@ -31,6 +63,24 @@ const hooksRegistry = () => {
return this; 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;
},
}; };
}; };

View File

@ -0,0 +1,5 @@
import { BaseContext, Middleware as KoaMiddleware } from 'koa';
import { Strapi } from '../../';
import { MiddlewareFactory } from '../../middlewares';
export type Middleware = KoaMiddleware | MiddlewareFactory;

View File

@ -3,16 +3,57 @@
const { pickBy, has } = require('lodash/fp'); const { pickBy, has } = require('lodash/fp');
const { addNamespace, hasNamespace } = require('../utils'); const { addNamespace, hasNamespace } = require('../utils');
/**
* @typedef {import('./middlewares').Middleware} Middleware
*/
// TODO: move instantiation part here instead of in the server service
const middlewaresRegistry = () => { const middlewaresRegistry = () => {
const middlewares = {}; const middlewares = {};
return { 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 ma with all the middlewares in a namespace
* @param {string} namespace
* @returns {{ [key: string]: Middleware }}
*/
getAll(namespace) { getAll(namespace) {
return pickBy((_, uid) => hasNamespace(uid, namespace))(middlewares); 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) { add(namespace, rawMiddlewares) {
for (const middlewareName in rawMiddlewares) { for (const middlewareName in rawMiddlewares) {
const middleware = rawMiddlewares[middlewareName]; const middleware = rawMiddlewares[middlewareName];
@ -24,6 +65,24 @@ const middlewaresRegistry = () => {
middlewares[uid] = middleware; 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;
},
}; };
}; };

View File

@ -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;

View File

@ -3,16 +3,57 @@
const { pickBy, has } = require('lodash/fp'); const { pickBy, has } = require('lodash/fp');
const { addNamespace, hasNamespace } = require('../utils'); const { addNamespace, hasNamespace } = require('../utils');
/**
* @typedef {import('./policies').Policy} Policy
*/
// TODO: move instantiation part here instead of in the policy utils
const policiesRegistry = () => { const policiesRegistry = () => {
const policies = {}; const policies = {};
return { 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 ma with all the policies in a namespace
* @param {string} namespace
* @returns {{ [key: string]: Policy }}
*/
getAll(namespace) { getAll(namespace) {
return pickBy((_, uid) => hasNamespace(uid, namespace))(policies); 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) { add(namespace, newPolicies) {
for (const policyName in newPolicies) { for (const policyName in newPolicies) {
const policy = newPolicies[policyName]; const policy = newPolicies[policyName];
@ -24,13 +65,23 @@ const policiesRegistry = () => {
policies[uid] = policy; 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) { if (!currentPolicy) {
throw new Error(`Policy ${policyUID} doesn't exist`); throw new Error(`Policy ${uid} doesn't exist`);
} }
const newPolicy = extendFn(currentPolicy); const newPolicy = extendFn(currentPolicy);
policies[policyUID] = newPolicy; policies[uid] = newPolicy;
return this;
}, },
}; };
}; };

View File

@ -0,0 +1,7 @@
import { Strapi } from '../../';
export type Service = {
[key: string]: (...args: any) => any;
};
export type ServiceFactory = ({ strapi: Strapi }) => Service;

View File

@ -4,11 +4,29 @@ const _ = require('lodash');
const { pickBy, has } = require('lodash/fp'); const { pickBy, has } = require('lodash/fp');
const { addNamespace, hasNamespace } = require('../utils'); const { addNamespace, hasNamespace } = require('../utils');
/**
* @typedef {import('./services').Service} Service
* @typedef {import('./services').ServiceFactory} ServiceFactory
*/
const servicesRegistry = strapi => { const servicesRegistry = strapi => {
const services = {}; const services = {};
const instantiatedServices = {}; const instantiatedServices = {};
return { 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) { get(uid) {
if (instantiatedServices[uid]) { if (instantiatedServices[uid]) {
return instantiatedServices[uid]; return instantiatedServices[uid];
@ -16,21 +34,40 @@ const servicesRegistry = strapi => {
const service = services[uid]; const service = services[uid];
if (service) { if (service) {
instantiatedServices[uid] = service({ strapi }); instantiatedServices[uid] = typeof service === 'function' ? service({ strapi }) : service;
return instantiatedServices[uid]; return instantiatedServices[uid];
} }
return undefined; return undefined;
}, },
/**
* Returns a ma with all the services in a namespace
* @param {string} namespace
* @returns {{ [key: string]: Service }}
*/
getAll(namespace) { getAll(namespace) {
const filteredServices = pickBy((_, uid) => hasNamespace(uid, namespace))(services); const filteredServices = pickBy((_, uid) => hasNamespace(uid, namespace))(services);
return _.mapValues(filteredServices, (service, serviceUID) => this.get(serviceUID)); 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) {
instantiatedServices[uid] = service;
return this; return this;
}, },
/**
* Registers a map of services for a specific namespace
* @param {string} namespace
* @param {{ [key: string]: Service|ServiceFactory }} newServices
* @returns
*/
add(namespace, newServices) { add(namespace, newServices) {
for (const serviceName in newServices) { for (const serviceName in newServices) {
const service = newServices[serviceName]; const service = newServices[serviceName];
@ -44,13 +81,23 @@ const servicesRegistry = strapi => {
return this; 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) { if (!currentService) {
throw new Error(`Service ${serviceUID} doesn't exist`); throw new Error(`Service ${uid} doesn't exist`);
} }
const newService = extendFn(currentService); const newService = extendFn(currentService);
instantiatedServices[serviceUID] = newService; instantiatedServices[uid] = newService;
return this;
}, },
}; };
}; };

View File

@ -1,4 +1,4 @@
import { Strapi } from '../'; import { Strapi } from '../';
import { Middleware } from 'koa'; import { Middleware } from 'koa';
export type MiddlewareFactory = (options: any, ctx: { strapi: Strapi }) => Middleware | null; export type MiddlewareFactory = (config: any, ctx: { strapi: Strapi }) => Middleware | null;

View File

@ -30,42 +30,44 @@ const createTelemetryInstance = strapi => {
const sender = createSender(strapi); const sender = createSender(strapi);
const sendEvent = wrapWithRateLimit(sender, { limitedEvents: LIMITED_EVENTS }); 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 { 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() { destroy() {
// clear open handles // clear open handles
crons.forEach(cron => cron.cancel()); crons.forEach(cron => cron.cancel());

View File

@ -82,7 +82,7 @@ const createAsyncSeriesWaterfallHook = () => ({
const createAsyncParallelHook = () => ({ const createAsyncParallelHook = () => ({
...createHook(), ...createHook(),
call(context) { async call(context) {
const promises = this.handlers.map(handler => handler(cloneDeep(context))); const promises = this.handlers.map(handler => handler(cloneDeep(context)));
return Promise.all(promises); return Promise.all(promises);