@sap/cds 7.6.4 → 7.7.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 +39 -1
- package/_i18n/i18n.properties +3 -0
- package/app/index.js +14 -8
- package/bin/serve.js +51 -19
- package/common.cds +16 -0
- package/lib/auth/ias-auth.js +2 -2
- package/lib/auth/index.js +1 -1
- package/lib/auth/jwt-auth.js +1 -1
- package/lib/compile/cdsc.js +23 -11
- package/lib/compile/for/nodejs.js +2 -2
- package/lib/compile/for/odata.js +4 -0
- package/lib/compile/load.js +7 -2
- package/lib/compile/to/sql.js +3 -0
- package/lib/dbs/cds-deploy.js +197 -220
- package/lib/env/defaults.js +2 -1
- package/lib/index.js +8 -2
- package/lib/linked/types.js +1 -0
- package/lib/log/format/json.js +4 -1
- package/lib/plugins.js +2 -2
- package/lib/ql/SELECT.js +8 -8
- package/lib/req/context.js +22 -13
- package/lib/req/request.js +10 -4
- package/lib/srv/cds-connect.js +9 -3
- package/lib/srv/cds-serve.js +5 -3
- package/lib/srv/middlewares/ctx-model.js +1 -1
- package/lib/srv/protocols/odata-v4.js +38 -9
- package/lib/srv/srv-api.js +98 -140
- package/lib/srv/srv-models.js +2 -2
- package/lib/srv/srv-tx.js +1 -0
- package/lib/utils/cds-utils.js +32 -23
- package/lib/utils/data.js +1 -1
- package/lib/utils/tar.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +1 -3
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +0 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +18 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/index.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/utils.js +7 -3
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriParser.js +2 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/index.js +5 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +71 -25
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +10 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +6 -1
- package/libx/_runtime/cds-services/util/assert.js +50 -240
- package/libx/_runtime/cds.js +5 -0
- package/libx/_runtime/common/aspects/any.js +53 -45
- package/libx/_runtime/common/generic/input.js +14 -10
- package/libx/_runtime/common/generic/paging.js +1 -1
- package/libx/_runtime/common/utils/cqn.js +1 -1
- package/libx/_runtime/common/utils/cqn2cqn4sql.js +1 -1
- package/libx/_runtime/common/utils/keys.js +1 -1
- package/libx/_runtime/common/utils/quotingStyles.js +1 -1
- package/libx/_runtime/common/utils/resolveStructured.js +4 -1
- package/libx/_runtime/common/utils/rewriteAsterisks.js +5 -12
- package/libx/_runtime/common/utils/stream.js +2 -16
- package/libx/_runtime/common/utils/streamProp.js +16 -6
- package/libx/_runtime/common/utils/ucsn.js +1 -0
- package/libx/_runtime/db/expand/expandCQNToJoin.js +1 -1
- package/libx/_runtime/db/sql-builder/InsertBuilder.js +1 -1
- package/libx/_runtime/db/utils/columns.js +6 -1
- package/libx/_runtime/fiori/generic/activate.js +11 -3
- package/libx/_runtime/fiori/generic/edit.js +8 -2
- package/libx/_runtime/fiori/lean-draft.js +94 -30
- package/libx/_runtime/hana/execute.js +2 -5
- package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +12 -22
- package/libx/_runtime/messaging/service.js +6 -2
- package/libx/common/assert/index.js +232 -0
- package/libx/common/assert/type.js +109 -0
- package/libx/common/assert/utils.js +125 -0
- package/libx/common/assert/validation.js +109 -0
- package/libx/odata/index.js +5 -5
- package/libx/odata/middleware/create.js +83 -0
- package/libx/odata/middleware/delete.js +38 -0
- package/libx/odata/middleware/error.js +8 -0
- package/libx/odata/{metadata.js → middleware/metadata.js} +8 -6
- package/libx/odata/middleware/operation.js +78 -0
- package/libx/odata/middleware/parse.js +11 -0
- package/libx/odata/{read.js → middleware/read.js} +42 -20
- package/libx/odata/{service-document.js → middleware/service-document.js} +2 -1
- package/libx/odata/middleware/stream.js +237 -0
- package/libx/odata/middleware/update.js +165 -0
- package/libx/odata/{afterburner.js → parse/afterburner.js} +79 -29
- package/libx/odata/{cqn2odata.js → parse/cqn2odata.js} +5 -3
- package/libx/odata/{parseToCqn.js → parse/parseToCqn.js} +3 -6
- package/libx/odata/{utils.js → utils/index.js} +95 -9
- package/libx/outbox/index.js +2 -1
- package/libx/rest/RestAdapter.js +0 -1
- package/libx/rest/middleware/operation.js +6 -4
- package/libx/rest/middleware/parse.js +20 -2
- package/package.json +1 -1
- package/server.js +43 -71
- package/libx/odata/create.js +0 -44
- package/libx/odata/delete.js +0 -25
- package/libx/odata/error.js +0 -12
- package/libx/odata/update.js +0 -110
- /package/libx/odata/{grammar.peggy → parse/grammar.peggy} +0 -0
- /package/libx/odata/{parser.js → parse/parser.js} +0 -0
- /package/libx/odata/{result.js → utils/result.js} +0 -0
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
const cds = require('
|
|
1
|
+
const cds = require('../../../')
|
|
2
2
|
|
|
3
|
-
const { where2obj, resolveFromSelect } = require('
|
|
4
|
-
const { findCsnTargetFor } = require('
|
|
5
|
-
const normalizeTimestamp = require('
|
|
6
|
-
const { rewriteExpandAsterisk } = require('
|
|
7
|
-
const resolveStructured = require('
|
|
3
|
+
const { where2obj, resolveFromSelect } = require('../../_runtime/common/utils/cqn')
|
|
4
|
+
const { findCsnTargetFor } = require('../../_runtime/common/utils/csn')
|
|
5
|
+
const normalizeTimestamp = require('../../_runtime/common/utils/normalizeTimestamp')
|
|
6
|
+
const { rewriteExpandAsterisk } = require('../../_runtime/common/utils/rewriteAsterisks')
|
|
7
|
+
const resolveStructured = require('../../_runtime/common/utils/resolveStructured')
|
|
8
8
|
|
|
9
9
|
const _addKeysDeep = (keys, keysCollector, ignoreManagedBacklinks) => {
|
|
10
10
|
for (const keyName in keys) {
|
|
@@ -165,28 +165,28 @@ function _processWhere(where, entity) {
|
|
|
165
165
|
|
|
166
166
|
if (element) {
|
|
167
167
|
i += 2
|
|
168
|
-
where[valIndex].val = _convertVal(
|
|
168
|
+
where[valIndex].val = _convertVal(where[valIndex].val, element)
|
|
169
169
|
}
|
|
170
170
|
}
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
-
function _convertVal(
|
|
173
|
+
function _convertVal(value, element) {
|
|
174
174
|
if (value === null) return value
|
|
175
175
|
switch (element._type) {
|
|
176
|
+
// numbers
|
|
176
177
|
case 'cds.UInt8':
|
|
177
178
|
case 'cds.Integer':
|
|
178
179
|
case 'cds.Int16':
|
|
179
180
|
case 'cds.Int32':
|
|
180
|
-
if (!/^-?\+?\d+$/.test(value))
|
|
181
|
+
if (!/^-?\+?\d+$/.test(value))
|
|
182
|
+
throw Object.assign(new Error(`${element.name} does not contain a valid Integer`), { statusCode: 400 })
|
|
181
183
|
// eslint-disable-next-line no-case-declarations
|
|
182
184
|
const n = Number(value)
|
|
183
|
-
if (!Number.isSafeInteger(n))
|
|
184
|
-
|
|
185
|
+
if (!Number.isSafeInteger(n))
|
|
186
|
+
throw Object.assign(new Error(`${element.name} does not contain a valid Integer`), { statusCode: 400 })
|
|
187
|
+
if (element._type === 'cds.UInt8' && n < 0)
|
|
188
|
+
throw Object.assign(new Error(`${element.name} does not contain a valid positive Integer`), { statusCode: 400 })
|
|
185
189
|
return n
|
|
186
|
-
|
|
187
|
-
case 'cds.String':
|
|
188
|
-
case 'cds.LargeString':
|
|
189
|
-
return String(value)
|
|
190
190
|
case 'cds.Double':
|
|
191
191
|
return parseFloat(value)
|
|
192
192
|
case 'cds.Decimal':
|
|
@@ -195,18 +195,44 @@ function _convertVal(element, value) {
|
|
|
195
195
|
case 'cds.Integer64':
|
|
196
196
|
if (typeof value === 'string') return value
|
|
197
197
|
return String(value)
|
|
198
|
+
// others
|
|
199
|
+
case 'cds.String':
|
|
200
|
+
case 'cds.LargeString':
|
|
201
|
+
return String(value)
|
|
198
202
|
case 'cds.Boolean':
|
|
199
203
|
return typeof value === 'string' ? value === 'true' : value
|
|
200
|
-
|
|
201
204
|
case 'cds.Timestamp':
|
|
202
205
|
return normalizeTimestamp(value)
|
|
203
|
-
|
|
204
206
|
default:
|
|
205
207
|
return value
|
|
206
208
|
}
|
|
207
209
|
}
|
|
208
210
|
|
|
209
|
-
|
|
211
|
+
const getStructRef = (element, ref = []) => {
|
|
212
|
+
if (element.kind === 'element') {
|
|
213
|
+
if (element.parent.kind === 'element') {
|
|
214
|
+
getStructRef(element.parent, ref)
|
|
215
|
+
ref.push(element.name)
|
|
216
|
+
}
|
|
217
|
+
if (element.parent.kind === 'entity') {
|
|
218
|
+
ref.push(element.name)
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return ref
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const getStructTargetName = element => {
|
|
225
|
+
if (element.kind === 'element') {
|
|
226
|
+
if (element.parent.kind === 'element') {
|
|
227
|
+
return getStructTargetName(element.parent)
|
|
228
|
+
}
|
|
229
|
+
if (element.elements && element.parent.kind === 'entity') {
|
|
230
|
+
return element.parent.name
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function _processSegments(from, model, namespace, cqn, protocol) {
|
|
210
236
|
const { ref } = from
|
|
211
237
|
|
|
212
238
|
let current = model
|
|
@@ -247,7 +273,7 @@ function _processSegments(from, model, namespace, cqn) {
|
|
|
247
273
|
|
|
248
274
|
if (incompleteKeys) {
|
|
249
275
|
// > key
|
|
250
|
-
keys = keys || _keysOf(current,
|
|
276
|
+
keys = keys || _keysOf(current, protocol !== 'rest') // if odata, skip backlinks as key as they are used from structure
|
|
251
277
|
let key = keys[keyCount++]
|
|
252
278
|
one = true
|
|
253
279
|
const element = current.elements[key]
|
|
@@ -262,7 +288,7 @@ function _processSegments(from, model, namespace, cqn) {
|
|
|
262
288
|
.join(',')})`
|
|
263
289
|
base.where.push({ ref: [key] }, '=', { val })
|
|
264
290
|
} else {
|
|
265
|
-
const val = _convertVal(
|
|
291
|
+
const val = _convertVal(seg, element)
|
|
266
292
|
base.where.push({ ref: [key] }, '=', { val })
|
|
267
293
|
}
|
|
268
294
|
ref[i] = null
|
|
@@ -306,7 +332,10 @@ function _processSegments(from, model, namespace, cqn) {
|
|
|
306
332
|
ref[i].where = undefined
|
|
307
333
|
if (ref[i + 1] !== 'Set') {
|
|
308
334
|
// /Set is missing
|
|
309
|
-
throw cds.error(`Invalid call to "${current.name}". You need to navigate to Set`, {
|
|
335
|
+
throw cds.error(`Invalid call to "${current.name}". You need to navigate to Set`, {
|
|
336
|
+
statusCode: 400,
|
|
337
|
+
code: 400
|
|
338
|
+
})
|
|
310
339
|
}
|
|
311
340
|
ref[++i] = null
|
|
312
341
|
} else if (current.kind === 'entity') {
|
|
@@ -352,13 +381,32 @@ function _processSegments(from, model, namespace, cqn) {
|
|
|
352
381
|
_resolveAliasesInXpr(ref[i].where, current)
|
|
353
382
|
_processWhere(ref[i].where, current)
|
|
354
383
|
}
|
|
384
|
+
} else if (current.kind === 'element' && current.elements && i < ref.length - 1) {
|
|
385
|
+
// > structured
|
|
386
|
+
continue
|
|
355
387
|
} else {
|
|
356
388
|
// > property
|
|
357
389
|
// we do not support navigations from properties yet
|
|
358
390
|
one = true
|
|
359
391
|
// if the last segment is a property, it must be removed and pushed to columns
|
|
360
392
|
target = target || _getDefinition(model, ref[0].id, namespace)
|
|
361
|
-
if (
|
|
393
|
+
if (getStructTargetName(current) === target.name) {
|
|
394
|
+
// TODO add simple isStructured check before
|
|
395
|
+
if (!cqn.SELECT.columns) cqn.SELECT.columns = []
|
|
396
|
+
const ref = getStructRef(current)
|
|
397
|
+
cqn.SELECT.columns.push({ ref }) // store struct as ref
|
|
398
|
+
// we need the keys to generate the correct @odata.context
|
|
399
|
+
for (const key in target.keys || {}) {
|
|
400
|
+
if (key !== 'IsActiveEntity' && !cqn.SELECT.columns.some(c => c.ref?.[0] === key))
|
|
401
|
+
cqn.SELECT.columns.push({ ref: [key] })
|
|
402
|
+
}
|
|
403
|
+
Object.defineProperty(cqn, '_propertyAccess', { value: current.name, enumerable: false })
|
|
404
|
+
// if we end up with structured, keep path as is, if we end up with property in structured, cut off property
|
|
405
|
+
if (!current.elements) {
|
|
406
|
+
from.ref.splice(-1)
|
|
407
|
+
}
|
|
408
|
+
break
|
|
409
|
+
} else if (Object.keys(target.elements).includes(current.name)) {
|
|
362
410
|
if (!cqn.SELECT.columns) cqn.SELECT.columns = []
|
|
363
411
|
cqn.SELECT.columns.push({ ref: ref.slice(i) })
|
|
364
412
|
// we need the keys to generate the correct @odata.context
|
|
@@ -366,6 +414,7 @@ function _processSegments(from, model, namespace, cqn) {
|
|
|
366
414
|
if (key !== 'IsActiveEntity' && !cqn.SELECT.columns.some(c => c.ref?.[0] === key))
|
|
367
415
|
cqn.SELECT.columns.push({ ref: [key] })
|
|
368
416
|
}
|
|
417
|
+
// REVISIT: remove hacky _propertyAccess
|
|
369
418
|
Object.defineProperty(cqn, '_propertyAccess', { value: current.name, enumerable: false })
|
|
370
419
|
from.ref.splice(i)
|
|
371
420
|
break
|
|
@@ -382,7 +431,7 @@ function _processSegments(from, model, namespace, cqn) {
|
|
|
382
431
|
keyCount === 1 ? 'was' : 'were'
|
|
383
432
|
} provided.`
|
|
384
433
|
),
|
|
385
|
-
{
|
|
434
|
+
{ statusCode: 400 }
|
|
386
435
|
)
|
|
387
436
|
}
|
|
388
437
|
|
|
@@ -430,7 +479,7 @@ function _removeDuplicateAsterisk(columns) {
|
|
|
430
479
|
}
|
|
431
480
|
}
|
|
432
481
|
|
|
433
|
-
function _processColumns(cqn, target) {
|
|
482
|
+
function _processColumns(cqn, target, protocol) {
|
|
434
483
|
if (cqn.SELECT.from.SELECT) _processColumns(cqn.SELECT.from, target)
|
|
435
484
|
|
|
436
485
|
let columns = cqn.SELECT.columns
|
|
@@ -445,7 +494,7 @@ function _processColumns(cqn, target) {
|
|
|
445
494
|
_removeDuplicateAsterisk(columns)
|
|
446
495
|
|
|
447
496
|
rewriteExpandAsterisk(columns, entity)
|
|
448
|
-
if (
|
|
497
|
+
if (protocol !== 'rest') _addKeys(columns, entity)
|
|
449
498
|
}
|
|
450
499
|
|
|
451
500
|
if (!Array.isArray(columns)) return
|
|
@@ -481,7 +530,7 @@ const _checkAllKeysProvided = (params, entity) => {
|
|
|
481
530
|
if (isView) {
|
|
482
531
|
// view with params
|
|
483
532
|
if (params === undefined) {
|
|
484
|
-
throw cds.error(`Invalid call to "${entity.name}". You need to navigate to Set`, {
|
|
533
|
+
throw cds.error(`Invalid call to "${entity.name}". You need to navigate to Set`, { statusCode: 400, code: 400 })
|
|
485
534
|
} else if (Object.keys(params).length === 0) {
|
|
486
535
|
throw new Error('KEY_EXPECTED')
|
|
487
536
|
}
|
|
@@ -505,7 +554,7 @@ const _checkAllKeysProvided = (params, entity) => {
|
|
|
505
554
|
entity.name
|
|
506
555
|
}"`
|
|
507
556
|
),
|
|
508
|
-
{
|
|
557
|
+
{ statusCode: 400 }
|
|
509
558
|
)
|
|
510
559
|
}
|
|
511
560
|
}
|
|
@@ -548,6 +597,7 @@ function _cleanupIgnored(SELECT, ignoredColumns, target, isOne) {
|
|
|
548
597
|
|
|
549
598
|
function _4service(service) {
|
|
550
599
|
const { namespace, model } = service
|
|
600
|
+
const protocol = service.options?.to
|
|
551
601
|
if (!model) return cqn => cqn
|
|
552
602
|
|
|
553
603
|
return cqn => {
|
|
@@ -575,7 +625,7 @@ function _4service(service) {
|
|
|
575
625
|
/*
|
|
576
626
|
* key vs. path segments (/Books/1/author/books/2/...) and more
|
|
577
627
|
*/
|
|
578
|
-
const { one, current, target } = _processSegments(from, model, namespace, cqn)
|
|
628
|
+
const { one, current, target } = _processSegments(from, model, namespace, cqn, protocol)
|
|
579
629
|
|
|
580
630
|
if (cqn.SELECT.where) {
|
|
581
631
|
_processWhere(cqn.SELECT.where, root)
|
|
@@ -594,7 +644,7 @@ function _4service(service) {
|
|
|
594
644
|
/*
|
|
595
645
|
* add default aggregation function (and alias)
|
|
596
646
|
*/
|
|
597
|
-
_processColumns(cqn, current)
|
|
647
|
+
_processColumns(cqn, current, protocol)
|
|
598
648
|
|
|
599
649
|
const ignoredColumns = Object.values(root.elements ?? {})
|
|
600
650
|
.filter(element => element['@cds.api.ignore'])
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
const
|
|
2
|
-
|
|
1
|
+
const cds = require('../../../')
|
|
2
|
+
|
|
3
|
+
const { formatVal } = require('../utils')
|
|
3
4
|
|
|
4
5
|
const OPERATORS = {
|
|
5
6
|
'=': 'eq',
|
|
@@ -119,7 +120,8 @@ const _format = (cur, elementName, target, kind, isLambda, func) => {
|
|
|
119
120
|
if (typeof cur !== 'object') return encodeURIComponent(formatVal(cur, elementName, target, kind))
|
|
120
121
|
if (hasValidProps(cur, 'ref'))
|
|
121
122
|
return encodeURIComponent(isLambda ? [LAMBDA_VARIABLE, ...cur.ref].join('/') : cur.ref[0].id || cur.ref.join('/'))
|
|
122
|
-
if (hasValidProps(cur, 'val'))
|
|
123
|
+
if (hasValidProps(cur, 'val'))
|
|
124
|
+
return encodeURIComponent(formatVal(cur.val, elementName, target, kind, func, cur.literal))
|
|
123
125
|
if (hasValidProps(cur, 'xpr')) return `(${_xpr(cur.xpr, target, kind, isLambda)})`
|
|
124
126
|
// REVISIT: How to detect the types for all functions?
|
|
125
127
|
if (hasValidProps(cur, 'func')) {
|
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
const cds = require('
|
|
1
|
+
const cds = require('../../../')
|
|
2
2
|
|
|
3
3
|
module.exports = (component, service, target, data, odataReq, upsert) => {
|
|
4
4
|
let query = cds.odata.parse(odataReq, { service })
|
|
5
5
|
|
|
6
|
-
//
|
|
7
|
-
if (component === 'READ' && Array.isArray(query))
|
|
8
|
-
return query
|
|
9
|
-
}
|
|
6
|
+
// for concat
|
|
7
|
+
if (component === 'READ' && Array.isArray(query)) return query
|
|
10
8
|
|
|
11
9
|
const _target = query.SELECT && query.SELECT.from
|
|
12
10
|
|
|
@@ -22,7 +20,6 @@ module.exports = (component, service, target, data, odataReq, upsert) => {
|
|
|
22
20
|
return INSERT.into(_target).entries(data)
|
|
23
21
|
case 'DELETE':
|
|
24
22
|
if (!one) cds.error('DELETE not allowed on collection', { code: 400 })
|
|
25
|
-
|
|
26
23
|
// eslint-disable-next-line no-case-declarations
|
|
27
24
|
const last = query._propertyAccess || (_target.ref && _target.ref[_target.ref.length - 1])
|
|
28
25
|
if (target.elements[last] || target.elements[query._propertyAccess]) {
|
|
@@ -1,7 +1,43 @@
|
|
|
1
|
-
|
|
2
|
-
const cds = require('../_runtime/cds')
|
|
1
|
+
// TODO: split into multiple files
|
|
3
2
|
|
|
4
|
-
const
|
|
3
|
+
const cds = require('../../../')
|
|
4
|
+
|
|
5
|
+
const { toBase64url } = require('../../_runtime/common/utils/binary')
|
|
6
|
+
const { where2obj } = require('../../_runtime/common/utils/cqn')
|
|
7
|
+
|
|
8
|
+
// copied from cds-compiler/lib/edm/edmUtils.js
|
|
9
|
+
const cds2edm = {
|
|
10
|
+
'cds.String': 'Edm.String',
|
|
11
|
+
// 'cds.hana.NCHAR': 'Edm.String',
|
|
12
|
+
'cds.LargeString': 'Edm.String',
|
|
13
|
+
// 'cds.hana.VARCHAR': 'Edm.String',
|
|
14
|
+
// 'cds.hana.CHAR': 'Edm.String',
|
|
15
|
+
// 'cds.hana.CLOB': 'Edm.String',
|
|
16
|
+
'cds.Binary': 'Edm.Binary',
|
|
17
|
+
// 'cds.hana.BINARY': 'Edm.Binary',
|
|
18
|
+
'cds.LargeBinary': 'Edm.Binary',
|
|
19
|
+
'cds.Decimal': 'Edm.Decimal',
|
|
20
|
+
'cds.DecimalFloat': 'Edm.Decimal',
|
|
21
|
+
// 'cds.hana.SMALLDECIMAL': 'Edm.Decimal', // V4: Scale="floating" Precision="16"
|
|
22
|
+
'cds.Integer64': 'Edm.Int64',
|
|
23
|
+
'cds.Integer': 'Edm.Int32',
|
|
24
|
+
'cds.Int64': 'Edm.Int64',
|
|
25
|
+
'cds.Int32': 'Edm.Int32',
|
|
26
|
+
'cds.Int16': 'Edm.Int16',
|
|
27
|
+
'cds.UInt8': 'Edm.Byte',
|
|
28
|
+
// 'cds.hana.SMALLINT': 'Edm.Int16',
|
|
29
|
+
// 'cds.hana.TINYINT': 'Edm.Byte',
|
|
30
|
+
'cds.Double': 'Edm.Double',
|
|
31
|
+
// 'cds.hana.REAL': 'Edm.Single',
|
|
32
|
+
'cds.Date': 'Edm.Date',
|
|
33
|
+
'cds.Time': 'Edm.TimeOfDay',
|
|
34
|
+
'cds.DateTime': 'Edm.DateTimeOffset',
|
|
35
|
+
'cds.Timestamp': 'Edm.DateTimeOffset',
|
|
36
|
+
'cds.Boolean': 'Edm.Boolean',
|
|
37
|
+
'cds.UUID': 'Edm.Guid'
|
|
38
|
+
// 'cds.hana.ST_POINT': 'Edm.GeometryPoint',
|
|
39
|
+
// 'cds.hana.ST_GEOMETRY': 'Edm.Geometry',
|
|
40
|
+
}
|
|
5
41
|
|
|
6
42
|
const odataError = (code, message) => ({ error: { code, message } })
|
|
7
43
|
|
|
@@ -26,11 +62,6 @@ const getSafeNumber = inputString => {
|
|
|
26
62
|
return inputString
|
|
27
63
|
}
|
|
28
64
|
|
|
29
|
-
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
|
30
|
-
const _PT = ([hh, mm, ss]) => `PT${hh}H${mm}M${ss}S`
|
|
31
|
-
const _isTimestamp = val =>
|
|
32
|
-
/^\d+-\d\d-\d\d(T\d\d:\d\d(:\d\d(\.\d+)?)?(Z|([+-]{1}\d\d:\d\d))?)?$/.test(val) && !isNaN(Date.parse(val))
|
|
33
|
-
|
|
34
65
|
const _getElement = (csnTarget, key) => {
|
|
35
66
|
if (csnTarget) {
|
|
36
67
|
if (csnTarget.elements) {
|
|
@@ -68,6 +99,8 @@ const _getElement = (csnTarget, key) => {
|
|
|
68
99
|
}
|
|
69
100
|
}
|
|
70
101
|
|
|
102
|
+
const _PT = ([hh, mm, ss]) => `PT${hh}H${mm}M${ss}S`
|
|
103
|
+
|
|
71
104
|
const _v2 = (val, element) => {
|
|
72
105
|
switch (element.type) {
|
|
73
106
|
case 'cds.UUID':
|
|
@@ -155,6 +188,12 @@ const _v4 = (val, element) => {
|
|
|
155
188
|
}
|
|
156
189
|
}
|
|
157
190
|
|
|
191
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
|
192
|
+
const MATH_FUNC = { round: 1, floor: 1, ceiling: 1 }
|
|
193
|
+
|
|
194
|
+
const _isTimestamp = val =>
|
|
195
|
+
/^\d+-\d\d-\d\d(T\d\d:\d\d(:\d\d(\.\d+)?)?(Z|([+-]{1}\d\d:\d\d))?)?$/.test(val) && !isNaN(Date.parse(val))
|
|
196
|
+
|
|
158
197
|
const formatVal = (val, elementName, csnTarget, kind, func, literal) => {
|
|
159
198
|
if (val === null || val === 'null') return 'null'
|
|
160
199
|
if (typeof val === 'boolean') return val
|
|
@@ -230,9 +269,56 @@ const skipToken = (token, cqn) => {
|
|
|
230
269
|
}
|
|
231
270
|
}
|
|
232
271
|
|
|
272
|
+
// REVISIT: do we already have something like this _without using okra api_?
|
|
273
|
+
const getKeysFromPath = (from, srv) => {
|
|
274
|
+
const keys = {}
|
|
275
|
+
// own
|
|
276
|
+
if (from.ref[from.ref.length - 1].where) {
|
|
277
|
+
Object.assign(keys, where2obj(from.ref[from.ref.length - 1].where))
|
|
278
|
+
}
|
|
279
|
+
// previous path segments
|
|
280
|
+
if (from.ref.length > 1) {
|
|
281
|
+
const entities = []
|
|
282
|
+
let cur = srv.model.definitions
|
|
283
|
+
for (let i = 0; i < from.ref.length; i++) {
|
|
284
|
+
const id = from.ref[i].id || from.ref[i]
|
|
285
|
+
const t = cur[id]._target || cur[id]
|
|
286
|
+
entities.push(t)
|
|
287
|
+
cur = t.elements
|
|
288
|
+
}
|
|
289
|
+
for (let i = from.ref.length - 2; i >= 0; i--) {
|
|
290
|
+
const ref = from.ref[i]
|
|
291
|
+
if (ref.where) {
|
|
292
|
+
const relation = entities[i]._relations[from.ref[i + 1].id || from.ref[i + 1]].join('target', 'source')
|
|
293
|
+
const seg_keys = where2obj(ref.where)
|
|
294
|
+
if (relation?.[0].xpr) {
|
|
295
|
+
const join = [...relation[0].xpr]
|
|
296
|
+
while (join.length >= 3) {
|
|
297
|
+
const [left, _, right] = join.splice(0, 4)
|
|
298
|
+
if (left.ref?.[0] === 'target') {
|
|
299
|
+
if (left.ref[1] in keys) break // we already added the foreign key for the last segment
|
|
300
|
+
keys[left.ref[1]] = 'val' in right ? right.val : seg_keys[right.ref[1]]
|
|
301
|
+
} else if (right.ref?.[0] === 'target') {
|
|
302
|
+
if (right.ref[1] in keys) break // we already added the foreign key for the last segment
|
|
303
|
+
keys[right.ref[1]] = 'val' in left ? left.val : seg_keys[left.ref[1]]
|
|
304
|
+
} else {
|
|
305
|
+
// REVISIT: what to do here?
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
} else {
|
|
309
|
+
// REVISIT: what to do here?
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return keys
|
|
315
|
+
}
|
|
316
|
+
|
|
233
317
|
module.exports = {
|
|
318
|
+
cds2edm,
|
|
234
319
|
getSafeNumber,
|
|
235
320
|
formatVal,
|
|
236
321
|
skipToken,
|
|
237
|
-
odataError
|
|
322
|
+
odataError,
|
|
323
|
+
getKeysFromPath
|
|
238
324
|
}
|
package/libx/outbox/index.js
CHANGED
|
@@ -31,7 +31,7 @@ const _getMessagesEntity = () => {
|
|
|
31
31
|
// REVISIT: Is this always a reliable way to identify the provider tenant?
|
|
32
32
|
// Are there scenarios where the credentials have a different format?
|
|
33
33
|
const _isProviderTenant = tenant =>
|
|
34
|
-
cds.requires.auth && cds.requires.auth.credentials && cds.requires.auth.credentials.identityzoneid === tenant
|
|
34
|
+
cds.requires.auth && cds.requires.auth.credentials && cds.requires.auth.credentials.identityzoneid === tenant || cds.requires.multitenancy.t0 === tenant
|
|
35
35
|
|
|
36
36
|
const hasPersistentOutbox = tenant => {
|
|
37
37
|
if (!cds.requires.outbox || cds.requires.outbox.kind !== 'persistent-outbox') return false
|
|
@@ -275,6 +275,7 @@ function outboxed(srv, customOpts) {
|
|
|
275
275
|
await writeInOutbox(srv.name, req, context)
|
|
276
276
|
return
|
|
277
277
|
}
|
|
278
|
+
|
|
278
279
|
if (!context[$stored_reqs]) {
|
|
279
280
|
context[$stored_reqs] = []
|
|
280
281
|
context.on('succeeded', async () => {
|
package/libx/rest/RestAdapter.js
CHANGED
|
@@ -2,9 +2,11 @@ const cds = require('../../_runtime/cds')
|
|
|
2
2
|
|
|
3
3
|
const RestRequest = require('../RestRequest')
|
|
4
4
|
|
|
5
|
-
const { checkStatic
|
|
5
|
+
const { checkStatic } = require('../../_runtime/cds-services/util/assert')
|
|
6
6
|
const getError = require('../../_runtime/common/error')
|
|
7
7
|
|
|
8
|
+
const typeCheckers = require('../../common/assert/type')
|
|
9
|
+
|
|
8
10
|
// REVISIT: use i18n
|
|
9
11
|
const _enrichErrorDetails = (isPrimitive, error) => {
|
|
10
12
|
const element = error.target ? ` '${error.target}' ` : ' '
|
|
@@ -38,10 +40,10 @@ const _validateReturnType = (operation, data) => {
|
|
|
38
40
|
// Return type contains primitives
|
|
39
41
|
// eslint-disable-next-line no-proto
|
|
40
42
|
const _type = typeof returnType._type === 'object' ? returnType.__proto__._type : returnType._type // REVISIT: super dirty hack for compiler's to.edmx polluting the csn definitions with ._type -> please use Symbols instead
|
|
41
|
-
const
|
|
42
|
-
if (
|
|
43
|
+
const typeChecker = typeCheckers[_type] // IMPORTANT: use ._type
|
|
44
|
+
if (typeChecker) {
|
|
43
45
|
const array = Array.isArray(data) ? data : [data]
|
|
44
|
-
checkResult = array.filter(value => !
|
|
46
|
+
checkResult = array.filter(value => !typeChecker(value)).map(value => ({ type: _type, value }))
|
|
45
47
|
} else {
|
|
46
48
|
if (typeof data !== 'object') {
|
|
47
49
|
throw new Error(
|
|
@@ -20,7 +20,11 @@ module.exports = srv => (req, res, next) => {
|
|
|
20
20
|
__target: definition,
|
|
21
21
|
SELECT: { one }
|
|
22
22
|
} = query
|
|
23
|
-
if (typeof definition === 'string')
|
|
23
|
+
if (typeof definition === 'string') {
|
|
24
|
+
definition =
|
|
25
|
+
srv.model.definitions[definition] ||
|
|
26
|
+
srv.model.definitions[definition.split(':$:')[0]].actions[definition.split(':$:')[1]]
|
|
27
|
+
}
|
|
24
28
|
delete query.__target
|
|
25
29
|
|
|
26
30
|
// REVISIT: hack for actions and functions
|
|
@@ -101,8 +105,22 @@ module.exports = srv => (req, res, next) => {
|
|
|
101
105
|
if (operation && (operation.kind === 'action' || operation.kind === 'function') && !operation.params) {
|
|
102
106
|
req._data = {}
|
|
103
107
|
} else {
|
|
108
|
+
// TODO: add keys from url into payload (overwriting if already present) -> document this behavior, also for OData
|
|
104
109
|
const payload = deepCopy(args || req.body)
|
|
105
|
-
|
|
110
|
+
if (cds.env.features.cds_assert) {
|
|
111
|
+
const assertOptions = {
|
|
112
|
+
filter: true,
|
|
113
|
+
http: { req },
|
|
114
|
+
mandatories: req.method === 'POST' || req.method === 'PUT' || undefined
|
|
115
|
+
}
|
|
116
|
+
const errs = cds.assert(payload, definition, assertOptions)
|
|
117
|
+
if (errs) {
|
|
118
|
+
if (errs.length === 1) throw errs[0]
|
|
119
|
+
throw Object.assign(new Error('MULTIPLE_ERRORS'), { statusCode: 400, details: errs })
|
|
120
|
+
}
|
|
121
|
+
} else {
|
|
122
|
+
convertStructured(srv, operation || definition, payload, { cleanupStruct: cds.env.features.rest_struct_data })
|
|
123
|
+
}
|
|
106
124
|
req._data = payload
|
|
107
125
|
}
|
|
108
126
|
}
|