Refactor column creation to use plain knex schema builder

This commit is contained in:
Alexandre Bodin 2019-12-05 11:37:07 +01:00
parent 909266232f
commit dd072c40c3
4 changed files with 192 additions and 183 deletions

View File

@ -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,15 +171,9 @@ 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) {
@ -110,7 +192,6 @@ module.exports = async ({
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;
}
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 false;
}
return 'timestamp';
}
case 'currentTimestamp': {
if (client === 'pg') {
return 'timestamp with time zone DEFAULT CURRENT_TIMESTAMP';
} else if (client === 'sqlite3' && tableExists) {
return 'timestamp DEFAULT NULL';
if (['component', 'dynamiczone'].includes(attribute.type)) {
return false;
}
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:
}
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`;

View File

@ -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,10 +37,6 @@ const defaultFormatter = {
return null;
}
},
};
const formatters = {
sqlite3: {
date: value => {
const cast = new Date(value);
return isValid(cast) ? formatISO(cast, { representation: 'date' }) : null;
@ -50,10 +49,30 @@ const formatters = {
const cast = new Date(value);
return isValid(cast) ? format(cast, 'T') : null;
},
};
const formatters = {
sqlite3: {
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 = {

View File

@ -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,16 +614,30 @@ 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 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',
@ -638,35 +653,15 @@ module.exports = ({ models, target, plugin = false }, ctx) => {
},
];
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);
formatOutput(instance);
return _.isFunction(target[model.toLowerCase()][event.target])
? target[model.toLowerCase()][event.target]
: Promise.resolve();
});
});
}
};
loadedModel.hidden = _.keys(

View File

@ -16,7 +16,7 @@ const createMockSchema = (attrs, timestamps = true) => {
type: 'timestamp',
},
updatedAt: {
type: 'timestampUpdate',
type: 'timestamp',
},
}
: {}),