@sap/cds 7.6.4 → 7.7.0

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 (92) hide show
  1. package/CHANGELOG.md +29 -1
  2. package/_i18n/i18n.properties +3 -0
  3. package/app/index.js +14 -8
  4. package/bin/serve.js +51 -19
  5. package/common.cds +16 -0
  6. package/lib/auth/ias-auth.js +2 -2
  7. package/lib/auth/index.js +1 -1
  8. package/lib/auth/jwt-auth.js +1 -1
  9. package/lib/compile/cdsc.js +23 -11
  10. package/lib/compile/for/nodejs.js +2 -2
  11. package/lib/compile/for/odata.js +4 -0
  12. package/lib/compile/load.js +7 -2
  13. package/lib/compile/to/sql.js +3 -0
  14. package/lib/dbs/cds-deploy.js +197 -220
  15. package/lib/env/defaults.js +2 -1
  16. package/lib/index.js +8 -2
  17. package/lib/linked/types.js +1 -0
  18. package/lib/log/format/json.js +1 -1
  19. package/lib/plugins.js +2 -2
  20. package/lib/ql/SELECT.js +8 -8
  21. package/lib/req/context.js +22 -13
  22. package/lib/req/request.js +10 -4
  23. package/lib/srv/cds-connect.js +9 -3
  24. package/lib/srv/cds-serve.js +5 -3
  25. package/lib/srv/middlewares/ctx-model.js +1 -1
  26. package/lib/srv/protocols/odata-v4.js +38 -9
  27. package/lib/srv/srv-api.js +98 -140
  28. package/lib/srv/srv-models.js +2 -2
  29. package/lib/srv/srv-tx.js +1 -0
  30. package/lib/utils/cds-utils.js +32 -23
  31. package/lib/utils/data.js +1 -1
  32. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +1 -3
  33. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +0 -2
  34. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +18 -1
  35. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/index.js +1 -1
  36. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/utils.js +7 -3
  37. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriParser.js +2 -1
  38. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/index.js +5 -0
  39. package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +71 -25
  40. package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +10 -2
  41. package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +6 -1
  42. package/libx/_runtime/cds-services/util/assert.js +50 -240
  43. package/libx/_runtime/cds.js +5 -0
  44. package/libx/_runtime/common/aspects/any.js +53 -45
  45. package/libx/_runtime/common/generic/input.js +14 -10
  46. package/libx/_runtime/common/generic/paging.js +1 -1
  47. package/libx/_runtime/common/utils/cqn2cqn4sql.js +1 -1
  48. package/libx/_runtime/common/utils/keys.js +1 -1
  49. package/libx/_runtime/common/utils/quotingStyles.js +1 -1
  50. package/libx/_runtime/common/utils/resolveStructured.js +4 -1
  51. package/libx/_runtime/common/utils/rewriteAsterisks.js +5 -12
  52. package/libx/_runtime/common/utils/stream.js +2 -16
  53. package/libx/_runtime/common/utils/streamProp.js +16 -6
  54. package/libx/_runtime/common/utils/ucsn.js +1 -0
  55. package/libx/_runtime/db/utils/columns.js +6 -1
  56. package/libx/_runtime/fiori/generic/activate.js +11 -3
  57. package/libx/_runtime/fiori/generic/edit.js +8 -2
  58. package/libx/_runtime/fiori/lean-draft.js +94 -30
  59. package/libx/_runtime/hana/execute.js +2 -5
  60. package/libx/_runtime/messaging/service.js +6 -2
  61. package/libx/common/assert/index.js +232 -0
  62. package/libx/common/assert/type.js +109 -0
  63. package/libx/common/assert/utils.js +125 -0
  64. package/libx/common/assert/validation.js +109 -0
  65. package/libx/odata/index.js +5 -5
  66. package/libx/odata/middleware/create.js +83 -0
  67. package/libx/odata/middleware/delete.js +38 -0
  68. package/libx/odata/middleware/error.js +8 -0
  69. package/libx/odata/{metadata.js → middleware/metadata.js} +8 -6
  70. package/libx/odata/middleware/operation.js +78 -0
  71. package/libx/odata/middleware/parse.js +11 -0
  72. package/libx/odata/{read.js → middleware/read.js} +42 -20
  73. package/libx/odata/{service-document.js → middleware/service-document.js} +2 -1
  74. package/libx/odata/middleware/stream.js +237 -0
  75. package/libx/odata/middleware/update.js +165 -0
  76. package/libx/odata/{afterburner.js → parse/afterburner.js} +79 -29
  77. package/libx/odata/{cqn2odata.js → parse/cqn2odata.js} +5 -3
  78. package/libx/odata/{parseToCqn.js → parse/parseToCqn.js} +3 -6
  79. package/libx/odata/{utils.js → utils/index.js} +91 -9
  80. package/libx/outbox/index.js +2 -1
  81. package/libx/rest/RestAdapter.js +0 -1
  82. package/libx/rest/middleware/operation.js +6 -4
  83. package/libx/rest/middleware/parse.js +20 -2
  84. package/package.json +1 -1
  85. package/server.js +43 -71
  86. package/libx/odata/create.js +0 -44
  87. package/libx/odata/delete.js +0 -25
  88. package/libx/odata/error.js +0 -12
  89. package/libx/odata/update.js +0 -110
  90. /package/libx/odata/{grammar.peggy → parse/grammar.peggy} +0 -0
  91. /package/libx/odata/{parser.js → parse/parser.js} +0 -0
  92. /package/libx/odata/{result.js → utils/result.js} +0 -0
@@ -0,0 +1,109 @@
1
+ const { cds } = global
2
+
3
+ const { Readable } = require('stream')
4
+
5
+ const { getNormalizedDecimal, getTarget, isBase64String } = require('./utils')
6
+
7
+ const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i //> "i" is acutally not OK, but we'll leave as is for now to avoid breaking changes
8
+ const RELAXED_UUID_REGEX = /^[0-9a-z]{8}-?[0-9a-z]{4}-?[0-9a-z]{4}-?[0-9a-z]{4}-?[0-9a-z]{12}$/i
9
+
10
+ const ISO_DATE_PART1 =
11
+ '[1-9]\\d{3}-(?:(?:0[1-9]|1[0-2])-(?:0[1-9]|1\\d|2[0-8])|(?:0[13-9]|1[0-2])-(?:29|30)|(?:0[13578]|1[02])-31)'
12
+ const ISO_DATE_PART2 = '(?:[1-9]\\d(?:0[48]|[2468][048]|[13579][26])|(?:[2468][048]|[13579][26])00)-02-29'
13
+ const ISO_DATE = `(?:${ISO_DATE_PART1}|${ISO_DATE_PART2})`
14
+ const ISO_TIME_NO_MILLIS = '(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d'
15
+ const ISO_TIME = `${ISO_TIME_NO_MILLIS}(?:\\.\\d{1,9})?`
16
+ const ISO_DATE_TIME = `${ISO_DATE}T${ISO_TIME_NO_MILLIS}(?:Z|[+-][01]\\d:?[0-5]\\d)`
17
+ const ISO_TIMESTAMP = `${ISO_DATE}T${ISO_TIME}(?:Z|[+-][01]\\d:?[0-5]\\d)`
18
+ const ISO_DATE_REGEX = new RegExp(`^${ISO_DATE}$`, 'i')
19
+ const ISO_TIME_NO_MILLIS_REGEX = new RegExp(`^${ISO_TIME_NO_MILLIS}$`, 'i')
20
+ const ISO_DATE_TIME_REGEX = new RegExp(`^${ISO_DATE_TIME}$`, 'i')
21
+ const ISO_TIMESTAMP_REGEX = new RegExp(`^${ISO_TIMESTAMP}$`, 'i')
22
+
23
+ const _checkString = value => typeof value === 'string'
24
+
25
+ const _checkNumber = value => typeof value === 'number' && !Number.isNaN(value)
26
+
27
+ const _oldCheckDecimal = (value, element) => {
28
+ const [left, right] = String(value).split('.')
29
+ return (
30
+ _checkNumber(value) &&
31
+ (!element.precision || left.length <= element.precision - (element.scale || 0)) &&
32
+ (!element.scale || ((right || '').length <= element.scale && parseFloat(right) !== 0))
33
+ )
34
+ }
35
+
36
+ // REVISIT: only use a cheaper check if not in strictDecimal mode?
37
+ const _checkDecimal = (v, ele, errs, path, k) => {
38
+ if (!errs) return _oldCheckDecimal(v, ele)
39
+
40
+ const { precision, scale } = ele
41
+ let val = getNormalizedDecimal(v)
42
+ if (precision != null && scale != null) {
43
+ let isValid = true
44
+ if (!val.match(/\./)) val += '.0'
45
+ if (precision === scale) {
46
+ if (!val.match(new RegExp(`^-?0\\.\\d{0,${scale}}$`, 'g'))) isValid = false
47
+ } else if (scale === 0) {
48
+ if (!val.match(new RegExp(`^-?\\d{1,${precision - scale}}\\.0{0,1}$`, 'g'))) isValid = false
49
+ } else if (!val.match(new RegExp(`^-?\\d{1,${precision - scale}}\\.\\d{0,${scale}}$`, 'g'))) {
50
+ isValid = false
51
+ }
52
+ if (!isValid) {
53
+ const args = [v, `Decimal(${precision},${scale})`]
54
+ const target = getTarget(path, k)
55
+ errs.push(new cds.error('ASSERT_DATA_TYPE', { args, target, statusCode: 400, code: '400' }))
56
+ }
57
+ } else if (precision != null) {
58
+ if (!val.match(new RegExp(`^-?\\d{1,${precision}}$`, 'g'))) {
59
+ const args = [v, `Decimal(${precision})`]
60
+ const target = getTarget(path, k)
61
+ errs.push(new cds.error('ASSERT_DATA_TYPE', { args, target, statusCode: 400, code: '400' }))
62
+ }
63
+ }
64
+ }
65
+
66
+ const _checkInt = value => _checkNumber(value) && parseInt(value, 10) === value
67
+
68
+ const _checkInt64 = value => typeof value === 'string' ? value.match(/^\d+$/) : _checkInt(value)
69
+
70
+ const _checkBoolean = value => typeof value === 'boolean'
71
+
72
+ const _checkBuffer = value => Buffer.isBuffer(value) || value.type === 'Buffer' || isBase64String(value)
73
+
74
+ const _checkStreamOrBuffer = value => value instanceof Readable || _checkBuffer(value)
75
+
76
+ const _checkUUID = value => _checkString(value) && UUID_REGEX.test(value)
77
+
78
+ const _checkRelaxedUUID = value => _checkString(value) && RELAXED_UUID_REGEX.test(value)
79
+
80
+ const _checkISODate = value => (_checkString(value) && ISO_DATE_REGEX.test(value)) || value instanceof Date
81
+
82
+ const _checkISOTime = value => _checkString(value) && ISO_TIME_NO_MILLIS_REGEX.test(value)
83
+
84
+ const _checkISODateTime = value => (_checkString(value) && ISO_DATE_TIME_REGEX.test(value)) || value instanceof Date
85
+
86
+ const _checkISOTimestamp = value => (_checkString(value) && ISO_TIMESTAMP_REGEX.test(value)) || value instanceof Date
87
+
88
+ module.exports = {
89
+ 'cds.UUID': _checkUUID,
90
+ 'relaxed.UUID': _checkRelaxedUUID,
91
+ 'cds.Boolean': _checkBoolean,
92
+ 'cds.Integer': _checkInt,
93
+ 'cds.UInt8': _checkInt,
94
+ 'cds.Int16': _checkInt,
95
+ 'cds.Int32': _checkInt,
96
+ 'cds.Integer64': _checkInt64,
97
+ 'cds.Int64': _checkInt64,
98
+ 'cds.Decimal': _checkDecimal,
99
+ 'cds.DecimalFloat': _checkNumber,
100
+ 'cds.Double': _checkNumber,
101
+ 'cds.Date': _checkISODate,
102
+ 'cds.Time': _checkISOTime,
103
+ 'cds.DateTime': _checkISODateTime,
104
+ 'cds.Timestamp': _checkISOTimestamp,
105
+ 'cds.String': _checkString,
106
+ 'cds.Binary': _checkBuffer,
107
+ 'cds.LargeString': _checkString,
108
+ 'cds.LargeBinary': _checkStreamOrBuffer
109
+ }
@@ -0,0 +1,125 @@
1
+ function getNested(k, obj) {
2
+ let cur = obj
3
+ let p = ''
4
+ const parts = k.split('_')
5
+ while (parts.length) {
6
+ const q = parts.shift()
7
+ if (q in cur) {
8
+ cur = cur[q]
9
+ p = ''
10
+ } else {
11
+ p = p ? p + '_' + q : q
12
+ if (p in cur) {
13
+ cur = cur[p]
14
+ p = ''
15
+ } else {
16
+ if (Object.keys(cur).some(k => k.startsWith(p + '_'))) {
17
+ // continue for now as there's still a chance
18
+ } else {
19
+ // abort
20
+ return undefined
21
+ }
22
+ }
23
+ }
24
+ }
25
+ return cur[p] || cur !== obj ? cur : undefined
26
+ }
27
+
28
+ const getNormalizedDecimal = val => {
29
+ let v = `${val}`
30
+ const cgs = v.match(/^(\d*\.*\d*)e([+|-]*)(\d*)$/)
31
+ if (cgs) {
32
+ let [l, r = ''] = cgs[1].split('.')
33
+ const dir = cgs[2] || '+'
34
+ const exp = Number(cgs[3])
35
+ if (dir === '+') {
36
+ // move decimal point to the right
37
+ r = r.padEnd(exp, '0')
38
+ l += r.substring(0, exp)
39
+ r = r.slice(exp)
40
+ v = `${l}${r ? '.' + r : ''}`
41
+ } else {
42
+ // move decimal point to the left
43
+ l = l.padStart(exp, '0')
44
+ r = l.substring(0, exp) + r
45
+ l = l.slice(exp)
46
+ v = `${l ? l : '0'}.${r}`
47
+ }
48
+ }
49
+ return v
50
+ }
51
+
52
+ function getTarget(path, k) {
53
+ return path.length && path[path.length - 1].match(/\[\d+\]$/) ? path.join('/') : path.concat(k).join('/')
54
+ }
55
+
56
+ // non-strict mode also allows url-safe base64 strings
57
+ function isBase64String(string, strict = false) {
58
+ if (typeof string !== 'string') return false
59
+
60
+ if (strict && string.length % 4 !== 0) return false
61
+
62
+ let length = string.length
63
+ if (string.endsWith('==')) length -= 2
64
+ else if (string.endsWith('=')) length -= 1
65
+
66
+ let char
67
+ for (let i = 0; i < length; i++) {
68
+ char = string[i]
69
+ if (char >= 'A' && char <= 'Z') continue
70
+ else if (char >= 'a' && char <= 'z') continue
71
+ else if (char >= '0' && char <= '9') continue
72
+ else if (char === '+' || char === '/') continue
73
+ else if (!strict && (char === '-' || char === '_')) continue
74
+ return false
75
+ }
76
+
77
+ return true
78
+ }
79
+
80
+ const resolveCDSType = ele => {
81
+ // REVISIT: when is ele._type not set and sufficient?
82
+ if (ele._type?.match(/^cds\./)) return ele._type
83
+ if (ele.type) {
84
+ if (ele.type.match(/^cds\./)) return ele.type
85
+ return resolveCDSType(ele.__proto__)
86
+ }
87
+ if (ele.items) return resolveCDSType(ele.items)
88
+ return ele
89
+ }
90
+
91
+ function resolveSegment(prev, obj, def) {
92
+ if (prev.keys) {
93
+ let keys = []
94
+ for (const k of prev.keys) {
95
+ let val
96
+ if (k in obj) val = obj[k]
97
+ else val = getNested(k, obj)
98
+ if (val == null) {
99
+ // in some cases, k is not given, e.g., POST into collection via navigation
100
+ // TODO: what to put in target? "null", "transient", ...?
101
+ if (k === 'IsActiveEntity')
102
+ keys.push(`${k}=false`) //> always false if not in obj as it must be a draft activate
103
+ else keys.push(`${k}=null`)
104
+ } else {
105
+ const type = resolveCDSType(def.elements[k])
106
+ if (type === 'cds.String') val = `'${val}'`
107
+ // TODO: more proper val encoding based on type
108
+ keys.push(`${k}=${val}`)
109
+ }
110
+ }
111
+ return `${prev.assoc}(${keys.join(',')})`
112
+ }
113
+ if (prev.index) {
114
+ return `${prev.prop}[${prev.index}]`
115
+ }
116
+ }
117
+
118
+ module.exports = {
119
+ getNested,
120
+ getNormalizedDecimal,
121
+ getTarget,
122
+ isBase64String,
123
+ resolveCDSType,
124
+ resolveSegment
125
+ }
@@ -0,0 +1,109 @@
1
+ const { cds } = global
2
+
3
+ const {
4
+ 'cds.Date': checkISODate,
5
+ 'cds.Time': checkISOTime,
6
+ 'cds.DateTime': checkISODateTime,
7
+ 'cds.Timestamp': checkISOTimestamp,
8
+ 'cds.String': checkString
9
+ } = require('./type')
10
+ const { getTarget, resolveCDSType } = require('./utils')
11
+
12
+ const _isNavigationColumn = (col, as) => col.ref?.length > 1 && (col.as === as || col.ref[col.ref.length - 1] === as)
13
+
14
+ // REVISIT: mandatory is actually not the same as not null or empty string
15
+ const _isNotFilled = val => val === null || val === undefined || (typeof val === 'string' && val.trim() === '')
16
+
17
+ const _getEnumElement = ele => ((ele['@assert.range'] && ele.enum) || ele['@assert.enum'] ? ele.enum : undefined)
18
+
19
+ const _enumValues = ele => {
20
+ return Object.keys(ele).map(enumKey => {
21
+ const enum_ = ele[enumKey]
22
+ const enumValue = enum_ && enum_.val
23
+ if (enumValue !== undefined) {
24
+ if (enumValue['=']) return enumValue['=']
25
+ if (enum_ && enum_.literal && enum_.literal === 'number') return Number(enumValue)
26
+ return enumValue
27
+ }
28
+ return enumKey
29
+ })
30
+ }
31
+
32
+ const _checkDateValue = (val, r1, r2) => {
33
+ const dateVal = new Date(val)
34
+ return (dateVal - new Date(r1)) * (dateVal - new Date(r2)) <= 0
35
+ }
36
+
37
+ const _toDate = val => `2000-01-01T${val}Z`
38
+
39
+ const _checkInRange = (val, range, type) => {
40
+ switch (type) {
41
+ case 'cds.Date':
42
+ return checkISODate(val) && _checkDateValue(val, range[0], range[1])
43
+ case 'cds.DateTime':
44
+ return checkISODateTime(val) && _checkDateValue(val, range[0], range[1])
45
+ case 'cds.Timestamp':
46
+ return checkISOTimestamp(val) && _checkDateValue(val, range[0], range[1])
47
+ case 'cds.Time':
48
+ return checkISOTime(val) && _checkDateValue(_toDate(val), _toDate(range[0]), _toDate(range[1]))
49
+ default:
50
+ return (val - range[0]) * (val - range[1]) <= 0
51
+ }
52
+ }
53
+
54
+ // process.env.CDS_ASSERT_FORMAT_FLAGS is not official!
55
+ const _checkRegExpFormat = (val, format) =>
56
+ checkString(val) && val.match(new RegExp(format, process.env.CDS_ASSERT_FORMAT_FLAGS || 'u'))
57
+
58
+ const checkMandatory = (v, ele, errs, path, k) => {
59
+ // REVISIT: correct to not complain?
60
+ // do not complain about missing foreign keys in children
61
+ if (path.length && ele['@odata.foreignKey4']) return
62
+
63
+ // TODO: which case is this?
64
+ // do not complain about ???
65
+ if (ele.parent?.query?.SELECT?.columns?.find(col => _isNavigationColumn(col, ele.name))) return
66
+
67
+ if (!ele.default && _isNotFilled(v)) {
68
+ const target = getTarget(path, k)
69
+ errs.push(new cds.error('ASSERT_NOT_NULL', { target, statusCode: 400, code: '400' }))
70
+ }
71
+ }
72
+
73
+ const checkEnum = (v, ele, errs, path, k) => {
74
+ const enumElements = _getEnumElement(ele)
75
+ const enumValues = enumElements && _enumValues(enumElements)
76
+ if (enumElements && !enumValues.includes(v)) {
77
+ const args =
78
+ typeof v === 'string'
79
+ ? ['"' + v + '"', enumValues.map(ele => '"' + ele + '"').join(', ')]
80
+ : [v, enumValues.join(', ')]
81
+ const target = getTarget(path, k)
82
+ errs.push(new cds.error('ASSERT_ENUM', { args, target, statusCode: 400, code: '400' }))
83
+ }
84
+ }
85
+
86
+ const checkRange = (v, ele, errs, path, k) => {
87
+ const rangeElements = ele['@assert.range'] && !_getEnumElement(ele) ? ele['@assert.range'] : undefined
88
+ if (rangeElements && !_checkInRange(v, rangeElements, resolveCDSType(ele))) {
89
+ const args = [v, ...ele['@assert.range']]
90
+ const target = getTarget(path, k)
91
+ errs.push(new cds.error('ASSERT_RANGE', { args, target, statusCode: 400, code: '400' }))
92
+ }
93
+ }
94
+
95
+ const checkFormat = (v, ele, errs, path, k) => {
96
+ const formatElements = ele['@assert.format']
97
+ if (formatElements && !_checkRegExpFormat(v, formatElements)) {
98
+ const args = [v, formatElements]
99
+ const target = getTarget(path, k)
100
+ errs.push(new cds.error('ASSERT_FORMAT', { args, target, statusCode: 400, code: '400' }))
101
+ }
102
+ }
103
+
104
+ module.exports = {
105
+ checkMandatory,
106
+ checkEnum,
107
+ checkRange,
108
+ checkFormat
109
+ }
@@ -1,11 +1,11 @@
1
- const cds = require('../_runtime/cds'),
2
- { decodeURIComponent } = cds.utils
1
+ const cds = require('../../')
3
2
  const { SELECT } = cds.ql
3
+ const { decodeURIComponent } = cds.utils
4
4
 
5
- const odata2cqn = require('./parser').parse
6
- const cqn2odata = require('./cqn2odata')
5
+ const odata2cqn = require('./parse/parser').parse
6
+ const cqn2odata = require('./parse/cqn2odata')
7
7
 
8
- const afterburner = require('./afterburner')
8
+ const afterburner = require('./parse/afterburner')
9
9
  const { getSafeNumber: safeNumber } = require('./utils')
10
10
  const getError = require('../_runtime/common/error')
11
11
 
@@ -0,0 +1,83 @@
1
+ const cds = require('../../../')
2
+ const { INSERT } = cds.ql
3
+
4
+ const { toODataResult } = require('../utils/result')
5
+ const { odataError, getKeysFromPath } = require('../utils')
6
+
7
+ const { deepCopy } = require('../../_runtime/common/utils/copy')
8
+
9
+ // REVISIT: move to or rewrite in libx/odata
10
+ const { readAfterWrite } = require('../../_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite')
11
+ const metaInfo = require('../../_runtime/cds-services/adapter/odata-v4/utils/metaInfo')
12
+
13
+ const _calculateLocationHeader = (target, srv, result) => {
14
+ const targetName = target.name.replace(`${srv.name}.`, '')
15
+
16
+ const keyValuePairs = Object.keys(target.keys).reduce((acc, key) => {
17
+ acc[key] = result[key]
18
+ return acc
19
+ }, {})
20
+
21
+ let keys
22
+ const entries = Object.entries(keyValuePairs)
23
+ if (entries.length === 1) {
24
+ keys = entries[0][1]
25
+ } else {
26
+ keys = entries.map(([key, value]) => `${key}=${value}`).join(',')
27
+ }
28
+
29
+ return `${targetName}(${keys})`
30
+ }
31
+
32
+ module.exports = srv =>
33
+ function create(req, res, next) {
34
+ const { _query: query } = req
35
+
36
+ const {
37
+ SELECT: { one, from }
38
+ } = query
39
+
40
+ if (one) {
41
+ const singleton = query.target._isSingleton
42
+ const error = odataError('405', `Method ${req.method} not allowed for ${singleton ? 'SINGLETON' : 'ENTITY'}`)
43
+ return res.status(405).json(error)
44
+ }
45
+
46
+ const data = deepCopy(req.body)
47
+
48
+ // add keys from url into payload (overwriting if already present)
49
+ Object.assign(data, getKeysFromPath(from, srv))
50
+
51
+ // assert payload
52
+ const assertOptions = { filter: true, http: { req }, mandatories: true }
53
+ const errs = cds.assert(data, query.target, assertOptions)
54
+ if (errs) {
55
+ if (errs.length === 1) throw errs[0]
56
+ throw Object.assign(new Error('MULTIPLE_ERRORS'), { statusCode: 400, details: errs })
57
+ }
58
+
59
+ const insertQuery = INSERT.into(from).entries(data)
60
+
61
+ // we need the cds request, so we can access req._.readAfterWrite
62
+ const cdsReq = new cds.Request({ query: insertQuery })
63
+
64
+ // rewrite event for draft-enabled entities
65
+ if (query.target._isDraftEnabled) cdsReq.event = 'NEW'
66
+
67
+ return srv
68
+ .dispatch(cdsReq)
69
+ .then(async result => {
70
+ if (cdsReq._.readAfterWrite) {
71
+ // TODO see if in old odata impl for other checks that should happen
72
+ result = await readAfterWrite(cdsReq, srv, { operation: { result } })
73
+ }
74
+
75
+ if (result == null || req._preferReturn === 'minimal') return res.sendStatus(204)
76
+
77
+ const location = _calculateLocationHeader(cdsReq.target, srv, result)
78
+ const info = metaInfo(insertQuery, 'CREATE', srv, result, req)
79
+ result = toODataResult(result, info)
80
+ res.set('location', location).status(201).send(result)
81
+ })
82
+ .catch(next)
83
+ }
@@ -0,0 +1,38 @@
1
+ const cds = require('../../../')
2
+ const { UPDATE, DELETE } = cds.ql
3
+
4
+ const { odataError, getKeysFromPath } = require('../utils')
5
+
6
+ module.exports = srv =>
7
+ function deleete(req, res, next) {
8
+ if (req._preferReturn) {
9
+ const message = `The 'return' preference is not allowed in ${req.method} requests`
10
+ return res.status(400).json({ error: { code: '400', message } })
11
+ }
12
+
13
+ const { _query: query } = req
14
+
15
+ const {
16
+ SELECT: { one, from }
17
+ } = query
18
+
19
+ if (!one) {
20
+ // REVISIT: don't use "ENTITY.COLLECTION" as that's an okra term
21
+ return res.status(405).json(odataError('405', `Method DELETE not allowed for ENTITY.COLLECTION`))
22
+ }
23
+
24
+ // for read and delete, we provide keys in req.data
25
+ const data = getKeysFromPath(query.SELECT.from, srv)
26
+
27
+ // REVISIT: better
28
+ if (query._propertyAccess) data[query._propertyAccess] = null
29
+
30
+ // REVISIT: maybe also just dispatch a cds request here?
31
+ return srv
32
+ .run(query._propertyAccess ? UPDATE(from).set({ [query._propertyAccess]: null }) : DELETE.from(from), data)
33
+ .then(result => {
34
+ if (result === 0) return res.status(404).json({ error: { code: '404', message: 'Not Found' } })
35
+ res.sendStatus(204)
36
+ })
37
+ .catch(next)
38
+ }
@@ -0,0 +1,8 @@
1
+ const { normalizeError } = require('../../_runtime/common/error/frontend')
2
+
3
+ module.exports = _srv => (err, req, res, _next) => {
4
+ const { error, statusCode } = normalizeError(err, req)
5
+
6
+ // NOTE: normalizeError already does sanatization -> we can use as is
7
+ res.status(statusCode).json({ error })
8
+ }
@@ -1,7 +1,9 @@
1
- const cds = require('../../lib')
1
+ const cds = require('../../../')
2
2
  const LOG = cds.log('odata')
3
+
3
4
  const crypto = require('crypto')
4
- const { odataError } = require('./utils')
5
+
6
+ const { odataError } = require('../utils')
5
7
 
6
8
  const _requestedFormat = (queryOption, header) => {
7
9
  if (queryOption) return queryOption.match(/json/i) ? 'json' : 'xml'
@@ -91,10 +93,10 @@ module.exports = srv =>
91
93
  // REVISIT: remove check later
92
94
  if (mpSupportsEmptyLocale()) {
93
95
  // If no extensibility nor fts, do not provide model to mtxs
94
- const modelNeeded =
95
- cds.env.requires.extensibility ||
96
- (cds.env.requires.toggles && Object.keys(cds.context.features || {}).length)
97
- edmx = metadataCache.edm || (await mps.getEdmx({ tenant, model: modelNeeded && srv.model, service: srv.definition.name }))
96
+ const modelNeeded = cds.env.requires.extensibility || cds.context.features?.given
97
+ edmx =
98
+ metadataCache.edm ||
99
+ (await mps.getEdmx({ tenant, model: modelNeeded && srv.model, service: srv.definition.name }))
98
100
  metadataCache.edm = edmx
99
101
  const extBundle = cds.env.requires.extensibility && (await mps.getI18n({ tenant, locale }))
100
102
  edmx = cds.localize(srv.model, locale, edmx, extBundle)
@@ -0,0 +1,78 @@
1
+ const cds = require('../../../')
2
+
3
+ const { toODataResult } = require('../utils/result')
4
+ const { cds2edm } = require('../utils')
5
+
6
+ const { deepCopy } = require('../../_runtime/common/utils/copy')
7
+
8
+ // REVISIT: move to or rewrite in libx/odata
9
+ const metaInfo = require('../../_runtime/cds-services/adapter/odata-v4/utils/metaInfo')
10
+
11
+ module.exports = srv => (req, res, next) => {
12
+ let { operation, args } = req._query.SELECT.from.ref.slice(-1)[0]
13
+ if (!operation) return next() //> create or read
14
+
15
+ // unbound vs. bound
16
+ let entity
17
+ if (srv.model.definitions[operation]) {
18
+ operation = srv.model.definitions[operation]
19
+ } else {
20
+ req._query.SELECT.from.ref.pop()
21
+ // TODO: this does not work when navigating to the entity
22
+ const lastRef = req._query.SELECT.from.ref.slice(-1)[0]
23
+ entity = lastRef.id || lastRef
24
+ entity = srv.model.definitions[entity]
25
+ operation = entity.actions[operation]
26
+ }
27
+
28
+ const data = args || deepCopy(req.body)
29
+
30
+ // assert payload
31
+ const assertOptions = { filter: true, http: { req }, mandatories: true }
32
+ const errs = cds.assert(data, operation, assertOptions)
33
+ if (errs) {
34
+ if (errs.length === 1) throw errs[0]
35
+ throw Object.assign(new Error('MULTIPLE_ERRORS'), { statusCode: 400, details: errs })
36
+ }
37
+
38
+ // REVISIT: when is operation.name actually prefixed with the service name?
39
+ const event = operation.name.replace(`${srv.name}.`, '')
40
+
41
+ // TODO: params
42
+ const cdsReq = new cds.Request({ query: entity ? req._query : undefined, event, data, params: [] })
43
+
44
+ srv
45
+ .dispatch(cdsReq)
46
+ .then(result => {
47
+ // REVISIT: result === undefined valid for modelled return type?
48
+ if (!operation.returns || result === undefined) return res.sendStatus(204)
49
+
50
+ if (operation.returns._type?.match?.(/^cds\./)) {
51
+ // TODO: check result type
52
+ return res.json({
53
+ '@odata.context': `${entity ? '../' : ''}$metadata#${cds2edm[operation.returns._type]}`,
54
+ value: result
55
+ })
56
+ }
57
+
58
+ const assertOptions = { mandatories: true } //> TODO: more needed?
59
+ // TODO: error targets are not correct if return type is "many X"
60
+ const assertDefinition = operation.returns.items || operation.returns
61
+ const errs = cds.assert(result, assertDefinition, assertOptions)
62
+ if (errs) {
63
+ // TODO: proper error handling
64
+ }
65
+
66
+ const info = metaInfo(req._query, event, srv, result, req)
67
+ // FIXME: info.metadata.isCollection is incorrect for draftActivate
68
+ if (event === 'draftActivate') info.metadata.isCollection = false
69
+ result = toODataResult(result, info)
70
+
71
+ // TODO: toODataResult() doesn't seem to handle this case
72
+ if (entity && !result['@odata.context'].match(/^\.\.\//))
73
+ result['@odata.context'] = '../' + result['@odata.context']
74
+
75
+ res.json(result)
76
+ })
77
+ .catch(next)
78
+ }
@@ -0,0 +1,11 @@
1
+ const cds = require('../../../')
2
+
3
+ module.exports = srv => (req, _, next) => {
4
+ // if not a GET, use req.path instead of req.url to ignore query parameters
5
+ req._query = cds.odata.parse(req.method === 'GET' ? req.url : req.path, { service: srv, baseUrl: req.baseUrl })
6
+
7
+ const preferReturn = req.headers.prefer?.match(/\W?return=(\w+)/i)
8
+ if (preferReturn) req._preferReturn = preferReturn[1]
9
+
10
+ next()
11
+ }