Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 15 additions & 5 deletions src/actions/bookshelf/getItem.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)

Expand Down Expand Up @@ -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)
Expand Down
20 changes: 15 additions & 5 deletions src/actions/bookshelf/getItems.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)

Expand Down Expand Up @@ -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)
Expand Down
43 changes: 37 additions & 6 deletions src/actions/bookshelf/utils/bookshelf-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand All @@ -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
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand All @@ -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]
Expand All @@ -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}`)
]
}
Expand Down
38 changes: 38 additions & 0 deletions test/functional/actions/bookshelf/crud_getItems.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down