731 lines
24 KiB
JavaScript

'use strict';
const _ = require('lodash');
// const pluralize = require('pluralize');
// const {
// getApis,
// getApisKeys,
// getApisUploadRelations,
// getEditDisplayAvailableFieldsPath,
// getEditDisplayDisplayedField,
// getEditDisplayFieldsPath,
// } = require('./utils/getters');
// const splitted = str => str.split('.');
// const pickData = model =>
// _.pick(model, [
// 'info',
// 'connection',
// 'collectionName',
// 'attributes',
// 'identity',
// 'globalId',
// 'globalName',
// 'orm',
// 'options',
// 'loadedModel',
// 'primaryKey',
// 'associations',
// ]);
function getContentManagerKeys({ model }, key) {
if (model.orm === 'mongoose') {
return model
.find({
$regex: `${key}.*`,
})
.then(results => results.map(({ value }) => JSON.parse(value)));
}
return model
.query(qb => {
qb.where('key', 'like', `${key}%`);
})
.fetchAll()
.then(config => config && config.toJSON())
.then(results => results.map(({ value }) => JSON.parse(value)));
}
async function updateGroups() {
const service = strapi.plugins['content-manager'].services.groups;
const configurations = await strapi
.query('core_store')
.custom(getContentManagerKeys)(
'plugin_content_manager_configuration_groups'
);
const realUIDs = Object.keys(strapi.groups);
const DBUIDs = configurations.map(({ uid }) => uid);
const groupsToUpdate = _.intersection(realUIDs, DBUIDs);
const groupsToAdd = _.difference(realUIDs, DBUIDs);
const groupsToDelete = _.difference(DBUIDs, realUIDs);
await Promise.all(
groupsToDelete.map(uid => service.deleteConfiguration(uid))
);
const generateNewConfiguration = async uid => {
return service.setConfiguration(
uid,
await buildDefaultConfiguration(strapi.groups[uid])
);
};
await Promise.all(groupsToAdd.map(uid => generateNewConfiguration(uid)));
const updateConfiguration = async uid => {
const conf = configurations.find(conf => conf.uid === uid);
const model = strapi.groups[uid];
return service.setConfiguration(uid, {
settings: await updateSettings(conf, model),
layouts: updateLayouts(conf, model),
metadatas: updateMetadatas(conf, model),
});
};
await Promise.all(groupsToUpdate.map(uid => updateConfiguration(uid)));
}
async function buildDefaultSettings() {
const generalSettings = await strapi.plugins[
'content-manager'
].services.generalsettings.getGeneralSettings();
return {
...generalSettings,
mainField: 'id',
defaultSortBy: 'id',
defaultSortOrder: 'ASC',
};
}
async function buildDefaultMetadata(name, attr) {
return {
edit: {
mainField: !_.has(attr, 'type') ? 'id' : undefined,
label: name,
description: '',
visible: true,
editable: true,
},
list: {
label: name,
searchable: true,
sortable: true,
},
};
}
async function buildDefaultMetadatas(model) {
return {
id: {
edit: {},
list: {
label: 'Id',
searchable: true,
sortable: true,
},
},
...Object.keys(model.allAttributes).reduce((acc, name) => {
acc[name] = buildDefaultMetadata(name, model.allAttributes[name]);
return acc;
}, {}),
};
}
function buildDefaultLayouts(model) {
return {
list: ['id'].concat(
Object.keys(model.allAttributes)
.filter(name => hasReadableAttribute(model, name))
.slice(0, 3)
),
editRelations: Object.keys(model.allAttributes).filter(name => {
const attr = model.allAttributes[name];
return !_.has(attr, 'type');
}),
edit: [],
};
}
async function buildDefaultConfiguration(model) {
return {
settings: await buildDefaultSettings(),
metadatas: buildDefaultMetadatas(model),
layouts: buildDefaultLayouts(model),
};
}
async function updateContentTypesScope(models, configurations, source) {
const service = strapi.plugins['content-manager'].services.contenttypes;
const realUIDs = Object.keys(models);
const DBUIDs = configurations.map(({ uid }) => uid);
const contentTypesToUpdate = _.intersection(realUIDs, DBUIDs);
const contentTypesToAdd = _.difference(realUIDs, DBUIDs);
const contentTypesToDelete = _.difference(DBUIDs, realUIDs);
await Promise.all(
contentTypesToDelete.map(uid =>
service.deleteContentTypeConfiguration({ uid, source })
)
);
const generateNewConfiguration = async uid => {
return service.setContentTypeConfiguration(
{ uid, source },
await buildDefaultConfiguration(models[uid])
);
};
await Promise.all(
contentTypesToAdd.map(uid => generateNewConfiguration(uid))
);
await Promise.all(contentTypesToUpdate.map(uid => {}));
}
async function updateContentTypes() {
const configurations = await strapi
.query('core_store')
.custom(getContentManagerKeys)(
'plugin_content_manager_configuration_content_types'
);
await updateContentTypesScope(
_.omit(strapi.models, ['core_store']),
configurations.filter(({ source }) => !source)
);
await updateContentTypesScope(
strapi.admin.models,
configurations.filter(({ source }) => source === 'admin'),
'admin'
);
await Promise.all(
Object.keys(strapi.plugins).map(pluginKey => {
const plugin = strapi.plugins[pluginKey];
return updateContentTypesScope(
plugin.models || {},
configurations.filter(({ source }) => source === pluginKey),
pluginKey
);
})
);
}
function updateMetadatas(configuration, model) {
// clear all keys that do not exist anymore
if (_.isEmpty(configuration.metadatas)) return buildDefaultMetadatas(model);
// remove old keys
const metasWithValidKeys = _.pick(
configuration.metadatas,
['id'].concat(Object.keys(model.allAttributes))
);
const metasWithDefaults = _.merge(
buildDefaultMetadatas(model),
metasWithValidKeys
);
// clear the invalid mainFields
const updatedMetas = Object.keys(metasWithDefaults).reduce((acc, key) => {
const meta = metasWithDefaults[key];
if (!_.has(meta, 'mainField')) return acc;
// remove mainField if the attribute is not a relation anymore
if (_.has(model.allAttributes[key], 'type')) {
acc[key] = _.omit(meta, 'mainField');
return acc;
}
// if the mainField is id you can keep it
if (meta.mainField === 'id') return acc;
// check the mainField in the targetModel
const attr = model.allAttributes[key];
const target = strapi.getModel(attr.model || attr.collection, attr.plugin);
if (!hasReadableAttribute(target, meta.mainField)) {
acc[key] = {
...meta,
mainField: 'id',
};
return acc;
}
return acc;
}, {});
return _.merge(metasWithDefaults, updatedMetas);
}
async function updateSettings(configuration, model) {
if (_.isEmpty(configuration.settings)) return buildDefaultSettings(model);
const { mainField = 'id', defaultSortBy = 'id' } = configuration.settings;
return {
...configuration.settings,
mainField: hasReadableAttribute(model, mainField) ? mainField : 'id',
defaultSortBy: hasReadableAttribute(model, defaultSortBy)
? defaultSortBy
: 'id',
};
}
function updateLayouts(configuration, model) {
if (_.isEmpty(configuration.layouts)) return buildDefaultLayouts(model);
return {
...configuration.layouts,
};
}
const hasReadableAttribute = (model, attr) => {
if (attr === 'id') return true;
if (!_.has(model.allAttributes, attr)) {
return false;
}
if (!_.has(model.allAttributes[attr], 'type')) {
return false;
}
if (['group', 'json', 'array'].includes(model.allAttributes[attr].type)) {
return false;
}
return true;
};
async function bootstrap() {
await updateGroups();
await updateContentTypes();
}
module.exports = cb => {
bootstrap().then(() => cb(), err => cb(err));
};
/* eslint-disable indent */
// module.exports = async cb => {
// // Retrieve all layout files from the plugins config folder
// const pluginsLayout = Object.keys(strapi.plugins).reduce((acc, current) => {
// const models = _.get(strapi.plugins, [current, 'config', 'layout'], {});
// Object.keys(models).forEach(model => {
// const layout = _.get(strapi.plugins, [current, 'config', 'layout', model], {});
// acc[model] = layout;
// });
// return acc;
// }, {});
// // Retrieve the administrator layout
// const administratorLayout = _.get(strapi.admin, 'config.layout.administrator', {});
// // Remove the core_store layout since it is not needed
// // And create a temporay one
// const tempLayout = Object.keys(strapi.models)
// .filter(m => m !== 'core_store')
// .reduce((acc, current) => {
// acc[current] = { attributes: {} };
// return acc;
// }, _.merge(pluginsLayout, { administrator: administratorLayout }));
// const models = _.mapValues(strapi.models, pickData);
// delete models['core_store'];
// const adminModels = {
// admin: {
// models: _.mapValues(strapi.admin.models, pickData),
// },
// };
// const pluginsModel = Object.keys(strapi.plugins).reduce((acc, current) => {
// acc[current] = {
// models: _.mapValues(strapi.plugins[current].models, pickData),
// };
// return acc;
// }, adminModels);
// // Reference all current models
// const appModels = Object.keys(pluginsModel).reduce((acc, curr) => {
// const models = Object.keys(_.get(pluginsModel, [curr, 'models'], {}));
// return acc.concat(models);
// }, Object.keys(strapi.models).filter(m => m !== 'core_store'));
// // Init schema
// const schema = {
// generalSettings: {
// search: true,
// filters: true,
// bulkActions: true,
// pageEntries: 10,
// },
// models: {
// plugins: {},
// },
// layout: {},
// };
// // Populate the schema object
// const buildSchema = (model, name, plugin = false) => {
// // Model data
// const schemaModel = Object.assign(
// {
// label: _.upperFirst(name),
// labelPlural: _.upperFirst(pluralize(name)),
// orm: model.orm || 'mongoose',
// search: true,
// filters: true,
// bulkActions: true,
// pageEntries: 10,
// defaultSort: model.primaryKey,
// sort: 'ASC',
// options: model.options,
// editDisplay: {
// availableFields: {},
// displayedField: model.primaryKey,
// fields: [],
// relations: [],
// },
// },
// model,
// );
// const fieldsToRemove = [];
// // Fields (non relation)
// const fields = _.mapValues(
// _.pickBy(model.attributes, attribute => !attribute.model && !attribute.collection),
// (value, attribute) => {
// const fieldClassName = _.get(tempLayout, [name, 'attributes', attribute, 'className'], '');
// if (fieldClassName === 'd-none') {
// fieldsToRemove.push(attribute);
// }
// return {
// label: _.upperFirst(attribute),
// description: '',
// type: value.type || 'string',
// disabled: false,
// };
// },
// );
// // Don't display fields that are hidden by default like the resetPasswordToken for the model user
// fieldsToRemove.forEach(field => {
// _.unset(fields, field);
// });
// schemaModel.attributes = _.omit(schemaModel.attributes, fieldsToRemove);
// schemaModel.fields = fields;
// schemaModel.editDisplay.availableFields = fields;
// // Select fields displayed in list view
// schemaModel.listDisplay = Object.keys(schemaModel.fields)
// // Construct Array of attr ex { type: 'string', label: 'Foo', name: 'Foo', description: '' }
// .map(attr => {
// const attrType = schemaModel.fields[attr].type;
// const sortable = attrType !== 'json' && attrType !== 'array';
// return Object.assign(schemaModel.fields[attr], {
// name: attr,
// sortable,
// searchable: sortable,
// });
// })
// // Retrieve only the fourth first items
// .slice(0, 4);
// schemaModel.listDisplay.splice(0, 0, {
// name: model.primaryKey || 'id',
// label: 'Id',
// type: 'string',
// sortable: true,
// searchable: true,
// });
// // This object will be used to customise the label and description and so on of an input.
// // TODO: maybe add the customBootstrapClass in it;
// schemaModel.editDisplay.availableFields = Object.keys(schemaModel.fields).reduce((acc, current) => {
// acc[current] = Object.assign(
// _.pick(_.get(schemaModel, ['fields', current], {}), ['label', 'type', 'description', 'name']),
// {
// editable: ['updatedAt', 'createdAt', 'updated_at', 'created_at'].indexOf(current) === -1,
// placeholder: '',
// },
// );
// return acc;
// }, {});
// if (model.associations) {
// // Model relations
// schemaModel.relations = model.associations.reduce((acc, current) => {
// const label = _.upperFirst(current.alias);
// const displayedAttribute = current.plugin // Value to modified to custom what's displayed in the react-select
// ? _.get(pluginsModel, [current.plugin, 'models', current.model || current.collection, 'info', 'mainField']) ||
// _.findKey(
// _.get(pluginsModel, [current.plugin, 'models', current.model || current.collection, 'attributes']),
// { type: 'string' },
// ) ||
// 'id'
// : _.get(models, [current.model || current.collection, 'info', 'mainField']) ||
// _.findKey(_.get(models, [current.model || current.collection, 'attributes']), { type: 'string' }) ||
// 'id';
// acc[current.alias] = {
// ...current,
// description: '',
// label,
// displayedAttribute,
// };
// return acc;
// }, {});
// const relationsArray = Object.keys(schemaModel.relations).filter(relation => {
// const isUploadRelation = _.get(schemaModel, ['relations', relation, 'plugin'], '') === 'upload';
// const isMorphSide =
// _.get(schemaModel, ['relations', relation, 'nature'], '')
// .toLowerCase()
// .includes('morp') && _.get(schemaModel, ['relations', relation, relation]) !== undefined;
// return !isUploadRelation && !isMorphSide;
// });
// const uploadRelations = Object.keys(schemaModel.relations).reduce((acc, current) => {
// if (_.get(schemaModel, ['relations', current, 'plugin']) === 'upload') {
// const model = _.get(schemaModel, ['relations', current]);
// acc[current] = {
// description: '',
// editable: true,
// label: _.upperFirst(current),
// multiple: _.has(model, 'collection'),
// name: current,
// placeholder: '',
// type: 'file',
// disabled: false,
// };
// }
// return acc;
// }, {});
// schemaModel.editDisplay.availableFields = _.merge(schemaModel.editDisplay.availableFields, uploadRelations);
// schemaModel.editDisplay.relations = relationsArray;
// }
// schemaModel.editDisplay.fields = Object.keys(schemaModel.editDisplay.availableFields);
// if (plugin) {
// return _.set(schema.models.plugins, `${plugin}.${name}`, schemaModel);
// }
// // Set the formatted model to the schema
// schema.models[name] = schemaModel;
// };
// // For each plugin's apis populate the schema object with the needed infos
// _.forEach(pluginsModel, (plugin, pluginName) => {
// _.forEach(plugin.models, (model, name) => {
// buildSchema(model, name, pluginName);
// });
// });
// // Generate schema for models.
// _.forEach(models, (model, name) => {
// buildSchema(model, name);
// });
// const pluginStore = strapi.store({
// environment: '',
// type: 'plugin',
// name: 'content-manager',
// });
// try {
// // Retrieve the previous schema from the db
// const prevSchema = await pluginStore.get({ key: 'schema' });
// // If no schema stored
// if (!prevSchema) {
// _.set(schema, 'layout', tempLayout);
// pluginStore.set({ key: 'schema', value: schema });
// return cb();
// } else {
// const modelsLayout = Object.keys(_.get(prevSchema, 'layout', {}));
// // Remove previous model from the schema.layout
// // Usually needed when renaming a model
// modelsLayout.forEach(model => {
// if (!appModels.includes(model)) {
// _.unset(prevSchema, ['layout', model]);
// }
// });
// }
// // Here we do the difference between the previous schema from the database and the new one
// // Retrieve all the api path, it generates an array
// const prevSchemaApis = getApis(prevSchema.models);
// const schemaApis = getApis(schema.models);
// // Array of apis to add
// const apisToAdd = schemaApis.filter(api => prevSchemaApis.indexOf(api) === -1).map(splitted);
// // Array of apis to remove
// const apisToRemove = prevSchemaApis.filter(api => schemaApis.indexOf(api) === -1).map(splitted);
// // Retrieve the same apis by name
// const sameApis = schemaApis.filter(api => prevSchemaApis.indexOf(api) !== -1).map(splitted);
// // Retrieve all the field's path of the current unchanged api name
// const schemaSameApisKeys = _.flattenDeep(getApisKeys(schema, sameApis));
// // Retrieve all the field's path of the previous unchanged api name
// const prevSchemaSameApisKeys = _.flattenDeep(getApisKeys(prevSchema, sameApis));
// // Determine for the same api if we need to add some fields
// const sameApisAttrToAdd = schemaSameApisKeys
// .filter(attr => prevSchemaSameApisKeys.indexOf(attr) === -1)
// .map(splitted);
// // Special case for the relations
// const prevSchemaSameApisUploadRelations = _.flattenDeep(getApisUploadRelations(prevSchema, sameApis));
// const schemaSameApisUploadRelations = _.flattenDeep(getApisUploadRelations(schema, sameApis));
// const sameApisUploadRelationsToAdd = schemaSameApisUploadRelations
// .filter(attr => prevSchemaSameApisUploadRelations.indexOf(attr) === -1)
// .map(splitted);
// // Determine the fields to remove for the unchanged api name
// const sameApisAttrToRemove = prevSchemaSameApisKeys
// .filter(attr => schemaSameApisKeys.indexOf(attr) === -1)
// .map(splitted);
// // Remove api
// apisToRemove.map(apiPath => {
// _.unset(prevSchema.models, apiPath);
// });
// // Remove API attribute
// sameApisAttrToRemove.map(attrPath => {
// const editDisplayPath = getEditDisplayAvailableFieldsPath(attrPath);
// // Remove the field from the available fields in the editDisplayObject
// _.unset(prevSchema.models, editDisplayPath);
// // Check default sort and change it if needed
// _.unset(prevSchema.models, attrPath);
// // Retrieve the api path in the schema Object
// const apiPath = attrPath.length > 3 ? _.take(attrPath, 3) : _.take(attrPath, 1);
// // Retrieve the listDisplay path in the schema Object
// const listDisplayPath = apiPath.concat('listDisplay');
// const prevListDisplay = _.get(prevSchema.models, listDisplayPath);
// const defaultSortPath = apiPath.concat('defaultSort');
// const currentAttr = attrPath.slice(-1);
// const defaultSort = _.get(prevSchema.models, defaultSortPath);
// const displayedFieldPath = getEditDisplayDisplayedField(attrPath);
// const displayedField = _.get(prevSchema.models, displayedFieldPath, null);
// const primaryKey = _.get(prevSchema.models, [...apiPath, 'primaryKey'], null);
// // If the user has deleted the default sort attribute in the content type builder
// // Replace it by new generated one from the current schema
// if (_.includes(currentAttr, defaultSort)) {
// _.set(prevSchema.models, defaultSortPath, _.get(schema.models, defaultSortPath));
// }
// // If the user has deleted the edit view displayed field (name in the header)
// // Replace it by the model's primary key.
// if (_.includes(currentAttr, displayedField)) {
// _.set(prevSchema.models, displayedFieldPath, primaryKey);
// }
// // Update the displayed fields
// const updatedListDisplay = prevListDisplay.filter(obj => obj.name !== currentAttr.join());
// // Retrieve the model's displayed fields for the `EditPage`
// const fieldsPath = getEditDisplayFieldsPath(attrPath);
// // Retrieve the previous settings
// const prevEditDisplayFields = _.get(prevSchema.models, fieldsPath);
// // Update the fields
// const updatedEditDisplayFields = prevEditDisplayFields.filter(field => field !== currentAttr.join());
// // Set the new layout
// _.set(prevSchema.models, fieldsPath, updatedEditDisplayFields);
// if (updatedListDisplay.length === 0) {
// // Update it with the one from the generated schema
// _.set(prevSchema.models, listDisplayPath, _.get(schema.models, listDisplayPath, []));
// } else {
// _.set(prevSchema.models, listDisplayPath, updatedListDisplay);
// }
// });
// // Add API
// // Here we just need to add the data from the current schema Object
// apisToAdd.map(apiPath => {
// const api = _.get(schema.models, apiPath);
// const { search, filters, bulkActions, pageEntries, options } = _.get(prevSchema, 'generalSettings');
// _.set(api, 'options', options);
// _.set(api, 'filters', filters);
// _.set(api, 'search', search);
// _.set(api, 'bulkActions', bulkActions);
// _.set(api, 'pageEntries', pageEntries);
// _.set(prevSchema.models, apiPath, api);
// });
// // Add attribute to an existing API
// sameApisAttrToAdd.map(attrPath => {
// const attr = _.get(schema.models, attrPath);
// _.set(prevSchema.models, attrPath, attr);
// // Add the field in the editDisplay object
// const path = getEditDisplayAvailableFieldsPath(attrPath);
// const availableAttrToAdd = _.get(schema.models, path);
// _.set(prevSchema.models, path, availableAttrToAdd);
// // Push the attr into the list
// const fieldsPath = getEditDisplayFieldsPath(attrPath);
// const currentFields = _.get(prevSchema.models, fieldsPath, []);
// currentFields.push(availableAttrToAdd.name);
// _.set(prevSchema.models, fieldsPath, currentFields);
// });
// // Update other keys
// sameApis.forEach(apiPath => {
// // This doesn't keep the prevSettings for the relations, the user will have to reset it.
// // We might have to improve this if we want the order of the relations to be kept
// ['relations', 'loadedModel', 'associations', 'attributes', ['editDisplay', 'relations']]
// .map(key => apiPath.concat(key))
// .forEach(keyPath => {
// const newValue = _.get(schema.models, keyPath);
// _.set(prevSchema.models, keyPath, newValue);
// });
// });
// // Special handler for the upload relations
// sameApisUploadRelationsToAdd.forEach(attrPath => {
// const attr = _.get(schema.models, attrPath);
// _.set(prevSchema.models, attrPath, attr);
// const fieldsPath = [..._.take(attrPath, attrPath.length - 2), 'fields'];
// const currentFields = _.get(prevSchema.models, fieldsPath, []);
// currentFields.push(attr.name);
// _.set(prevSchema.models, fieldsPath, currentFields);
// });
// schemaApis.map(model => {
// const isPlugin = model.includes('plugins.');
// _.set(prevSchema.models[model], 'info', _.get(!isPlugin ? strapi.models[model] : strapi[model], 'info'));
// _.set(prevSchema.models[model], 'options', _.get(!isPlugin ? strapi.models[model] : strapi[model], 'options'));
// });
// await pluginStore.set({ key: 'schema', value: prevSchema });
// } catch (err) {
// console.log('error', err); // eslint-disable-line no-console
// }
// cb();
// };