@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.
- package/CHANGELOG.md +56 -0
- package/app/index.css +3 -0
- package/app/index.js +50 -4
- package/bin/serve.js +1 -1
- package/lib/compile/cdsc.js +2 -2
- package/lib/compile/etc/_localized.js +1 -1
- package/lib/compile/for/lean_drafts.js +1 -0
- package/lib/compile/to/sql.js +2 -2
- package/lib/env/cds-requires.js +6 -0
- package/lib/env/defaults.js +14 -3
- package/lib/env/plugins.js +6 -22
- package/lib/linked/classes.js +0 -14
- package/lib/linked/types.js +12 -0
- package/lib/linked/validate.js +13 -8
- package/lib/log/cds-log.js +3 -3
- package/lib/log/format/aspects/als.js +23 -29
- package/lib/log/format/aspects/cls.js +9 -0
- package/lib/log/format/json.js +42 -6
- package/lib/ql/Whereable.js +5 -1
- package/lib/srv/cds-connect.js +33 -32
- package/lib/srv/cds-serve.js +2 -1
- package/lib/srv/middlewares/cds-context.js +2 -1
- package/lib/utils/cds-utils.js +4 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/validator/ValueValidator.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +1 -5
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/result.js +2 -31
- package/libx/_runtime/common/generic/auth/utils.js +2 -0
- package/libx/_runtime/common/generic/input.js +2 -11
- package/libx/_runtime/common/generic/put.js +1 -10
- package/libx/_runtime/common/utils/binary.js +1 -7
- package/libx/_runtime/common/utils/resolveView.js +2 -2
- package/libx/_runtime/common/utils/search2cqn4sql.js +1 -1
- package/libx/_runtime/common/utils/streamProp.js +19 -6
- package/libx/_runtime/common/utils/template.js +26 -16
- package/libx/_runtime/common/utils/templateProcessor.js +8 -7
- package/libx/_runtime/common/utils/ucsn.js +2 -5
- package/libx/_runtime/db/expand/expandCQNToJoin.js +10 -0
- package/libx/_runtime/db/generic/input.js +1 -5
- package/libx/_runtime/fiori/lean-draft.js +272 -90
- package/libx/_runtime/messaging/event-broker.js +105 -40
- package/libx/_runtime/remote/utils/client.js +12 -4
- package/libx/_runtime/ucl/Service.js +16 -6
- package/libx/odata/middleware/batch.js +2 -2
- package/libx/odata/middleware/read.js +6 -10
- package/libx/odata/middleware/stream.js +4 -5
- package/libx/odata/parse/afterburner.js +3 -2
- package/libx/odata/parse/multipartToJson.js +3 -1
- package/libx/odata/utils/index.js +3 -3
- package/libx/odata/utils/postProcess.js +3 -25
- package/libx/rest/middleware/parse.js +1 -6
- package/package.json +2 -2
package/lib/srv/cds-connect.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const cds = require('..'), LOG = cds.log('cds.connect')
|
|
2
|
-
const _pending = cds.services._pending
|
|
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 =
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
srv = await new Service (datasource,m,o); await srv._init()
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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) {
|
package/lib/srv/cds-serve.js
CHANGED
|
@@ -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
|
|
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)
|
package/lib/utils/cds-utils.js
CHANGED
|
@@ -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])))
|
package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/validator/ValueValidator.js
CHANGED
|
@@ -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
|
-
|
|
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) }
|
|
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
|
}
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: [
|
|
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,
|
|
24
|
-
const _picked = pick(element, target
|
|
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,
|
|
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 = {
|
|
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,
|
|
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,
|
|
108
|
+
_pick(pick, element, targetEntity, templateElements, elementName)
|
|
96
109
|
|
|
97
110
|
if (element.items) {
|
|
98
|
-
_pick(pick, element.items, targetEntity,
|
|
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,
|
|
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,
|
|
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(
|
|
148
|
+
let tmplt = model._templateCache.get(usecase).get(target)
|
|
139
149
|
if (!tmplt) {
|
|
140
|
-
tmplt = _getTemplate(model, model._templateCache.get(usecase),
|
|
141
|
-
model._templateCache.get(usecase).set(
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
110
|
-
|
|
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
|
-
|
|
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
|