Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(core/strapi): move components to registry #16273

Closed
wants to merge 7 commits into from
Expand Up @@ -108,7 +108,7 @@ const buildNode = (model, attributeName, attribute) => {
}

if (attribute.type === 'component') {
const component = strapi.components[attribute.component];
const component = strapi.component(attribute.component);
return { ...node, children: buildDeepAttributesCollection(component) };
}

Expand Down
Expand Up @@ -29,7 +29,7 @@ module.exports = ({ strapi }) => ({
findComponent(uid) {
const { toContentManagerModel } = getService('data-mapper');

const component = strapi.components[uid];
const component = strapi.component(uid);

return isNil(component) ? component : toContentManagerModel(component);
},
Expand Down
Expand Up @@ -19,7 +19,7 @@ module.exports = {
const componentService = getService('components');

const data = Object.keys(strapi.components).map((uid) => {
return componentService.formatComponent(strapi.components[uid]);
return componentService.formatComponent(strapi.component(uid));
});

ctx.send({ data });
Expand All @@ -33,7 +33,7 @@ module.exports = {
async getComponent(ctx) {
const { uid } = ctx.params;

const component = strapi.components[uid];
const component = strapi.component(uid);

if (!component) {
return ctx.send({ error: 'component.notFound' }, 404);
Expand Down
Expand Up @@ -211,7 +211,7 @@ const getTypeShape = (attribute, { modelType, attributes } = {}) => {
.test({
name: 'Check max component nesting is 1 lvl',
test(compoUID) {
const targetCompo = strapi.components[compoUID];
const targetCompo = strapi.component(compoUID);
if (!targetCompo) return true; // ignore this error as it will fail beforehand

if (modelType === modelTypes.COMPONENT && hasComponent(targetCompo)) {
Expand Down
Expand Up @@ -15,7 +15,7 @@ const createContentTypeBuilder = require('./content-type-builder');
*/
module.exports = function createBuilder() {
const components = Object.keys(strapi.components).map((key) => {
const compo = strapi.components[key];
const compo = strapi.component(key);

return {
category: compo.category,
Expand Down
12 changes: 11 additions & 1 deletion packages/core/strapi/lib/Strapi.js
Expand Up @@ -31,6 +31,7 @@ const createStrapiFetch = require('./utils/fetch');
const { LIFECYCLES } = require('./utils/lifecycles');
const ee = require('./utils/ee');
const contentTypesRegistry = require('./core/registries/content-types');
const componentsRegistry = require('./core/registries/components');
const servicesRegistry = require('./core/registries/services');
const policiesRegistry = require('./core/registries/policies');
const middlewaresRegistry = require('./core/registries/middlewares');
Expand Down Expand Up @@ -87,6 +88,7 @@ class Strapi {
// Register every Strapi registry in the container
this.container.register('config', createConfigProvider(appConfig));
this.container.register('content-types', contentTypesRegistry(this));
this.container.register('components', componentsRegistry(this));
this.container.register('services', servicesRegistry(this));
this.container.register('policies', policiesRegistry(this));
this.container.register('middlewares', middlewaresRegistry(this));
Expand Down Expand Up @@ -160,6 +162,14 @@ class Strapi {
return this.container.get('content-types').get(name);
}

get components() {
return this.container.get('components').getAll();
}

component(name) {
return this.container.get('components').get(name);
}

get policies() {
return this.container.get('policies').getAll();
}
Expand Down Expand Up @@ -354,7 +364,7 @@ class Strapi {
}

async loadComponents() {
this.components = await loaders.loadComponents(this);
await loaders.loadComponents(this);
}

async loadMiddlewares() {
Expand Down
4 changes: 2 additions & 2 deletions packages/core/strapi/lib/core-api/controller/transform.js
Expand Up @@ -65,14 +65,14 @@ const transformEntry = (entry, type) => {

attributeValues[key] = { data };
} else if (attribute && attribute.type === 'component') {
attributeValues[key] = transformComponent(property, strapi.components[attribute.component]);
attributeValues[key] = transformComponent(property, strapi.component(attribute.component));
} else if (attribute && attribute.type === 'dynamiczone') {
if (isNil(property)) {
attributeValues[key] = property;
}

attributeValues[key] = property.map((subProperty) => {
return transformComponent(subProperty, strapi.components[subProperty.__component]);
return transformComponent(subProperty, strapi.component(subProperty.__component));
});
} else if (attribute && attribute.type === 'media') {
const data = transformEntry(property, strapi.contentType('plugin::upload.file'));
Expand Down
22 changes: 9 additions & 13 deletions packages/core/strapi/lib/core/domain/component/index.js
@@ -1,22 +1,18 @@
'use strict';

const { cloneDeep, camelCase } = require('lodash/fp');
const cloneDeep = require('lodash/cloneDeep');
const { validateComponentDefinition } = require('./validator');

const createComponent = (definition = {}) => {
validateComponentDefinition(definition);
const createComponent = (uid, definition = {}) => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method was dead code previously

try {
validateComponentDefinition(definition);
} catch (e) {
throw new Error(`Component Definition is invalid for ${uid}'.\n${e.errors}`);
}

const createdComponent = cloneDeep(definition);
const category = camelCase(definition.info.category);
const { schema } = cloneDeep(definition);

const uid = `${category}.${definition.info.singularModelName}`;

Object.assign(createdComponent, {
uid,
category,
});

return createdComponent;
return schema;
};

module.exports = {
Expand Down
11 changes: 6 additions & 5 deletions packages/core/strapi/lib/core/domain/component/validator.js
Expand Up @@ -2,17 +2,18 @@

const { yup } = require('@strapi/utils');

const componentSchemaValidator = () =>
yup.object().shape({
const componentSchemaValidator = yup.object().shape({
schema: yup.object().shape({
info: yup
.object()
.shape({
singularName: yup.string().isCamelCase().required(),
pluralName: yup.string().isCamelCase().required(),
displayName: yup.string().required(),
singularName: yup.string().isKebabCase().required(),
})
.required(),
});
attributes: yup.object(),
}),
});

const validateComponentDefinition = (data) => {
return componentSchemaValidator.validateSync(data, { strict: true, abortEarly: false });
Expand Down
39 changes: 23 additions & 16 deletions packages/core/strapi/lib/core/loaders/components.js
Expand Up @@ -12,10 +12,8 @@ module.exports = async (strapi) => {

const map = await loadFiles(strapi.dirs.dist.components, '*/*.*(js|json)');

return Object.keys(map).reduce((acc, category) => {
Object.keys(map[category]).forEach((key) => {
const schema = map[category][key];

Object.entries(map).forEach(([category, schemas]) => {
const entries = Object.entries(schemas).map(([key, schema]) => {
if (!schema.collectionName) {
// NOTE: We're using the filepath from the app directory instead of the dist for information purpose
const filePath = join(strapi.dirs.app.components, category, schema.__filename__);
Expand All @@ -27,16 +25,25 @@ module.exports = async (strapi) => {

const uid = `${category}.${key}`;

acc[uid] = Object.assign(schema, {
__schema__: _.cloneDeep(schema),
uid,
category,
modelType: 'component',
modelName: key,
globalId: schema.globalId || _.upperFirst(_.camelCase(`component_${uid}`)),
});
});

return acc;
}, {});
// TODO: Most of this should go into core/strapi/lib/core/domain/component/index.js
const definition = {
schema: Object.assign(schema, {
__schema__: _.cloneDeep(schema),
iamandrewluca marked this conversation as resolved.
Show resolved Hide resolved
uid,
category,
modelType: 'component',
modelName: key,
globalId: schema.globalId || _.upperFirst(_.camelCase(`component_${uid}`)),
info: Object.assign(schema.info, {
singularName: key,
}),
}),
};

return [key, definition];
}, {});

const components = Object.fromEntries(entries);
strapi.container.get('components').add(category, components);
});
};
97 changes: 97 additions & 0 deletions packages/core/strapi/lib/core/registries/components.js
@@ -0,0 +1,97 @@
'use strict';

const { pickBy, has } = require('lodash/fp');
const { createComponent } = require('../domain/component');
const { hasNamespace, addNamespace } = require('../utils');

const validateKeySameToSingularName = (components) => {
for (const cName of Object.keys(components)) {
const component = components[cName];

if (cName !== component.schema.info.singularName) {
throw new Error(
`The key of the component should be the same as its singularName. Found ${cName} and ${component.schema.info.singularName}.`
);
}
}
};

const componentsRegistry = () => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This registry was copied from content-types and adjusted comments and variables.

const components = {};

return {
/**
* Returns this list of registered components uids
* @returns {string[]}
*/
keys() {
return Object.keys(components);
},

/**
* Returns the instance of a component. Instantiate the component if not already done
* @param {string} uid
* @returns
*/
get(uid) {
return components[uid];
},

/**
* Returns a map with all the components in a namespace
* @param {string} namespace
*/
getAll(namespace) {
return pickBy((_, uid) => hasNamespace(uid, namespace))(components);
},

/**
* Registers a component
* @param {string} uid
* @param {Object} component
*/
set(uid, component) {
components[uid] = component;
return this;
},

/**
* Registers a map of components for a specific namespace
* @param {string} namespace
* @param {{ [key: string]: Object }} newComponents
*/
add(namespace, newComponents) {
validateKeySameToSingularName(newComponents);

for (const rawComponentName of Object.keys(newComponents)) {
const uid = addNamespace(rawComponentName, namespace);

if (has(uid, components)) {
throw new Error(`Component ${uid} has already been registered.`);
}

components[uid] = createComponent(uid, newComponents[rawComponentName]);
}
},

/**
* Wraps a component to extend it
* @param {string} uid
* @param {(component: Object) => Object} extendFn
*/
extend(cUID, extendFn) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the registry need to implement any interface? Can these unused methods get deleted?

const currentComponent = this.get(cUID);

if (!currentComponent) {
throw new Error(`Component ${cUID} doesn't exist`);
}

const newComponent = extendFn(currentComponent);
components[cUID] = newComponent;

return this;
},
};
};

module.exports = componentsRegistry;
4 changes: 2 additions & 2 deletions packages/core/strapi/lib/services/utils/upload-files.js
Expand Up @@ -43,15 +43,15 @@ module.exports = async (uid, entity, files) => {

if (attr.type === 'component') {
modelUID = attr.component;
tmpModel = strapi.components[attr.component];
tmpModel = strapi.component(attr.component);
} else if (attr.type === 'dynamiczone') {
const entryIdx = path[i + 1]; // get component index
const value = _.get(entity, [...currentPath, entryIdx]);

if (!value) return {};

modelUID = value.__component; // get component type
tmpModel = strapi.components[modelUID];
tmpModel = strapi.component(modelUID);
} else if (attr.type === 'relation') {
modelUID = attr.target;
tmpModel = strapi.getModel(modelUID);
Expand Down
5 changes: 5 additions & 0 deletions packages/core/strapi/lib/types/core/strapi/index.d.ts
Expand Up @@ -107,6 +107,11 @@ export interface Strapi {
*/
readonly components: any;

/**
* Find a component using its unique identifier
*/
component(uid: string): any;

/**
* The custom fields registry
*
Expand Down
Expand Up @@ -93,7 +93,7 @@ const cleanSchemaAttributes = (
break;
}
case 'component': {
const componentAttributes = strapi.components[attribute.component].attributes;
const componentAttributes = strapi.component(attribute.component).attributes;
const rawComponentSchema = {
type: 'object',
properties: {
Expand Down Expand Up @@ -124,7 +124,7 @@ const cleanSchemaAttributes = (
}
case 'dynamiczone': {
const components = attribute.components.map((component) => {
const componentAttributes = strapi.components[component].attributes;
const componentAttributes = strapi.component(component).attributes;
const rawComponentSchema = {
type: 'object',
properties: {
Expand Down
Expand Up @@ -11,7 +11,7 @@ module.exports = ({ strapi }) => {
const isEmpty = components.length === 0;

const componentsTypeNames = components.map((componentUID) => {
const component = strapi.components[componentUID];
const component = strapi.component(componentUID);

if (!component) {
throw new ApplicationError(
Expand All @@ -30,7 +30,7 @@ module.exports = ({ strapi }) => {
return ERROR_TYPE_NAME;
}

return strapi.components[obj.__component].globalId;
return strapi.component(obj.__component).globalId;
},

definition(t) {
Expand All @@ -48,7 +48,7 @@ module.exports = ({ strapi }) => {
if (!component) {
throw new ApplicationError(
`Component not found. expected one of: ${components
.map((uid) => strapi.components[uid].globalId)
.map((uid) => strapi.component(uid).globalId)
.join(', ')}`
);
}
Expand Down
2 changes: 1 addition & 1 deletion packages/plugins/graphql/server/services/builders/input.js
Expand Up @@ -97,7 +97,7 @@ module.exports = (context) => {
// Components
else if (isComponent(attribute)) {
const isRepeatable = attribute.repeatable === true;
const component = strapi.components[attribute.component];
const component = strapi.component(attribute.component);
const componentInputType = getComponentInputName(component);

if (isRepeatable) {
Expand Down