use columnName snake casing and rename timestamp attributes

This commit is contained in:
Alexandre Bodin 2021-09-22 10:49:43 +02:00
parent 872e7317ca
commit 58e024d3ad
18 changed files with 205 additions and 184 deletions

View File

@ -5,12 +5,14 @@
"displayName": "Category",
"singularName": "category",
"pluralName": "categories",
"description": ""
"description": "",
"name": "Category"
},
"options": {
"draftAndPublish": true,
"comment": ""
},
"pluginOptions": {},
"attributes": {
"name": {
"type": "string"

View File

@ -34,7 +34,7 @@
"targetField": "name",
"pluginOptions": {}
},
"price_range": {
"priceRange": {
"enum": [
"very_cheap",
"cheap",
@ -49,7 +49,7 @@
}
}
},
"closing_period": {
"closingPeriod": {
"component": "default.closingperiod",
"type": "component",
"pluginOptions": {
@ -58,7 +58,7 @@
}
}
},
"contact_email": {
"contactEmail": {
"type": "email",
"pluginOptions": {
"i18n": {
@ -115,7 +115,7 @@
}
}
},
"short_description": {
"shortDescription": {
"type": "text",
"pluginOptions": {
"i18n": {
@ -163,7 +163,7 @@
"target": "api::menu.menu",
"inversedBy": "restaurant"
},
"opening_times": {
"openingTimes": {
"component": "default.openingtimes",
"type": "component",
"repeatable": true,

View File

@ -17,7 +17,7 @@ const data = {
editorRole: undefined,
};
const omitTimestamps = obj => _.omit(obj, ['updatedAt', 'createdAt', 'updated_at', 'created_at']);
const omitTimestamps = obj => _.omit(obj, ['updatedAt', 'createdAt']);
describe('Role CRUD End to End', () => {
let rq;

View File

@ -7,7 +7,7 @@ const { createUtils } = require('../../../../../test/helpers/utils');
const edition = process.env.STRAPI_DISABLE_EE === 'true' ? 'CE' : 'EE';
const omitTimestamps = obj => _.omit(obj, ['updatedAt', 'createdAt', 'updated_at', 'created_at']);
const omitTimestamps = obj => _.omit(obj, ['updatedAt', 'createdAt']);
/**
* == Test Suite Overview ==

View File

@ -9,8 +9,6 @@ const { createAuthRequest } = require('../../../../test/helpers/request');
const cleanDate = entry => {
delete entry.updatedAt;
delete entry.createdAt;
delete entry.created_at;
delete entry.updated_at;
};
const builder = createTestBuilder();

View File

@ -29,7 +29,7 @@ const toAssocs = data => {
});
};
const toRow = (metadata, data = {}, { withDefaults = false } = {}) => {
const processData = (metadata, data = {}, { withDefaults = false } = {}) => {
const { attributes } = metadata;
const obj = {};
@ -37,11 +37,9 @@ const toRow = (metadata, data = {}, { withDefaults = false } = {}) => {
for (const attributeName in attributes) {
const attribute = attributes[attributeName];
// TODO: convert to column name
if (types.isScalar(attribute.type)) {
const field = createField(attribute);
// TODO: move application level default to entity service
if (_.isUndefined(data[attributeName])) {
if (!_.isUndefined(attribute.default) && withDefaults) {
if (typeof attribute.default === 'function') {
@ -65,7 +63,6 @@ const toRow = (metadata, data = {}, { withDefaults = false } = {}) => {
if (types.isRelation(attribute.type)) {
// oneToOne & manyToOne
if (attribute.joinColumn && attribute.owner) {
// TODO: ensure joinColumn name respect convention ?
const joinColumnName = attribute.joinColumn.name;
// allow setting to null
@ -167,7 +164,7 @@ const createEntityManager = db => {
throw new Error('Create expects a data object');
}
const dataToInsert = toRow(metadata, data, { withDefaults: true });
const dataToInsert = processData(metadata, data, { withDefaults: true });
const [id] = await this.createQueryBuilder(uid)
.insert(dataToInsert)
@ -199,7 +196,7 @@ const createEntityManager = db => {
throw new Error('CreateMany expects data to be an array');
}
const dataToInsert = data.map(datum => toRow(metadata, datum, { withDefaults: true }));
const dataToInsert = data.map(datum => processData(metadata, datum, { withDefaults: true }));
if (_.isEmpty(dataToInsert)) {
throw new Error('Nothing to insert');
@ -242,7 +239,7 @@ const createEntityManager = db => {
const { id } = entity;
const dataToUpdate = toRow(metadata, data);
const dataToUpdate = processData(metadata, data);
if (!_.isEmpty(dataToUpdate)) {
await this.createQueryBuilder(uid)
@ -272,7 +269,7 @@ const createEntityManager = db => {
const metadata = db.metadata.get(uid);
const { where, data } = params;
const dataToUpdate = toRow(metadata, data);
const dataToUpdate = processData(metadata, data);
if (_.isEmpty(dataToUpdate)) {
throw new Error('Update requires data');

View File

@ -14,18 +14,18 @@ const _ = require('lodash');
*/
const timestampsLifecyclesSubscriber = {
/**
* Init created_at & updated_at before create
* Init createdAt & updatedAt before create
* @param {Event} event
*/
beforeCreate(event) {
const { data } = event.params;
const now = new Date();
_.defaults(data, { created_at: now, updated_at: now });
_.defaults(data, { createdAt: now, updatedAt: now });
},
/**
* Init created_at & updated_at before create
* Init createdAt & updatedAt before create
* @param {Event} event
*/
beforeCreateMany(event) {
@ -33,23 +33,23 @@ const timestampsLifecyclesSubscriber = {
const now = new Date();
if (_.isArray(data)) {
data.forEach(data => _.defaults(data, { created_at: now, updated_at: now }));
data.forEach(data => _.defaults(data, { createdAt: now, updatedAt: now }));
}
},
/**
* Update updated_at before update
* Update updatedAt before update
* @param {Event} event
*/
beforeUpdate(event) {
const { data } = event.params;
const now = new Date();
_.assign(data, { updated_at: now });
_.assign(data, { updatedAt: now });
},
/**
* Update updated_at before update
* Update updatedAt before update
* @param {Event} event
*/
beforeUpdateMany(event) {
@ -57,7 +57,7 @@ const timestampsLifecyclesSubscriber = {
const now = new Date();
if (_.isArray(data)) {
data.forEach(data => _.assign(data, { updated_at: now }));
data.forEach(data => _.assign(data, { updatedAt: now }));
}
},
};

View File

@ -1,8 +1,3 @@
/**
* @module metadata
*
*/
'use strict';
const _ = require('lodash/fp');
@ -16,43 +11,13 @@ class Metadata extends Map {
}
}
// TODO: check if there isn't an attribute with an id already
/**
* Create Metadata from models configurations
*
* timestamps => not optional anymore but auto added. Auto added on the content type or in the db layer ?
*
* options => options are handled on the layer above. Options convert to fields on the CT
*
* filters => not in v1
*
* attributes
*
* - type
* - mapping field name - column name
* - mapping field type - column type
* - formatter / parser => coming from field type so no
* - indexes / checks / contstraints
* - relations => reference to the target model (function or string to avoid circular deps ?)
* - name of the LEFT/RIGHT side foreign keys
* - name of join table
*
* - compo/dz => reference to the components
* - validators
* - hooks
* - default value
* - required -> should add a not null option instead of the API required
* - unique -> should add a DB unique option instead of the unique in the API (Unique by locale or something else for example)
*
* lifecycles
*
* private fields ? => handled on a different layer
* @param {object[]} models
* @returns {Metadata}
*/
const createMetadata = (models = []) => {
// TODO: reorder to make sure we can create everything or delete everything in the right order
// TODO: allow passing the join config in the attribute
// TODO: allow passing column config in the attribute
const metadata = new Metadata();
// init pass
@ -62,7 +27,6 @@ const createMetadata = (models = []) => {
uid: model.uid,
tableName: model.tableName,
attributes: {
// TODO: check if there isn't an attribute with an id already
id: {
type: 'increments',
},
@ -83,67 +47,12 @@ const createMetadata = (models = []) => {
for (const [attributeName, attribute] of Object.entries(meta.attributes)) {
try {
if (types.isComponent(attribute.type)) {
// convert component to relation
Object.assign(attribute, {
type: 'relation',
relation: attribute.repeatable === true ? 'oneToMany' : 'oneToOne',
target: attribute.component,
joinTable: {
name: meta.componentLink.tableName,
joinColumn: {
name: 'entity_id',
referencedColumn: 'id',
},
inverseJoinColumn: {
name: 'component_id',
referencedColumn: 'id',
},
on: {
field: attributeName,
},
orderBy: {
order: 'asc',
},
},
});
createComponent(attributeName, attribute, meta, metadata);
continue;
}
if (types.isDynamicZone(attribute.type)) {
//
Object.assign(attribute, {
type: 'relation',
relation: 'morphToMany',
// TODO: handle restrictions at some point
// target: attribute.components,
joinTable: {
name: meta.componentLink.tableName,
joinColumn: {
name: 'entity_id',
referencedColumn: 'id',
},
morphColumn: {
idColumn: {
name: 'component_id',
referencedColumn: 'id',
},
typeColumn: {
name: 'component_type',
},
typeField: '__component',
},
on: {
field: attributeName,
},
orderBy: {
order: 'asc',
},
},
});
createDynamicZone(attributeName, attribute, meta, metadata);
continue;
}
@ -151,7 +60,10 @@ const createMetadata = (models = []) => {
createRelation(attributeName, attribute, meta, metadata);
continue;
}
createAttribute(attributeName, attribute, meta, metadata);
} catch (error) {
console.log(error);
throw new Error(
`Error on attribute ${attributeName} in model ${meta.singularName}(${meta.uid}): ${error.message}`
);
@ -159,6 +71,15 @@ const createMetadata = (models = []) => {
}
}
for (const meta of metadata.values()) {
const columnToAttribute = Object.keys(meta.attributes).reduce((acc, key) => {
const attribute = meta.attributes[key];
return Object.assign(acc, { [attribute.columnName || key]: key });
}, {});
meta.columnToAttribute = columnToAttribute;
}
return metadata;
};
@ -233,4 +154,66 @@ const createCompoLinkModelMeta = baseModelMeta => {
};
};
const createDynamicZone = (attributeName, attribute, meta) => {
Object.assign(attribute, {
type: 'relation',
relation: 'morphToMany',
// TODO: handle restrictions at some point
// target: attribute.components,
joinTable: {
name: meta.componentLink.tableName,
joinColumn: {
name: 'entity_id',
referencedColumn: 'id',
},
morphColumn: {
idColumn: {
name: 'component_id',
referencedColumn: 'id',
},
typeColumn: {
name: 'component_type',
},
typeField: '__component',
},
on: {
field: attributeName,
},
orderBy: {
order: 'asc',
},
},
});
};
const createComponent = (attributeName, attribute, meta) => {
Object.assign(attribute, {
type: 'relation',
relation: attribute.repeatable === true ? 'oneToMany' : 'oneToOne',
target: attribute.component,
joinTable: {
name: meta.componentLink.tableName,
joinColumn: {
name: 'entity_id',
referencedColumn: 'id',
},
inverseJoinColumn: {
name: 'component_id',
referencedColumn: 'id',
},
on: {
field: attributeName,
},
orderBy: {
order: 'asc',
},
},
});
};
const createAttribute = (attributeName, attribute) => {
const columnName = _.snakeCase(attributeName);
Object.assign(attribute, { columnName });
};
module.exports = createMetadata;

View File

@ -4,10 +4,12 @@ const _ = require('lodash/fp');
const types = require('../../types');
const { createJoin } = require('./join');
const { toColumnName } = require('./transform');
const processOrderBy = (orderBy, ctx) => {
const { db, uid, qb, alias } = ctx;
const { attributes } = db.metadata.get(uid);
const meta = db.metadata.get(uid);
const { attributes } = meta;
if (typeof orderBy === 'string') {
const attribute = attributes[orderBy];
@ -16,7 +18,9 @@ const processOrderBy = (orderBy, ctx) => {
throw new Error(`Attribute ${orderBy} not found on model ${uid}`);
}
return [{ column: qb.aliasColumn(orderBy, alias) }];
const columnName = toColumnName(meta, orderBy);
return [{ column: qb.aliasColumn(columnName, alias) }];
}
if (Array.isArray(orderBy)) {
@ -33,7 +37,9 @@ const processOrderBy = (orderBy, ctx) => {
}
if (types.isScalar(attribute.type)) {
return { column: qb.aliasColumn(key, alias), order: direction };
const columnName = toColumnName(meta, key);
return { column: qb.aliasColumn(columnName, alias), order: direction };
}
if (attribute.type === 'relation') {

View File

@ -3,11 +3,13 @@
const _ = require('lodash/fp');
const types = require('../../types');
const { toColumnName } = require('./transform');
const applySearch = (qb, query, ctx) => {
const { alias, uid, db } = ctx;
const applySearch = (knex, query, ctx) => {
const { qb, uid, db } = ctx;
const meta = db.metadata.get(uid);
const { attributes } = db.metadata.get(uid);
const { attributes } = meta;
const searchColumns = ['id'];
@ -29,22 +31,34 @@ const applySearch = (qb, query, ctx) => {
switch (db.dialect.client) {
case 'postgres': {
searchColumns.forEach(attr =>
qb.orWhereRaw(`"${alias}"."${attr}"::text ILIKE ?`, `%${escapeQuery(query, '*%\\')}%`)
);
searchColumns.forEach(attr => {
const columnName = toColumnName(meta, attr);
return knex.orWhereRaw(`??::text ILIKE ?`, [
qb.aliasColumn(columnName),
`%${escapeQuery(query, '*%\\')}%`,
]);
});
break;
}
case 'sqlite': {
searchColumns.forEach(attr =>
qb.orWhereRaw(`"${alias}"."${attr}" LIKE ? ESCAPE '\\'`, `%${escapeQuery(query, '*%\\')}%`)
);
searchColumns.forEach(attr => {
const columnName = toColumnName(meta, attr);
return knex.orWhereRaw(`?? LIKE ? ESCAPE '\\'`, [
qb.aliasColumn(columnName),
`%${escapeQuery(query, '*%\\')}%`,
]);
});
break;
}
case 'mysql': {
searchColumns.forEach(attr =>
qb.orWhereRaw(`\`${alias}\`.\`${attr}\` LIKE ?`, `%${escapeQuery(query, '*%\\')}%`)
);
searchColumns.forEach(attr => {
const columnName = toColumnName(meta, attr);
return knex.orWhereRaw(`?? LIKE ?`, [
qb.aliasColumn(columnName),
`%${escapeQuery(query, '*%\\')}%`,
]);
});
break;
}
default: {

View File

@ -5,12 +5,12 @@ const _ = require('lodash/fp');
const types = require('../../types');
const { createField } = require('../../fields');
const fromRow = (metadata, row) => {
const fromRow = (meta, row) => {
if (Array.isArray(row)) {
return row.map(singleRow => fromRow(metadata, singleRow));
return row.map(singleRow => fromRow(meta, singleRow));
}
const { attributes } = metadata;
const { attributes } = meta;
if (_.isNil(row)) {
return null;
@ -19,25 +19,16 @@ const fromRow = (metadata, row) => {
const obj = {};
for (const column in row) {
// to field Name
const attributeName = column;
if (!attributes[attributeName]) {
// ignore value that are not related to an attribute (join columns ...)
if (!_.has(column, meta.columnToAttribute)) {
continue;
}
const attributeName = meta.columnToAttribute[column];
const attribute = attributes[attributeName];
if (types.isScalar(attribute.type)) {
// TODO: we convert to column name
// TODO: handle default value too
// TODO: format data & use dialect to know which type they support (json particularly)
const field = createField(attribute);
// TODO: validate data on creation
// field.validate(data[attributeName]);
const val = row[column] === null ? null : field.fromDB(row[column]);
obj[attributeName] = val;
@ -51,6 +42,43 @@ const fromRow = (metadata, row) => {
return obj;
};
module.exports = {
fromRow,
const toRow = (meta, data = {}) => {
if (_.isNil(data)) {
return data;
}
if (_.isArray(data)) {
return data.map(datum => toRow(meta, datum));
}
const { attributes } = meta;
for (const key in data) {
const attribute = attributes[key];
if (!attribute || attribute.columnName === key) {
continue;
}
data[attribute.columnName] = data[key];
delete data[key];
}
return data;
};
const toColumnName = (meta, name) => {
const attribute = meta.attributes[name];
if (!attribute) {
return name;
}
return attribute.columnName || name;
};
module.exports = {
toRow,
fromRow,
toColumnName,
};

View File

@ -4,6 +4,7 @@ const _ = require('lodash/fp');
const types = require('../../types');
const { createJoin } = require('./join');
const { toColumnName } = require('./transform');
const GROUP_OPERATORS = ['$and', '$or'];
const OPERATORS = [
@ -56,13 +57,14 @@ const processWhere = (where, ctx, depth = 0) => {
};
const { db, uid, qb, alias } = ctx;
const meta = db.metadata.get(uid);
const filters = {};
// for each key in where
for (const key in where) {
const value = where[key];
const attribute = db.metadata.get(uid).attributes[key];
const attribute = meta.attributes[key];
// if operator $and $or then loop over them
if (GROUP_OPERATORS.includes(key)) {
@ -87,14 +89,8 @@ const processWhere = (where, ctx, depth = 0) => {
}
if (!attribute) {
// TODO: if targeting a column name instead of an attribute
// if key as an alias don't add one
if (key.indexOf('.') >= 0) {
filters[key] = processNested(value, ctx);
} else {
filters[qb.aliasColumn(key, alias)] = processNested(value, ctx);
}
continue;
// throw new Error(`Attribute ${key} not found on model ${uid}`);
@ -130,9 +126,10 @@ const processWhere = (where, ctx, depth = 0) => {
}
if (types.isScalar(attribute.type)) {
// TODO: convert attribute name to column name
const columnName = toColumnName(meta, key);
// TODO: cast to DB type
filters[qb.aliasColumn(key, alias)] = processNested(value, ctx);
filters[qb.aliasColumn(columnName, alias)] = processNested(value, ctx);
continue;
}

View File

@ -57,7 +57,7 @@ const createQueryBuilder = (uid, db) => {
},
ref(name) {
return db.connection.ref(name);
return db.connection.ref(helpers.toColumnName(meta, name));
},
update(data) {
@ -168,20 +168,20 @@ const createQueryBuilder = (uid, db) => {
return ['select', 'count'].includes(state.type);
},
aliasColumn(columnName, alias) {
if (typeof columnName !== 'string') {
return columnName;
aliasColumn(key, alias) {
if (typeof key !== 'string') {
return key;
}
if (columnName.indexOf('.') >= 0) {
return columnName;
if (key.indexOf('.') >= 0) {
return key;
}
if (!_.isNil(alias)) {
return `${alias}.${columnName}`;
return `${alias}.${key}`;
}
return this.mustUseAlias() ? `${this.alias}.${columnName}` : columnName;
return this.mustUseAlias() ? `${this.alias}.${key}` : key;
},
raw(...args) {
@ -205,9 +205,11 @@ const createQueryBuilder = (uid, db) => {
},
processState() {
state.select = state.select.map(field => helpers.toColumnName(meta, field));
state.orderBy = helpers.processOrderBy(state.orderBy, { qb: this, uid, db });
state.where = helpers.processWhere(state.where, { qb: this, uid, db });
state.populate = helpers.processPopulate(state.populate, { qb: this, uid, db });
state.data = helpers.toRow(meta, state.data);
},
getKnexQuery() {
@ -299,7 +301,7 @@ const createQueryBuilder = (uid, db) => {
// if there are joins and it is a delete or update use a sub query
if (state.search) {
qb.where(subQb => {
helpers.applySearch(subQb, state.search, { alias: this.alias, db, uid });
helpers.applySearch(subQb, state.search, { qb: this, db, uid });
});
}

View File

@ -14,17 +14,11 @@ const createColumn = (name, attribute) => {
unsigned: false,
...opts,
...(attribute.column || {}),
// TODO: allow passing custom params to the DB from the model definition
};
};
const shouldCreateColumn = attribute => {
return types.isScalar(attribute.type);
};
const createTable = meta => {
const table = {
// TODO: allow passing custom params to the DB from the model definition
name: meta.tableName,
indexes: meta.indexes || [],
foreignKeys: meta.foreignKeys || [],
@ -76,8 +70,8 @@ const createTable = meta => {
columns: [columnName],
});
}
} else if (shouldCreateColumn(attribute)) {
const column = createColumn(key, meta.attributes[key]);
} else if (types.isScalar(attribute.type)) {
const column = createColumn(attribute.columnName || key, attribute);
if (column.unique) {
table.indexes.push({

View File

@ -20,7 +20,7 @@ const enableDraftAndPublish = async ({ oldContentTypes, contentTypes }) => {
if (!hasDraftAndPublish(oldContentType) && hasDraftAndPublish(contentType)) {
const qb = strapi.db.queryBuilder(uid);
await qb
.update({ published_at: qb.ref('created_at') })
.update({ published_at: qb.ref('createdAt') })
.where({ published_at: null })
.execute();
}

View File

@ -64,8 +64,8 @@ describe('Content types utils', () => {
expect(getNonWritableAttributes(model)).toEqual([
'id',
'created_at',
'updated_at',
'createdAt',
'updatedAt',
'non_writable_field',
]);
});

View File

@ -10,8 +10,8 @@ const PUBLISHED_AT_ATTRIBUTE = 'published_at';
const CREATED_BY_ATTRIBUTE = 'created_by';
const UPDATED_BY_ATTRIBUTE = 'updated_by';
const CREATED_AT_ATTRIBUTE = 'created_at';
const UPDATED_AT_ATTRIBUTE = 'updated_at';
const CREATED_AT_ATTRIBUTE = 'createdAt';
const UPDATED_AT_ATTRIBUTE = 'updatedAt';
const DP_PUB_STATE_LIVE = 'live';
const DP_PUB_STATE_PREVIEW = 'preview';

View File

@ -11,7 +11,7 @@ const data = {
deletedLocales: [],
};
const omitTimestamps = omit(['updatedAt', 'createdAt', 'updated_at', 'created_at']);
const omitTimestamps = omit(['updatedAt', 'createdAt']);
const compareLocales = (a, b) => (a.code < b.code ? -1 : 1);
const productModel = {