@sap/cds 8.1.1 → 8.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/app/index.css +3 -0
  3. package/app/index.js +50 -4
  4. package/bin/serve.js +1 -1
  5. package/lib/compile/cdsc.js +2 -2
  6. package/lib/compile/etc/_localized.js +1 -1
  7. package/lib/compile/for/lean_drafts.js +1 -0
  8. package/lib/compile/to/sql.js +2 -2
  9. package/lib/env/cds-requires.js +6 -0
  10. package/lib/env/defaults.js +14 -3
  11. package/lib/env/plugins.js +6 -22
  12. package/lib/linked/classes.js +0 -14
  13. package/lib/linked/types.js +12 -0
  14. package/lib/linked/validate.js +13 -8
  15. package/lib/log/cds-log.js +3 -3
  16. package/lib/log/format/aspects/als.js +23 -29
  17. package/lib/log/format/aspects/cls.js +9 -0
  18. package/lib/log/format/json.js +42 -6
  19. package/lib/ql/Whereable.js +5 -1
  20. package/lib/srv/cds-connect.js +33 -32
  21. package/lib/srv/cds-serve.js +2 -1
  22. package/lib/srv/middlewares/cds-context.js +2 -1
  23. package/lib/utils/cds-utils.js +4 -2
  24. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/validator/ValueValidator.js +1 -1
  25. package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +1 -5
  26. package/libx/_runtime/cds-services/adapter/odata-v4/utils/result.js +2 -31
  27. package/libx/_runtime/common/generic/auth/utils.js +2 -0
  28. package/libx/_runtime/common/generic/input.js +2 -11
  29. package/libx/_runtime/common/generic/put.js +1 -10
  30. package/libx/_runtime/common/utils/binary.js +1 -7
  31. package/libx/_runtime/common/utils/resolveView.js +2 -2
  32. package/libx/_runtime/common/utils/search2cqn4sql.js +1 -1
  33. package/libx/_runtime/common/utils/streamProp.js +19 -6
  34. package/libx/_runtime/common/utils/template.js +26 -16
  35. package/libx/_runtime/common/utils/templateProcessor.js +8 -7
  36. package/libx/_runtime/common/utils/ucsn.js +2 -5
  37. package/libx/_runtime/db/expand/expandCQNToJoin.js +10 -0
  38. package/libx/_runtime/db/generic/input.js +1 -5
  39. package/libx/_runtime/fiori/lean-draft.js +272 -90
  40. package/libx/_runtime/messaging/event-broker.js +105 -40
  41. package/libx/_runtime/remote/utils/client.js +12 -4
  42. package/libx/_runtime/ucl/Service.js +16 -6
  43. package/libx/odata/middleware/batch.js +2 -2
  44. package/libx/odata/middleware/read.js +6 -10
  45. package/libx/odata/middleware/stream.js +4 -5
  46. package/libx/odata/parse/afterburner.js +3 -2
  47. package/libx/odata/parse/multipartToJson.js +3 -1
  48. package/libx/odata/utils/index.js +3 -3
  49. package/libx/odata/utils/postProcess.js +3 -25
  50. package/libx/rest/middleware/parse.js +1 -6
  51. package/package.json +2 -2
@@ -1,5 +1,5 @@
1
1
  const cds = require('..'), LOG = cds.log('cds.connect')
2
- const _pending = cds.services._pending || {} // used below to chain parallel connect.to(<same>)
2
+ const _pending = cds.services._pending ??= {} // used below to chain parallel connect.to(<same>)
3
3
  const TRACE = cds.debug('trace')
4
4
 
5
5
  /**
@@ -17,45 +17,46 @@ const connect = module.exports = async function cds_connect (options) {
17
17
  /**
18
18
  * Connect to a specific service, either served locally, with ad-hoc options
19
19
  * or with options configured in cds.env.requires.<datasource>.
20
- * @param {string} [datasource]
20
+ * @param { string|Function|object } [datasource]
21
21
  * @param {{ kind?:String, impl?:String }} [options]
22
22
  * @returns { Promise<import('./srv-api')> }
23
23
  */
24
- connect.to = async (datasource, options) => {
25
- TRACE?.time(`cds.connect.to ${datasource}`.padEnd(22).slice(0,22))
26
- let Service = cds.service.factory, _done = x=>x
27
- if (typeof datasource === 'object') [options,datasource] = [datasource]
24
+ connect.to = (datasource, options) => {
25
+ let Service = cds.service.factory
26
+ if (typeof datasource === 'object' && !datasource.name) [options,datasource] = [datasource] // .to({ options })
28
27
  else if (datasource) {
29
- if (datasource._is_service_class) [ Service, datasource ] = [ datasource, datasource.name ]
28
+ if (datasource._is_service_class) [ Service, datasource ] = [ datasource, datasource.name ] // .to(ServiceClass)
29
+ else if (datasource.name) datasource = datasource.name // .to({ name: 'Service' }) from cds-typer
30
30
  if (!options) { //> specifying ad-hoc options disallows caching
31
- if (datasource in cds.services) return cds.services[datasource]
31
+ if (datasource in cds.services) return Promise.resolve (cds.services[datasource])
32
32
  if (datasource in _pending) return _pending[datasource]
33
- // queue parallel requests to a single promise, to avoid creating multiple services
34
- _pending[datasource] = new Promise (r=>_done=r).finally(()=> delete _pending[datasource])
35
33
  }
36
34
  }
37
- const o = Service === cds.service.factory ? options4 (datasource, options) : {}
38
- const m = await model4 (o)
39
- // check if required service definition exists
40
- const required = cds.requires[datasource]
41
- if (required?.model?.length && datasource !== 'db' && !m.definitions[required.service||datasource]) {
42
- LOG.error(`No service definition found for '${required.service || datasource}', as required by 'cds.requires.${datasource}':`, required)
43
- throw new Error (`No service definition found for '${required.service || datasource}'`)
44
- }
45
- // construct new service instance
46
- let srv
47
- try {
48
- srv = await new Service (datasource,m,o); await srv._init()
49
- } catch (e) {
50
- _pending[datasource] = Promise.reject(e)
51
- throw e
52
- }
53
- if (o.outbox) srv = cds.outboxed(srv)
54
- if (datasource === 'db') cds.db = srv
55
- _done (cds.services[datasource] = srv)
56
- if (!o.silent) cds.emit ('connect',srv)
57
- TRACE?.timeEnd(`cds.connect.to ${datasource}`.padEnd(22).slice(0,22))
58
- return srv
35
+ const promise = (async()=>{
36
+ TRACE?.time(`cds.connect.to ${datasource}`.padEnd(22).slice(0,22))
37
+ const o = Service === cds.service.factory ? options4 (datasource, options) : {}
38
+ const m = await model4 (o)
39
+ // check if required service definition exists
40
+ const required = cds.requires[datasource]
41
+ if (required?.model?.length && datasource !== 'db' && !m.definitions[required.service||datasource]) {
42
+ LOG.error(`No service definition found for '${required.service || datasource}', as required by 'cds.requires.${datasource}':`, required)
43
+ throw new Error (`No service definition found for '${required.service || datasource}'`)
44
+ }
45
+ // construct new service instance
46
+ let srv = await new Service (datasource,m,o); await srv._init()
47
+ if (o.outbox) srv = cds.outboxed(srv)
48
+ if (datasource) {
49
+ if (datasource === 'db') cds.db = srv
50
+ cds.services[datasource] = srv
51
+ delete _pending[datasource]
52
+ }
53
+ if (!o.silent) cds.emit ('connect',srv)
54
+ TRACE?.timeEnd(`cds.connect.to ${datasource}`.padEnd(22).slice(0,22))
55
+ return srv
56
+ })()
57
+ // queue parallel requests to a single promise, to avoid creating multiple services
58
+ if (datasource && !options) _pending[datasource] = promise
59
+ return promise
59
60
  }
60
61
 
61
62
  function options4 (name, _o) {
@@ -1,7 +1,8 @@
1
1
  const cds = require ('..')
2
2
  const { Service } = cds.service.factory
3
3
  const { serve } = cds.service.protocols
4
- const _ready = Symbol(), _pending = cds.services._pending || {}
4
+ const _pending = cds.services._pending ??= {}
5
+ const _ready = Symbol()
5
6
  const TRACE = cds.debug('trace')
6
7
  if (TRACE && !cds.env.features.odata_new_adapter) require('./../../libx/_runtime/cds-services/adapter/odata-v4/to')
7
8
 
@@ -1,5 +1,6 @@
1
1
  const cds = require ('../../index')
2
2
  const corr_id = 'x-correlation-id'
3
+ const crippled_corr_id = 'x-correlationid'
3
4
  const req_id = 'x-request-id'
4
5
  const vr_id = 'x-vcap-request-id'
5
6
  const { uuid } = cds.utils
@@ -8,7 +9,7 @@ const { EventContext } = cds
8
9
  module.exports = () => {
9
10
  /** @type { import('express').Handler } */
10
11
  return function cds_context (req, res, next) {
11
- const id = req.headers[corr_id] ??= req.headers[req_id] || req.headers[vr_id] || uuid()
12
+ const id = req.headers[corr_id] ??= req.headers[req_id] || req.headers[vr_id] || req.headers[crippled_corr_id] || uuid()
12
13
  const ctx = EventContext.for ({ id, http: { req, res } })
13
14
  res.set ('X-Correlation-ID', id) // Note: we use capitalized style here as that's common standard in HTTP world
14
15
  cds._context.run (ctx, next)
@@ -289,13 +289,15 @@ if (process.env.JEST_WORKER_ID === undefined) { // jest's ESM support is experi
289
289
  }
290
290
  }
291
291
 
292
+ const SECRETS = /(passw)|(cert)|(ca)|(secret)|(key)/i
292
293
  /**
293
294
  * Masks password-like strings, also reducing clutter in output
295
+ * @param {any} cred - object or array with credentials
296
+ * @returns {any}
294
297
  */
295
- const SECRETS = /(password)|(certificate)|(ca)|(clientsecret)|(secret)|(key)|(clientcert)/i
296
298
  exports._redacted = function _redacted(cred) {
297
299
  if (!cred) return cred
298
- if (Array.isArray(cred)) return cred.map(_redacted)
300
+ if (Array.isArray(cred)) return cred.map(c => typeof c === 'string' ? '...' : _redacted(c))
299
301
  if (typeof cred === 'object') {
300
302
  const newCred = Object.assign({}, cred)
301
303
  Object.keys(newCred).forEach(k => (typeof newCred[k] === 'string' && SECRETS.test(k)) ? (newCred[k] = '...') : (newCred[k] = _redacted(newCred[k])))
@@ -4,7 +4,7 @@ const { big } = require('@sap/cds-foss')
4
4
  const { isInvalidBase64string } = require('../../../../../../common/utils/binary')
5
5
  const IllegalArgumentError = require('../errors/IllegalArgumentError')
6
6
 
7
- const YEAR_RE = '(?:-?(?:(?:(?:0\\d{3})|(?:[1-9]\\d{3,}))))'
7
+ const YEAR_RE = '(?:-?(?:(?:(?:0\\d{3})|(?:[1-9]\\d{3}))))'
8
8
  const MONTH_RE = '(?:(?:0[1-9])|(?:1[012]))'
9
9
  const DAY_RE = '(?:(?:0[1-9])|(?:[12]\\d)|(?:3[01]))'
10
10
  const HOURS_RE = '(?:(?:[01]\\d)|(?:2[0-3]))'
@@ -5,7 +5,6 @@ const { resolveFromSelect, targetFromPath } = require('../../../../common/utils/
5
5
  const { setEntityContained } = require('../../../../common/utils/csn')
6
6
  const { getNavigationIfStruct } = require('../../../../common/utils/structured')
7
7
  const getTemplate = require('../../../../common/utils/template')
8
- const templateProcessor = require('../../../../common/utils/templateProcessor')
9
8
 
10
9
  const _ignoreColumns = columns => {
11
10
  if (!(Array.isArray(columns) && columns.some(c => c === '*' || c.as || c.ref))) return true
@@ -111,10 +110,7 @@ const _columnsFromData = (data, definition, service) => {
111
110
  const columns = {}
112
111
  const template = getTemplate('odata-context', service, definition, { pick: element => element.isAssociation })
113
112
  if (!template || !template.elements.size) return ''
114
- const arrayData = Array.isArray(data) ? data : data ? [data] : []
115
- for (const row of arrayData) {
116
- templateProcessor({ processFn: _processFn(columns), row, template, pathOptions: { pathSegmentsInfo: [] } })
117
- }
113
+ template.process(data, _processFn(columns), { pathSegmentsInfo: [] })
118
114
  return _stringifyColumnsFromData(columns)
119
115
  }
120
116
 
@@ -4,7 +4,6 @@ const { Readable } = require('stream')
4
4
  const { big } = require('@sap/cds-foss')
5
5
 
6
6
  const getTemplate = require('../../../../common/utils/template')
7
- const templateProcessor = require('../../../../common/utils/templateProcessor')
8
7
  const { omitValue, applyOmitValuesPreference } = require('./omitValues')
9
8
  const { setLocationHeader } = require('./readAfterWrite')
10
9
  const normalizeTimestamp = require('../../../../common/utils/normalizeTimestamp')
@@ -251,19 +250,6 @@ const _processorFn = (req, previousResult, options) => elementInfo => {
251
250
  }
252
251
  }
253
252
 
254
- const _getParent = (model, name) => {
255
- const target = model.definitions[name]
256
-
257
- if (target && target.elements) {
258
- for (const elementName in target.elements) {
259
- const element = target.elements[elementName]
260
- if (element._anchor && element._anchor._isContained) return element._anchor
261
- }
262
- }
263
-
264
- return null
265
- }
266
-
267
253
  const _isUpAssoc = element => element && /^up_(_up_)*$/.test(element.name) && _isContainedOrBackLink(element)
268
254
 
269
255
  const _isContainedOrBackLink = element =>
@@ -365,29 +351,14 @@ const postProcess = (req, res, service, result, previousResult) => {
365
351
 
366
352
  const options = _getOptions(req)
367
353
  const cacheKey = _generateCacheKey(headers, options)
368
- const parent = _getParent(model, target.name)
369
354
  // REVISIT: Why so many templates? -> Create only one (superset) template to reduce memory consumption
370
- const template = getTemplate(cacheKey, service, target, { pick: _pick(options) }, parent)
355
+ const template = getTemplate(cacheKey, service, target, { pick: _pick(options) })
371
356
 
372
357
  if (template.elements.size === 0) return
373
358
 
374
359
  // normalize result to rows
375
360
  result = result.value != null && Object.keys(result).filter(k => !k.match(/^\W/)).length === 1 ? result.value : result
376
-
377
- if (typeof result === 'object' && result != null) {
378
- const rows = Array.isArray(result) ? result : [result]
379
-
380
- // process each row
381
- const processFn = _processorFn(req, previousResult, options)
382
-
383
- for (const row of rows) {
384
- templateProcessor({
385
- processFn,
386
- row,
387
- template
388
- })
389
- }
390
- }
361
+ template.process(result, _processorFn(req, previousResult, options))
391
362
 
392
363
  applyOmitValuesPreference(res, options.omitValuesPreference)
393
364
  }
@@ -133,6 +133,8 @@ const resolveUserAttrs = (where, req) => {
133
133
  r.ref.forEach(el => {
134
134
  if (el.where) el.where = resolveUserAttrs(el.where, req)
135
135
  })
136
+ } else if (r.func) {
137
+ r.args = resolveUserAttrs(r.args, req)
136
138
  }
137
139
  }
138
140
 
@@ -15,7 +15,6 @@ const { DRAFT_COLUMNS_MAP } = require('../../common/constants/draft')
15
15
  const propagateForeignKeys = require('../utils/propagateForeignKeys')
16
16
  const { checkInputConstraints, assertTargets } = require('../../cds-services/util/assert')
17
17
  const getTemplate = require('../utils/template')
18
- const templateProcessor = require('../utils/templateProcessor')
19
18
  const { getDataFromCQN, setDataFromCQN } = require('../utils/data')
20
19
  const getRowUUIDGeneratorFn = require('../utils/rowUUIDGenerator')
21
20
 
@@ -276,11 +275,8 @@ async function commonGenericInput(req) {
276
275
 
277
276
  const data = getDataFromCQN(req.query) // REVISIT: req.data should point into req.query
278
277
  enrichDataWithKeysFromWhere(data, req, this)
279
- const arrayData = Array.isArray(data) ? data : [data]
280
278
 
281
- for (const row of arrayData) {
282
- templateProcessor({ processFn: _getProcessorFn(req, errors, assertMap), row, template, pathOptions })
283
- }
279
+ template.process(data, _getProcessorFn(req, errors, assertMap), pathOptions)
284
280
 
285
281
  if (assertMap.targets.size > 0) {
286
282
  await assertTargets(assertMap, errors)
@@ -314,12 +310,7 @@ const _processActionFunctionRow = (row, param, key, errors, event, service) => {
314
310
  ignore: element => element._isAssociationStrict
315
311
  })
316
312
 
317
- if (template && template.elements.size) {
318
- for (const value of values) {
319
- const args = { processFn: _getProcessorFnForActionsFunctions(errors, key), row: value, template }
320
- templateProcessor(args)
321
- }
322
- }
313
+ template.process(values, _getProcessorFnForActionsFunctions(errors, key))
323
314
  }
324
315
 
325
316
  const _processActionFunction = (row, eventParams, errors, event, service) => {
@@ -7,7 +7,6 @@
7
7
 
8
8
  const cds = require('../../cds')
9
9
  const getTemplate = require('../utils/template')
10
- const templateProcessor = require('../utils/templateProcessor')
11
10
  const { getDataFromCQN, setDataFromCQN } = require('../utils/data')
12
11
 
13
12
  const _fillStructure = (row, parts, element, category, args) => {
@@ -73,15 +72,7 @@ function commonGenericPut(req) {
73
72
  // REVISIT: req.data should point into req.query
74
73
  const data = getDataFromCQN(req.query)
75
74
 
76
- const arrayData = Array.isArray(data) ? data : [data]
77
- for (const row of arrayData) {
78
- const args = {
79
- processFn: _getProcessorFn(req),
80
- row,
81
- template
82
- }
83
- templateProcessor(args)
84
- }
75
+ template.process(data, _getProcessorFn(req))
85
76
 
86
77
  // REVISIT: req.data should point into req.query
87
78
  setDataFromCQN(req)
@@ -1,5 +1,4 @@
1
1
  const getTemplate = require('./template')
2
- const templateProcessor = require('./templateProcessor')
3
2
 
4
3
  // convert the standard base64 encoding to the URL-safe variant
5
4
  const toBase64url = value => {
@@ -49,12 +48,7 @@ const _processorFn =
49
48
 
50
49
  const _processBinaryData = (data, srv, definition, toBuffer) => {
51
50
  const template = getTemplate('rest-payload', srv, definition, { pick: _picker })
52
- if (template && template.elements.size) {
53
- const rows = Array.isArray(data) ? data : [data]
54
- for (const row of rows) {
55
- templateProcessor({ processFn: _processorFn(toBuffer), row, template })
56
- }
57
- }
51
+ template.process(data, _processorFn(toBuffer))
58
52
  }
59
53
 
60
54
  const base64ToBuffer = (data, srv, definition) => {
@@ -95,7 +95,7 @@ const _newNestedData = (queryTarget, newData, ref, value) => {
95
95
 
96
96
  for (let i = 0; i < ref.length; i++) {
97
97
  currentEntity = currentEntity.elements[ref[i]]
98
- if (currentEntity.isAssociation) {
98
+ if (!currentEntity || currentEntity.isAssociation) {
99
99
  // > don't follow associations
100
100
  break
101
101
  } else {
@@ -510,7 +510,7 @@ const _queryColumns = (target, columns = [], persistenceTable = false, force = f
510
510
 
511
511
  // There could be some `where` clause inside `ref` which we don't support yet
512
512
  if (!renamed.ref || renamed.ref.some(e => typeof e !== 'string') || renamed.xpr) return res
513
- if (isTargetAliased) renamed.ref.shift()
513
+ if (isTargetAliased && renamed.ref[0] === from.as) renamed.ref.shift()
514
514
 
515
515
  // If the entity is annotated with the annotation `@cds.persistence.table`
516
516
  // and elements aliases exist, the aliases must be used as column references.
@@ -28,7 +28,7 @@ const search2cqn4sql = (query, model, options = {}) => {
28
28
  const columnsToBeSearched = computeColumnsToBeSearched(query, entity, alias)
29
29
  const expression = columnsToBeSearched?.length
30
30
  ? searchToLike(cqnSearchPhrase, columnsToBeSearched)
31
- : [{ val: 0 }, '=', { val: 1 }]
31
+ : [{ val: '0' }, '=', { val: '1' }]
32
32
 
33
33
  // REVISIT: find out here if where or having must be used
34
34
  aggregated ? query.having(expression) : query.where(expression)
@@ -2,10 +2,23 @@ const cds = require('../../cds')
2
2
  const { ensureNoDraftsSuffix, ensureUnlocalized } = require('./draft')
3
3
  const { isDuplicate } = require('./rewriteAsterisks')
4
4
 
5
- const _addColumn = (name, type, columns, url) => {
6
- const mediaType = typeof type === 'object' ? { ref: [type['='].replaceAll(/\./g, '_')] } : { val: type }
5
+ const _addColumn = (name, type, columns, url, target) => {
6
+ let mediaType = typeof type === 'object' && type['=']
7
+ if (mediaType && target.elements[mediaType]?.virtual) return
8
+ mediaType = mediaType ? { ref: [mediaType.replaceAll(/\./g, '_')] } : { val: type }
7
9
  const col = {
8
- xpr: ['case', 'when', { ref: [name] }, '=', { val: null }, 'then', 'NULL', 'else', mediaType, 'end'],
10
+ xpr: [
11
+ 'case',
12
+ 'when',
13
+ { ref: [name] },
14
+ '=',
15
+ { val: null },
16
+ 'then',
17
+ { val: null },
18
+ 'else',
19
+ { func: 'coalesce', args: [mediaType, { val: 'application/octet-stream' }] },
20
+ 'end'
21
+ ],
9
22
  as: `${name}@odata.mediaContentType`
10
23
  }
11
24
 
@@ -23,8 +36,8 @@ const _addColumn = (name, type, columns, url) => {
23
36
  const _addColumns = (target, columns) => {
24
37
  for (const k in target.elements) {
25
38
  const el = target.elements[k]
26
- if (el['@Core.MediaType']) {
27
- _addColumn(el.name, el['@Core.MediaType'], columns, el['@Core.IsURL'] && el.type === 'cds.String')
39
+ if (el['@Core.MediaType'] && !el.virtual) {
40
+ _addColumn(el.name, el['@Core.MediaType'], columns, el['@Core.IsURL'] && el.type === 'cds.String', target)
28
41
  }
29
42
  }
30
43
  }
@@ -47,7 +60,7 @@ const handleStreamProperties = (target, columns, model) => {
47
60
  _addColumns(target, columns)
48
61
  } else if (col.ref && (type === 'cds.LargeBinary' || (mediaType && !ignoreMediaType))) {
49
62
  if (mediaType) {
50
- _addColumn(name, mediaType, columns, element['@Core.IsURL'])
63
+ _addColumn(name, mediaType, columns, element['@Core.IsURL'], target)
51
64
  columns.splice(index, 1)
52
65
  } else if (!cds.env.features.stream_compat) {
53
66
  columns.splice(index, 1)
@@ -1,5 +1,7 @@
1
1
  const DELIMITER = require('./templateDelimiter')
2
2
 
3
+ const templateProcessor = require('./templateProcessor')
4
+
3
5
  const _addSubTemplate = (templateElements, elementName, subTemplate) => {
4
6
  if (subTemplate.elements.size > 0) {
5
7
  const t = templateElements.get(elementName)
@@ -20,8 +22,8 @@ const _addCacheToTemplateElements = (templateElements, elementName, cached) => {
20
22
  else templateElements.set(elementName, cached)
21
23
  }
22
24
 
23
- const _pick = (pick, element, target, parent, templateElements, elementName) => {
24
- const _picked = pick(element, target, parent)
25
+ const _pick = (pick, element, target, templateElements, elementName) => {
26
+ const _picked = pick(element, target)
25
27
  if (_picked) _addToTemplateElements(templateElements, elementName, { plain: _picked })
26
28
  }
27
29
 
@@ -68,14 +70,25 @@ const _getNextTarget = (model, element, currentPath = []) => {
68
70
  * @param {object} callbacks
69
71
  * @param {function} callbacks.pick Callback function to pick elements. If it returns a truthy value, the element will be picked. The returned value is part of the template.
70
72
  * @param {function} callbacks.ignore Callback function to ignore the target of an element. If it returns a truthy value, the element's target will be ignored.
71
- * @param {object} [parent=null] The parent entity
72
73
  * @param {Map} [_entityMap] This parameter is an implementation side-effect — don't use it
73
74
  * @param {array} [targetPath=[]]
74
75
  */
75
- function _getTemplate(model, cache, targetEntity, callbacks, parent = null, _entityMap = new Map(), targetPath = []) {
76
+ function _getTemplate(model, cache, targetEntity, callbacks, _entityMap = new Map(), targetPath = []) {
76
77
  const { pick, ignore, flatAccess } = callbacks
77
78
  const templateElements = new Map()
78
- const template = { target: targetEntity, elements: templateElements }
79
+ const template = {
80
+ target: targetEntity,
81
+ elements: templateElements,
82
+ process(data, fn, pathOptions) {
83
+ templateProcessor({
84
+ processFn: fn,
85
+ data,
86
+ template,
87
+ pathOptions,
88
+ isRoot: true
89
+ })
90
+ }
91
+ }
79
92
  const currentPath = [...targetPath, targetEntity.name]
80
93
  _entityMap.set(currentPath.join(DELIMITER), { template })
81
94
  const elements = targetEntity.elements || targetEntity.params
@@ -85,17 +98,17 @@ function _getTemplate(model, cache, targetEntity, callbacks, parent = null, _ent
85
98
  if (targetEntity._flat2struct) {
86
99
  for (const elementName in targetEntity._flat2struct) {
87
100
  const element = targetEntity._flat2struct[elementName]
88
- _pick(pick, element, targetEntity, parent, templateElements, elementName)
101
+ _pick(pick, element, targetEntity, templateElements, elementName)
89
102
  }
90
103
  }
91
104
  }
92
105
 
93
106
  for (const elementName in elements) {
94
107
  const element = elements[elementName]
95
- _pick(pick, element, targetEntity, parent, templateElements, elementName)
108
+ _pick(pick, element, targetEntity, templateElements, elementName)
96
109
 
97
110
  if (element.items) {
98
- _pick(pick, element.items, targetEntity, parent, templateElements, ['_itemsOf', elementName].join(DELIMITER))
111
+ _pick(pick, element.items, targetEntity, templateElements, ['_itemsOf', elementName].join(DELIMITER))
99
112
  }
100
113
 
101
114
  const { nextTargetName, nextTarget } = _getNextTarget(model, element, currentPath)
@@ -109,10 +122,10 @@ function _getTemplate(model, cache, targetEntity, callbacks, parent = null, _ent
109
122
  // inline structures must be handled separately.
110
123
  let subTemplate
111
124
  if (_isInlineStructured(element))
112
- subTemplate = _getTemplate(model, cache, nextTarget, callbacks, targetEntity, _entityMap, currentPath)
125
+ subTemplate = _getTemplate(model, cache, nextTarget, callbacks, _entityMap, currentPath)
113
126
  else if (cache.has(nextTarget)) subTemplate = cache.get(nextTarget)
114
127
  else {
115
- subTemplate = _getTemplate(model, cache, nextTarget, callbacks, targetEntity, _entityMap)
128
+ subTemplate = _getTemplate(model, cache, nextTarget, callbacks, _entityMap)
116
129
  cache.set(nextTarget, subTemplate)
117
130
  }
118
131
  _addSubTemplate(templateElements, elementName, subTemplate)
@@ -129,16 +142,13 @@ module.exports = (usecase, tx, target, ...args) => {
129
142
  const model = tx.model
130
143
  if (!model) return
131
144
 
132
- const root = model.definitions[target.name] || target
133
- if (!root) return
134
-
135
145
  if (!model._templateCache) Object.defineProperty(model, '_templateCache', { value: new Map() })
136
146
  if (!model._templateCache.get(usecase)) model._templateCache.set(usecase, new WeakMap())
137
147
 
138
- let tmplt = model._templateCache.get(usecase).get(root)
148
+ let tmplt = model._templateCache.get(usecase).get(target)
139
149
  if (!tmplt) {
140
- tmplt = _getTemplate(model, model._templateCache.get(usecase), root, ...args)
141
- model._templateCache.get(usecase).set(root, tmplt)
150
+ tmplt = _getTemplate(model, model._templateCache.get(usecase), target, ...args)
151
+ model._templateCache.get(usecase).set(target, tmplt)
142
152
  }
143
153
  return tmplt
144
154
  }
@@ -48,7 +48,7 @@ const _processComplex = (processFn, row, template, key, pathOptions) => {
48
48
 
49
49
  for (const row of rows) {
50
50
  if (row == null) continue
51
- const args = { processFn, row, template, isRoot: false, pathOptions }
51
+ const args = { processFn, data: row, template, isRoot: false, pathOptions }
52
52
 
53
53
  let pathSegmentInfo
54
54
  if (pathOptions.includeKeyValues) {
@@ -63,12 +63,13 @@ const _processComplex = (processFn, row, template, key, pathOptions) => {
63
63
  }
64
64
  }
65
65
 
66
- /**
67
- * @param {import("../../types/api").TemplateProcessor} args
68
- */
69
- const templateProcessor = ({ processFn, row, template, isRoot = true, pathOptions = {} }) => {
70
- for (const [tKey, tValue] of template.elements) {
71
- _processRow(processFn, row, template, tKey, tValue, isRoot, pathOptions)
66
+ const templateProcessor = ({ processFn, data, template, isRoot = true, pathOptions = {} }) => {
67
+ if (!template || !template.elements.size || !data || typeof data !== 'object') return
68
+ const dataArr = Array.isArray(data) ? data : [data]
69
+ for (const row of dataArr) {
70
+ for (const [tKey, tValue] of template.elements) {
71
+ _processRow(processFn, row, template, tKey, tValue, isRoot, pathOptions)
72
+ }
72
73
  }
73
74
  }
74
75
 
@@ -1,7 +1,6 @@
1
1
  const cds = require('../../cds')
2
2
  const getError = require('../error')
3
3
  const getTemplate = require('./template')
4
- const templateProcessor = require('./templateProcessor')
5
4
  const IS_PROXY = Symbol('flat2structProxy')
6
5
 
7
6
  const proxifyIfFlattened = (definition, payload) => {
@@ -106,10 +105,8 @@ function convertStructured(service, definition, data, { cleanupNull = false, cle
106
105
  const template = getTemplate('universal-input', service, definition, { pick: _picker, flatAccess })
107
106
  const arrayData = Array.isArray(data) ? data : [data]
108
107
  if (template && template.elements.size) {
109
- for (let i = 0; i < arrayData.length; i++) {
110
- const row = proxifyIfFlattened(definition, arrayData[i])
111
- templateProcessor({ processFn: _processor, row, template })
112
- }
108
+ const _data = arrayData.map(d => proxifyIfFlattened(definition, d))
109
+ template.process(_data, _processor)
113
110
  }
114
111
  for (const row of arrayData) {
115
112
  _cleanup(row, definition, cleanupNull, cleanupStruct, errors)
@@ -999,6 +999,16 @@ class JoinCQNFromExpanded {
999
999
  const res = this._buildNewAliasColumn(x, entity, tableAlias, mappings, true)
1000
1000
  delete res.as
1001
1001
  return res
1002
+ }
1003
+ if (x.func && x.args) {
1004
+ x.args = x.args.map(arg => {
1005
+ if (arg.ref) {
1006
+ const res = this._buildNewAliasColumn(arg, entity, tableAlias, mappings, true)
1007
+ delete res.as
1008
+ return res
1009
+ } else return arg
1010
+ })
1011
+ return x
1002
1012
  } else return x
1003
1013
  })
1004
1014
  },
@@ -16,7 +16,6 @@ const normalizeTimeData = require('../utils/normalizeTimeData')
16
16
  const { enrichDataWithKeysFromWhere } = require('../../common/utils/keys')
17
17
  const propagateForeignKeys = require('../../common/utils/propagateForeignKeys')
18
18
  const getTemplate = require('../../common/utils/template')
19
- const templateProcessor = require('../../common/utils/templateProcessor')
20
19
 
21
20
  const { DRAFT_COLUMNS_MAP } = require('../../common/constants/draft')
22
21
 
@@ -220,10 +219,7 @@ function dbGenericInput(req) {
220
219
 
221
220
  if (!draft) enrichDataWithKeysFromWhere(req.data, req, this)
222
221
 
223
- const data = Array.isArray(req.data) ? req.data : [req.data]
224
- for (const row of data) {
225
- templateProcessor({ processFn: _processorFn(req), row, template })
226
- }
222
+ template.process(req.data, _processorFn(req))
227
223
  }
228
224
 
229
225
  dbGenericInput._initial = true