@sap/cds 7.2.0 → 7.3.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 (63) hide show
  1. package/CHANGELOG.md +174 -126
  2. package/README.md +1 -1
  3. package/apis/connect.d.ts +1 -1
  4. package/apis/core.d.ts +6 -4
  5. package/apis/serve.d.ts +1 -1
  6. package/apis/services.d.ts +51 -31
  7. package/apis/test.d.ts +24 -10
  8. package/bin/serve.js +4 -3
  9. package/common.cds +4 -4
  10. package/lib/auth/ias-auth.js +7 -8
  11. package/lib/compile/cdsc.js +5 -7
  12. package/lib/compile/etc/csv.js +22 -11
  13. package/lib/dbs/cds-deploy.js +1 -2
  14. package/lib/env/cds-env.js +26 -20
  15. package/lib/env/defaults.js +4 -3
  16. package/lib/env/schema.js +9 -0
  17. package/lib/i18n/localize.js +83 -77
  18. package/lib/index.js +6 -2
  19. package/lib/linked/classes.js +13 -13
  20. package/lib/plugins.js +41 -45
  21. package/lib/req/user.js +2 -2
  22. package/lib/srv/protocols/_legacy.js +0 -1
  23. package/lib/srv/protocols/odata-v4.js +4 -0
  24. package/lib/utils/axios.js +7 -1
  25. package/lib/utils/cds-test.js +140 -133
  26. package/lib/utils/cds-utils.js +1 -1
  27. package/lib/utils/check-version.js +6 -0
  28. package/lib/utils/data.js +19 -6
  29. package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +20 -19
  30. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +10 -1
  31. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +1 -1
  32. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +2 -3
  33. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/ValueConverter.js +0 -14
  34. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/core/OdataRequest.js +1 -0
  35. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/deserializer/BatchRequestListBuilder.js +5 -2
  36. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/handler/MetadataHandler.js +1 -1
  37. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/handler/ServiceHandler.js +1 -1
  38. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/invocation/DispatcherCommand.js +2 -2
  39. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/utils/BufferedWriter.js +1 -3
  40. package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +1 -1
  41. package/libx/_runtime/common/composition/update.js +18 -2
  42. package/libx/_runtime/common/error/frontend.js +46 -34
  43. package/libx/_runtime/common/generic/auth/capabilities.js +33 -14
  44. package/libx/_runtime/common/generic/input.js +1 -1
  45. package/libx/_runtime/common/generic/paging.js +1 -0
  46. package/libx/_runtime/common/i18n/messages.properties +1 -0
  47. package/libx/_runtime/common/utils/cqn2cqn4sql.js +3 -3
  48. package/libx/_runtime/db/query/update.js +48 -30
  49. package/libx/_runtime/fiori/lean-draft.js +23 -24
  50. package/libx/_runtime/hana/conversion.js +3 -2
  51. package/libx/_runtime/messaging/enterprise-messaging-utils/getTenantInfo.js +1 -1
  52. package/libx/_runtime/messaging/outbox/utils.js +1 -1
  53. package/libx/_runtime/remote/Service.js +11 -26
  54. package/libx/_runtime/remote/utils/client.js +3 -2
  55. package/libx/_runtime/remote/utils/data.js +5 -7
  56. package/libx/odata/{grammar.pegjs → grammar.peggy} +1 -1
  57. package/libx/odata/metadata.js +121 -0
  58. package/libx/odata/parser.js +1 -1
  59. package/libx/odata/service-document.js +61 -0
  60. package/libx/odata/utils.js +102 -48
  61. package/libx/rest/RestAdapter.js +2 -2
  62. package/libx/rest/middleware/error.js +1 -1
  63. package/package.json +1 -1
@@ -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
  }
@@ -35,6 +35,7 @@ const commonGenericPaging = function (req) {
35
35
  }
36
36
 
37
37
  const _addPaging = function ({ SELECT }, target) {
38
+ if (SELECT.limit === false) return
38
39
  const { rows } = SELECT.limit || (SELECT.limit = {})
39
40
  const conf = getPageSize(target)
40
41
  SELECT.limit.rows = {
@@ -83,6 +83,7 @@ CRUD_VIA_NAVIGATION_NOT_SUPPORTED=CRUD via navigations is not yet supported
83
83
 
84
84
  # draft
85
85
  DRAFT_ALREADY_EXISTS=A draft for this entity already exists
86
+ DRAFT_NOT_EXISTING=No draft for this entity exists
86
87
  DRAFT_LOCKED_BY_ANOTHER_USER=The entity is locked by user "{0}"
87
88
  DRAFT_MODIFICATION_ONLY_VIA_ROOT=A draft can only be modified via its root entity
88
89
 
@@ -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
 
@@ -48,6 +48,10 @@ const _promiseAll = async array => {
48
48
 
49
49
  const _isCount = query => query.SELECT.columns?.length === 1 && query.SELECT.columns[0].func === 'count'
50
50
 
51
+ const entity_keys = e => {
52
+ return Object_keys(e.keys).filter(k => k !== 'IsActiveEntity' && !e.keys[k].isAssociation)
53
+ }
54
+
51
55
  const _inProcessByUserXpr = lockShiftedNow => ({
52
56
  xpr: [
53
57
  'case',
@@ -257,7 +261,7 @@ cds.ApplicationService.prototype.handle = async function (req) {
257
261
  { ref: ['DraftAdministrativeData'], expand: [{ ref: ['InProcessByUser'] }] }
258
262
  ])
259
263
  )
260
- if (!res) req.reject(_etagValidationType ? 412 : 404)
264
+ if (!res) req.reject(_etagValidationType ? 412 : { code: 'DRAFT_NOT_EXISTING', status: 404 })
261
265
  if (res.DraftAdministrativeData?.InProcessByUser !== cds.context.user.id)
262
266
  req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER', [res.DraftAdministrativeData?.InProcessByUser])
263
267
  const DraftAdministrativeData_DraftUUID = res.DraftAdministrativeData_DraftUUID
@@ -265,11 +269,10 @@ cds.ApplicationService.prototype.handle = async function (req) {
265
269
  delete res.DraftAdministrativeData
266
270
  const HasActiveEntity = res.HasActiveEntity
267
271
  delete res.HasActiveEntity
268
- delete res.IsActiveEntity
269
272
  // First run the handlers as they might need access to DraftAdministrativeData or the draft entities
270
273
  const result = await run(
271
274
  HasActiveEntity ? UPDATE(req.target).data(res).where(targetWhere) : INSERT.into(req.target).entries(res),
272
- { headers: { 'if-match': '*' } }
275
+ { headers: Object.assign({}, req.headers, { 'if-match': '*' }) }
273
276
  )
274
277
  await _promiseAll([
275
278
  DELETE.from(targetDraft).where(targetWhere),
@@ -287,9 +290,7 @@ cds.ApplicationService.prototype.handle = async function (req) {
287
290
  if (req.target.actions?.[req.event] && draftParams.IsActiveEntity === false) {
288
291
  if (query.SELECT?.from?.ref) query.SELECT.from.ref = _redirectRefToDrafts(query.SELECT.from.ref, this.model)
289
292
  const rootQuery = query.clone()
290
- const columns = Object_keys(query._target.keys)
291
- .filter(k => k !== 'IsActiveEntity')
292
- .map(k => ({ ref: [k] }))
293
+ const columns = entity_keys(query._target).map(k => ({ ref: [k] }))
293
294
  columns.push({ ref: ['DraftAdministrativeData'], expand: [{ ref: ['InProcessByUser'] }] })
294
295
  rootQuery.SELECT.columns = columns
295
296
  rootQuery.SELECT.one = true
@@ -377,7 +378,7 @@ const Read = {
377
378
  if (query._target.name.endsWith('.DraftAdministrativeData')) return run(query._drafts)
378
379
  if (!query._target._isDraftEnabled) return run(query)
379
380
  if (!query.SELECT.groupBy && query.SELECT.columns && !query.SELECT.columns.some(c => c === '*')) {
380
- const keys = Object_keys(query._target.keys).filter(k => k !== 'IsActiveEntity')
381
+ const keys = entity_keys(query._target)
381
382
  for (const key of keys) {
382
383
  if (!query.SELECT.columns.some(c => c.ref?.[0] === key)) query.SELECT.columns.push({ ref: [key] })
383
384
  }
@@ -409,11 +410,10 @@ const Read = {
409
410
  unchanged: async function (run, query) {
410
411
  LOG.debug('List Editing Status: Unchanged')
411
412
  const draftsQuery = query._drafts
412
- const keys = Object_keys(query._target.keys).filter(k => k !== 'IsActiveEntity')
413
413
  draftsQuery.SELECT.count = undefined
414
- draftsQuery.SELECT.limit = undefined
415
414
  draftsQuery.SELECT.orderBy = undefined
416
- draftsQuery.SELECT.columns = keys.map(k => ({ ref: [k] }))
415
+ draftsQuery.SELECT.limit = false
416
+ draftsQuery.SELECT.columns = entity_keys(query._target).map(k => ({ ref: [k] }))
417
417
 
418
418
  const drafts = await draftsQuery.where({ HasActiveEntity: true })
419
419
  const res = await Read.onlyActives(run, query.where(Read.whereNotIn(query._target, drafts)), {
@@ -457,11 +457,10 @@ const Read = {
457
457
  LOG.debug('List Editing Status: All')
458
458
  if (!query._drafts) return []
459
459
  query._drafts.SELECT.count = false
460
- query._drafts.SELECT.limit = undefined // We need all entries for the keys to properly select actives (count)
460
+ query._drafts.SELECT.limit = false // We need all entries for the keys to properly select actives (count)
461
461
  const isCount = _isCount(query._drafts)
462
462
  if (isCount) {
463
- const keys = Object_keys(query._target.keys).filter(k => k !== 'IsActiveEntity')
464
- query._drafts.SELECT.columns = keys.map(k => ({ ref: [k] }))
463
+ query._drafts.SELECT.columns = entity_keys(query._target).map(k => ({ ref: [k] }))
465
464
  }
466
465
  if (!query._drafts.SELECT.columns) query._drafts.SELECT.columns = ['*']
467
466
  if (!query._drafts.SELECT.columns.some(c => c.ref?.[0] === 'HasActiveEntity'))
@@ -525,13 +524,14 @@ const Read = {
525
524
  },
526
525
  activesFromDrafts: async function (run, query, { isLocked = true }) {
527
526
  const draftsQuery = query._drafts
528
- const keys = Object_keys(query._target.keys).filter(k => k !== 'IsActiveEntity')
529
527
  const additionalCols = draftsQuery.SELECT.columns
530
528
  ? draftsQuery.SELECT.columns.filter(
531
529
  c => c.ref && ['DraftAdministrativeData', 'DraftAdministrativeData_DraftUUID'].includes(c.ref[0])
532
530
  )
533
531
  : [{ ref: ['DraftAdministrativeData_DraftUUID'] }]
534
- draftsQuery.SELECT.columns = keys.map(k => ({ ref: [k] })).concat(additionalCols)
532
+ draftsQuery.SELECT.columns = entity_keys(query._target)
533
+ .map(k => ({ ref: [k] }))
534
+ .concat(additionalCols)
535
535
  draftsQuery.where({
536
536
  HasActiveEntity: true,
537
537
  'DraftAdministrativeData.InProcessByUser': { '!=': cds.context.user.id },
@@ -560,7 +560,7 @@ const Read = {
560
560
  },
561
561
  whereNotIn: (target, data) => Read.whereIn(target, data, true),
562
562
  whereIn: (target, data, not = false) => {
563
- const keys = Object_keys(target.keys).filter(k => k !== 'IsActiveEntity')
563
+ const keys = entity_keys(target)
564
564
  const dataArray = data ? (Array.isArray(data) ? data : [data]) : []
565
565
  if (not && !dataArray.length) return []
566
566
  const left = { list: keys.map(k => ({ ref: [k] })) }
@@ -573,9 +573,7 @@ const Read = {
573
573
  if (!actives.length) return []
574
574
  const drafts = cds.ql.clone(query._drafts)
575
575
  drafts.SELECT.where = Read.whereIn(query._target, actives)
576
- const newColumns = Object_keys(query._target.keys)
577
- .filter(k => k !== 'IsActiveEntity')
578
- .map(k => ({ ref: [k] }))
576
+ const newColumns = entity_keys(query._target).map(k => ({ ref: [k] }))
579
577
  if (
580
578
  !drafts.SELECT.columns ||
581
579
  drafts.SELECT.columns.some(c => c === '*' || c.ref?.[0] === 'DraftAdministrativeData_DraftUUID')
@@ -594,8 +592,10 @@ const Read = {
594
592
  // Indexes the data for fast key access
595
593
  const dataArray = Read._makeArray(data)
596
594
  if (!dataArray.length) return
597
- const _keys = Object_keys(target.keys).filter(k => k !== 'IsActiveEntity')
598
- const hash = row => _keys.map(k => row[k]).reduce((res, curr) => res + '|$|' + curr, '')
595
+ const hash = row =>
596
+ entity_keys(target)
597
+ .map(k => row[k])
598
+ .reduce((res, curr) => res + '|$|' + curr, '')
599
599
  const hashMap = new Map()
600
600
  for (const row of dataArray) hashMap.set(hash(row), row)
601
601
  return { hashMap, hash }
@@ -1053,7 +1053,7 @@ async function onCancel(req) {
1053
1053
  )
1054
1054
  else
1055
1055
  queries.push(
1056
- UPDATE('Draft.DraftAdministrativeData')
1056
+ UPDATE('DRAFT.DraftAdministrativeData')
1057
1057
  .data({
1058
1058
  InProcessByUser: cds.context.user.id,
1059
1059
  LastChangedByUser: cds.context.user.id,
@@ -1074,12 +1074,11 @@ async function onPrepare(req) {
1074
1074
  }
1075
1075
  const where = req.query.SELECT.from.ref[0].where
1076
1076
 
1077
- const keys = Object_keys(req.target.keys).filter(k => k !== 'IsActiveEntity')
1078
1077
  const draftQuery = SELECT.one
1079
1078
  .from(req.target, d => {
1080
1079
  d.DraftAdministrativeData(a => a.InProcessByUser)
1081
1080
  })
1082
- .columns(keys)
1081
+ .columns(entity_keys(req.target))
1083
1082
  .where(where)
1084
1083
  draftQuery[DRAFT_PARAMS] = draftParams
1085
1084
  const data = await draftQuery
@@ -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 {