@sap/cds 7.2.1 → 7.3.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 (59) hide show
  1. package/CHANGELOG.md +168 -126
  2. package/README.md +1 -1
  3. package/apis/core.d.ts +6 -4
  4. package/apis/services.d.ts +24 -4
  5. package/apis/test.d.ts +24 -10
  6. package/bin/serve.js +4 -3
  7. package/lib/auth/ias-auth.js +7 -8
  8. package/lib/compile/cdsc.js +5 -7
  9. package/lib/compile/etc/csv.js +22 -11
  10. package/lib/compile/for/lean_drafts.js +1 -1
  11. package/lib/dbs/cds-deploy.js +1 -2
  12. package/lib/env/cds-env.js +26 -20
  13. package/lib/env/defaults.js +4 -3
  14. package/lib/env/schema.js +9 -0
  15. package/lib/i18n/localize.js +83 -77
  16. package/lib/index.js +6 -2
  17. package/lib/linked/classes.js +13 -13
  18. package/lib/plugins.js +41 -45
  19. package/lib/req/user.js +2 -2
  20. package/lib/srv/protocols/_legacy.js +0 -1
  21. package/lib/srv/protocols/odata-v4.js +4 -0
  22. package/lib/utils/axios.js +7 -1
  23. package/lib/utils/cds-test.js +140 -133
  24. package/lib/utils/cds-utils.js +10 -3
  25. package/lib/utils/check-version.js +6 -0
  26. package/lib/utils/data.js +19 -6
  27. package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +20 -19
  28. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +10 -1
  29. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +1 -1
  30. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +2 -3
  31. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/ValueConverter.js +0 -14
  32. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/core/OdataRequest.js +1 -0
  33. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/deserializer/BatchRequestListBuilder.js +5 -2
  34. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/handler/MetadataHandler.js +1 -1
  35. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/handler/ServiceHandler.js +1 -1
  36. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/invocation/DispatcherCommand.js +2 -2
  37. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/utils/BufferedWriter.js +1 -3
  38. package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +1 -1
  39. package/libx/_runtime/common/composition/update.js +18 -2
  40. package/libx/_runtime/common/error/frontend.js +46 -34
  41. package/libx/_runtime/common/generic/auth/capabilities.js +33 -14
  42. package/libx/_runtime/common/generic/input.js +1 -1
  43. package/libx/_runtime/common/utils/cqn2cqn4sql.js +3 -3
  44. package/libx/_runtime/db/query/update.js +48 -30
  45. package/libx/_runtime/fiori/lean-draft.js +2 -3
  46. package/libx/_runtime/hana/conversion.js +3 -2
  47. package/libx/_runtime/messaging/enterprise-messaging-utils/getTenantInfo.js +1 -1
  48. package/libx/_runtime/messaging/outbox/utils.js +1 -1
  49. package/libx/_runtime/remote/Service.js +1 -17
  50. package/libx/_runtime/remote/utils/client.js +3 -3
  51. package/libx/_runtime/remote/utils/data.js +5 -7
  52. package/libx/odata/{grammar.pegjs → grammar.peggy} +1 -1
  53. package/libx/odata/metadata.js +121 -0
  54. package/libx/odata/parser.js +1 -1
  55. package/libx/odata/service-document.js +61 -0
  56. package/libx/odata/utils.js +102 -48
  57. package/libx/rest/RestAdapter.js +2 -2
  58. package/libx/rest/middleware/error.js +1 -1
  59. package/package.json +1 -1
@@ -1,3 +1,5 @@
1
+ const parentDataSymbol = Symbol('parentData')
2
+ const cqnSymbol = Symbol('cqn')
1
3
  const cds = require('../../cds')
2
4
  const { getCompositionTree } = require('./tree')
3
5
  const { getDeepInsertCQNs } = require('./insert')
@@ -138,7 +140,13 @@ function _addSubDeepUpdateCQNForUpdateInsert({ entity, entityName, data, selectD
138
140
  const oldData = ctUtils.cleanDeepData(entity, selectEntry)
139
141
  const diff = _diffData(newData, oldData, entity, entry, selectEntry, model)
140
142
  // empty updates will be removed later
141
- updateCQNs.push({ UPDATE: { entity: entityName, data: diff, where: ctUtils.whereKey(key) } })
143
+ updateCQNs.push({
144
+ UPDATE: { entity: entityName, data: Object.assign({}, key, diff), where: ctUtils.whereKey(key) },
145
+ // We take tree information from data and store
146
+ // it in the `updateCQN` array (which itself is just a flat list)
147
+ parent: entry[parentDataSymbol]?.[cqnSymbol]
148
+ })
149
+ entry[cqnSymbol] = updateCQNs[updateCQNs.length - 1]
142
150
  } else {
143
151
  insertCQN.INSERT.entries.push(entry)
144
152
  // inserts are handled deep so they must not be put into deepUpdateData
@@ -189,7 +197,15 @@ const _addToData = (subData, entity, element, entry) => {
189
197
  const value = ctUtils.val(entry[element.name])
190
198
  const subDataEntries = ctUtils.array(value)
191
199
  const unwrappedSubData = subDataEntries.map(entry => _unwrapIfNotArray(entry))
192
- for (const val of unwrappedSubData) subData.push(val)
200
+ for (const val of unwrappedSubData) {
201
+ if (val != null) {
202
+ // We need to conserve the tree information which gets
203
+ // lost because we're creating flat arrays of layers.
204
+ Object.defineProperty(val, parentDataSymbol, { value: entry })
205
+ }
206
+
207
+ subData.push(val)
208
+ }
193
209
  }
194
210
 
195
211
  async function _addSubDeepUpdateCQNRecursion({ model, compositionTree, entity, data, selectData, cqns, draft, req }) {
@@ -39,42 +39,48 @@ const _getFiltered = err => {
39
39
  }
40
40
 
41
41
  const _rewriteDBError = error => {
42
+ let { code, message } = error
43
+ code = String(code || 'null')
44
+
42
45
  // REVISIT: db stuff probably shouldn't be here
43
- if (error.code === 'SQLITE_ERROR') {
46
+ if (code === 'SQLITE_ERROR') {
44
47
  error.code = 'null'
45
- } else if (
46
- (error.code.startsWith('SQLITE_CONSTRAINT') && error.message.match(/COMMIT/)) ||
47
- (error.code.startsWith('SQLITE_CONSTRAINT') && error.message.match(/FOREIGN KEY/)) ||
48
- (error.code === '155' && error.message.match(/fk constraint violation/))
48
+ return
49
+ }
50
+
51
+ if (
52
+ (code.startsWith('SQLITE_CONSTRAINT') && (message.match(/COMMIT/) || message.match(/FOREIGN KEY/))) ||
53
+ (code === '155' && message.match(/fk constraint violation/))
49
54
  ) {
50
55
  // > foreign key constaint violation no sqlite/ hana
51
56
  error.code = '400'
52
- error.message = i18n('FK_CONSTRAINT_VIOLATION')
53
- } else if (error.code.startsWith('ASSERT_')) {
57
+ error.message = 'FK_CONSTRAINT_VIOLATION'
58
+ return
59
+ }
60
+
61
+ if (code.startsWith('ASSERT_')) {
54
62
  error.code = '400'
63
+ return
55
64
  }
56
65
  }
57
66
 
58
- const _normalize = (err, locale, inner = false) => {
67
+ const _normalize = (err, locale, formatterFn = _getFiltered) => {
68
+ // REVISIT: code and message rewriting
69
+ _rewriteDBError(err)
70
+
59
71
  // message (i18n)
60
72
  err.message = getErrorMessage(err, locale)
61
73
 
62
- // only allowed properties
63
- const error = _getFiltered(err)
64
-
65
74
  // ensure code is set and a string
66
- error.code = String(error.code || 'null')
67
-
68
- // REVISIT: code and message rewriting
69
- _rewriteDBError(error)
75
+ err.code = String(err.code || 'null')
70
76
 
71
- let statusCode = err.status || err.statusCode || (_isAllowedError(error.code) && error.code)
77
+ let statusCode = err.status || err.statusCode || (_isAllowedError(err.code) && err.code)
72
78
 
73
79
  // details
74
- if (!inner && err.details) {
80
+ if (err.details) {
75
81
  const childErrorCodes = new Set()
76
- error.details = err.details.map(ele => {
77
- const { error: childError, statusCode: childStatusCode } = _normalize(ele, locale, true)
82
+ err.details = err.details.map(ele => {
83
+ const { error: childError, statusCode: childStatusCode } = _normalize(ele, locale, formatterFn)
78
84
  childErrorCodes.add(childStatusCode)
79
85
  return childError
80
86
  })
@@ -84,12 +90,13 @@ const _normalize = (err, locale, inner = false) => {
84
90
  // make sure it's a number if set, otherwise will be assigned as 500 in normalizeError
85
91
  statusCode = statusCode ? Number(statusCode) : undefined
86
92
 
93
+ // only allowed properties
94
+ const error = formatterFn(err)
95
+
87
96
  return { error, statusCode }
88
97
  }
89
98
 
90
- const _isAllowedError = errorCode => {
91
- return errorCode >= 300 && errorCode < 505
92
- }
99
+ const _isAllowedError = errorCode => errorCode >= 300 && errorCode < 505
93
100
 
94
101
  // - for one unique value, we use it
95
102
  // - if at least one 5xx exists, we use 500
@@ -100,32 +107,37 @@ const _statusCodeFromDetails = uniqueStatusCodes => {
100
107
  if ([...uniqueStatusCodes].some(s => s >= 400)) return 400
101
108
  }
102
109
 
103
- const normalizeError = (err, req) => {
110
+ const _getSanitizedError = (statusCode, locale) => ({
111
+ error: { code: String(statusCode), message: i18n(statusCode, locale) },
112
+ statusCode
113
+ })
114
+
115
+ const normalizeError = (err, req, formatterFn) => {
104
116
  const locale = req.locale || (req.locale = localeFrom(req))
105
- let { error, statusCode } = _normalize(err, locale)
117
+ let { error, statusCode } = _normalize(err, locale, formatterFn)
106
118
 
107
- if ((!statusCode || (statusCode >= 500 && err._thrownByFramework)) && process.env.NODE_ENV === 'production') {
119
+ if (process.env.NODE_ENV === 'production') {
108
120
  // > return sanitized error to client
109
- return {
110
- error: { code: statusCode ? `${statusCode}` : '500', message: i18n(statusCode || 500, locale) },
111
- statusCode: Number(statusCode) || 500
121
+
122
+ if (!statusCode) return _getSanitizedError(500, locale)
123
+
124
+ if (statusCode >= 500 && err._thrownByFramework) {
125
+ return _getSanitizedError(statusCode, locale)
112
126
  }
113
127
  }
114
128
 
115
129
  if (!statusCode) statusCode = 500
116
130
 
117
131
  // no top level null codes
118
- if (error.code === 'null') {
119
- error.code = String(statusCode)
120
- }
132
+ if (error.code === 'null') error.code = String(statusCode)
121
133
 
122
134
  return { error, statusCode }
123
135
  }
124
136
 
125
137
  const _ensureSeverity = arg => {
126
- if (typeof arg === 'number' && arg >= MIN_SEVERITY && arg <= MAX_SEVERITY) {
127
- return arg
128
- }
138
+ if (typeof arg !== 'number') return DEFAULT_SEVERITY
139
+
140
+ if (arg >= MIN_SEVERITY && arg <= MAX_SEVERITY) return arg
129
141
 
130
142
  return DEFAULT_SEVERITY
131
143
  }
@@ -1,14 +1,14 @@
1
1
  const { cqnFrom } = require('./utils')
2
2
  const { RESTRICTIONS } = require('./constants')
3
3
 
4
- const _isRestricted = (req, capability, capabilityReadByKey) => {
5
- if (capabilityReadByKey !== undefined && req.query.SELECT && req.query.SELECT.one) {
6
- return capabilityReadByKey === false
4
+ const _getRestriction = (req, capability, capabilityReadByKey) => {
5
+ if (capabilityReadByKey !== undefined && req.query.SELECT?.one) {
6
+ return capabilityReadByKey
7
7
  }
8
- return capability === false
8
+ return capability
9
9
  }
10
10
 
11
- const _isNavigationRestricted = (target, path, annotation, req) => {
11
+ const _getNavigationRestriction = (target, path, annotation, req) => {
12
12
  if (!target) return
13
13
  if (!Array.isArray(target['@Capabilities.NavigationRestrictions.RestrictedProperties'])) return
14
14
 
@@ -17,7 +17,7 @@ const _isNavigationRestricted = (target, path, annotation, req) => {
17
17
  // prefix check to support both notations: { InsertRestrictions: { Insertable: false } } and { InsertRestrictions.Insertable: false }
18
18
  if (r.NavigationProperty['='] === path && Object.keys(r).some(k => k.startsWith(restriction))) {
19
19
  const capability = r[annotation] ?? r[restriction]?.[operation]
20
- return _isRestricted(req, capability, r.ReadRestrictions?.['ReadByKeyRestrictions.Readable'])
20
+ return _getRestriction(req, capability, r.ReadRestrictions?.['ReadByKeyRestrictions.Readable'])
21
21
  }
22
22
  }
23
23
  }
@@ -32,22 +32,41 @@ function handler(req) {
32
32
 
33
33
  const action = annotation.split('.').pop().toUpperCase()
34
34
  const from = cqnFrom(req)
35
- const nav = (from && from.ref && from.ref.map(el => el.id || el)) || []
35
+ const nav = (from?.ref && from.ref.map(el => el.id || el)) || []
36
36
 
37
+ let navRestriction
37
38
  if (nav.length > 1) {
38
- const path = nav.slice(1).join('.')
39
- const target = this.model.definitions[nav[0]]
40
- if (_isNavigationRestricted(target, path, annotation, req)) {
39
+ const navs = nav.slice(1)
40
+ let lastTarget, target, element, navigation, path
41
+ target = this.model.definitions[nav[0]]
42
+ for (let i = 0; i < navs.length && target; i++) {
43
+ element = !element || element.isAssociation ? target.elements[navs[i]] : element.elements[navs[i]]
44
+ if (element.isAssociation) {
45
+ navigation = path ? `${path}.${navs[i]}` : navs[i]
46
+ path = undefined
47
+ lastTarget = target
48
+ target = this.model.definitions[element.target]
49
+ } else {
50
+ path = path ? `${path}.${navs[i]}` : navs[i]
51
+ }
52
+ }
53
+ if (lastTarget && navigation) {
54
+ navRestriction = _getNavigationRestriction(lastTarget, navigation, annotation, req)
55
+ }
56
+ if (navRestriction === false) {
41
57
  // REVISIT: rework exception with using target
42
- const trgt = `${_localName(target)}.${path}`
58
+ const trgt = `${_localName(lastTarget)}.${navs.join('.')}`
43
59
  req.reject(405, 'ENTITY_IS_NOT_CRUD_VIA_NAVIGATION', [_localName(req.target), action, trgt])
44
60
  }
45
- } else if (
46
- _isRestricted(
61
+ }
62
+
63
+ if (
64
+ !navRestriction &&
65
+ _getRestriction(
47
66
  req,
48
67
  req.target['@Capabilities.' + annotation],
49
68
  req.target['@Capabilities.' + RESTRICTIONS.READABLE_BY_KEY]
50
- )
69
+ ) === false
51
70
  ) {
52
71
  req.reject(405, 'ENTITY_IS_NOT_CRUD', [_localName(req.target), action])
53
72
  }
@@ -246,7 +246,7 @@ async function commonGenericInput(req) {
246
246
  if (boundAction) {
247
247
  const pathSegment = _getBoundActionBindingParameter(boundAction)
248
248
  if (pathSegment) pathOptions.pathSegmentsInfo.push(pathSegment)
249
- const keys = req._?.params?.[0]
249
+ const keys = req.params?.[0]
250
250
  if (keys && 'IsActiveEntity' in keys) {
251
251
  pathOptions.draftKeys = { IsActiveEntity: keys.IsActiveEntity }
252
252
  }
@@ -729,9 +729,6 @@ const _convertSelect = (query, model, _options) => {
729
729
  }
730
730
  }
731
731
 
732
- // lambda functions
733
- convertWhereExists(query.SELECT, model, options)
734
-
735
732
  // add 'or is null' in case of not equal: '!=' or '<>'
736
733
  if (query.SELECT._4odata) {
737
734
  _convertNotEqual(query.SELECT, 'where')
@@ -739,6 +736,9 @@ const _convertSelect = (query, model, _options) => {
739
736
  }
740
737
 
741
738
  _convertPathExpression(query, model, options)
739
+
740
+ convertWhereExists(query.SELECT, model, options)
741
+
742
742
  rewriteAsterisks(query, model, options)
743
743
  const entity =
744
744
  (query.SELECT.from.ref && (query.SELECT.from.ref[0].id || query.SELECT.from.ref[0])) || query.SELECT.from
@@ -1,48 +1,64 @@
1
1
  const cds = require('../../cds')
2
-
2
+ const preservedSymbol = Symbol('preserved')
3
3
  const { hasDeepUpdate, getDeepUpdateCQNs, selectDeepUpdateData } = require('../../common/composition')
4
4
  const { getFlatArray, processCQNs } = require('../utils/deep')
5
5
  const normalizeTimestamp = require('../../common/utils/normalizeTimestamp')
6
+ const onlyKeysRemain = require('../../common/utils/onlyKeysRemain')
7
+
8
+ const isMoreThanManaged = (cqn, entity) => {
9
+ return (
10
+ cqn[preservedSymbol] ||
11
+ Object.keys(cqn.UPDATE.data).some(
12
+ key =>
13
+ !(key in entity.keys) &&
14
+ (entity.elements[key]['@cds.on.update'] === undefined || !cqn.UPDATE.data[key]?.startsWith('$'))
15
+ )
16
+ )
17
+ }
6
18
 
7
- const _includesCompositionTarget = (cqns, target) => {
8
- return cqns.find(cqn => {
9
- return (cqn.UPDATE && cqn.UPDATE.entity === target) || (cqn.DELETE && cqn.DELETE.from === target)
10
- })
19
+ const isManaged = (cqn, entity) => {
20
+ return Object.keys(cqn.UPDATE.data).some(
21
+ key => entity.elements[key]['@cds.on.update'] && cqn.UPDATE.data[key] !== undefined
22
+ )
11
23
  }
12
24
 
13
25
  const _getFilteredCqns = (cqns, model) => {
14
26
  // right to left processing necessary!
15
- // no need to process first (= root)
16
27
  for (let i = cqns.length - 1; i > 0; i--) {
17
28
  const cqn = cqns[i]
18
29
 
19
30
  const entity = model && cqn.UPDATE && model.definitions[cqn.UPDATE.entity]
20
- if (!entity) continue
21
-
22
- // do not filter if:
23
- // - there is a property that is not managed or managed but filled by custom handler (i.e., its value doesn't start
24
- // with $) a composition target is updated as well
25
- let moreThanManaged = Object.keys(cqn.UPDATE.data).some(
26
- key => entity.elements[key]['@cds.on.update'] === undefined || !cqn.UPDATE.data[key]?.startsWith('$')
27
- )
31
+ if (!entity) {
32
+ Object.defineProperty(cqn, preservedSymbol, { value: true })
33
+ continue
34
+ }
28
35
 
29
- if (moreThanManaged) continue
36
+ // do not filter if there is a property that is not a key or managed by us (its value starts with $)
37
+ let moreThanManaged = isMoreThanManaged(cqn, entity)
30
38
 
31
- const comps = Object.values(entity.associations || {}).filter(assoc => assoc.isComposition)
32
- for (const comp of comps) {
33
- if (_includesCompositionTarget(cqns, comp.target)) {
34
- moreThanManaged = true
35
- break
39
+ if (moreThanManaged) {
40
+ Object.defineProperty(cqn, preservedSymbol, { value: true })
41
+ const parentEntity = cqn.parent?.UPDATE && model.definitions[cqn.parent.UPDATE.entity]
42
+ if (parentEntity && isManaged(cqn.parent, parentEntity)) {
43
+ Object.defineProperty(cqn.parent, preservedSymbol, { value: true })
36
44
  }
45
+ continue
37
46
  }
47
+ }
38
48
 
39
- if (moreThanManaged) continue
40
-
41
- // remove current cqn
42
- cqns.splice(i, 1)
49
+ const rootCqn = cqns[0]
50
+ if (rootCqn) {
51
+ // no need to process first (= root)
52
+ Object.defineProperty(rootCqn, preservedSymbol, { value: true })
43
53
  }
44
54
 
45
- return cqns
55
+ return cqns.filter(cqn => {
56
+ const entity = model && cqn.UPDATE && model.definitions[cqn.UPDATE.entity]
57
+ if (entity && onlyKeysRemain({ query: cqn, target: entity, data: cqn.UPDATE.data })) {
58
+ return false
59
+ }
60
+ return cqn[preservedSymbol]
61
+ })
46
62
  }
47
63
 
48
64
  const update = executeUpdateCQN => async (model, dbc, req) => {
@@ -52,20 +68,22 @@ const update = executeUpdateCQN => async (model, dbc, req) => {
52
68
  if (model && hasDeepUpdate(model, query)) {
53
69
  // REVISIT: avoid additional read
54
70
  const selectData = await selectDeepUpdateData(cds.db, model, req)
55
- let cqns = await getDeepUpdateCQNs(model, req, selectData)
71
+ const cqns = await getDeepUpdateCQNs(model, req, selectData)
56
72
 
57
73
  // the delete chunks, i.e., how many deletes can be processed in parallel
58
74
  const chunks = []
59
75
  for (const each of cqns) chunks.push(each.filter(e => e.DELETE).length)
60
76
 
61
77
  // remove child queries that only want to update @cds.on.update properties
62
- cqns = _getFilteredCqns(getFlatArray(cqns), model)
78
+ let _cqns = Array.from(cqns)
79
+ _cqns = getFlatArray(_cqns)
80
+ _cqns = _getFilteredCqns(_cqns, model)
63
81
 
64
- if (cqns.length === 0) return 0
65
- const results = await processCQNs(executeUpdateCQN, cqns, model, dbc, user, locale, isoTs, chunks)
82
+ if (_cqns.length === 0) return 0
83
+ const results = await processCQNs(executeUpdateCQN, _cqns, model, dbc, user, locale, isoTs, chunks)
66
84
 
67
85
  // return number of affected rows of "root cqn", if an update, 1 otherwise (as not update of root but its children)
68
- if (cqns[0].UPDATE) return results[0]
86
+ if (_cqns[0].UPDATE) return results[0]
69
87
  return 1
70
88
  }
71
89
 
@@ -269,11 +269,10 @@ cds.ApplicationService.prototype.handle = async function (req) {
269
269
  delete res.DraftAdministrativeData
270
270
  const HasActiveEntity = res.HasActiveEntity
271
271
  delete res.HasActiveEntity
272
- delete res.IsActiveEntity
273
272
  // First run the handlers as they might need access to DraftAdministrativeData or the draft entities
274
273
  const result = await run(
275
274
  HasActiveEntity ? UPDATE(req.target).data(res).where(targetWhere) : INSERT.into(req.target).entries(res),
276
- { headers: { 'if-match': '*' } }
275
+ { headers: Object.assign({}, req.headers, { 'if-match': '*' }) }
277
276
  )
278
277
  await _promiseAll([
279
278
  DELETE.from(targetDraft).where(targetWhere),
@@ -1054,7 +1053,7 @@ async function onCancel(req) {
1054
1053
  )
1055
1054
  else
1056
1055
  queries.push(
1057
- UPDATE('Draft.DraftAdministrativeData')
1056
+ UPDATE('DRAFT.DraftAdministrativeData')
1058
1057
  .data({
1059
1058
  InProcessByUser: cds.context.user.id,
1060
1059
  LastChangedByUser: cds.context.user.id,
@@ -21,15 +21,16 @@ const convertInt64ToString = int64 => {
21
21
  const convertToISO = element => {
22
22
  if (!element) return null
23
23
 
24
+ let dateTime = element.replace(' ', 'T')
24
25
  if (cds.env.features.precise_timestamps) {
25
- const dateTime = element.slice(0, 19).replace(' ', 'T')
26
+ dateTime = dateTime.slice(0, 19)
26
27
  let millis = element.slice(20)
27
28
  if (millis.at(-1) === 'Z') millis = millis.slice(0, -1)
28
29
  millis = millis.slice(0, 7).padEnd(7, '0')
29
30
  return dateTime + '.' + millis + 'Z'
30
31
  }
31
32
 
32
- return new Date(element + 'Z').toISOString()
33
+ return new Date(dateTime + 'Z').toISOString()
33
34
  }
34
35
 
35
36
  const convertToISONoMillis = element => {
@@ -4,7 +4,7 @@ const _transform = o => ({ subdomain: o.subscribedSubdomain, tenant: o.subscribe
4
4
  // REVISIT: Looks ugly -> can we simplify that?
5
5
  const getTenantInfo = async tenant => {
6
6
  const provisioning = await cds.connect.to('cds.xt.SaasProvisioningService')
7
- const tx = provisioning.tx({ user: new cds.User.Privileged() })
7
+ const tx = provisioning.tx({ user: cds.User.privileged })
8
8
  try {
9
9
  const result = tenant
10
10
  ? _transform(await tx.get('/tenant', { ['subscribedTenantId']: tenant }))
@@ -99,7 +99,7 @@ const processMessages = async (service, tenant, _opts = {}) => {
99
99
 
100
100
  outboxRunner.run({ name, tenant }, () => {
101
101
  let letAppCrash = false
102
- const config = tenant ? { tenant, user: new cds.User.Privileged() } : { user: new cds.User.Privileged() }
102
+ const config = tenant ? { tenant, user: cds.User.privileged } : { user: cds.User.privileged }
103
103
  const spawn = cds.spawn(async () => {
104
104
  let messages
105
105
  try {
@@ -34,6 +34,7 @@ const _setHeaders = (defaultHeaders, req) => {
34
34
  }
35
35
 
36
36
  const _setCorrectValue = (el, data, params, kind) => {
37
+ if (data[el] === undefined) return "'undefined'"
37
38
  return typeof data[el] === 'object' && kind !== 'odata-v2'
38
39
  ? JSON.stringify(data[el])
39
40
  : formatVal(data[el], el, { elements: params }, kind)
@@ -180,7 +181,6 @@ const resolvedTargetOfQuery = q => {
180
181
  }
181
182
 
182
183
  let logged
183
- let sdkLoggerDisabled
184
184
 
185
185
  const _resolveSelectionStrategy = options => {
186
186
  if (typeof options?.selectionStrategy !== 'string') return
@@ -228,22 +228,6 @@ class RemoteService extends cds.Service {
228
228
  'Configuration option "cds.env.features.fetch_csrf" is deprecated.\n Please use "csrf"/"csrfInBatch" as described in https://cap.cloud.sap/docs/node.js/remote-services'
229
229
  )
230
230
  }
231
-
232
- // REVISIT: use cds.log's logger in cloud sdk
233
-
234
- // disable sdk logger if not in debug mode
235
- if (!LOG._debug && !sdkLoggerDisabled) {
236
- try {
237
- // eslint-disable-next-line cds/no-missing-dependencies -- needs to be added by app dev
238
- const sdkUtils = require('@sap-cloud-sdk/util')
239
- sdkUtils.setGlobalLogLevel('error')
240
-
241
- // disable sdk logger once
242
- sdkLoggerDisabled = true
243
- } catch (err) {
244
- /* might fail in cds repl due to winston's exception handler, see cap/issues#10134 */
245
- }
246
- }
247
231
  } else if ([...this.entities].length || [...this.operations].length) {
248
232
  throw new Error(`No credentials configured for "${this.name}".`)
249
233
  }
@@ -427,13 +427,13 @@ const _stringToReqOptions = (query, data, target) => {
427
427
  return reqOptions
428
428
  }
429
429
 
430
- const _pathToReqOptions = (method, path, data, target, namespace) => {
430
+ const _pathToReqOptions = (method, path, data, target, srvName) => {
431
431
  let url = path
432
432
  if (!url.startsWith('/')) {
433
433
  // extract entity name and instance identifier (either in "()" or after "/") from fully qualified path
434
434
  const parts = path.match(/([\w.]*)([\W.]*)(.*)/)
435
435
  if (!parts) url = '/' + path.match(/\w*$/)[0]
436
- else if (url.startsWith(namespace)) url = '/' + parts[1].replace(namespace + '.', '') + parts[2] + parts[3]
436
+ else if (url.startsWith(srvName)) url = '/' + parts[1].replace(srvName + '.', '') + parts[2] + parts[3]
437
437
  else url = '/' + parts[1].match(/\w*$/)[0] + parts[2] + parts[3]
438
438
 
439
439
  // normalize in case parts[2] already starts with /
@@ -460,7 +460,7 @@ const getReqOptions = (req, query, service) => {
460
460
  ? _cqnToReqOptions(query, service, req)
461
461
  : typeof query === 'string'
462
462
  ? _stringToReqOptions(query, req.data, req.target)
463
- : _pathToReqOptions(req.method, req.path, req.data, req.target, service.namespace)
463
+ : _pathToReqOptions(req.method, req.path, req.data, req.target, service.name)
464
464
 
465
465
  if (service.kind === 'odata-v2' && req.event === 'READ' && reqOptions.url?.match(/(\/any\()|(\/all\()/)) {
466
466
  req.reject(501, 'Lambda expressions are not supported in OData v2')
@@ -66,12 +66,9 @@ const _convertActionFuncResponse = (returnType, convertValueFn) => data => {
66
66
 
67
67
  // eslint-disable-next-line complexity
68
68
  const _convertValue = (ieee754Compatible, exponentialDecimals) => (value, element) => {
69
- if (value == null) {
70
- return value
71
- }
69
+ if (value == null) return value
72
70
 
73
71
  const type = _elementType(element)
74
-
75
72
  if (type === 'cds.Boolean') {
76
73
  if (value === 'true') {
77
74
  value = true
@@ -119,18 +116,19 @@ const _convertValue = (ieee754Compatible, exponentialDecimals) => (value, elemen
119
116
 
120
117
  return value
121
118
  }
119
+
122
120
  const _PT = ([hh, mm, ss]) => `PT${hh}H${mm}M${ss}S`
123
121
 
124
122
  const _convertPayloadValue = (value, element) => {
125
- const type = _elementType(element)
123
+ if (value == null) return value
126
124
 
127
125
  // see https://www.odata.org/documentation/odata-version-2-0/json-format/
128
- if (value == null) return value
126
+ const type = _elementType(element)
129
127
  switch (type) {
130
128
  case 'cds.Time':
131
129
  return value.match(/^(PT)([H,M,S,0-9])*$/) ? value : _PT(value.split(':'))
132
130
  case 'cds.Decimal':
133
- return typeof value === 'string' ? value : new String(value)
131
+ return typeof value === 'string' ? value : `${value}`
134
132
  case 'cds.Date':
135
133
  case 'cds.DateTime':
136
134
  return `/Date(${new Date(value).getTime()})/`
@@ -33,7 +33,7 @@
33
33
  const stack = []
34
34
  let SELECT, count
35
35
  const TECHNICAL_OPTS = ['$value'] // odata parts to be handled somewhere else
36
- // we keep that here to allow for usage in https://pegjs.org/online
36
+ // we keep that here to allow for usage in https://peggyjs.org/online
37
37
  const safeNumber =
38
38
  options.safeNumber ||
39
39
  function (str) {