mirror of
https://github.com/strapi/strapi.git
synced 2025-08-07 16:29:18 +00:00

* Add a domain layer for the permission, rework the engine handling of the permissions Signed-off-by: Convly <jean-sebastien.herbaux@epitech.eu> * Add permissions-fields-to-properties migration for the admin Signed-off-by: Convly <jean-sebastien.herbaux@epitech.eu> * Removes useless console.log Signed-off-by: Convly <jean-sebastien.herbaux@epitech.eu> * Remove debug logLevel from provider-login.test.e2e.js Signed-off-by: Convly <jean-sebastien.herbaux@epitech.eu> * Adds the new layout for the GET permissions, allow to subscribe to actionRegistered events, adds i18n handlers Signed-off-by: Convly <jean-sebastien.herbaux@epitech.eu> * Fix typo Signed-off-by: Convly <jean-sebastien.herbaux@epitech.eu> * Update permissions validators Signed-off-by: Convly <jean-sebastien.herbaux@epitech.eu> * Update unit tests Signed-off-by: Convly <jean-sebastien.herbaux@epitech.eu> * Update integrations test + fix some validation issues Signed-off-by: Convly <jean-sebastien.herbaux@epitech.eu> * Change plugins & settings section format for the permissions layout * only return locales property to localized subjects for the permission's layout * Do not send the locales property to the permission's layout when there is no locales created * Add the 'locales' property to publish & delete routes * Fix unwanted mutation of the sections builder states on multiple builds * Fix units tests with (new engine) * Fix admin-role e2e test - Add locales property to the update payload * fix e2e testsé * Update e2e snapshots * Fix unit test for i18n bootstrap * Add mocks for i18n/bootstrap test * Fix has-locale condition & updatePermission validator * Avoid mutation in migration, always authorize super admin for has-locales condition * Rework rbac domain objects, add a hook module and a provider factory * Remove old providers * Update the admin services & tests for the new rbac domain & providers * Fix tests, bootstrap functions & services following rbac domain rework * Update migration runner * PR comments Signed-off-by: Convly <jean-sebastien.herbaux@epitech.eu> * Remove useless console.log * Fix sanitizeCondition bug * Section builder rework * Add test for the section-builder section & add jsdoc for the permission domain * pr comments (without the migrations) * fix fields-to-properties migration * Add jsdoc for the sections-builder * Moves createBoundAbstractDomain from permission domain to the engine service * Remove debug logLevel for admin role test (e2e) * Fix core-store * Fix hooks & move business logic from i18n bootstrap to dedicated services * add route get-non-localized-fields * use write and read permission * refacto * add input validator * add route doc * handle ST Co-authored-by: Pierre Noël <petersg83@gmail.com> Co-authored-by: Alexandre BODIN <alexandrebodin@users.noreply.github.com>
419 lines
12 KiB
JavaScript
419 lines
12 KiB
JavaScript
'use strict';
|
|
|
|
const _ = require('lodash');
|
|
const { singular } = require('pluralize');
|
|
const { contentTypes: contentTypesUtils } = require('strapi-utils');
|
|
|
|
const {
|
|
getDefinitionFromStore,
|
|
storeDefinition,
|
|
getColumnsWhereDefinitionChanged,
|
|
} = require('./utils/store-definition');
|
|
const { getManyRelations } = require('./utils/associations');
|
|
|
|
const migrateSchemas = async ({ ORM, loadedModel, definition, connection, model }, context) => {
|
|
// Add created_at and updated_at field if timestamp option is true
|
|
if (loadedModel.hasTimestamps) {
|
|
definition.attributes[loadedModel.hasTimestamps[0]] = { type: 'currentTimestamp' };
|
|
definition.attributes[loadedModel.hasTimestamps[1]] = { type: 'currentTimestamp' };
|
|
}
|
|
|
|
// Equilize tables
|
|
if (connection.options && connection.options.autoMigration !== false) {
|
|
await createOrUpdateTable(
|
|
{
|
|
table: loadedModel.tableName,
|
|
attributes: definition.attributes,
|
|
definition,
|
|
ORM,
|
|
model,
|
|
},
|
|
context
|
|
);
|
|
}
|
|
|
|
// Equilize polymorphic relations
|
|
const morphRelations = definition.associations.filter(association => {
|
|
return association.nature.toLowerCase().includes('morphto');
|
|
});
|
|
|
|
for (const morphRelation of morphRelations) {
|
|
const attributes = {
|
|
[`${loadedModel.tableName}_id`]: { type: definition.primaryKeyType },
|
|
[`${morphRelation.alias}_id`]: { type: definition.primaryKeyType },
|
|
[`${morphRelation.alias}_type`]: { type: 'text' },
|
|
[definition.attributes[morphRelation.alias].filter]: { type: 'text' },
|
|
order: { type: 'integer' },
|
|
};
|
|
|
|
if (connection.options && connection.options.autoMigration !== false) {
|
|
await createOrUpdateTable(
|
|
{
|
|
table: `${loadedModel.tableName}_morph`,
|
|
attributes,
|
|
definition,
|
|
ORM,
|
|
model,
|
|
},
|
|
context
|
|
);
|
|
}
|
|
}
|
|
|
|
// Equilize many to many relations
|
|
const manyRelations = getManyRelations(definition);
|
|
|
|
for (const manyRelation of manyRelations) {
|
|
const { plugin, collection, via, dominant, alias } = manyRelation;
|
|
|
|
if (dominant) {
|
|
const targetCollection = strapi.db.getModel(collection, plugin);
|
|
|
|
const targetAttr = via
|
|
? targetCollection.attributes[via]
|
|
: {
|
|
attribute: singular(definition.collectionName),
|
|
column: definition.primaryKey,
|
|
};
|
|
|
|
const defAttr = definition.attributes[alias];
|
|
|
|
const targetCol = `${targetAttr.attribute}_${targetAttr.column}`;
|
|
let rootCol = `${defAttr.attribute}_${defAttr.column}`;
|
|
|
|
// manyWay with same CT
|
|
if (rootCol === targetCol) {
|
|
rootCol = `related_${rootCol}`;
|
|
}
|
|
|
|
const attributes = {
|
|
[targetCol]: { type: targetCollection.primaryKeyType },
|
|
[rootCol]: { type: definition.primaryKeyType },
|
|
};
|
|
|
|
const table = manyRelation.tableCollectionName;
|
|
if (connection.options && connection.options.autoMigration !== false) {
|
|
await createOrUpdateTable({ table, attributes, definition, ORM, model }, context);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Remove from attributes (auto handled by bookshelf and not displayed on ctb)
|
|
if (loadedModel.hasTimestamps) {
|
|
delete definition.attributes[loadedModel.hasTimestamps[0]];
|
|
delete definition.attributes[loadedModel.hasTimestamps[1]];
|
|
}
|
|
};
|
|
|
|
const getColumnInfo = async (columnName, tableName, ORM) => {
|
|
const exists = await ORM.knex.schema.hasColumn(tableName, columnName);
|
|
|
|
return {
|
|
columnName,
|
|
exists,
|
|
};
|
|
};
|
|
|
|
const isColumn = ({ definition, attribute, name }) => {
|
|
if (!_.has(attribute, 'type')) {
|
|
const relation = definition.associations.find(association => {
|
|
return association.alias === name;
|
|
});
|
|
|
|
if (!relation) return false;
|
|
|
|
if (['oneToOne', 'manyToOne', 'oneWay'].includes(relation.nature)) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
if (['component', 'dynamiczone'].includes(attribute.type)) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
const uniqueColName = (table, key) => `${table}_${key}_unique`;
|
|
|
|
const buildColType = ({ name, attribute, table, tableExists = false, definition, ORM }) => {
|
|
if (!attribute.type) {
|
|
const relation = definition.associations.find(association => association.alias === name);
|
|
|
|
if (['oneToOne', 'manyToOne', 'oneWay'].includes(relation.nature)) {
|
|
return buildColType({
|
|
name,
|
|
attribute: { type: definition.primaryKeyType },
|
|
table,
|
|
tableExists,
|
|
definition,
|
|
ORM,
|
|
});
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
// allow custom data type for a column
|
|
if (_.has(attribute, 'columnType')) {
|
|
return table.specificType(name, attribute.columnType);
|
|
}
|
|
|
|
switch (attribute.type) {
|
|
case 'uuid':
|
|
return table.uuid(name);
|
|
case 'uid': {
|
|
table.unique(name);
|
|
return table.string(name);
|
|
}
|
|
case 'richtext':
|
|
case 'text':
|
|
return table.text(name, 'longtext');
|
|
case 'json':
|
|
return definition.client === 'pg' ? table.jsonb(name) : table.text(name, 'longtext');
|
|
case 'enumeration':
|
|
case 'string':
|
|
case 'password':
|
|
case 'email':
|
|
return table.string(name);
|
|
case 'integer':
|
|
return table.integer(name);
|
|
case 'biginteger':
|
|
return table.bigInteger(name);
|
|
case 'float':
|
|
return table.double(name);
|
|
case 'decimal':
|
|
return table.decimal(name, 10, 2);
|
|
case 'date':
|
|
return table.date(name);
|
|
case 'time':
|
|
return table.time(name, 3);
|
|
case 'datetime':
|
|
return table.datetime(name);
|
|
case 'timestamp':
|
|
return table.timestamp(name);
|
|
case 'currentTimestamp': {
|
|
const col = table.timestamp(name);
|
|
|
|
if (definition.client !== 'sqlite3' && tableExists) {
|
|
return col;
|
|
}
|
|
|
|
return col.defaultTo(ORM.knex.fn.now());
|
|
}
|
|
case 'boolean':
|
|
return table.boolean(name);
|
|
default:
|
|
return null;
|
|
}
|
|
};
|
|
|
|
// Equilize database tables
|
|
const createOrUpdateTable = async ({ table, attributes, definition, ORM, model }, context) => {
|
|
const tableExists = await ORM.knex.schema.hasTable(table);
|
|
|
|
const createIdType = table => {
|
|
if (definition.primaryKeyType === 'uuid' && definition.client === 'pg') {
|
|
return table
|
|
.specificType('id', 'uuid DEFAULT uuid_generate_v4()')
|
|
.notNullable()
|
|
.primary();
|
|
}
|
|
|
|
return table.increments('id');
|
|
};
|
|
|
|
const createColumns = (tbl, columns, opts = {}) => {
|
|
const { tableExists, alter = false } = opts;
|
|
|
|
Object.keys(columns).forEach(key => {
|
|
const attribute = columns[key];
|
|
|
|
const col = buildColType({
|
|
name: key,
|
|
attribute,
|
|
table: tbl,
|
|
tableExists,
|
|
definition,
|
|
ORM,
|
|
});
|
|
if (!col) return;
|
|
|
|
if (attribute.required === true) {
|
|
if (
|
|
(definition.client !== 'sqlite3' || !tableExists) &&
|
|
!contentTypesUtils.hasDraftAndPublish(model) && // no require constraint to allow drafts
|
|
definition.modelType !== 'component' // no require constraint to allow components in drafts
|
|
) {
|
|
col.notNullable();
|
|
}
|
|
} else {
|
|
col.nullable();
|
|
}
|
|
|
|
if (attribute.unique === true) {
|
|
if (definition.client !== 'sqlite3' || !tableExists) {
|
|
tbl.unique(key, uniqueColName(table, key));
|
|
}
|
|
}
|
|
|
|
if (alter) {
|
|
col.alter();
|
|
}
|
|
});
|
|
};
|
|
|
|
const alterColumns = (tbl, columns, opts = {}) => {
|
|
return createColumns(tbl, columns, { ...opts, alter: true });
|
|
};
|
|
|
|
const createTable = (table, { trx = ORM.knex, ...opts } = {}) => {
|
|
return trx.schema.createTable(table, tbl => {
|
|
createIdType(tbl);
|
|
createColumns(tbl, attributes, { ...opts, tableExists: false });
|
|
});
|
|
};
|
|
|
|
if (!tableExists) {
|
|
await createTable(table);
|
|
return;
|
|
}
|
|
|
|
const attributesNames = Object.keys(attributes);
|
|
|
|
// Fetch existing column
|
|
const columnsInfo = await Promise.all(
|
|
attributesNames.map(attributeName => getColumnInfo(attributeName, table, ORM))
|
|
);
|
|
const nameOfColumnsToAdd = columnsInfo.filter(info => !info.exists).map(info => info.columnName);
|
|
|
|
const columnsToAdd = _.pick(attributes, nameOfColumnsToAdd);
|
|
|
|
// Generate and execute query to add missing column
|
|
if (Object.keys(columnsToAdd).length > 0) {
|
|
await ORM.knex.schema.table(table, tbl => {
|
|
createColumns(tbl, columnsToAdd, { tableExists });
|
|
});
|
|
}
|
|
|
|
const attrsNameWithoutTimestamps = attributesNames.filter(
|
|
columnName => !(definition.options.timestamps || []).includes(columnName)
|
|
);
|
|
|
|
const columnsToAlter = await getColumnsWhereDefinitionChanged(
|
|
attrsNameWithoutTimestamps,
|
|
definition,
|
|
ORM
|
|
);
|
|
|
|
const shouldRebuild =
|
|
columnsToAlter.length > 0 || (definition.client === 'sqlite3' && context.recreateSqliteTable);
|
|
|
|
if (shouldRebuild) {
|
|
switch (definition.client) {
|
|
case 'sqlite3': {
|
|
const tmpTable = `tmp_${table}`;
|
|
|
|
const rebuildTable = async trx => {
|
|
await trx.schema.renameTable(table, tmpTable);
|
|
|
|
// drop possible conflicting indexes
|
|
await Promise.all(
|
|
attributesNames.map(key =>
|
|
trx.raw('DROP INDEX IF EXISTS ??', uniqueColName(table, key))
|
|
)
|
|
);
|
|
|
|
// create the table
|
|
await createTable(table, { trx });
|
|
|
|
const attrs = attributesNames.filter(attributeName =>
|
|
isColumn({
|
|
definition,
|
|
attribute: attributes[attributeName],
|
|
name: attributeName,
|
|
})
|
|
);
|
|
|
|
const allAttrs = ['id', ...attrs];
|
|
|
|
await trx.insert(qb => qb.select(allAttrs).from(tmpTable)).into(table);
|
|
await trx.schema.dropTableIfExists(tmpTable);
|
|
};
|
|
|
|
try {
|
|
await ORM.knex.transaction(trx => rebuildTable(trx));
|
|
} catch (err) {
|
|
if (err.message.includes('UNIQUE constraint failed')) {
|
|
strapi.log.error(
|
|
`Unique constraint fails, make sure to update your data and restart to apply the unique constraint.\n\t- ${err.stack}`
|
|
);
|
|
} else {
|
|
strapi.log.error(`Migration failed`);
|
|
strapi.log.error(err);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
break;
|
|
}
|
|
default: {
|
|
const alterTable = async trx => {
|
|
await Promise.all(
|
|
columnsToAlter.map(col => {
|
|
return ORM.knex.schema
|
|
.alterTable(table, tbl => {
|
|
tbl.dropUnique(col, uniqueColName(table, col));
|
|
})
|
|
.catch(() => {});
|
|
})
|
|
);
|
|
await trx.schema.alterTable(table, tbl => {
|
|
alterColumns(tbl, _.pick(attributes, columnsToAlter), {
|
|
tableExists,
|
|
});
|
|
});
|
|
};
|
|
|
|
try {
|
|
await ORM.knex.transaction(trx => alterTable(trx));
|
|
} catch (err) {
|
|
if (err.code === '23505' && definition.client === 'pg') {
|
|
strapi.log.error(
|
|
`Unique constraint fails, make sure to update your data and restart to apply the unique constraint.\n\t- ${err.message}\n\t- ${err.detail}`
|
|
);
|
|
} else if (definition.client === 'mysql' && err.errno === 1062) {
|
|
strapi.log.error(
|
|
`Unique constraint fails, make sure to update your data and restart to apply the unique constraint.\n\t- ${err.sqlMessage}`
|
|
);
|
|
} else {
|
|
strapi.log.error(`Migration failed`);
|
|
strapi.log.error(err);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
module.exports = async ({ ORM, loadedModel, definition, connection, model }) => {
|
|
const previousDefinition = await getDefinitionFromStore(definition, ORM);
|
|
|
|
// run migrations
|
|
await strapi.db.migrations.run(migrateSchemas, {
|
|
ORM,
|
|
loadedModel,
|
|
previousDefinition,
|
|
definition,
|
|
connection,
|
|
model,
|
|
});
|
|
|
|
// store new definitions
|
|
await storeDefinition(definition, ORM);
|
|
};
|