@sap/cds
Version:
SAP Cloud Application Programming Model - CDS for Node.js
718 lines (601 loc) • 23.1 kB
JavaScript
const cds = require('../../../../lib')
let LOG = cds.log('app')
const { rewriteAsterisks } = require('./rewriteAsterisks')
const _setInverseTransition = (mapping, ref, mapped) => {
const existing = mapping.get(ref)
if (!existing) mapping.set(ref, mapped)
else {
const alternatives = existing.alternatives || []
alternatives.push(mapped)
existing.alternatives = alternatives
mapping.set(ref, existing)
}
}
const _inverseTransition = transition => {
const inverseTransition = {}
inverseTransition.target = transition.queryTarget
inverseTransition.queryTarget = transition.target
inverseTransition.mapping = new Map()
if (!transition.mapping.size) inverseTransition.mapping = new Map()
for (const [key, value] of transition.mapping) {
const mapped = {}
if (value.ref) {
if (value.transition) mapped.transition = _inverseTransition(value.transition)
const ref0 = value.ref[0]
if (value.ref.length > 1) {
// ignore flattened columns like author.name
if (transition.target.elements[ref0]?.isAssociation) continue
const nested = inverseTransition.mapping.get(ref0) || {}
if (!nested.transition) nested.transition = { mapping: new Map() }
let current = nested.transition.mapping
for (let i = 1; i < value.ref.length; i++) {
const last = i === value.ref.length - 1
const obj = last ? { ref: [key] } : { transition: { mapping: new Map() } }
_setInverseTransition(current, value.ref[i], obj)
if (!last) current = current.get(value.ref[i]).transition.mapping
}
inverseTransition.mapping.set(ref0, nested)
} else {
mapped.ref = [key]
_setInverseTransition(inverseTransition.mapping, ref0, mapped)
}
}
}
return inverseTransition
}
const revertData = (data, transition, service, options) => {
if (!transition || !transition.mapping.size) return data
const inverseTransition = _inverseTransition(transition)
return Array.isArray(data)
? data.map(entry => _newData(entry, inverseTransition, true, service, options))
: _newData(data, inverseTransition, true, service, options)
}
const _newSubData = (val, key, transition, el, inverse, service, options) => {
if ((!Array.isArray(val) && typeof val === 'object') || (Array.isArray(val) && val.length !== 0)) {
let mapped = transition.mapping.get(key)
if (!mapped) {
mapped = {}
transition.mapping.set(key, mapped)
}
if (!mapped.transition) {
const subTransition = getTransition(el._target, service, undefined, options?.event, { abort: options?.abort })
mapped.transition = inverse ? _inverseTransition(subTransition) : subTransition
}
if (Array.isArray(val)) {
return val.map(singleVal => _newData(singleVal, mapped.transition, inverse, service, options))
} else {
return _newData(val, mapped.transition, inverse, service, options)
}
}
return val //Case of empty array
}
const _newNestedData = (queryTarget, newData, ref, value) => {
const parent = queryTarget.query && queryTarget.query._target
let currentEntity = parent
let currentData = newData
for (let i = 0; i < ref.length; i++) {
currentEntity = currentEntity.elements[ref[i]]
if (!currentEntity || currentEntity.isAssociation) {
// > don't follow associations
break
} else {
// > intermediate or final struct element
if (i === ref.length - 1) currentData[ref[i]] = value
else currentData = currentData[ref[i]] = currentData[ref[i]] || {}
}
}
}
const _newData = (data, transition, inverse, service, options) => {
if (data === null) return null
// no transition -> nothing to do
if (transition.target && transition.target.name === transition.queryTarget.name) return data
const newData = {}
const queryTarget = transition.queryTarget
for (const key in data) {
const el = queryTarget && queryTarget?.elements[key]
const isAssoc = el && el.isAssociation
const mapped = transition.mapping.get(key)
if (!mapped) {
//In this condition the data is needed
if (
((typeof data[key] === 'object' && data[key] !== null) || transition.target.elements[key]) &&
newData[key] === undefined
)
newData[key] = data[key]
continue
}
let value = data[key]
if (isAssoc) {
if (value || (value === null && service.name === 'db')) {
value = _newSubData(value, key, transition, el, inverse, service, options)
}
}
if (!isAssoc && mapped.transition) {
value = _newSubData(value, key, transition, el, inverse, undefined, options)
Object.assign(newData, value)
}
if (mapped.ref) {
const { ref } = mapped
if (ref.length === 1) {
newData[ref[0]] = value
if (mapped.alternatives) mapped.alternatives.forEach(({ ref }) => (newData[ref[0]] = value))
} else {
_newNestedData(queryTarget, newData, ref, value)
}
}
}
return newData
}
const _newColumns = (columns = [], transition, service, withAlias = false, options) => {
const newColumns = []
columns.forEach(column => {
let newColumn
if (column.func) {
newColumn = { ...column }
newColumn.args = _newColumns(column.args, transition, service, withAlias, options)
newColumns.push(newColumn)
return newColumns
}
const mapped = column.ref && transition.mapping.get(column.ref[0])
if (mapped && mapped.ref) {
newColumn = { ...column }
if (withAlias) {
newColumn.as = column.ref[column.ref.length - 1]
}
newColumn.ref = [...mapped.ref, ...column.ref.slice(mapped.ref.length)]
} else if (mapped && mapped.val) {
newColumn = {}
newColumn.as = column.ref[0]
newColumn.val = mapped.val
} else if (column.ref && !transition.target.elements[column.ref[0]]) {
return // ignore columns which are not part of the entity
} else {
newColumn = column
}
// ensure that renaming of a redirected assoc are also respected
if (mapped && column.expand) {
// column.ref might be structured elements
let def
column.ref.forEach((ref, i) => {
if (i === 0) {
def = transition.queryTarget.elements[ref]
} else {
def = def.elements[ref]
}
})
// reuse _newColumns with new transition
const expandTarget = def._target
const subtransition = getTransition(expandTarget, service, undefined, options.event, { abort: options.abort })
mapped.transition = subtransition
newColumn.expand = _newColumns(column.expand, subtransition, service, withAlias, options)
}
newColumns.push(newColumn)
})
return newColumns
}
const _resolveColumn = (column, transition) => {
const mapped = transition.mapping.get(column)
if (mapped && mapped.ref) {
return mapped.ref[0]
} else if (!mapped) {
return column
}
}
const _newInsertColumns = (columns = [], transition) => {
const newColumns = []
columns.forEach(column => {
const resolvedColumn = _resolveColumn(column, transition)
if (resolvedColumn) {
newColumns.push(resolvedColumn)
}
})
return newColumns
}
// REVISIT: this hard-coding on ref indexes does not support path expressions
const _newWhereRef = (newWhereElement, transition, tableName, options) => {
let newRef = Array.isArray(newWhereElement.ref) ? [...newWhereElement.ref] : [newWhereElement.ref]
if (newRef.length === 1 && typeof newRef[0] === 'string') {
const mapped = transition.mapping.get(newRef[0])
if (mapped) {
newRef[0] = mapped.ref.join('_')
}
} else {
// REVISIT: aliases in sub selects not yet supported
if (newRef[0] !== tableName) options.previousEntity = transition.queryTarget
const nestedTransitions = _entityTransitionsForTarget(
newWhereElement,
options.model,
options.service,
options
).filter(nT => nT?.target)
newRef = _rewriteQueryPath(newWhereElement, [transition, ...nestedTransitions], options)
}
newWhereElement.ref = newRef
}
const _newEntries = (entries = [], transition, service, options) =>
entries.map(entry => _newData(entry, transition, false, service, options))
const _newWhere = (where = [], transition, tableName, alias, isSubselect = false, options) => {
const newWhere = where.map(whereElement => {
if (whereElement.xpr) {
return { xpr: _newWhere(whereElement.xpr, transition, tableName, alias, isSubselect, options) }
}
if (whereElement.list) {
return { list: _newWhere(whereElement.list, transition, tableName, alias, isSubselect, options) }
}
const newWhereElement = { ...whereElement }
if (!whereElement.ref && !whereElement.SELECT && !whereElement.func) return whereElement
if (whereElement.SELECT && whereElement.SELECT.where && !whereElement._doNotResolve) {
newWhereElement.SELECT.where = _newWhere(whereElement.SELECT.where, transition, tableName, alias, true, options)
return newWhereElement
}
if (newWhereElement.ref) {
options.alias = alias
_newWhereRef(newWhereElement, transition, tableName, options)
return newWhereElement
}
if (newWhereElement.func) {
newWhereElement.args = _newWhere(newWhereElement.args, transition, tableName, alias, undefined, options)
return newWhereElement
}
return whereElement
})
return newWhere
}
const _initialColumns = transition => {
const columns = []
for (const [transitionEl] of transition.mapping) {
// REVISIT: structured elements
if (!transition.queryTarget.elements[transitionEl] || transition.queryTarget.elements[transitionEl].isAssociation) {
continue
}
columns.push({ ref: [transitionEl] })
}
return columns
}
const _rewriteQueryPath = (path, transitions, options) => {
const alias = options?.alias
let hasAlias = false
let target = options?.previousEntity
return path.ref.map((f, i) => {
if (f === alias) {
hasAlias = true
return alias
}
if (options?.previousEntity && !hasAlias) i++
if (i === 0) {
target = transitions[0].target
if (typeof f === 'string') {
return target.name
}
if (f.id) {
return {
id: target.name,
where: _newWhere(f.where, transitions[0], f.id, undefined, undefined, options)
}
}
} else {
// REVISIT: alias in sub selects not yet supported
if (transitions[i - 1]) {
if (typeof f === 'string') {
const transitionMapping = transitions[i - 1].mapping.get(f)
return (transitionMapping && transitionMapping.ref && transitionMapping.ref[0]) || f
}
if (f.id) {
const transitionMapping = transitions[i - 1].mapping.get(f.id)
return {
id: (transitionMapping && transitionMapping.ref && transitionMapping.ref[0]) || f.id,
where: _newWhere(f.where, transitions[i], f.id, undefined, undefined, options)
}
}
}
return f
}
})
}
const _newUpdate = (query, transitions, options) => {
const targetTransition = transitions.at(-1)
const targetName = targetTransition.target.name
const newUpdate = Object.create(query.UPDATE)
newUpdate.entity = newUpdate.entity.ref
? {
...newUpdate.entity,
ref: _rewriteQueryPath(query.UPDATE.entity, transitions, options)
}
: targetName
if (newUpdate.data) newUpdate.data = _newData(newUpdate.data, targetTransition, false, options.service, options)
if (newUpdate.with) newUpdate.with = _newData(newUpdate.with, targetTransition, false, options.service, options)
if (newUpdate.where) {
newUpdate.where = _newWhere(
newUpdate.where,
targetTransition,
query._target.name,
query.UPDATE.entity.as,
undefined,
options
)
}
return newUpdate
}
const _newSelect = (query, transitions, options) => {
const service = options.service
const targetTransition = transitions.at(-1)
const newSelect = Object.create(query.SELECT)
newSelect.from = {
...newSelect.from,
ref: _rewriteQueryPath(query.SELECT.from, transitions, options)
}
if (!newSelect.columns && targetTransition.mapping.size) newSelect.columns = _initialColumns(targetTransition)
if (newSelect.columns) {
rewriteAsterisks({ SELECT: query.SELECT }, service.model, {
_4db: service.isDatabaseService,
target: targetTransition.queryTarget
})
newSelect.columns = _newColumns(
newSelect.columns,
targetTransition,
service,
service.kind !== 'app-service',
options
)
}
if (newSelect.having)
newSelect.having = _newColumns(newSelect.having, targetTransition, undefined, undefined, options)
if (newSelect.groupBy)
newSelect.groupBy = _newColumns(newSelect.groupBy, targetTransition, undefined, undefined, options)
if (newSelect.orderBy)
newSelect.orderBy = _newColumns(newSelect.orderBy, targetTransition, undefined, undefined, options)
if (newSelect.where) {
newSelect.where = _newWhere(
newSelect.where,
targetTransition,
query.SELECT.from && query.SELECT.from.ref[0],
query.SELECT.from && query.SELECT.from.as,
undefined,
options
)
}
return newSelect
}
const _newInsert = (query, transitions, options) => {
const targetTransition = transitions.at(-1)
const targetName = targetTransition.target.name
const newInsert = Object.create(query.INSERT)
if (newInsert.into) {
const refObject = newInsert.into.ref ? newInsert.into : { ref: [query.INSERT.into] }
newInsert.into = {
...refObject,
ref: _rewriteQueryPath(refObject, transitions, options)
}
if (!query.INSERT.into.ref) newInsert.into = newInsert.into.ref[0] // leave as string
} else {
newInsert.into = targetName
}
if (newInsert.columns) newInsert.columns = _newInsertColumns(newInsert.columns, targetTransition)
if (newInsert.entries) newInsert.entries = _newEntries(newInsert.entries, targetTransition, options.service, options)
return newInsert
}
const _newUpsert = (query, transitions, options) => {
const targetTransition = transitions.at(-1)
const targetName = targetTransition.target.name
const newUpsert = Object.create(query.UPSERT)
newUpsert.into = newUpsert.into.ref
? {
...newUpsert.into,
ref: _rewriteQueryPath(query.UPSERT.into, transitions, options)
}
: targetName
if (newUpsert.columns) newUpsert.columns = _newInsertColumns(newUpsert.columns, targetTransition)
if (newUpsert.entries) newUpsert.entries = _newEntries(newUpsert.entries, targetTransition, options.service, options)
return newUpsert
}
const _newDelete = (query, transitions, options) => {
const targetTransition = transitions[transitions.length - 1]
const targetName = targetTransition.target.name
const newDelete = Object.create(query.DELETE)
newDelete.from = newDelete.from.ref
? {
...newDelete.from,
ref: _rewriteQueryPath(query.DELETE.from, transitions, options)
}
: targetName
if (newDelete.where) {
const from = typeof query.DELETE.from === 'string' ? query.DELETE.from : query.DELETE.from.ref[0]
newDelete.where = _newWhere(newDelete.where, targetTransition, from, query.DELETE.from.as, undefined, options)
}
return newDelete
}
const _findRenamed = (cqnColumns, column) =>
cqnColumns.find(
cqnColumn =>
cqnColumn.as &&
(column?.ref?.at(-1) === cqnColumn.as ||
(column.as === cqnColumn.as && Object.prototype.hasOwnProperty.call(cqnColumn, 'val')))
)
const _queryColumns = (target, columns = [], isAborted) => {
if (!(target && target.query && target.query.SELECT)) return columns
const cqnColumns = target.query.SELECT.columns || []
const from = target.query.SELECT.from
const isTargetAliased = from.as && cqnColumns.some(c => c.ref?.[0] === from.as)
if (!columns.length) columns = Object.keys(target.elements).map(e => ({ ref: [e], as: e }))
const queryColumns = columns.reduce((res, column) => {
const renamed = _findRenamed(cqnColumns, column)
if (renamed) {
if (renamed.val) return res.concat({ as: renamed.as, val: renamed.val })
// There could be some `where` clause inside `ref` which we don't support yet
if (!renamed.ref || renamed.ref.some(e => typeof e !== 'string') || renamed.xpr) return res
if (isTargetAliased && renamed.ref[0] === from.as) renamed.ref.shift()
column.ref = isAborted ? [renamed.as] : [...renamed.ref]
}
res.push(column)
return _appendForeignKeys(res, target, columns, column)
}, [])
return queryColumns
}
const _mappedValue = (col, alias) => {
const key = col.as || col.ref[0]
if (col.ref) {
const columnRef = col.ref.filter(columnName => columnName !== alias)
return [key, { ref: columnRef }]
}
return [key, { val: col.val }]
}
const getDBTable = target => cds.ql.resolve.table(target)
const _appendForeignKeys = (newColumns, target, columns, { as, ref = [] }) => {
const el = target.elements[as] || target.query._target?.elements[ref.at(-1)]
if (el && el.isAssociation && el.keys) {
for (const key of el.keys) {
// .as and .ref has a different meaning here
// .as means the original property name, if the foreign key is renamed
const keyName = key.as || key.ref[0]
const keyAlias = key.ref[0]
const found = columns.find(col => col.as === `${as}_${keyAlias}`)
if (found) {
found.ref = [`${ref.join('_')}_${keyName}`]
} else {
newColumns.push({
ref: [`${ref.join('_')}_${keyName}`],
as: `${as}_${keyAlias}`
})
}
}
}
return newColumns
}
const _checkForForbiddenViews = (queryTarget, event) => {
const select = queryTarget && queryTarget.query && queryTarget.query.SELECT
if (select) {
if (!select.from || select.from.join || select.from.length > 1) {
throw cds.error(501, `${event || 'INSERT|UPDATE|DELETE'} on views with join and/or union is not supported`, {
target: queryTarget.name
})
}
if (select.where) {
LOG._debug &&
LOG.debug(`Ignoring where clause during ${event || 'INSERT|UPDATE|DELETE'} on view "${queryTarget.name}".`)
}
}
}
const _getTransitionData = (target, columns, service, options) => {
let { abort, skipForbiddenViewCheck, event } = options
// REVISIT revert after cds-dbs pr
if (!abort) abort = cds.ql.resolve.abortDB
// REVISIT: Find less param polluting way to skip forbidden view check for reads
if (!skipForbiddenViewCheck) _checkForForbiddenViews(target, event)
const isAborted = abort(target)
columns = _queryColumns(target, columns, isAborted)
if (isAborted) return { target, transitionColumns: columns }
if (!target.query?._target) {
// for cross service in x4 and DRAFT.DraftAdministrativeData we cannot abort properly
// therefore return last resolved target
if (cds.env.features.restrict_service_scope === false) return { target, transitionColumns: columns }
return undefined
} else {
const newTarget = target.query._target
// continue projection resolving for projections
return _getTransitionData(newTarget, columns, service, options)
}
}
/**
* If no entity definition is found, no transition is done.
*
* @param queryTarget
* @param service
* @param skipForbiddenViewCheck
*/
const getTransition = (queryTarget, service, skipForbiddenViewCheck, event, options) => {
// Never resolve unknown targets (e.g. for drafts)
if (!queryTarget) {
return { target: queryTarget, queryTarget, mapping: new Map() }
}
const transitionData = _getTransitionData(queryTarget, [], service, {
skipForbiddenViewCheck,
event,
abort: options?.abort
})
if (!transitionData) return undefined
const { target: _target, transitionColumns } = transitionData
const query = queryTarget.query
const alias = query && query.SELECT && query.SELECT.from && query.SELECT.from.as
const mappedColumns = transitionColumns.map(column => _mappedValue(column, alias))
const mapping = new Map(mappedColumns)
return { target: _target, queryTarget, mapping }
}
const _entityTransitionsForTarget = (from, model, service, options) => {
let previousEntity = options.previousEntity
if (typeof from === 'string') {
return (
model.definitions[from] && [
getTransition(model.definitions[from], service, undefined, options.event, { abort: options.abort })
]
)
}
return from.ref.map((f, i) => {
const element = f.id || f
if (element === options.alias) return
if (i === 0 && !previousEntity) {
const entity = model.definitions[element]
if (entity) {
previousEntity = entity
return getTransition(entity, service, undefined, options.event, { abort: options.abort })
}
}
if (previousEntity) {
const entity = previousEntity.elements[element] && previousEntity.elements[element]._target
if (entity) {
// > assoc
previousEntity = entity
return getTransition(entity, service, undefined, options.event, { abort: options.abort })
}
// > struct
previousEntity = previousEntity.elements[element]
return {
target: previousEntity,
queryTarget: previousEntity,
mapping: new Map()
}
}
})
}
const resolveView = (query, model, service, abort) => {
// swap logger
const _LOG = LOG
LOG = cds.log(service.kind) // REVISIT: Avoid obtaining loggers per request!
// If the query is a projection, one must follow it
// to let the underlying service know its true entity.
// prettier-ignore
const kind = query.kind || (
query.SELECT ? 'SELECT' :
query.INSERT ? 'INSERT' :
query.UPSERT ? 'UPSERT' :
query.UPDATE ? 'UPDATE' :
query.DELETE ? 'DELETE' :
undefined
)
const [_prop, _func] = {
SELECT: ['from', _newSelect],
INSERT: ['into', _newInsert],
UPSERT: ['into', _newUpsert],
UPDATE: ['entity', _newUpdate],
DELETE: ['from', _newDelete]
}[kind]
const options = { abort, event: kind, service, model }
const transitions = _entityTransitionsForTarget(query[kind][_prop], model, service, options)
if (!service.isDatabaseService && cds.env.features.restrict_service_scope !== false && transitions.some(t => !t))
return
const newQuery = Object.create(query)
newQuery[kind] = (transitions?.[0] && _func(newQuery, transitions, options)) || { ...query[kind] }
const target = transitions?.at(-1)?.target || query._target //> IMPORtANT!
Object.defineProperties(newQuery, {
_target: { value: target, enumerable: false, writable: true },
_transitions: { value: transitions }
})
// restore logger
LOG = _LOG // REVISIT: Don't do such global variables juggling !!!
return newQuery
}
module.exports = {
getDBTable,
resolveView,
getTransition,
revertData
}