diff --git a/packages/core/database/lib/query/helpers/index.js b/packages/core/database/lib/query/helpers/index.js new file mode 100644 index 0000000000..c238602679 --- /dev/null +++ b/packages/core/database/lib/query/helpers/index.js @@ -0,0 +1,10 @@ +'use strict'; + +module.exports = { + ...require('./search'), + ...require('./order-by'), + ...require('./join'), + ...require('./populate'), + ...require('./where'), + ...require('./transform'), +}; diff --git a/packages/core/database/lib/query/helpers/join.js b/packages/core/database/lib/query/helpers/join.js new file mode 100644 index 0000000000..6aab745a63 --- /dev/null +++ b/packages/core/database/lib/query/helpers/join.js @@ -0,0 +1,98 @@ +'use strict'; + +const createPivotJoin = (qb, joinTable, alias, tragetMeta) => { + const joinAlias = qb.getAlias(); + qb.join({ + alias: joinAlias, + referencedTable: joinTable.name, + referencedColumn: joinTable.joinColumn.name, + rootColumn: joinTable.joinColumn.referencedColumn, + rootTable: alias, + on: joinTable.on, + }); + + const subAlias = qb.getAlias(); + qb.join({ + alias: subAlias, + referencedTable: tragetMeta.tableName, + referencedColumn: joinTable.inverseJoinColumn.referencedColumn, + rootColumn: joinTable.inverseJoinColumn.name, + rootTable: joinAlias, + }); + + return subAlias; +}; + +const createJoin = (ctx, { alias, attributeName, attribute }) => { + const { db, qb } = ctx; + + if (attribute.type !== 'relation') { + throw new Error(`Cannot join on non relational field ${attributeName}`); + } + + const tragetMeta = db.metadata.get(attribute.target); + + const joinColumn = attribute.joinColumn; + + if (joinColumn) { + const subAlias = qb.getAlias(); + qb.join({ + alias: subAlias, + referencedTable: tragetMeta.tableName, + referencedColumn: joinColumn.referencedColumn, + rootColumn: joinColumn.name, + rootTable: alias, + }); + return subAlias; + } + + const joinTable = attribute.joinTable; + if (joinTable) { + return createPivotJoin(qb, joinTable, alias, tragetMeta); + } + + // TODO: polymorphic relations + // NOTE: using the joinColumn / joinTable syntax we don't really care about the relation type here + + return alias; +}; + +// TODO: allow for more conditions +const applyJoin = (qb, join) => { + const { + method = 'leftJoin', + alias, + referencedTable, + referencedColumn, + rootColumn, + rootTable = this.alias, + on, + orderBy, + } = join; + + qb[method]({ [alias]: referencedTable }, inner => { + inner.on(`${rootTable}.${rootColumn}`, `${alias}.${referencedColumn}`); + + if (on) { + for (const key in on) { + inner.onVal(`${alias}.${key}`, on[key]); + } + } + }); + + if (orderBy) { + Object.keys(orderBy).forEach(column => { + const direction = orderBy[column]; + qb.orderBy(`${alias}.${column}`, direction); + }); + } +}; + +const applyJoins = (qb, joins) => joins.forEach(join => applyJoin(qb, join)); + +module.exports = { + createJoin, + createPivotJoin, + applyJoins, + applyJoin, +}; diff --git a/packages/core/database/lib/query/helpers/order-by.js b/packages/core/database/lib/query/helpers/order-by.js new file mode 100644 index 0000000000..e48acc161c --- /dev/null +++ b/packages/core/database/lib/query/helpers/order-by.js @@ -0,0 +1,60 @@ +'use strict'; + +const _ = require('lodash/fp'); + +const types = require('../../types'); +const { createJoin } = require('./join'); + +// TODO: convert field names to columns names +const processOrderBy = (orderBy, ctx) => { + const { db, uid, qb, alias = qb.alias } = ctx; + + if (typeof orderBy === 'string') { + const attribute = db.metadata.get(uid).attributes[orderBy]; + + if (!attribute) { + throw new Error(`Attribute ${orderBy} not found on model ${uid}`); + } + + return [{ column: `${alias}.${orderBy}` }]; + } + + if (Array.isArray(orderBy)) { + return orderBy.flatMap(value => processOrderBy(value, ctx)); + } + + if (_.isPlainObject(orderBy)) { + return Object.entries(orderBy).flatMap(([key, direction]) => { + const value = orderBy[key]; + const attribute = db.metadata.get(uid).attributes[key]; + + if (!attribute) { + throw new Error(`Attribute ${key} not found on model ${uid}`); + } + + if (attribute.type === 'relation') { + // TODO: pass down some filters (e.g published at) + const subAlias = createJoin(ctx, { alias, uid, attributeName: key, attribute }); + + return processOrderBy(value, { + db, + qb, + alias: subAlias, + uid: attribute.target, + }); + } + + if (types.isScalar(attribute.type)) { + return { column: `${alias}.${key}`, order: direction }; + } + + throw new Error(`You cannot order on ${attribute.type} types`); + }); + } + + throw new Error('Invalid orderBy syntax'); +}; + +module.exports = { + processOrderBy, +}; diff --git a/packages/core/database/lib/query/helpers.js b/packages/core/database/lib/query/helpers/populate.js similarity index 57% rename from packages/core/database/lib/query/helpers.js rename to packages/core/database/lib/query/helpers/populate.js index 8d4aa2589c..35efdb057d 100644 --- a/packages/core/database/lib/query/helpers.js +++ b/packages/core/database/lib/query/helpers/populate.js @@ -2,457 +2,8 @@ const _ = require('lodash/fp'); -const types = require('../types'); -const { createField } = require('../fields'); - -const GROUP_OPERATORS = ['$and', '$or']; -const OPERATORS = [ - '$not', - '$in', - '$notIn', - '$eq', - '$ne', - '$gt', - '$gte', - '$lt', - '$lte', - '$null', - '$notNull', - '$between', - // '$like', - // '$regexp', - '$startsWith', - '$endsWith', - '$contains', - '$notContains', -]; - -const ARRAY_OPERATORS = ['$in', '$notIn', '$between']; - -const createPivotJoin = (qb, joinTable, alias, tragetMeta) => { - const joinAlias = qb.getAlias(); - qb.join({ - alias: joinAlias, - referencedTable: joinTable.name, - referencedColumn: joinTable.joinColumn.name, - rootColumn: joinTable.joinColumn.referencedColumn, - rootTable: alias, - on: joinTable.on, - }); - - const subAlias = qb.getAlias(); - qb.join({ - alias: subAlias, - referencedTable: tragetMeta.tableName, - referencedColumn: joinTable.inverseJoinColumn.referencedColumn, - rootColumn: joinTable.inverseJoinColumn.name, - rootTable: joinAlias, - }); - - return subAlias; -}; - -const createJoin = (ctx, { alias, attributeName, attribute }) => { - const { db, qb } = ctx; - - if (attribute.type !== 'relation') { - throw new Error(`Cannot join on non relational field ${attributeName}`); - } - - const tragetMeta = db.metadata.get(attribute.target); - - const joinColumn = attribute.joinColumn; - - if (joinColumn) { - const subAlias = qb.getAlias(); - qb.join({ - alias: subAlias, - referencedTable: tragetMeta.tableName, - referencedColumn: joinColumn.referencedColumn, - rootColumn: joinColumn.name, - rootTable: alias, - }); - return subAlias; - } - - const joinTable = attribute.joinTable; - if (joinTable) { - return createPivotJoin(qb, joinTable, alias, tragetMeta); - } - - // NOTE: using the joinColumn / joinTable syntax we don't really care about the relation type here - switch (attribute.relation) { - case 'oneToOne': { - break; - } - case 'oneToMany': { - break; - } - case 'manyToOne': { - break; - } - case 'manyToMany': { - break; - } - - // TODO: polymorphic relations - // TODO: components -> they are converted to relation so not needed either - } - - return alias; -}; - -// TODO: convert field names to columns names -const processOrderBy = (orderBy, ctx) => { - const { db, uid, qb, alias = qb.alias } = ctx; - - if (typeof orderBy === 'string') { - const attribute = db.metadata.get(uid).attributes[orderBy]; - - if (!attribute) { - throw new Error(`Attribute ${orderBy} not found on model ${uid}`); - } - - return [{ column: `${alias}.${orderBy}` }]; - } - - if (Array.isArray(orderBy)) { - return orderBy.flatMap(value => processOrderBy(value, ctx)); - } - - if (_.isPlainObject(orderBy)) { - return Object.entries(orderBy).flatMap(([key, direction]) => { - const value = orderBy[key]; - const attribute = db.metadata.get(uid).attributes[key]; - - if (!attribute) { - throw new Error(`Attribute ${key} not found on model ${uid}`); - } - - if (attribute.type === 'relation') { - // TODO: pass down some filters (e.g published at) - const subAlias = createJoin(ctx, { alias, uid, attributeName: key, attribute }); - - return processOrderBy(value, { - db, - qb, - alias: subAlias, - uid: attribute.target, - }); - } - - if (types.isScalar(attribute.type)) { - return { column: `${alias}.${key}`, order: direction }; - } - - throw new Error(`You cannot order on ${attribute.type} types`); - }); - } - - throw new Error('Invalid orderBy syntax'); -}; - -const isOperator = key => OPERATORS.includes(key); - -const processWhere = (where, ctx, depth = 0) => { - if (depth === 0 && !_.isPlainObject(where)) { - throw new Error('Where must be an object'); - } - - const processNested = (where, ctx) => { - if (!_.isPlainObject(where)) { - return where; - } - - return processWhere(where, ctx, depth + 1); - }; - - const { db, uid, qb, alias = qb.alias } = ctx; - - const filters = {}; - - // for each key in where - for (const key in where) { - const value = where[key]; - const attribute = db.metadata.get(uid).attributes[key]; - - // if operator $and $or then loop over them - if (GROUP_OPERATORS.includes(key)) { - filters[key] = value.map(sub => processNested(sub, ctx)); - continue; - } - - if (key === '$not') { - filters[key] = processNested(value, ctx); - continue; - } - - if (isOperator(key)) { - if (depth == 0) { - throw new Error( - `Only $and, $or and $not can by used as root level operators. Found ${key}.` - ); - } - - filters[key] = processNested(value, ctx); - continue; - } - - 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[`${alias || qb.alias}.${key}`] = processNested(value, ctx); - } - continue; - - // throw new Error(`Attribute ${key} not found on model ${uid}`); - } - - // move to if else to check for scalar / relation / components & throw for other types - if (attribute.type === 'relation') { - // TODO: pass down some filters (e.g published at) - - // attribute - const subAlias = createJoin(ctx, { alias, uid, attributeName: key, attribute }); - - let nestedWhere = processNested(value, { - db, - qb, - alias: subAlias, - uid: attribute.target, - }); - - if (!_.isPlainObject(nestedWhere) || isOperator(_.keys(nestedWhere)[0])) { - nestedWhere = { [`${subAlias}.id`]: nestedWhere }; - } - - // TODO: use a better merge logic (push to $and when collisions) - Object.assign(filters, nestedWhere); - - continue; - } - - if (types.isScalar(attribute.type)) { - // TODO: convert attribute name to column name - // TODO: cast to DB type - filters[`${alias || qb.alias}.${key}`] = processNested(value, ctx); - continue; - } - - throw new Error(`You cannot filter on ${attribute.type} types`); - } - - return filters; -}; - -const applyOperator = (qb, column, operator, value) => { - if (Array.isArray(value) && !ARRAY_OPERATORS.includes(operator)) { - return qb.where(subQB => { - value.forEach(subValue => - subQB.orWhere(innerQB => { - applyOperator(innerQB, column, operator, subValue); - }) - ); - }); - } - - switch (operator) { - case '$not': { - qb.whereNot(qb => applyWhereToColumn(qb, column, value)); - break; - } - - case '$in': { - qb.whereIn(column, _.castArray(value)); - break; - } - - case '$notIn': { - qb.whereNotIn(column, _.castArray(value)); - break; - } - - case '$eq': { - if (value === null) { - qb.whereNull(column); - break; - } - - qb.where(column, value); - break; - } - case '$ne': { - if (value === null) { - qb.whereNotNull(column); - break; - } - - qb.where(column, '<>', value); - break; - } - case '$gt': { - qb.where(column, '>', value); - break; - } - case '$gte': { - qb.where(column, '>=', value); - break; - } - case '$lt': { - qb.where(column, '<', value); - break; - } - case '$lte': { - qb.where(column, '<=', value); - break; - } - case '$null': { - // TODO: make this better - if (value) { - qb.whereNull(column); - } - break; - } - case '$notNull': { - if (value) { - qb.whereNotNull(column); - } - - break; - } - case '$between': { - qb.whereBetween(column, value); - break; - } - // case '$regexp': { - // // TODO: - // - // break; - // } - // // string - // // TODO: use $case to make it case insensitive - // case '$like': { - // qb.where(column, 'like', value); - // break; - // } - - // TODO: add casting logic - case '$startsWith': { - qb.where(column, 'like', `${value}%`); - break; - } - case '$endsWith': { - qb.where(column, 'like', `%${value}`); - break; - } - case '$contains': { - // TODO: handle insensitive - - qb.where(column, 'like', `%${value}%`); - break; - } - - case '$notContains': { - // TODO: handle insensitive - qb.whereNot(column, 'like', `%${value}%`); - break; - } - - // TODO: json operators - - // TODO: relational operators every/some/exists/size ... - - default: { - throw new Error(`Undefined operator ${operator}`); - } - } -}; - -const applyWhereToColumn = (qb, column, columnWhere) => { - if (!_.isPlainObject(columnWhere)) { - if (Array.isArray(columnWhere)) { - return qb.whereIn(column, columnWhere); - } - - return qb.where(column, columnWhere); - } - - // TODO: Transform into if has($in, value) then to handle cases with two keys doing one thing (like $contains with $case) - Object.keys(columnWhere).forEach(operator => { - const value = columnWhere[operator]; - - applyOperator(qb, column, operator, value); - }); -}; - -const applyWhere = (qb, where) => { - if (Array.isArray(where)) { - return qb.where(subQB => where.forEach(subWhere => applyWhere(subQB, subWhere))); - } - - if (!_.isPlainObject(where)) { - throw new Error('Where must be an object'); - } - - Object.keys(where).forEach(key => { - const value = where[key]; - - if (key === '$and') { - return qb.where(subQB => { - value.forEach(v => applyWhere(subQB, v)); - }); - } - - if (key === '$or') { - return qb.where(subQB => { - value.forEach(v => subQB.orWhere(inner => applyWhere(inner, v))); - }); - } - - if (key === '$not') { - return qb.whereNot(qb => applyWhere(qb, value)); - } - - applyWhereToColumn(qb, key, value); - }); -}; - -// TODO: allow for more conditions -const applyJoin = (qb, join) => { - const { - method = 'leftJoin', - alias, - referencedTable, - referencedColumn, - rootColumn, - rootTable = this.alias, - on, - orderBy, - } = join; - - qb[method]({ [alias]: referencedTable }, inner => { - inner.on(`${rootTable}.${rootColumn}`, `${alias}.${referencedColumn}`); - - if (on) { - for (const key in on) { - inner.onVal(`${alias}.${key}`, on[key]); - } - } - }); - - if (orderBy) { - Object.keys(orderBy).forEach(column => { - const direction = orderBy[column]; - qb.orderBy(`${alias}.${column}`, direction); - }); - } -}; - -const applyJoins = (qb, joins) => joins.forEach(join => applyJoin(qb, join)); +const types = require('../../types'); +const { fromRow } = require('./transform'); /** * Converts and prepares the query for populate @@ -473,7 +24,14 @@ const processPopulate = (populate, ctx) => { return null; } - if (Array.isArray(populate)) { + if (populate === true) { + for (const attributeName in meta.attributes) { + const attribute = meta.attributes[attributeName]; + if (attribute.type === 'relation') { + populateMap[attributeName] = true; + } + } + } else if (Array.isArray(populate)) { for (const key of populate) { const [root, ...rest] = key.split('.'); @@ -1061,121 +619,7 @@ const applyPopulate = async (results, populate, ctx) => { } }; -const fromRow = (metadata, row) => { - if (Array.isArray(row)) { - return row.map(singleRow => fromRow(metadata, singleRow)); - } - - const { attributes } = metadata; - - if (_.isNil(row)) { - return null; - } - - 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 ...) - continue; - } - - 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; - } - - if (types.isRelation(attribute.type)) { - obj[attributeName] = row[column]; - } - } - - return obj; -}; - -const applySearch = (qb, query, ctx) => { - const { alias, uid, db } = ctx; - - const { attributes } = db.metadata.get(uid); - - const searchColumns = ['id']; - - const stringColumns = Object.keys(attributes).filter(attributeName => { - const attribute = attributes[attributeName]; - return types.isString(attribute.type) && attribute.searchable !== false; - }); - - searchColumns.push(...stringColumns); - - if (!_.isNaN(_.toNumber(query))) { - const numberColumns = Object.keys(attributes).filter(attributeName => { - const attribute = attributes[attributeName]; - return types.isNumber(attribute.type) && attribute.searchable !== false; - }); - - searchColumns.push(...numberColumns); - } - - switch (db.dialect.client) { - case 'postgres': { - searchColumns.forEach(attr => - qb.orWhereRaw(`"${alias}"."${attr}"::text ILIKE ?`, `%${escapeQuery(query, '*%\\')}%`) - ); - - break; - } - case 'sqlite': { - searchColumns.forEach(attr => - qb.orWhereRaw(`"${alias}"."${attr}" LIKE ? ESCAPE '\\'`, `%${escapeQuery(query, '*%\\')}%`) - ); - break; - } - case 'mysql': { - searchColumns.forEach(attr => - qb.orWhereRaw(`\`${alias}\`.\`${attr}\` LIKE ?`, `%${escapeQuery(query, '*%\\')}%`) - ); - break; - } - default: { - // do nothing - } - } -}; - -const escapeQuery = (query, charsToEscape, escapeChar = '\\') => { - return query - .split('') - .reduce( - (escapedQuery, char) => - charsToEscape.includes(char) - ? `${escapedQuery}${escapeChar}${char}` - : `${escapedQuery}${char}`, - '' - ); -}; - module.exports = { - applyWhere, - processWhere, - applyJoins, - applyJoin, - processOrderBy, processPopulate, - applySearch, applyPopulate, - fromRow, }; diff --git a/packages/core/database/lib/query/helpers/search.js b/packages/core/database/lib/query/helpers/search.js new file mode 100644 index 0000000000..19ebb64cbf --- /dev/null +++ b/packages/core/database/lib/query/helpers/search.js @@ -0,0 +1,70 @@ +'use strict'; + +const _ = require('lodash/fp'); + +const types = require('../../types'); + +const applySearch = (qb, query, ctx) => { + const { alias, uid, db } = ctx; + + const { attributes } = db.metadata.get(uid); + + const searchColumns = ['id']; + + const stringColumns = Object.keys(attributes).filter(attributeName => { + const attribute = attributes[attributeName]; + return types.isString(attribute.type) && attribute.searchable !== false; + }); + + searchColumns.push(...stringColumns); + + if (!_.isNaN(_.toNumber(query))) { + const numberColumns = Object.keys(attributes).filter(attributeName => { + const attribute = attributes[attributeName]; + return types.isNumber(attribute.type) && attribute.searchable !== false; + }); + + searchColumns.push(...numberColumns); + } + + switch (db.dialect.client) { + case 'postgres': { + searchColumns.forEach(attr => + qb.orWhereRaw(`"${alias}"."${attr}"::text ILIKE ?`, `%${escapeQuery(query, '*%\\')}%`) + ); + + break; + } + case 'sqlite': { + searchColumns.forEach(attr => + qb.orWhereRaw(`"${alias}"."${attr}" LIKE ? ESCAPE '\\'`, `%${escapeQuery(query, '*%\\')}%`) + ); + break; + } + case 'mysql': { + searchColumns.forEach(attr => + qb.orWhereRaw(`\`${alias}\`.\`${attr}\` LIKE ?`, `%${escapeQuery(query, '*%\\')}%`) + ); + break; + } + default: { + // do nothing + } + } +}; + +const escapeQuery = (query, charsToEscape, escapeChar = '\\') => { + return query + .split('') + .reduce( + (escapedQuery, char) => + charsToEscape.includes(char) + ? `${escapedQuery}${escapeChar}${char}` + : `${escapedQuery}${char}`, + '' + ); +}; + +module.exports = { + applySearch, +}; diff --git a/packages/core/database/lib/query/helpers/transform.js b/packages/core/database/lib/query/helpers/transform.js new file mode 100644 index 0000000000..8711ebe485 --- /dev/null +++ b/packages/core/database/lib/query/helpers/transform.js @@ -0,0 +1,56 @@ +'use strict'; + +const _ = require('lodash/fp'); + +const types = require('../../types'); +const { createField } = require('../../fields'); + +const fromRow = (metadata, row) => { + if (Array.isArray(row)) { + return row.map(singleRow => fromRow(metadata, singleRow)); + } + + const { attributes } = metadata; + + if (_.isNil(row)) { + return null; + } + + 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 ...) + continue; + } + + 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; + } + + if (types.isRelation(attribute.type)) { + obj[attributeName] = row[column]; + } + } + + return obj; +}; + +module.exports = { + fromRow, +}; diff --git a/packages/core/database/lib/query/helpers/where.js b/packages/core/database/lib/query/helpers/where.js new file mode 100644 index 0000000000..54c7d6a597 --- /dev/null +++ b/packages/core/database/lib/query/helpers/where.js @@ -0,0 +1,311 @@ +'use strict'; + +const _ = require('lodash/fp'); + +const types = require('../../types'); +const { createJoin } = require('./join'); + +const GROUP_OPERATORS = ['$and', '$or']; +const OPERATORS = [ + '$not', + '$in', + '$notIn', + '$eq', + '$ne', + '$gt', + '$gte', + '$lt', + '$lte', + '$null', + '$notNull', + '$between', + // '$like', + // '$regexp', + '$startsWith', + '$endsWith', + '$contains', + '$notContains', +]; + +const ARRAY_OPERATORS = ['$in', '$notIn', '$between']; + +const isOperator = key => OPERATORS.includes(key); + +/** + * Process where parameter + * @param {Object} where + * @param {Object} ctx + * @param {number} depth + * @returns {Object} + */ +const processWhere = (where, ctx, depth = 0) => { + if (depth === 0 && !_.isPlainObject(where)) { + throw new Error('Where must be an object'); + } + + const processNested = (where, ctx) => { + if (!_.isPlainObject(where)) { + return where; + } + + return processWhere(where, ctx, depth + 1); + }; + + const { db, uid, qb, alias = qb.alias } = ctx; + + const filters = {}; + + // for each key in where + for (const key in where) { + const value = where[key]; + const attribute = db.metadata.get(uid).attributes[key]; + + // if operator $and $or then loop over them + if (GROUP_OPERATORS.includes(key)) { + filters[key] = value.map(sub => processNested(sub, ctx)); + continue; + } + + if (key === '$not') { + filters[key] = processNested(value, ctx); + continue; + } + + if (isOperator(key)) { + if (depth == 0) { + throw new Error( + `Only $and, $or and $not can by used as root level operators. Found ${key}.` + ); + } + + filters[key] = processNested(value, ctx); + continue; + } + + 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[`${alias || qb.alias}.${key}`] = processNested(value, ctx); + } + continue; + + // throw new Error(`Attribute ${key} not found on model ${uid}`); + } + + // move to if else to check for scalar / relation / components & throw for other types + if (attribute.type === 'relation') { + // TODO: pass down some filters (e.g published at) + + // attribute + const subAlias = createJoin(ctx, { alias, uid, attributeName: key, attribute }); + + let nestedWhere = processNested(value, { + db, + qb, + alias: subAlias, + uid: attribute.target, + }); + + if (!_.isPlainObject(nestedWhere) || isOperator(_.keys(nestedWhere)[0])) { + nestedWhere = { [`${subAlias}.id`]: nestedWhere }; + } + + // TODO: use a better merge logic (push to $and when collisions) + Object.assign(filters, nestedWhere); + + continue; + } + + if (types.isScalar(attribute.type)) { + // TODO: convert attribute name to column name + // TODO: cast to DB type + filters[`${alias || qb.alias}.${key}`] = processNested(value, ctx); + continue; + } + + throw new Error(`You cannot filter on ${attribute.type} types`); + } + + return filters; +}; + +const applyOperator = (qb, column, operator, value) => { + if (Array.isArray(value) && !ARRAY_OPERATORS.includes(operator)) { + return qb.where(subQB => { + value.forEach(subValue => + subQB.orWhere(innerQB => { + applyOperator(innerQB, column, operator, subValue); + }) + ); + }); + } + + switch (operator) { + case '$not': { + qb.whereNot(qb => applyWhereToColumn(qb, column, value)); + break; + } + + case '$in': { + qb.whereIn(column, _.castArray(value)); + break; + } + + case '$notIn': { + qb.whereNotIn(column, _.castArray(value)); + break; + } + + case '$eq': { + if (value === null) { + qb.whereNull(column); + break; + } + + qb.where(column, value); + break; + } + case '$ne': { + if (value === null) { + qb.whereNotNull(column); + break; + } + + qb.where(column, '<>', value); + break; + } + case '$gt': { + qb.where(column, '>', value); + break; + } + case '$gte': { + qb.where(column, '>=', value); + break; + } + case '$lt': { + qb.where(column, '<', value); + break; + } + case '$lte': { + qb.where(column, '<=', value); + break; + } + case '$null': { + // TODO: make this better + if (value) { + qb.whereNull(column); + } + break; + } + case '$notNull': { + if (value) { + qb.whereNotNull(column); + } + + break; + } + case '$between': { + qb.whereBetween(column, value); + break; + } + // case '$regexp': { + // // TODO: + // + // break; + // } + // // string + // // TODO: use $case to make it case insensitive + // case '$like': { + // qb.where(column, 'like', value); + // break; + // } + + // TODO: add casting logic + case '$startsWith': { + qb.where(column, 'like', `${value}%`); + break; + } + case '$endsWith': { + qb.where(column, 'like', `%${value}`); + break; + } + case '$contains': { + // TODO: handle insensitive + + qb.where(column, 'like', `%${value}%`); + break; + } + + case '$notContains': { + // TODO: handle insensitive + qb.whereNot(column, 'like', `%${value}%`); + break; + } + + // TODO: json operators + + // TODO: relational operators every/some/exists/size ... + + default: { + throw new Error(`Undefined operator ${operator}`); + } + } +}; + +const applyWhereToColumn = (qb, column, columnWhere) => { + if (!_.isPlainObject(columnWhere)) { + if (Array.isArray(columnWhere)) { + return qb.whereIn(column, columnWhere); + } + + return qb.where(column, columnWhere); + } + + // TODO: Transform into if has($in, value) then to handle cases with two keys doing one thing (like $contains with $case) + Object.keys(columnWhere).forEach(operator => { + const value = columnWhere[operator]; + + applyOperator(qb, column, operator, value); + }); +}; + +const applyWhere = (qb, where) => { + if (Array.isArray(where)) { + return qb.where(subQB => where.forEach(subWhere => applyWhere(subQB, subWhere))); + } + + if (!_.isPlainObject(where)) { + throw new Error('Where must be an object'); + } + + Object.keys(where).forEach(key => { + const value = where[key]; + + if (key === '$and') { + return qb.where(subQB => { + value.forEach(v => applyWhere(subQB, v)); + }); + } + + if (key === '$or') { + return qb.where(subQB => { + value.forEach(v => subQB.orWhere(inner => applyWhere(inner, v))); + }); + } + + if (key === '$not') { + return qb.whereNot(qb => applyWhere(qb, value)); + } + + applyWhereToColumn(qb, key, value); + }); +}; + +module.exports = { + applyWhere, + processWhere, +}; diff --git a/packages/core/strapi/lib/services/entity-service/index.js b/packages/core/strapi/lib/services/entity-service/index.js index b0ed428302..9ec81ea1e5 100644 --- a/packages/core/strapi/lib/services/entity-service/index.js +++ b/packages/core/strapi/lib/services/entity-service/index.js @@ -155,6 +155,7 @@ const createDefaultImplementation = ({ strapi, db, eventHub, entityValidator }) }); // TODO: upload the files then set the links in the entity like with compo to avoid making too many queries + // FIXME: upload in components if (files && Object.keys(files).length > 0) { await this.uploadFiles(uid, entity, files); entity = await this.findOne(uid, entity.id, { params }); @@ -194,6 +195,7 @@ const createDefaultImplementation = ({ strapi, db, eventHub, entityValidator }) }); // TODO: upload the files then set the links in the entity like with compo to avoid making too many queries + // FIXME: upload in components if (files && Object.keys(files).length > 0) { await this.uploadFiles(uid, entity, files); entity = await this.findOne(uid, entity.id, { params }); diff --git a/packages/core/strapi/lib/services/entity-service/params.js b/packages/core/strapi/lib/services/entity-service/params.js index d3bf7cb57f..10a55fed20 100644 --- a/packages/core/strapi/lib/services/entity-service/params.js +++ b/packages/core/strapi/lib/services/entity-service/params.js @@ -77,7 +77,14 @@ const transformParamsToQuery = (uid, params = {}) => { if (populate) { // TODO: handle *.* syntax const { populate } = params; - query.populate = typeof populate === 'object' ? populate : _.castArray(populate); + + if (populate === '*') { + query.populate = true; + } else if (typeof populate === 'object') { + query.populate = populate; + } else { + query.populate = _.castArray(populate); + } } // TODO: move to layer above ? diff --git a/packages/core/strapi/tests/publication-state.test.e2e.js b/packages/core/strapi/tests/publication-state.test.e2e.js index c9d2cf0e8d..087ce0d27c 100644 --- a/packages/core/strapi/tests/publication-state.test.e2e.js +++ b/packages/core/strapi/tests/publication-state.test.e2e.js @@ -154,6 +154,8 @@ describe('Publication State', () => { strapi = await createStrapiInstance(); rq = await createAuthRequest({ strapi }); + console.log(JSON.stringify(builder.sanitizedFixtures(strapi), null, 2)); + Object.assign(data, builder.sanitizedFixtures(strapi)); });