From dd072c40c3efeb3114e80b873c1643dc738530c7 Mon Sep 17 00:00:00 2001 From: Alexandre Bodin Date: Thu, 5 Dec 2019 11:37:07 +0100 Subject: [PATCH] Refactor column creation to use plain knex schema builder --- .../lib/buildDatabaseSchema.js | 239 +++++++++--------- .../lib/formatter.js | 45 +++- .../lib/mount-models.js | 89 +++---- .../__tests__/attributes.test.js | 2 +- 4 files changed, 192 insertions(+), 183 deletions(-) diff --git a/packages/strapi-connector-bookshelf/lib/buildDatabaseSchema.js b/packages/strapi-connector-bookshelf/lib/buildDatabaseSchema.js index e156ae735a..e368207359 100644 --- a/packages/strapi-connector-bookshelf/lib/buildDatabaseSchema.js +++ b/packages/strapi-connector-bookshelf/lib/buildDatabaseSchema.js @@ -9,6 +9,94 @@ module.exports = async ({ connection, model, }) => { + const createIdType = (table, definition) => { + if (definition.primaryKeyType === 'uuid' && definition.client === 'pg') { + return table + .specificType('id', 'uuid DEFAULT uuid_generate_v4()') + .notNullable() + .primary(); + } + + if (definition.primaryKeyType !== 'integer') { + const col = buildColType({ + name: 'id', + table, + definition, + attribute: { + type: definition.primaryKeyType, + }, + }); + + if (!col) throw new Error('Invalid primaryKeyType'); + + return col.notNullable().primary(); + } + + return table.increments('id'); + }; + + const buildColType = ({ name, attribute, table }) => { + if (!attribute.type) { + const relation = definition.associations.find(association => { + return association.alias === name; + }); + + if (['oneToOne', 'manyToOne', 'oneWay'].includes(relation.nature)) { + return buildColType({ + name, + attribute: { type: definition.primaryKeyType }, + table, + }); + } + + return null; + } + + // allo 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 'richtext': + case 'text': + return table.text(name, 'longtext'); + case 'json': + // return client === 'pg' ? 'jsonb' : 'longtext'; + return table.jsonb(name); + case 'enumeration': + return table.enu(name, attribute.enum || []); + 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); + case 'datetime': + return table.datetime(name); + case 'timestamp': + return table.timestamp(name); + case 'currentTimestamp': + return table.timestamp(name).defaultTo(ORM.knex.fn.now()); + case 'boolean': + return table.boolean(name); + default: + return null; + } + }; + const { hasTimestamps } = loadedModel; let [createAtCol, updatedAtCol] = ['created_at', 'updated_at']; @@ -83,33 +171,26 @@ module.exports = async ({ Object.keys(columns).forEach(key => { const attribute = columns[key]; - const type = getType({ - definition, - attribute, - name: key, - tableExists, - }); - if (type) { - const col = tbl.specificType(key, type); + const col = buildColType({ name: key, attribute, table: tbl }); + if (!col) return; - if (attribute.required === true) { - if (definition.client !== 'sqlite3' || !tableExists) { - col.notNullable(); - } - } else { - col.nullable(); + if (attribute.required === true) { + if (definition.client !== 'sqlite3' || !tableExists) { + col.notNullable(); } + } else { + col.nullable(); + } - if (attribute.unique === true) { - if (definition.client !== 'sqlite3' || !tableExists) { - tbl.unique(key, uniqueColName(table, key)); - } + if (attribute.unique === true) { + if (definition.client !== 'sqlite3' || !tableExists) { + tbl.unique(key, uniqueColName(table, key)); } + } - if (alter) { - col.alter(); - } + if (alter) { + col.alter(); } }); }; @@ -124,7 +205,7 @@ module.exports = async ({ const createTable = (table, { trx = ORM.knex, ...opts } = {}) => { return trx.schema.createTable(table, tbl => { - tbl.specificType('id', getIdType(definition)); + createIdType(tbl, definition); createColumns(tbl, attributes, { ...opts, tableExists: false }); }); }; @@ -201,7 +282,7 @@ module.exports = async ({ await createTable(table, { trx }); const attrs = Object.keys(attributes).filter(attribute => - getType({ + isColumn({ definition, attribute: attributes[attribute], name: attribute, @@ -286,7 +367,7 @@ module.exports = async ({ type: 'currentTimestamp', }; definition.attributes[updatedAtCol] = { - type: 'timestampUpdate', + type: 'currentTimestamp', }; } @@ -376,87 +457,26 @@ module.exports = async ({ } }; -const getType = ({ definition, attribute, name, tableExists = false }) => { - const { client } = definition; - - if (!attribute.type) { - // Add integer value if there is a relation +const isColumn = ({ definition, attribute, name }) => { + if (!_.has(attribute, 'type')) { const relation = definition.associations.find(association => { return association.alias === name; }); - switch (relation.nature) { - case 'oneToOne': - case 'manyToOne': - case 'oneWay': - return definition.primaryKeyType; - default: - return null; + if (!relation) return false; + + if (['oneToOne', 'manyToOne', 'oneWay'].includes(relation.nature)) { + return true; } + + return false; } - switch (attribute.type) { - case 'uuid': - return client === 'pg' ? 'uuid' : 'varchar(36)'; - case 'richtext': - case 'text': - return client === 'pg' ? 'text' : 'longtext'; - case 'json': - return client === 'pg' ? 'jsonb' : 'longtext'; - case 'string': - case 'enumeration': - case 'password': - case 'email': - return 'varchar(255)'; - case 'integer': - return client === 'pg' ? 'integer' : 'int'; - case 'biginteger': - if (client === 'sqlite3') return 'bigint(53)'; // no choice until the sqlite3 package supports returning strings for big integers - return 'bigint'; - case 'float': - return client === 'pg' ? 'double precision' : 'double'; - case 'decimal': - return 'decimal(10,2)'; - case 'date': - return 'date'; - case 'time': - return 'time'; - case 'datetime': { - if (client === 'pg') return 'timestampz'; - return 'datetime'; - } - case 'timestamp': { - if (client === 'pg') { - return 'timestampz'; - } - - return 'timestamp'; - } - case 'currentTimestamp': { - if (client === 'pg') { - return 'timestamp with time zone DEFAULT CURRENT_TIMESTAMP'; - } else if (client === 'sqlite3' && tableExists) { - return 'timestamp DEFAULT NULL'; - } - - return 'timestamp DEFAULT CURRENT_TIMESTAMP'; - } - case 'timestampUpdate': - switch (client) { - case 'pg': - return 'timestamp with time zone DEFAULT CURRENT_TIMESTAMP'; - case 'sqlite3': - if (tableExists) { - return 'timestamp DEFAULT NULL'; - } - return 'timestamp DEFAULT CURRENT_TIMESTAMP'; - default: - return 'timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'; - } - case 'boolean': - return 'boolean'; - default: + if (['component', 'dynamiczone'].includes(attribute.type)) { + return false; } + + return true; }; const storeTable = async (table, attributes) => { @@ -479,29 +499,4 @@ const storeTable = async (table, attributes) => { }).save(); }; -const defaultIdType = { - mysql: 'INT AUTO_INCREMENT NOT NULL PRIMARY KEY', - pg: 'SERIAL NOT NULL PRIMARY KEY', - sqlite3: 'INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL', -}; - -const getIdType = definition => { - if (definition.primaryKeyType === 'uuid' && definition.client === 'pg') { - return 'uuid NOT NULL DEFAULT uuid_generate_v4() NOT NULL PRIMARY KEY'; - } - - if (definition.primaryKeyType !== 'integer') { - const type = getType({ - definition, - attribute: { - type: definition.primaryKeyType, - }, - }); - - return `${type} NOT NULL PRIMARY KEY`; - } - - return defaultIdType[definition.client]; -}; - const uniqueColName = (table, key) => `${table}_${key}_unique`; diff --git a/packages/strapi-connector-bookshelf/lib/formatter.js b/packages/strapi-connector-bookshelf/lib/formatter.js index 265c82e3e6..0a8cbdfa44 100644 --- a/packages/strapi-connector-bookshelf/lib/formatter.js +++ b/packages/strapi-connector-bookshelf/lib/formatter.js @@ -19,7 +19,10 @@ const createFormatter = client => ({ type }, value) => { }; const defaultFormatter = { - json: value => JSON.parse(value), + json: value => { + if (typeof value === 'object') return value; + return JSON.parse(value); + }, boolean: value => { if (typeof value === 'boolean') { return value; @@ -34,26 +37,42 @@ const defaultFormatter = { return null; } }, + date: value => { + const cast = new Date(value); + return isValid(cast) ? formatISO(cast, { representation: 'date' }) : null; + }, + datetime: value => { + const cast = new Date(value); + return isValid(cast) ? cast.toISOString() : null; + }, + timestamp: value => { + const cast = new Date(value); + return isValid(cast) ? format(cast, 'T') : null; + }, }; const formatters = { sqlite3: { - date: value => { - const cast = new Date(value); - return isValid(cast) ? formatISO(cast, { representation: 'date' }) : null; - }, - datetime: value => { - const cast = new Date(value); - return isValid(cast) ? cast.toISOString() : null; - }, - timestamp: value => { - const cast = new Date(value); - return isValid(cast) ? format(cast, 'T') : null; - }, biginteger: value => { return value.toString(); }, }, + mysql: { + boolean: value => { + if (typeof value === 'boolean') { + return value; + } + + const strVal = value.toString(); + if (strVal === '1') { + return true; + } else if (strVal === '0') { + return false; + } else { + return null; + } + }, + }, }; module.exports = { diff --git a/packages/strapi-connector-bookshelf/lib/mount-models.js b/packages/strapi-connector-bookshelf/lib/mount-models.js index 8e85af495b..e78251f92c 100644 --- a/packages/strapi-connector-bookshelf/lib/mount-models.js +++ b/packages/strapi-connector-bookshelf/lib/mount-models.js @@ -464,7 +464,8 @@ module.exports = ({ models, target, plugin = false }, ctx) => { const mapper = (params = {}) => { Object.keys(params).map(key => { const attr = definition.attributes[key] || {}; - params[key] = parseValue(attr.type, params[key], definition); + + params[key] = parseValue(attr.type, params[key]); }); return _.mapKeys(params, (value, key) => { @@ -613,60 +614,54 @@ module.exports = ({ models, target, plugin = false }, ctx) => { : Promise.resolve(); }); - this.on('saving', instance => { - instance.attributes = mapper(instance.attributes); + this.on('saving', (instance, attrs) => { + instance.attributes = mapper(attrs); return _.isFunction(target[model.toLowerCase()]['beforeSave']) ? target[model.toLowerCase()]['beforeSave'] : Promise.resolve(); }); - // Convert to JSON format stringify json for mysql database - if (definition.client === 'mysql' || definition.client === 'sqlite3') { - const events = [ - { - name: 'saved', - target: 'afterSave', - }, - { - name: 'fetched', - target: 'afterFetch', - }, - { - name: 'fetched:collection', - target: 'afterFetchAll', - }, - ]; - - const formatValue = createFormatter(definition.client); - const formatter = attributes => { - Object.keys(attributes).forEach(key => { - const attr = definition.attributes[key] || {}; - attributes[key] = formatValue(attr, attributes[key]); - }); - }; - - events.forEach(event => { - let fn; - - if (event.name.indexOf('collection') !== -1) { - fn = instance => - instance.models.map(entry => { - formatter(entry.attributes); - }); - } else { - fn = instance => formatter(instance.attributes); - } - - this.on(event.name, instance => { - fn(instance); - - return _.isFunction(target[model.toLowerCase()][event.target]) - ? target[model.toLowerCase()][event.target] - : Promise.resolve(); - }); + const formatValue = createFormatter(definition.client); + function formatEntry(entry) { + Object.keys(entry.attributes).forEach(key => { + const attr = definition.attributes[key] || {}; + entry.attributes[key] = formatValue(attr, entry.attributes[key]); }); } + + function formatOutput(instance) { + if (Array.isArray(instance.models)) { + instance.models.map(formatEntry); + } else { + formatEntry(instance); + } + } + + const events = [ + { + name: 'saved', + target: 'afterSave', + }, + { + name: 'fetched', + target: 'afterFetch', + }, + { + name: 'fetched:collection', + target: 'afterFetchAll', + }, + ]; + + events.forEach(event => { + this.on(event.name, instance => { + formatOutput(instance); + + return _.isFunction(target[model.toLowerCase()][event.target]) + ? target[model.toLowerCase()][event.target] + : Promise.resolve(); + }); + }); }; loadedModel.hidden = _.keys( diff --git a/packages/strapi-plugin-content-manager/services/utils/configuration/__tests__/attributes.test.js b/packages/strapi-plugin-content-manager/services/utils/configuration/__tests__/attributes.test.js index aec2118a79..2d1b95b528 100644 --- a/packages/strapi-plugin-content-manager/services/utils/configuration/__tests__/attributes.test.js +++ b/packages/strapi-plugin-content-manager/services/utils/configuration/__tests__/attributes.test.js @@ -16,7 +16,7 @@ const createMockSchema = (attrs, timestamps = true) => { type: 'timestamp', }, updatedAt: { - type: 'timestampUpdate', + type: 'timestamp', }, } : {}),