strapi/packages/strapi-connector-bookshelf/lib/build-database-schema.js
Jean-Sébastien Herbaux 1e5b1c99f6
I18n/ permissions rework (#9535)
* 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>
2021-03-25 14:59:44 +01:00

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