diff --git a/src/actions/bookshelf/getItem.js b/src/actions/bookshelf/getItem.js index 087c9a7..1f51fd5 100644 --- a/src/actions/bookshelf/getItem.js +++ b/src/actions/bookshelf/getItem.js @@ -26,6 +26,16 @@ export default async function getItem (joint, spec = {}, input = {}, output) { if (debug) console.log(`[JOINT] [action:getItem] The model "${modelName}" is not recognized`) return Promise.reject(StatusErrors.generateModelNotRecognizedError(modelName)) } + const mainTableName = joint.model[modelName].prototype.tableName + + const ensureFlatIncludesId = (columns = []) => { + if (output !== 'flat' || !Array.isArray(columns)) return columns + if (columns.includes('*') || columns.includes(`${mainTableName}.*`)) return columns + + const idColumn = `${mainTableName}.id` + const hasId = columns.includes('id') || columns.includes(idColumn) + return hasId ? columns : columns.concat(idColumn) + } // Reject when required fields are not provided const requiredFieldCheck = ActionUtils.checkRequiredFields(specFields, inputFields) @@ -43,15 +53,15 @@ export default async function getItem (joint, spec = {}, input = {}, output) { if (returnColsDef) { // If a single set (array) is defined, honor the setting if (Array.isArray(returnColsDef)) { - actionOpts.columns = returnColsDef + actionOpts.columns = ensureFlatIncludesId(returnColsDef) // Otherwise, try to honor the set requested by the input } else if (input[ACTION.INPUT_FIELD_SET] && objectUtils.has(returnColsDef, input[ACTION.INPUT_FIELD_SET])) { - actionOpts.columns = returnColsDef[input[ACTION.INPUT_FIELD_SET]] + actionOpts.columns = ensureFlatIncludesId(returnColsDef[input[ACTION.INPUT_FIELD_SET]]) // If the input does not declare a set, check for a "default" set } else if (returnColsDef.default && Array.isArray(returnColsDef.default)) { - actionOpts.columns = returnColsDef.default + actionOpts.columns = ensureFlatIncludesId(returnColsDef.default) } } // end-if (returnColsDef) @@ -89,9 +99,9 @@ export default async function getItem (joint, spec = {}, input = {}, output) { const inputValue = (hasInput) ? inputFields[fieldName] : defaultValue - BookshelfUtils.appendWhereClause(joint, queryBuilder, modelName, fieldName, inputValue) + BookshelfUtils.appendWhereClause(joint, queryBuilder, modelName, fieldName, inputValue, fieldSpec.type, { columns: actionOpts.columns }) } else if (isLocked && hasDefault) { - BookshelfUtils.appendWhereClause(joint, queryBuilder, modelName, fieldName, defaultValue) + BookshelfUtils.appendWhereClause(joint, queryBuilder, modelName, fieldName, defaultValue, fieldSpec.type, { columns: actionOpts.columns }) } }) // end-specFields.forEach } // end-if (inputFields && specFields) diff --git a/src/actions/bookshelf/getItems.js b/src/actions/bookshelf/getItems.js index 9b8830c..4e65024 100644 --- a/src/actions/bookshelf/getItems.js +++ b/src/actions/bookshelf/getItems.js @@ -25,6 +25,16 @@ export default async function getItems (joint, spec = {}, input = {}, output) { if (!model) { return Promise.reject(StatusErrors.generateModelNotRecognizedError(modelName)) } + const mainTableName = joint.model[modelName].prototype.tableName + + const ensureFlatIncludesId = (columns = []) => { + if (output !== 'flat' || !Array.isArray(columns)) return columns + if (columns.includes('*') || columns.includes(`${mainTableName}.*`)) return columns + + const idColumn = `${mainTableName}.id` + const hasId = columns.includes('id') || columns.includes(idColumn) + return hasId ? columns : columns.concat(idColumn) + } // Reject when required fields are not provided const requiredFieldCheck = ActionUtils.checkRequiredFields(specFields, inputFields) @@ -48,15 +58,15 @@ export default async function getItems (joint, spec = {}, input = {}, output) { if (returnColsDef) { // If a single set (array) is defined, honor the setting if (Array.isArray(returnColsDef)) { - actionOpts.columns = returnColsDef + actionOpts.columns = ensureFlatIncludesId(returnColsDef) // Otherwise, try to honor the set requested by the input } else if (input[ACTION.INPUT_FIELD_SET] && objectUtils.has(returnColsDef, input[ACTION.INPUT_FIELD_SET])) { - actionOpts.columns = returnColsDef[input[ACTION.INPUT_FIELD_SET]] + actionOpts.columns = ensureFlatIncludesId(returnColsDef[input[ACTION.INPUT_FIELD_SET]]) // If the input does not declare a set, check for a "default" set } else if (returnColsDef.default && Array.isArray(returnColsDef.default)) { - actionOpts.columns = returnColsDef.default + actionOpts.columns = ensureFlatIncludesId(returnColsDef.default) } } // end-if (returnColsDef) @@ -101,9 +111,9 @@ export default async function getItems (joint, spec = {}, input = {}, output) { if (!isLocked && (hasInput || hasDefault)) { const inputValue = (hasInput) ? inputFields[fieldName] : defaultValue - BookshelfUtils.appendWhereClause(joint, queryBuilder, modelName, fieldName, inputValue, fieldSpec.type) + BookshelfUtils.appendWhereClause(joint, queryBuilder, modelName, fieldName, inputValue, fieldSpec.type, { columns: actionOpts.columns }) } else if (isLocked && hasDefault) { - BookshelfUtils.appendWhereClause(joint, queryBuilder, modelName, fieldName, defaultValue, fieldSpec.type) + BookshelfUtils.appendWhereClause(joint, queryBuilder, modelName, fieldName, defaultValue, fieldSpec.type, { columns: actionOpts.columns }) } }) // end-specFields.forEach } // end-if (inputFields && specFields) diff --git a/src/actions/bookshelf/utils/bookshelf-utils.js b/src/actions/bookshelf/utils/bookshelf-utils.js index 1726165..a102506 100644 --- a/src/actions/bookshelf/utils/bookshelf-utils.js +++ b/src/actions/bookshelf/utils/bookshelf-utils.js @@ -117,7 +117,7 @@ export function loadRelationsToItemBase (itemData, loadDirect = {}, keepAsRelati // ----------------------------------------------------------------------------- // Append a where clause to an existing query, per the provided input data. // ----------------------------------------------------------------------------- -export function appendWhereClause (joint, queryBuilder, modelName, fieldName, value, dataType) { +export function appendWhereClause (joint, queryBuilder, modelName, fieldName, value, dataType, options = {}) { // Load assets for query logic const mainTableName = joint.model[modelName].prototype.tableName // Required for association field queries @@ -149,6 +149,16 @@ export function appendWhereClause (joint, queryBuilder, modelName, fieldName, va // Association query logic for reusability let assocJoinApplied = false + const columns = options.columns + const shouldSelectMainFields = () => { + if (!Array.isArray(columns) || columns.length === 0) return true + return columns.includes('*') || columns.includes(`${mainTableName}.*`) + } + const shouldSelectAssocField = () => { + if (!Array.isArray(columns) || columns.length === 0) return true + if (columns.includes('*') || columns.includes(`${mainTableName}.*`)) return false + return columns.includes(assocField) || columns.includes(`${assocTableName}.${assocField}`) || columns.includes(`${assocName}.${assocField}`) + } const ensureAssociationJoin = () => { if (!isAssocClause || assocJoinApplied) return @@ -162,7 +172,12 @@ export function appendWhereClause (joint, queryBuilder, modelName, fieldName, va queryBuilder .leftJoin(assocTableName, `${mainTableName}.${assocPathInfo.sourceField}`, `${assocTableName}.${assocPathInfo.targetField}`) - .select(`${mainTableName}.*`, `${assocTableName}.${assocField}`) + if (shouldSelectMainFields()) { + queryBuilder.select(`${mainTableName}.*`) + } + if (shouldSelectAssocField()) { + queryBuilder.select(`${assocTableName}.${assocField}`) + } assocJoinApplied = true } @@ -341,10 +356,26 @@ export function appendWhereClause (joint, queryBuilder, modelName, fieldName, va const assocPathInfo = CoreUtils.parseAssociationPath(assocConfig.path) // console.log('[DEVING] assocPathInfo:', assocPathInfo) + const columns = options.columns + const shouldSelectMainFields = () => { + if (!Array.isArray(columns) || columns.length === 0) return true + return columns.includes('*') || columns.includes(`${mainTableName}.*`) + } + const shouldSelectAssocField = () => { + if (!Array.isArray(columns) || columns.length === 0) return true + if (columns.includes('*') || columns.includes(`${mainTableName}.*`)) return false + return columns.includes(assocField) || columns.includes(`${assocTableName}.${assocField}`) || columns.includes(`${assocName}.${assocField}`) + } + queryBuilder .leftJoin(assocTableName, `${mainTableName}.${assocPathInfo.sourceField}`, `${assocTableName}.${assocPathInfo.targetField}`) - .select(`${mainTableName}.*`, `${assocTableName}.${assocField}`) .where(`${assocTableName}.${assocField}`, '=', value) + if (shouldSelectMainFields()) { + queryBuilder.select(`${mainTableName}.*`) + } + if (shouldSelectAssocField()) { + queryBuilder.select(`${assocTableName}.${assocField}`) + } // Direct match on MAIN RESOURCE } else { @@ -372,11 +403,13 @@ export function appendWhereClause (joint, queryBuilder, modelName, fieldName, va // + NULLS are always returned last in both ASC and DESC orders. // ----------------------------------------------------------------------------- export function appendOrderByClause (joint, queryBuilder, modelName, fieldValue) { + const mainTableName = joint.model[modelName].prototype.tableName + // Iterate orderBy arguments const results = buildOrderBy(fieldValue).map(orderOpt => { // Support column from main model if (!orderOpt.col.includes('.')) { - return [true, (_queryBuilder) => _queryBuilder.orderBy(orderOpt.col, orderOpt.order)] + return [true, (_queryBuilder) => _queryBuilder.orderBy(`${mainTableName}.${orderOpt.col}`, orderOpt.order)] // Support column from association } else { @@ -391,7 +424,6 @@ export function appendOrderByClause (joint, queryBuilder, modelName, fieldValue) } // Obtain model config info to build raw query - const mainTableName = joint.model[modelName].prototype.tableName const assocTableName = joint.model[assocModelName].prototype.tableName const mainModelConfig = joint.modelConfig.find(it => it.name === modelName) const assocConfig = mainModelConfig.associations[assocName] @@ -405,7 +437,6 @@ export function appendOrderByClause (joint, queryBuilder, modelName, fieldValue) const assocPathInfo = CoreUtils.parseAssociationPath(assocConfig.path) return [true, (_queryBuilder) => _queryBuilder .leftJoin(assocTableName, `${mainTableName}.${assocPathInfo.sourceField}`, `${assocTableName}.${assocPathInfo.targetField}`) - .select(`${mainTableName}.*`, `${assocTableName}.${colName}`) .orderByRaw(`${assocTableName}.${colName} IS NULL, ${assocTableName}.${colName} ${orderOpt.order}`) ] } diff --git a/test/functional/actions/bookshelf/crud_getItems.spec.js b/test/functional/actions/bookshelf/crud_getItems.spec.js index a081b5c..ab39d5b 100644 --- a/test/functional/actions/bookshelf/crud_getItems.spec.js +++ b/test/functional/actions/bookshelf/crud_getItems.spec.js @@ -234,6 +234,44 @@ describe('ACTION: getItems [bookshelf]', () => { `) }) + it(`should support the "spec.${ACTION.SPEC_FIELDS_TO_RETURN}" option, permitting various sets of returned field data when searching by association field and flat options`, async () => { + const specColsWithDefault = {} + specColsWithDefault.fieldsToReturn = { + default: ['username', 'external_id'], + flat: ['_model', 'id', 'username', 'external_id'] + } + + const specUser = { + modelName: 'User', + fields: [ + { name: 'preferred_locale', type: 'String' }, + { name: 'info.professional_title', type: 'String' } + ], + fieldsToReturn: ['username', 'external_id'], + defaultOrderBy: '-created_at' + } + const usersInfoProTitle = { + fields: { + 'info.professional_title': 'EdgeCaser' + } + } + + const getSpecifiedColsFromUserFlat = projectApp.getItems(specUser, usersInfoProTitle, 'flat') + .then((user) => { + expect(user.data[0]).to.have.keys(specColsWithDefault.fieldsToReturn.flat) + }) + + const getSpecifiedColsFromUserWithoutFlat = projectApp.getItems(specUser, usersInfoProTitle) + .then((user) => { + expect(user.models[0].attributes).to.have.keys(specColsWithDefault.fieldsToReturn.default) + }) + + return Promise.all([ + getSpecifiedColsFromUserFlat, + getSpecifiedColsFromUserWithoutFlat + ]) + }) + it(`should support the "input.${ACTION.INPUT_FIELD_SET}" syntax, permitting various sets of returned field data`, () => { const specBase = { modelName: 'User',