mirror of
https://github.com/strapi/strapi.git
synced 2025-08-19 06:08:50 +00:00
Merge branch 'features/dynamic-zones' of github.com:strapi/strapi into front/dynamic-zones-ctb-listview
This commit is contained in:
commit
7b6a24c99e
@ -9,6 +9,95 @@ 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 definition.client === 'pg'
|
||||
? table.jsonb(name)
|
||||
: table.text(name, 'longtext');
|
||||
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, 3);
|
||||
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 +172,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 +206,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 +283,7 @@ module.exports = async ({
|
||||
await createTable(table, { trx });
|
||||
|
||||
const attrs = Object.keys(attributes).filter(attribute =>
|
||||
getType({
|
||||
isColumn({
|
||||
definition,
|
||||
attribute: attributes[attribute],
|
||||
name: attribute,
|
||||
@ -283,15 +365,22 @@ module.exports = async ({
|
||||
// Add created_at and updated_at field if timestamp option is true
|
||||
if (hasTimestamps) {
|
||||
definition.attributes[createAtCol] = {
|
||||
type: 'timestamp',
|
||||
type: 'currentTimestamp',
|
||||
};
|
||||
definition.attributes[updatedAtCol] = {
|
||||
type: 'timestampUpdate',
|
||||
type: 'currentTimestamp',
|
||||
};
|
||||
}
|
||||
|
||||
// Save all attributes (with timestamps)
|
||||
model.allAttributes = _.clone(definition.attributes);
|
||||
// Save all attributes (with timestamps) and right type
|
||||
model.allAttributes = _.assign(_.clone(definition.attributes), {
|
||||
[createAtCol]: {
|
||||
type: 'timestamp',
|
||||
},
|
||||
[updatedAtCol]: {
|
||||
type: 'timestamp',
|
||||
},
|
||||
});
|
||||
|
||||
// Equilize tables
|
||||
if (connection.options && connection.options.autoMigration !== false) {
|
||||
@ -369,79 +458,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)';
|
||||
// TODO: split time types as they should be different
|
||||
case 'date':
|
||||
case 'time':
|
||||
case 'datetime':
|
||||
if (client === 'pg') {
|
||||
return 'timestamp with time zone';
|
||||
}
|
||||
|
||||
return 'timestamp';
|
||||
case 'timestamp':
|
||||
if (client === 'pg') {
|
||||
return 'timestamp with time zone';
|
||||
} 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';
|
||||
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) => {
|
||||
@ -464,29 +500,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`;
|
||||
|
64
packages/strapi-connector-bookshelf/lib/formatter.js
Normal file
64
packages/strapi-connector-bookshelf/lib/formatter.js
Normal file
@ -0,0 +1,64 @@
|
||||
'use strict';
|
||||
|
||||
const { isValid, format, formatISO } = require('date-fns');
|
||||
const { has } = require('lodash');
|
||||
|
||||
const createFormatter = client => ({ type }, value) => {
|
||||
if (value === null) return null;
|
||||
|
||||
const formatter = {
|
||||
...defaultFormatter,
|
||||
...formatters[client],
|
||||
};
|
||||
|
||||
if (has(formatter, type)) {
|
||||
return formatter[type](value);
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
const defaultFormatter = {
|
||||
json: value => {
|
||||
if (typeof value === 'object') return value;
|
||||
return JSON.parse(value);
|
||||
},
|
||||
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;
|
||||
}
|
||||
},
|
||||
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: {
|
||||
biginteger: value => {
|
||||
return value.toString();
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
createFormatter,
|
||||
};
|
@ -78,7 +78,10 @@ const createComponentJoinTables = async ({ definition, ORM }) => {
|
||||
.notNullable();
|
||||
table.string('component_type').notNullable();
|
||||
table.integer('component_id').notNullable();
|
||||
table.integer(joinColumn).notNullable();
|
||||
table
|
||||
.integer(joinColumn)
|
||||
.unsigned()
|
||||
.notNullable();
|
||||
|
||||
table
|
||||
.foreign(joinColumn)
|
||||
|
@ -1,7 +1,6 @@
|
||||
'use strict';
|
||||
const _ = require('lodash');
|
||||
const { singular } = require('pluralize');
|
||||
const dateFns = require('date-fns');
|
||||
|
||||
const utilsModels = require('strapi-utils').models;
|
||||
const relations = require('./relations');
|
||||
@ -10,6 +9,8 @@ const {
|
||||
createComponentJoinTables,
|
||||
createComponentModels,
|
||||
} = require('./generate-component-relations');
|
||||
const { createParser } = require('./parser');
|
||||
const { createFormatter } = require('./formatter');
|
||||
|
||||
const populateFetch = require('./populate');
|
||||
|
||||
@ -457,12 +458,14 @@ module.exports = ({ models, target, plugin = false }, ctx) => {
|
||||
// Call this callback function after we are done parsing
|
||||
// all attributes for relationships-- see below.
|
||||
|
||||
const parseValue = createParser();
|
||||
try {
|
||||
// External function to map key that has been updated with `columnName`
|
||||
const mapper = (params = {}) => {
|
||||
Object.keys(params).map(key => {
|
||||
const attr = definition.attributes[key] || {};
|
||||
params[key] = castValueFromType(attr.type, params[key], definition);
|
||||
|
||||
params[key] = parseValue(attr.type, params[key]);
|
||||
});
|
||||
|
||||
return _.mapKeys(params, (value, key) => {
|
||||
@ -611,92 +614,54 @@ module.exports = ({ models, target, plugin = false }, ctx) => {
|
||||
: Promise.resolve();
|
||||
});
|
||||
|
||||
//eslint-disable-next-line
|
||||
this.on('saving', (instance, attrs, options) => {
|
||||
instance.attributes = mapper(instance.attributes);
|
||||
attrs = mapper(attrs);
|
||||
this.on('saving', (instance, attrs) => {
|
||||
instance.attributes = _.assign(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 formatter = attributes => {
|
||||
Object.keys(attributes).forEach(key => {
|
||||
const attr = definition.attributes[key] || {};
|
||||
|
||||
if (attributes[key] === null) return;
|
||||
|
||||
if (attr.type === 'json') {
|
||||
attributes[key] = JSON.parse(attributes[key]);
|
||||
}
|
||||
|
||||
if (attr.type === 'boolean') {
|
||||
if (typeof attributes[key] === 'boolean') {
|
||||
return;
|
||||
}
|
||||
|
||||
const strVal = attributes[key].toString();
|
||||
if (strVal === '1') {
|
||||
attributes[key] = true;
|
||||
} else if (strVal === '0') {
|
||||
attributes[key] = false;
|
||||
} else {
|
||||
attributes[key] = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (attr.type === 'date' && definition.client === 'sqlite3') {
|
||||
attributes[key] = dateFns.parse(attributes[key]);
|
||||
}
|
||||
|
||||
if (
|
||||
attr.type === 'biginteger' &&
|
||||
definition.client === 'sqlite3'
|
||||
) {
|
||||
attributes[key] = attributes[key].toString();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
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.forEach(entry => formatEntry(entry));
|
||||
} 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(
|
||||
@ -748,34 +713,3 @@ module.exports = ({ models, target, plugin = false }, ctx) => {
|
||||
|
||||
return Promise.all(updates);
|
||||
};
|
||||
|
||||
const castValueFromType = (type, value /* definition */) => {
|
||||
// do not cast null values
|
||||
if (value === null) return null;
|
||||
|
||||
switch (type) {
|
||||
case 'json':
|
||||
return JSON.stringify(value);
|
||||
// TODO: handle real date format 1970-01-01
|
||||
// TODO: handle real time format 12:00:00
|
||||
case 'time':
|
||||
case 'timestamp':
|
||||
case 'date':
|
||||
case 'datetime': {
|
||||
const date = dateFns.parse(value);
|
||||
if (dateFns.isValid(date)) return date;
|
||||
|
||||
date.setTime(value);
|
||||
|
||||
if (!dateFns.isValid(date)) {
|
||||
throw new Error(
|
||||
`Invalid ${type} format, expected a timestamp or an ISO date`
|
||||
);
|
||||
}
|
||||
|
||||
return date;
|
||||
}
|
||||
default:
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
16
packages/strapi-connector-bookshelf/lib/parser.js
Normal file
16
packages/strapi-connector-bookshelf/lib/parser.js
Normal file
@ -0,0 +1,16 @@
|
||||
'use strict';
|
||||
|
||||
const { parseType } = require('strapi-utils');
|
||||
|
||||
const createParser = () => (type, value) => {
|
||||
if (value === null) return null;
|
||||
|
||||
switch (type) {
|
||||
case 'json':
|
||||
return JSON.stringify(value);
|
||||
default:
|
||||
return parseType({ type, value });
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = { createParser };
|
@ -17,7 +17,7 @@
|
||||
"main": "./lib",
|
||||
"dependencies": {
|
||||
"bookshelf": "^1.0.1",
|
||||
"date-fns": "^1.30.1",
|
||||
"date-fns": "^2.8.1",
|
||||
"inquirer": "^6.3.1",
|
||||
"lodash": "^4.17.11",
|
||||
"pluralize": "^7.0.0",
|
||||
|
@ -71,7 +71,7 @@ module.exports = ({ models, target, plugin = false }, ctx) => {
|
||||
|
||||
definition.loadedModel[name] = {
|
||||
...attr,
|
||||
type: utils(instance).convertType(attr.type),
|
||||
...utils(instance).convertType(attr.type),
|
||||
};
|
||||
});
|
||||
|
||||
@ -203,7 +203,7 @@ module.exports = ({ models, target, plugin = false }, ctx) => {
|
||||
type: 'timestamp',
|
||||
};
|
||||
target[model].allAttributes[updatedAtCol] = {
|
||||
type: 'timestampUpdate',
|
||||
type: 'timestamp',
|
||||
};
|
||||
} else if (timestampsOption === true) {
|
||||
schema.set('timestamps', true);
|
||||
@ -214,7 +214,7 @@ module.exports = ({ models, target, plugin = false }, ctx) => {
|
||||
type: 'timestamp',
|
||||
};
|
||||
target[model].allAttributes.updatedAt = {
|
||||
type: 'timestampUpdate',
|
||||
type: 'timestamp',
|
||||
};
|
||||
}
|
||||
schema.set(
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
const _ = require('lodash');
|
||||
const Mongoose = require('mongoose');
|
||||
const { parseType } = require('strapi-utils');
|
||||
|
||||
/**
|
||||
* Module dependencies
|
||||
@ -29,35 +30,50 @@ module.exports = (mongoose = Mongoose) => {
|
||||
const convertType = mongooseType => {
|
||||
switch (mongooseType.toLowerCase()) {
|
||||
case 'array':
|
||||
return Array;
|
||||
return { type: Array };
|
||||
case 'boolean':
|
||||
return 'Boolean';
|
||||
return { type: 'Boolean' };
|
||||
case 'binary':
|
||||
return 'Buffer';
|
||||
case 'date':
|
||||
case 'datetime':
|
||||
return { type: 'Buffer' };
|
||||
case 'time':
|
||||
return {
|
||||
type: String,
|
||||
validate: value => parseType({ type: 'time', value }),
|
||||
set: value => parseType({ type: 'time', value }),
|
||||
};
|
||||
case 'date':
|
||||
return {
|
||||
type: String,
|
||||
validate: value => parseType({ type: 'date', value }),
|
||||
set: value => parseType({ type: 'date', value }),
|
||||
};
|
||||
case 'datetime':
|
||||
return {
|
||||
type: Date,
|
||||
};
|
||||
case 'timestamp':
|
||||
return Date;
|
||||
return {
|
||||
type: Date,
|
||||
};
|
||||
case 'decimal':
|
||||
return 'Decimal';
|
||||
return { type: 'Decimal' };
|
||||
case 'float':
|
||||
return 'Float';
|
||||
return { type: 'Float' };
|
||||
case 'json':
|
||||
return 'Mixed';
|
||||
return { type: 'Mixed' };
|
||||
case 'biginteger':
|
||||
return 'Long';
|
||||
return { type: 'Long' };
|
||||
case 'integer':
|
||||
return 'Number';
|
||||
return { type: 'Number' };
|
||||
case 'uuid':
|
||||
return 'ObjectId';
|
||||
return { type: 'ObjectId' };
|
||||
case 'email':
|
||||
case 'enumeration':
|
||||
case 'password':
|
||||
case 'string':
|
||||
case 'text':
|
||||
case 'richtext':
|
||||
return 'String';
|
||||
return { type: 'String' };
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
|
@ -138,6 +138,7 @@ module.exports = (scope, cb) => {
|
||||
// Get default connection
|
||||
try {
|
||||
scope.connection =
|
||||
scope.args.connection ||
|
||||
JSON.parse(
|
||||
fs.readFileSync(
|
||||
path.resolve(
|
||||
@ -148,7 +149,8 @@ module.exports = (scope, cb) => {
|
||||
'database.json'
|
||||
)
|
||||
)
|
||||
).defaultConnection || '';
|
||||
).defaultConnection ||
|
||||
'';
|
||||
} catch (err) {
|
||||
return cb.invalid(err);
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ const createMockSchema = (attrs, timestamps = true) => {
|
||||
type: 'timestamp',
|
||||
},
|
||||
updatedAt: {
|
||||
type: 'timestampUpdate',
|
||||
type: 'timestamp',
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
|
@ -15,6 +15,7 @@ const typeToSize = type => {
|
||||
case 'checkbox':
|
||||
case 'boolean':
|
||||
case 'date':
|
||||
case 'time':
|
||||
case 'biginteger':
|
||||
case 'decimal':
|
||||
case 'float':
|
||||
|
@ -24,14 +24,14 @@ describe('Test type date', () => {
|
||||
'/content-manager/explorer/application::withdate.withdate',
|
||||
{
|
||||
body: {
|
||||
field: '2019-08-08T10:10:57.000Z',
|
||||
field: '2019-08-08',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.body).toMatchObject({
|
||||
field: '2019-08-08T10:10:57.000Z',
|
||||
field: '2019-08-08',
|
||||
});
|
||||
});
|
||||
|
||||
@ -49,58 +49,53 @@ describe('Test type date', () => {
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.body).toMatchObject({
|
||||
field: now.toISOString(),
|
||||
field: '2019-01-12',
|
||||
});
|
||||
});
|
||||
|
||||
test('Create entry with timestamp value should be converted to ISO', async () => {
|
||||
const now = new Date(2016, 4, 8);
|
||||
test.each([
|
||||
'2019-08-08',
|
||||
'2019-08-08 12:11:12',
|
||||
'2019-08-08T00:00:00',
|
||||
'2019-08-08T00:00:00Z',
|
||||
'2019-08-08 00:00:00.123',
|
||||
'2019-08-08 00:00:00.123Z',
|
||||
'2019-08-08T00:00:00.123',
|
||||
'2019-08-08T00:00:00.123Z',
|
||||
])(
|
||||
'Date can be sent in any iso format and the date part will be kept, (%s)',
|
||||
async input => {
|
||||
const res = await rq.post(
|
||||
'/content-manager/explorer/application::withdate.withdate',
|
||||
{
|
||||
body: {
|
||||
field: input,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const res = await rq.post(
|
||||
'/content-manager/explorer/application::withdate.withdate',
|
||||
{
|
||||
body: {
|
||||
field: now.getTime(),
|
||||
},
|
||||
}
|
||||
);
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.body).toMatchObject({
|
||||
field: '2019-08-08',
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.body).toMatchObject({
|
||||
field: now.toISOString(),
|
||||
});
|
||||
});
|
||||
test.each([1234567891012, '1234567891012', '2019/12/11', '12:11:11'])(
|
||||
'Throws on invalid date (%s)',
|
||||
async value => {
|
||||
const res = await rq.post(
|
||||
'/content-manager/explorer/application::withdate.withdate',
|
||||
{
|
||||
body: {
|
||||
field: value,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
test('Accepts string timestamp', async () => {
|
||||
const now = new Date(2000, 0, 1);
|
||||
|
||||
const res = await rq.post(
|
||||
'/content-manager/explorer/application::withdate.withdate',
|
||||
{
|
||||
body: {
|
||||
field: `${now.getTime()}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.body).toMatchObject({
|
||||
field: now.toISOString(),
|
||||
});
|
||||
});
|
||||
|
||||
test('Throws on invalid date format', async () => {
|
||||
const res = await rq.post(
|
||||
'/content-manager/explorer/application::withdate.withdate',
|
||||
{
|
||||
body: {
|
||||
field: 'azdazindoaizdnoainzd',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
expect(res.statusCode).toBe(400);
|
||||
});
|
||||
expect(res.statusCode).toBe(400);
|
||||
}
|
||||
);
|
||||
|
||||
test('Reading entry, returns correct value', async () => {
|
||||
const res = await rq.get(
|
||||
@ -110,7 +105,9 @@ describe('Test type date', () => {
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(Array.isArray(res.body)).toBe(true);
|
||||
res.body.forEach(entry => {
|
||||
expect(new Date(entry.field).toISOString()).toBe(entry.field);
|
||||
expect(entry.field).toMatch(
|
||||
/^([12]\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01]))$/
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@ -121,7 +118,7 @@ describe('Test type date', () => {
|
||||
'/content-manager/explorer/application::withdate.withdate',
|
||||
{
|
||||
body: {
|
||||
field: now.getTime(),
|
||||
field: now,
|
||||
},
|
||||
}
|
||||
);
|
||||
@ -139,7 +136,7 @@ describe('Test type date', () => {
|
||||
expect(updateRes.statusCode).toBe(200);
|
||||
expect(updateRes.body).toMatchObject({
|
||||
id: res.body.id,
|
||||
field: newDate.toISOString(),
|
||||
field: '2017-11-23',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -0,0 +1,145 @@
|
||||
const { registerAndLogin } = require('../../../../test/helpers/auth');
|
||||
const createModelsUtils = require('../../../../test/helpers/models');
|
||||
const { createAuthRequest } = require('../../../../test/helpers/request');
|
||||
|
||||
let modelsUtils;
|
||||
let rq;
|
||||
|
||||
describe('Test type date', () => {
|
||||
beforeAll(async () => {
|
||||
const token = await registerAndLogin();
|
||||
rq = createAuthRequest(token);
|
||||
|
||||
modelsUtils = createModelsUtils({ rq });
|
||||
|
||||
await modelsUtils.createModelWithType('withdatetime', 'datetime');
|
||||
}, 60000);
|
||||
|
||||
afterAll(async () => {
|
||||
await modelsUtils.deleteModel('withdatetime');
|
||||
}, 60000);
|
||||
|
||||
test('Create entry with valid value JSON', async () => {
|
||||
const res = await rq.post(
|
||||
'/content-manager/explorer/application::withdatetime.withdatetime',
|
||||
{
|
||||
body: {
|
||||
field: '2019-08-08T10:10:57.000Z',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.body).toMatchObject({
|
||||
field: '2019-08-08T10:10:57.000Z',
|
||||
});
|
||||
});
|
||||
|
||||
test('Create entry with valid value FormData', async () => {
|
||||
const now = new Date(2019, 0, 12);
|
||||
|
||||
const res = await rq.post(
|
||||
'/content-manager/explorer/application::withdatetime.withdatetime',
|
||||
{
|
||||
formData: {
|
||||
data: JSON.stringify({ field: now }),
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.body).toMatchObject({
|
||||
field: now.toISOString(),
|
||||
});
|
||||
});
|
||||
|
||||
test('Create entry with timestamp value should be converted to ISO', async () => {
|
||||
const now = new Date(2016, 4, 8);
|
||||
|
||||
const res = await rq.post(
|
||||
'/content-manager/explorer/application::withdatetime.withdatetime',
|
||||
{
|
||||
body: {
|
||||
field: now.getTime(),
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.body).toMatchObject({
|
||||
field: now.toISOString(),
|
||||
});
|
||||
});
|
||||
|
||||
test('Accepts string timestamp', async () => {
|
||||
const now = new Date(2000, 0, 1);
|
||||
|
||||
const res = await rq.post(
|
||||
'/content-manager/explorer/application::withdatetime.withdatetime',
|
||||
{
|
||||
body: {
|
||||
field: `${now.getTime()}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.body).toMatchObject({
|
||||
field: now.toISOString(),
|
||||
});
|
||||
});
|
||||
|
||||
test('Throws on invalid date format', async () => {
|
||||
const res = await rq.post(
|
||||
'/content-manager/explorer/application::withdatetime.withdatetime',
|
||||
{
|
||||
body: {
|
||||
field: 'azdazindoaizdnoainzd',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
expect(res.statusCode).toBe(400);
|
||||
});
|
||||
|
||||
test('Reading entry, returns correct value', async () => {
|
||||
const res = await rq.get(
|
||||
'/content-manager/explorer/application::withdatetime.withdatetime'
|
||||
);
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(Array.isArray(res.body)).toBe(true);
|
||||
res.body.forEach(entry => {
|
||||
expect(new Date(entry.field).toISOString()).toBe(entry.field);
|
||||
});
|
||||
});
|
||||
|
||||
test('Updating entry sets the right value and format JSON', async () => {
|
||||
const now = new Date(2018, 7, 5);
|
||||
|
||||
const res = await rq.post(
|
||||
'/content-manager/explorer/application::withdatetime.withdatetime',
|
||||
{
|
||||
body: {
|
||||
field: now.getTime(),
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const newDate = new Date(2017, 10, 23);
|
||||
const updateRes = await rq.put(
|
||||
`/content-manager/explorer/application::withdatetime.withdatetime/${res.body.id}`,
|
||||
{
|
||||
body: {
|
||||
field: newDate,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
expect(updateRes.statusCode).toBe(200);
|
||||
expect(updateRes.body).toMatchObject({
|
||||
id: res.body.id,
|
||||
field: newDate.toISOString(),
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,109 @@
|
||||
const { registerAndLogin } = require('../../../../test/helpers/auth');
|
||||
const createModelsUtils = require('../../../../test/helpers/models');
|
||||
const { createAuthRequest } = require('../../../../test/helpers/request');
|
||||
|
||||
let modelsUtils;
|
||||
let rq;
|
||||
|
||||
describe('Test type time', () => {
|
||||
beforeAll(async () => {
|
||||
const token = await registerAndLogin();
|
||||
rq = createAuthRequest(token);
|
||||
|
||||
modelsUtils = createModelsUtils({ rq });
|
||||
|
||||
await modelsUtils.createModelWithType('withtime', 'time');
|
||||
}, 60000);
|
||||
|
||||
afterAll(async () => {
|
||||
await modelsUtils.deleteModel('withtime');
|
||||
}, 60000);
|
||||
|
||||
test('Create entry with valid value JSON', async () => {
|
||||
const res = await rq.post(
|
||||
'/content-manager/explorer/application::withtime.withtime',
|
||||
{
|
||||
body: {
|
||||
field: '10:10:57.123',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.body).toMatchObject({
|
||||
field: '10:10:57.123',
|
||||
});
|
||||
});
|
||||
|
||||
test.each(['00:00:00', '01:03:11.2', '01:03:11.93', '01:03:11.123'])(
|
||||
'Accepts multiple time formats %s',
|
||||
async input => {
|
||||
const res = await rq.post(
|
||||
'/content-manager/explorer/application::withtime.withtime',
|
||||
{
|
||||
body: {
|
||||
field: input,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
}
|
||||
);
|
||||
|
||||
test.each(['24:11:23', '23:72:11', '12:45:83', 1234, {}, 'test', new Date()])(
|
||||
'Throws on invalid time (%s)',
|
||||
async input => {
|
||||
const res = await rq.post(
|
||||
'/content-manager/explorer/application::withtime.withtime',
|
||||
{
|
||||
body: {
|
||||
field: input,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
expect(res.statusCode).toBe(400);
|
||||
}
|
||||
);
|
||||
|
||||
test('Reading entry, returns correct value', async () => {
|
||||
const res = await rq.get(
|
||||
'/content-manager/explorer/application::withtime.withtime'
|
||||
);
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(Array.isArray(res.body)).toBe(true);
|
||||
res.body.forEach(entry => {
|
||||
expect(entry.field).toMatch(
|
||||
/^2[0-3]|[01][0-9]:[0-5][0-9]:[0-5][0-9](.[0-9]{1,3})?$/
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('Updating entry sets the right value and format JSON', async () => {
|
||||
const res = await rq.post(
|
||||
'/content-manager/explorer/application::withtime.withtime',
|
||||
{
|
||||
body: {
|
||||
field: '12:11:04',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const uptimeRes = await rq.put(
|
||||
`/content-manager/explorer/application::withtime.withtime/${res.body.id}`,
|
||||
{
|
||||
body: {
|
||||
field: '13:45:19.123',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
expect(uptimeRes.statusCode).toBe(200);
|
||||
expect(uptimeRes.body).toMatchObject({
|
||||
id: res.body.id,
|
||||
field: '13:45:19.123',
|
||||
});
|
||||
});
|
||||
});
|
@ -6,31 +6,10 @@ const yup = require('yup');
|
||||
const { isValidName, isValidIcon } = require('./common');
|
||||
const formatYupErrors = require('./yup-formatter');
|
||||
const createSchema = require('./model-schema');
|
||||
const { modelTypes } = require('./constants');
|
||||
const { modelTypes, DEFAULT_TYPES } = require('./constants');
|
||||
|
||||
const VALID_RELATIONS = ['oneWay', 'manyWay'];
|
||||
const VALID_TYPES = [
|
||||
// advanced types
|
||||
'media',
|
||||
|
||||
// scalar types
|
||||
'string',
|
||||
'text',
|
||||
'richtext',
|
||||
'json',
|
||||
'enumeration',
|
||||
'password',
|
||||
'email',
|
||||
'integer',
|
||||
'biginteger',
|
||||
'float',
|
||||
'decimal',
|
||||
'date',
|
||||
'boolean',
|
||||
|
||||
// nested component
|
||||
'component',
|
||||
];
|
||||
const VALID_TYPES = [...DEFAULT_TYPES, 'component'];
|
||||
|
||||
const componentSchema = createSchema(VALID_TYPES, VALID_RELATIONS, {
|
||||
modelType: modelTypes.COMPONENT,
|
||||
|
@ -3,7 +3,31 @@
|
||||
const CONTENT_TYPE = 'CONTENT_TYPE';
|
||||
const COMPONENT = 'COMPONENT';
|
||||
|
||||
const DEFAULT_TYPES = [
|
||||
// advanced types
|
||||
'media',
|
||||
|
||||
// scalar types
|
||||
'string',
|
||||
'text',
|
||||
'richtext',
|
||||
'json',
|
||||
'enumeration',
|
||||
'password',
|
||||
'email',
|
||||
'integer',
|
||||
'biginteger',
|
||||
'float',
|
||||
'decimal',
|
||||
'date',
|
||||
'time',
|
||||
'datetime',
|
||||
'timestamp',
|
||||
'boolean',
|
||||
];
|
||||
|
||||
module.exports = {
|
||||
DEFAULT_TYPES,
|
||||
modelTypes: {
|
||||
CONTENT_TYPE,
|
||||
COMPONENT,
|
||||
|
@ -6,7 +6,7 @@ const formatYupErrors = require('./yup-formatter');
|
||||
|
||||
const createSchema = require('./model-schema');
|
||||
const { nestedComponentSchema } = require('./component');
|
||||
const { modelTypes } = require('./constants');
|
||||
const { modelTypes, DEFAULT_TYPES } = require('./constants');
|
||||
|
||||
const VALID_RELATIONS = [
|
||||
'oneWay',
|
||||
@ -16,29 +16,7 @@ const VALID_RELATIONS = [
|
||||
'manyToOne',
|
||||
'manyToMany',
|
||||
];
|
||||
const VALID_TYPES = [
|
||||
// advanced types
|
||||
'media',
|
||||
|
||||
// scalar types
|
||||
'string',
|
||||
'text',
|
||||
'richtext',
|
||||
'json',
|
||||
'enumeration',
|
||||
'password',
|
||||
'email',
|
||||
'integer',
|
||||
'biginteger',
|
||||
'float',
|
||||
'decimal',
|
||||
'date',
|
||||
'boolean',
|
||||
|
||||
// nested component
|
||||
'component',
|
||||
'dynamiczone',
|
||||
];
|
||||
const VALID_TYPES = [...DEFAULT_TYPES, 'component', 'dynamiczone'];
|
||||
|
||||
const contentTypeSchema = createSchema(VALID_TYPES, VALID_RELATIONS, {
|
||||
modelType: modelTypes.CONTENT_TYPE,
|
||||
|
@ -80,6 +80,10 @@ module.exports = function createComponentBuilder() {
|
||||
.set('collectionName', infos.collectionName || defaultCollectionName)
|
||||
.set(['info', 'name'], infos.name)
|
||||
.set(['info', 'description'], infos.description)
|
||||
.set('options', {
|
||||
increments: true,
|
||||
timestamps: true,
|
||||
})
|
||||
.set('attributes', this.convertAttributes(infos.attributes));
|
||||
|
||||
Object.keys(infos.attributes).forEach(key => {
|
||||
|
@ -481,7 +481,7 @@ const formatModelConnectionsGQL = function(fields, model, name, modelResolver) {
|
||||
groupBy: `${globalId}GroupBy`,
|
||||
aggregate: `${globalId}Aggregator`,
|
||||
};
|
||||
const pluralName = pluralize.plural(name);
|
||||
const pluralName = pluralize.plural(_.camelCase(name));
|
||||
|
||||
let modelConnectionTypes = `type ${connectionGlobalId} {${Schema.formatGQL(
|
||||
connectionFields
|
||||
|
89
packages/strapi-utils/lib/__tests__/parse-type.js
Normal file
89
packages/strapi-utils/lib/__tests__/parse-type.js
Normal file
@ -0,0 +1,89 @@
|
||||
const parseType = require('../parse-type');
|
||||
|
||||
describe('parseType', () => {
|
||||
describe('boolean', () => {
|
||||
it('Handles string booleans', () => {
|
||||
expect(parseType({ type: 'boolean', value: 'true' })).toBe(true);
|
||||
expect(parseType({ type: 'boolean', value: 't' })).toBe(true);
|
||||
expect(parseType({ type: 'boolean', value: '1' })).toBe(true);
|
||||
|
||||
expect(parseType({ type: 'boolean', value: 'false' })).toBe(false);
|
||||
expect(parseType({ type: 'boolean', value: 'f' })).toBe(false);
|
||||
expect(parseType({ type: 'boolean', value: '0' })).toBe(false);
|
||||
|
||||
expect(() => parseType({ type: 'boolean', value: 'test' })).toThrow();
|
||||
});
|
||||
|
||||
it('Handles numerical booleans', () => {
|
||||
expect(parseType({ type: 'boolean', value: 1 })).toBe(true);
|
||||
|
||||
expect(parseType({ type: 'boolean', value: 0 })).toBe(false);
|
||||
|
||||
expect(() => parseType({ type: 'boolean', value: 12 })).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Time', () => {
|
||||
it('Always returns the same time format', () => {
|
||||
expect(parseType({ type: 'time', value: '12:31:11' })).toBe(
|
||||
'12:31:11.000'
|
||||
);
|
||||
expect(parseType({ type: 'time', value: '12:31:11.2' })).toBe(
|
||||
'12:31:11.200'
|
||||
);
|
||||
expect(parseType({ type: 'time', value: '12:31:11.31' })).toBe(
|
||||
'12:31:11.310'
|
||||
);
|
||||
expect(parseType({ type: 'time', value: '12:31:11.319' })).toBe(
|
||||
'12:31:11.319'
|
||||
);
|
||||
});
|
||||
|
||||
it('Throws on invalid time format', () => {
|
||||
expect(() => parseType({ type: 'time', value: '25:12:09' })).toThrow();
|
||||
expect(() => parseType({ type: 'time', value: '23:78:09' })).toThrow();
|
||||
expect(() => parseType({ type: 'time', value: '23:11:99' })).toThrow();
|
||||
|
||||
expect(() => parseType({ type: 'time', value: '12:12' })).toThrow();
|
||||
expect(() => parseType({ type: 'time', value: 'test' })).toThrow();
|
||||
expect(() => parseType({ type: 'time', value: 122 })).toThrow();
|
||||
expect(() => parseType({ type: 'time', value: {} })).toThrow();
|
||||
expect(() => parseType({ type: 'time', value: [] })).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Date', () => {
|
||||
it('Supports ISO formats and always returns the right format', () => {
|
||||
expect(parseType({ type: 'date', value: '2019-01-01 12:01:11' })).toBe(
|
||||
'2019-01-01'
|
||||
);
|
||||
|
||||
expect(parseType({ type: 'date', value: '2018-11-02' })).toBe(
|
||||
'2018-11-02'
|
||||
);
|
||||
|
||||
expect(
|
||||
parseType({ type: 'date', value: '2019-04-21T00:00:00.000Z' })
|
||||
).toBe('2019-04-21');
|
||||
});
|
||||
|
||||
it('Throws on invalid formator dates', () => {
|
||||
expect(() => parseType({ type: 'date', value: '-1029-11-02' })).toThrow();
|
||||
expect(() => parseType({ type: 'date', value: '2019-13-02' })).toThrow();
|
||||
expect(() => parseType({ type: 'date', value: '2019-12-32' })).toThrow();
|
||||
expect(() => parseType({ type: 'date', value: '2019-02-31' })).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Datetime', () => {
|
||||
it.each([
|
||||
'2019-01-01',
|
||||
'2019-01-01 10:11:12',
|
||||
'1234567890111',
|
||||
'2019-01-01T10:11:12.123Z',
|
||||
])('Supports ISO formats and always returns a date %s', value => {
|
||||
const r = parseType({ type: 'datetime', value });
|
||||
expect(r instanceof Date).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
@ -1,6 +1,7 @@
|
||||
//TODO: move to dbal
|
||||
|
||||
const _ = require('lodash');
|
||||
const parseType = require('./parse-type');
|
||||
|
||||
const findModelByAssoc = assoc => {
|
||||
const { models } = assoc.plugin ? strapi.plugins[assoc.plugin] : strapi;
|
||||
@ -58,33 +59,16 @@ const getAssociationFromFieldKey = ({ model, field }) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Cast basic values based on attribute type
|
||||
* Cast an input value
|
||||
* @param {Object} options - Options
|
||||
* @param {string} options.type - type of the atribute
|
||||
* @param {*} options.value - value tu cast
|
||||
* @param {string} options.operator - name of operator
|
||||
*/
|
||||
const castValueToType = ({ type, value }) => {
|
||||
switch (type) {
|
||||
case 'boolean': {
|
||||
if (['true', 't', '1', 1, true].includes(value)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (['false', 'f', '0', 0].includes(value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Boolean(value);
|
||||
}
|
||||
case 'integer':
|
||||
case 'biginteger':
|
||||
case 'float':
|
||||
case 'decimal': {
|
||||
return _.toNumber(value);
|
||||
}
|
||||
default:
|
||||
return value;
|
||||
}
|
||||
const castInput = ({ type, value, operator }) => {
|
||||
return Array.isArray(value)
|
||||
? value.map(val => castValue({ type, operator, value: val }))
|
||||
: castValue({ type, operator, value: value });
|
||||
};
|
||||
|
||||
/**
|
||||
@ -95,8 +79,8 @@ const castValueToType = ({ type, value }) => {
|
||||
* @param {string} options.operator - name of operator
|
||||
*/
|
||||
const castValue = ({ type, value, operator }) => {
|
||||
if (operator === 'null') return castValueToType({ type: 'boolean', value });
|
||||
return castValueToType({ type, value });
|
||||
if (operator === 'null') return parseType({ type: 'boolean', value });
|
||||
return parseType({ type, value });
|
||||
};
|
||||
/**
|
||||
*
|
||||
@ -126,12 +110,10 @@ const buildQuery = ({ model, filters = {}, ...rest }) => {
|
||||
field,
|
||||
});
|
||||
|
||||
const { type } = _.get(assocModel, ['attributes', attribute], {});
|
||||
const { type } = _.get(assocModel, ['allAttributes', attribute], {});
|
||||
|
||||
// cast value or array of values
|
||||
const castedValue = Array.isArray(value)
|
||||
? value.map(val => castValue({ type, operator, value: val }))
|
||||
: castValue({ type, operator, value: value });
|
||||
const castedValue = castInput({ type, operator, value });
|
||||
|
||||
return {
|
||||
field: field === 'id' ? model.primaryKey : field,
|
||||
|
@ -8,6 +8,7 @@ const convertRestQueryParams = require('./convertRestQueryParams');
|
||||
const buildQuery = require('./buildQuery');
|
||||
const parseMultipartData = require('./parse-multipart');
|
||||
const sanitizeEntity = require('./sanitize-entity');
|
||||
const parseType = require('./parse-type');
|
||||
|
||||
module.exports = {
|
||||
cli: require('./cli'),
|
||||
@ -25,4 +26,5 @@ module.exports = {
|
||||
buildQuery,
|
||||
parseMultipartData,
|
||||
sanitizeEntity,
|
||||
parseType,
|
||||
};
|
||||
|
100
packages/strapi-utils/lib/parse-type.js
Normal file
100
packages/strapi-utils/lib/parse-type.js
Normal file
@ -0,0 +1,100 @@
|
||||
'use strict';
|
||||
|
||||
const _ = require('lodash');
|
||||
const dates = require('date-fns');
|
||||
|
||||
const timeRegex = new RegExp(
|
||||
'^(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])(.[0-9]{1,3})?$'
|
||||
);
|
||||
|
||||
const parseTime = value => {
|
||||
if (dates.isDate(value)) return dates.format(value, 'HH:mm:ss.SSS');
|
||||
|
||||
if (typeof value !== 'string') {
|
||||
throw new Error(`Expected a string, got a ${typeof value}`);
|
||||
}
|
||||
const result = value.match(timeRegex);
|
||||
|
||||
if (result === null) {
|
||||
throw new Error('Invalid time format, expected HH:mm:ss.SSS');
|
||||
}
|
||||
|
||||
const [, hours, minutes, seconds, fraction = '.000'] = result;
|
||||
const fractionPart = _.padEnd(fraction.slice(1), 3, '0');
|
||||
|
||||
return `${hours}:${minutes}:${seconds}.${fractionPart}`;
|
||||
};
|
||||
|
||||
const parseDate = value => {
|
||||
if (dates.isDate(value)) return dates.format(value, 'yyyy-MM-dd');
|
||||
try {
|
||||
let date = dates.parseISO(value);
|
||||
|
||||
if (dates.isValid(date)) return dates.format(date, 'yyyy-MM-dd');
|
||||
|
||||
throw new Error(`Invalid format, expected an ISO compatble date`);
|
||||
} catch (error) {
|
||||
throw new Error(`Invalid format, expected an ISO compatble date`);
|
||||
}
|
||||
};
|
||||
|
||||
const parseDateTimeOrTimestamp = value => {
|
||||
if (dates.isDate(value)) return value;
|
||||
try {
|
||||
const date = dates.parseISO(value);
|
||||
if (dates.isValid(date)) return date;
|
||||
|
||||
const milliUnixDate = dates.parse(value, 'T', new Date());
|
||||
if (dates.isValid(milliUnixDate)) return milliUnixDate;
|
||||
|
||||
throw new Error(`Invalid format, expected a timestamp or an ISO date`);
|
||||
} catch (error) {
|
||||
throw new Error(`Invalid format, expected a timestamp or an ISO date`);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Cast basic values based on attribute type
|
||||
* @param {Object} options - Options
|
||||
* @param {string} options.type - type of the atribute
|
||||
* @param {*} options.value - value tu cast
|
||||
*/
|
||||
const parseType = ({ type, value }) => {
|
||||
switch (type) {
|
||||
case 'boolean': {
|
||||
if (typeof value === 'boolean') return value;
|
||||
|
||||
if (['true', 't', '1', 1].includes(value)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (['false', 'f', '0', 0].includes(value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
'Invalid boolean input. Expected "t","1","true","false","0","f"'
|
||||
);
|
||||
}
|
||||
case 'integer':
|
||||
case 'biginteger':
|
||||
case 'float':
|
||||
case 'decimal': {
|
||||
return _.toNumber(value);
|
||||
}
|
||||
case 'time': {
|
||||
return parseTime(value);
|
||||
}
|
||||
case 'date': {
|
||||
return parseDate(value);
|
||||
}
|
||||
case 'timestamp':
|
||||
case 'datetime': {
|
||||
return parseDateTimeOrTimestamp(value);
|
||||
}
|
||||
default:
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = parseType;
|
@ -19,6 +19,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"commander": "^2.20.0",
|
||||
"date-fns": "^2.8.1",
|
||||
"joi-json": "^2.1.0",
|
||||
"knex": "^0.16.5",
|
||||
"lodash": "^4.17.11",
|
||||
|
@ -144,6 +144,7 @@ program
|
||||
.option('-a, --api <api>', 'API name to generate a sub API')
|
||||
.option('-p, --plugin <api>', 'plugin name')
|
||||
.option('-t, --tpl <template>', 'template name')
|
||||
.option('-c, --connection <connection>', 'The name of the connection to use')
|
||||
.description('generate a model for an API')
|
||||
.action((id, attributes, cliArguments) => {
|
||||
cliArguments.attributes = attributes;
|
||||
|
@ -5954,11 +5954,16 @@ date-and-time@^0.6.3:
|
||||
resolved "https://registry.yarnpkg.com/date-and-time/-/date-and-time-0.6.3.tgz#2daee52df67c28bd93bce862756ac86b68cf4237"
|
||||
integrity sha512-lcWy3AXDRJOD7MplwZMmNSRM//kZtJaLz4n6D1P5z9wEmZGBKhJRBIr1Xs9KNQJmdXPblvgffynYji4iylUTcA==
|
||||
|
||||
date-fns@^1.27.2, date-fns@^1.30.1:
|
||||
date-fns@^1.27.2:
|
||||
version "1.30.1"
|
||||
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.30.1.tgz#2e71bf0b119153dbb4cc4e88d9ea5acfb50dc05c"
|
||||
integrity sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==
|
||||
|
||||
date-fns@^2.8.1:
|
||||
version "2.8.1"
|
||||
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.8.1.tgz#2109362ccb6c87c3ca011e9e31f702bc09e4123b"
|
||||
integrity sha512-EL/C8IHvYRwAHYgFRse4MGAPSqlJVlOrhVYZ75iQBKrnv+ZedmYsgwH3t+BCDuZDXpoo07+q9j4qgSSOa7irJg==
|
||||
|
||||
date-now@^0.1.4:
|
||||
version "0.1.4"
|
||||
resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b"
|
||||
|
Loading…
x
Reference in New Issue
Block a user