@sap/cds 7.4.2 → 7.5.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 (115) hide show
  1. package/CHANGELOG.md +100 -0
  2. package/apis/cds.d.ts +1 -38
  3. package/apis/core.d.ts +21 -101
  4. package/apis/cqn.d.ts +18 -76
  5. package/apis/csn.d.ts +18 -114
  6. package/apis/events.d.ts +16 -123
  7. package/apis/internal/inference.d.ts +18 -32
  8. package/apis/linked.d.ts +18 -97
  9. package/apis/log.d.ts +19 -164
  10. package/apis/models.d.ts +18 -180
  11. package/apis/ql.d.ts +16 -323
  12. package/apis/reflect.d.ts +32 -0
  13. package/apis/server.d.ts +18 -135
  14. package/apis/services.d.ts +18 -380
  15. package/bin/cds-serve.js +5 -2
  16. package/bin/serve.js +7 -16
  17. package/lib/auth/basic-auth.js +3 -1
  18. package/lib/auth/ias-auth.js +62 -48
  19. package/lib/auth/ias-claims.js +34 -0
  20. package/lib/auth/index.js +55 -33
  21. package/lib/auth/jwt-auth.js +55 -52
  22. package/lib/compile/cdsc.js +2 -2
  23. package/lib/compile/to/edm.js +4 -4
  24. package/lib/compile/to/hdbtabledata.js +5 -8
  25. package/lib/compile/to/srvinfo.js +2 -2
  26. package/lib/env/cds-env.js +3 -9
  27. package/lib/env/cds-requires.js +16 -17
  28. package/lib/env/compat.js +0 -9
  29. package/lib/env/defaults.js +17 -6
  30. package/lib/i18n/localize.js +46 -42
  31. package/lib/index.js +6 -8
  32. package/lib/linked/classes.js +7 -118
  33. package/lib/linked/entities.js +1 -1
  34. package/lib/log/cds-log.js +15 -10
  35. package/lib/log/format/aspects/als.js +41 -0
  36. package/lib/log/format/aspects/cf.js +36 -0
  37. package/lib/log/format/json.js +96 -0
  38. package/lib/plugins.js +7 -3
  39. package/lib/req/context.js +4 -2
  40. package/lib/srv/cds-connect.js +3 -5
  41. package/lib/srv/cds-serve.js +13 -26
  42. package/lib/srv/factory.js +3 -3
  43. package/lib/srv/middlewares/index.js +0 -2
  44. package/lib/srv/middlewares/trace.js +2 -3
  45. package/lib/srv/protocols/_legacy.js +27 -30
  46. package/lib/srv/protocols/index.js +173 -58
  47. package/lib/srv/protocols/odata-v4.js +29 -16
  48. package/lib/srv/srv-api.js +8 -13
  49. package/lib/srv/srv-handlers.js +14 -14
  50. package/lib/utils/cds-utils.js +15 -0
  51. package/libx/_runtime/auth/index.js +4 -5
  52. package/libx/_runtime/auth/strategies/basic.js +2 -2
  53. package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +23 -13
  54. package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +6 -15
  55. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +10 -3
  56. package/libx/_runtime/cds-services/adapter/odata-v4/to.js +5 -2
  57. package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +2 -1
  58. package/libx/_runtime/cds-services/services/utils/columns.js +3 -9
  59. package/libx/_runtime/cds.js +13 -0
  60. package/libx/_runtime/common/composition/data.js +3 -0
  61. package/libx/_runtime/common/composition/delete.js +1 -1
  62. package/libx/_runtime/common/error/frontend.js +2 -2
  63. package/libx/_runtime/common/generic/auth/readOnly.js +1 -1
  64. package/libx/_runtime/common/generic/auth/restrictions.js +1 -1
  65. package/libx/_runtime/common/generic/sorting.js +4 -5
  66. package/libx/_runtime/common/utils/csn.js +23 -18
  67. package/libx/_runtime/common/utils/restrictions.js +6 -15
  68. package/libx/_runtime/db/generic/input.js +3 -2
  69. package/libx/_runtime/fiori/generic/readOverDraft.js +2 -5
  70. package/libx/_runtime/fiori/lean-draft.js +69 -5
  71. package/libx/_runtime/hana/Service.js +1 -1
  72. package/libx/_runtime/messaging/AMQPWebhookMessaging.js +1 -1
  73. package/libx/_runtime/messaging/Outbox.js +3 -8
  74. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +1 -0
  75. package/libx/_runtime/messaging/enterprise-messaging.js +1 -1
  76. package/libx/_runtime/messaging/file-based.js +1 -1
  77. package/libx/_runtime/messaging/service.js +7 -10
  78. package/libx/_runtime/remote/Service.js +15 -45
  79. package/libx/_runtime/remote/utils/client.js +20 -33
  80. package/libx/_runtime/remote/utils/cloudSdkProvider.js +30 -0
  81. package/libx/_runtime/sqlite/Service.js +2 -2
  82. package/libx/odata/afterburner.js +29 -21
  83. package/libx/odata/cqn2odata.js +1 -1
  84. package/libx/odata/error.js +7 -0
  85. package/libx/odata/grammar.peggy +16 -20
  86. package/libx/odata/metadata.js +73 -78
  87. package/libx/odata/parser.js +1 -1
  88. package/libx/odata/read.js +94 -0
  89. package/libx/odata/result.js +91 -0
  90. package/libx/odata/service-document.js +31 -37
  91. package/libx/odata/utils.js +2 -1
  92. package/libx/outbox/index.js +9 -4
  93. package/libx/rest/RestAdapter.js +68 -67
  94. package/libx/rest/middleware/create.js +20 -26
  95. package/libx/rest/middleware/delete.js +5 -3
  96. package/libx/rest/middleware/error.js +2 -3
  97. package/libx/rest/middleware/input.js +5 -5
  98. package/libx/rest/middleware/operation.js +96 -41
  99. package/libx/rest/middleware/parse.js +4 -6
  100. package/libx/rest/middleware/payload.js +5 -5
  101. package/libx/rest/middleware/read.js +11 -17
  102. package/libx/rest/middleware/update.js +20 -25
  103. package/package.json +2 -1
  104. package/server.js +7 -4
  105. package/srv/outbox.cds +9 -10
  106. package/apis/env.d.ts +0 -25
  107. package/apis/test.d.ts +0 -81
  108. package/apis/utils.d.ts +0 -15
  109. package/lib/auth/passport-basic.js +0 -14
  110. package/lib/auth/passport-digest.js +0 -16
  111. package/lib/env/presets.js +0 -35
  112. package/lib/log/format/cf.js +0 -16
  113. package/lib/log/format/kibana.js +0 -92
  114. package/lib/srv/middlewares/ctx-auth.js +0 -11
  115. package/libx/_runtime/cds-services/adapter/rest/utils/validation-checks.js +0 -119
@@ -10,9 +10,9 @@ let passport, logged
10
10
  // strategy initializers for lazy loading of dependencies
11
11
  const _initializers = {
12
12
  // REVISIT: support basic authentication?
13
- basic: ({ credentials }) => {
13
+ basic: ({ credentials, users }) => {
14
14
  const BasicStrategy = require('./strategies/basic')
15
- passport.use(new BasicStrategy(credentials))
15
+ passport.use(new BasicStrategy(credentials || users))
16
16
  },
17
17
  dummy: () => {
18
18
  const DummyStrategy = require('./strategies/dummy')
@@ -165,7 +165,7 @@ module.exports = (srv, options = srv.options) => {
165
165
  if (config.impl) {
166
166
  // mount custom authentication middleware
167
167
  _mountCustomAuth(srv, app, config)
168
- } else if (config.kind === 'ias-auth') {
168
+ } else if (config.kind === 'ias' || config.kind === 'ias-auth') {
169
169
  // ias-auth follows the new implementation pattern for auth middlewares
170
170
  const iasAuth = require('./strategies/ias-auth')(config)
171
171
  if (iasAuth) app.use(iasAuth)
@@ -186,8 +186,7 @@ module.exports = (srv, options = srv.options) => {
186
186
  }
187
187
 
188
188
  const _strategy4 = config => {
189
- const strategy = config.strategy ? config.strategy.toLowerCase() : config.kind.replace('-auth', '')
190
- if (strategy === 'mocked') return 'mock'
189
+ const strategy = config.kind.replace('mocked', 'mock').replace(/-auth$/, '')
191
190
  if (strategy in _initializers) return strategy
192
191
  process.exitCode = 1 // REVISIT: why exitCode needed?
193
192
  throw new Error(`Authentication kind "${config.kind}" is not supported`)
@@ -8,8 +8,8 @@ const { BasicStrategy: BS } = _require('passport-http')
8
8
  class BasicStrategy extends BS {
9
9
  constructor(credentials) {
10
10
  super(function (user, password, done) {
11
- if (credentials[user] === password) {
12
- done(null, new cds.User({ id: user }))
11
+ if ((credentials[user]?.password || credentials[user]) === password) {
12
+ done(null, new cds.User({ id: user, roles: credentials[user].roles || [] }))
13
13
  } else {
14
14
  this.fail()
15
15
  }
@@ -29,6 +29,7 @@ const _action = require('./handlers/action')
29
29
  const { normalizeError, isClientError } = require('../../../common/error/frontend')
30
30
  const { getErrorMessage } = require('../../../common/error/utils')
31
31
 
32
+ // eslint-disable-next-line complexity
32
33
  function _log(level, arg) {
33
34
  const { params } = arg
34
35
 
@@ -46,34 +47,43 @@ function _log(level, arg) {
46
47
  if (!obj.level) obj.level = arg.level
47
48
  if (!obj.timestamp) obj.timestamp = arg.timestamp
48
49
 
49
- if (level === 'error') {
50
- // reduce 4xx to warning
51
- if (isClientError(obj)) {
52
- if (!LOG._warn) {
53
- return
54
- }
55
- level = 'warn'
56
- }
57
- }
58
-
59
50
  // replace messages in toLog with developer texts (i.e., undefined locale) (cf. req.reject() etc.)
60
51
  const _message = obj.message
52
+ const _code = obj.code
61
53
  const _details = obj.details
62
54
 
55
+ // REVISIT: the (ugly!) stuff re code is somewhat of a duplicate to what we do in normalizeError -> refactor with new adapters!
63
56
  obj.message = getErrorMessage(obj)
64
- if (obj.details) {
57
+ if (_code) obj.code = obj.code.match?.(/^ASSERT_/) ? 400 : obj.code
58
+ if (_details) {
65
59
  const details = []
60
+ const codes = new Set()
66
61
  for (const d of obj.details) {
67
- details.push(Object.assign({}, d, { message: getErrorMessage(d) }))
62
+ const entry = Object.assign({}, d, { message: getErrorMessage(d) })
63
+ if (!_code) {
64
+ const k = d.statusCode ? 'statusCode' : d.status ? 'status' : d.code ? 'code' : undefined
65
+ const v = d[k]?.match?.(/^ASSERT_/) ? 400 : d[k]
66
+ codes.add(v)
67
+ entry[k] = v
68
+ }
69
+ details.push(entry)
68
70
  }
69
71
  obj.details = details
72
+ if (!_code && codes.size === 1) {
73
+ const n = Number(codes.values().next().value)
74
+ if (!isNaN(n)) obj.code = n
75
+ }
70
76
  }
71
77
 
78
+ // reduce 4xx to warning
79
+ if (level === 'error') if (isClientError(obj)) level = 'warn'
80
+
72
81
  // log it
73
- LOG[level](obj)
82
+ LOG[`_${level}`] && LOG[level](obj)
74
83
 
75
84
  // restore
76
85
  obj.message = _message
86
+ if (_code) obj.code = _code
77
87
  if (_details) obj.details = _details
78
88
  }
79
89
 
@@ -199,16 +199,11 @@ class ODataRequest extends cds.Request {
199
199
  const that = this
200
200
  Object.defineProperty(this._, 'shared', {
201
201
  get() {
202
- if (!cds._deprecationWarningForShared) {
203
- LOG._warn && LOG.warn('req._.shared is deprecated and will be removed.')
204
- cds._deprecationWarningForShared = true
205
- }
206
-
207
- if (that.context) {
208
- that._shared = that.context._shared = that.context._shared || { req, res }
209
- } else {
210
- that._shared = that._shared || { req, res }
211
- }
202
+ cds._logDeprecation('req._.shared is deprecated and will be removed.')
203
+
204
+ if (that.context) that._shared = that.context._shared = that.context._shared || { req, res }
205
+ else that._shared = that._shared || { req, res }
206
+
212
207
  return that._shared
213
208
  }
214
209
  })
@@ -217,11 +212,7 @@ class ODataRequest extends cds.Request {
217
212
  const attr = { identityZone: this.tenant }
218
213
  Object.defineProperty(this, 'attr', {
219
214
  get() {
220
- if (!cds._deprecationWarningForAttr) {
221
- LOG._warn && LOG.warn('req.attr is deprecated and will be removed.')
222
- cds._deprecationWarningForAttr = true
223
- }
224
-
215
+ cds._logDeprecation('req.attr is deprecated and will be removed.')
225
216
  return attr
226
217
  }
227
218
  })
@@ -14,8 +14,15 @@ const { hasOmitValuesPreference } = require('../utils/omitValues')
14
14
 
15
15
  const { getSapMessages } = require('../../../../common/error/frontend')
16
16
 
17
- const _isUpsertAllowed = target => {
18
- return !(cds.env.runtime && cds.env.runtime.allow_upsert === false) && !(target && target._isDraftEnabled)
17
+ const _isUpsertAllowed = req => {
18
+ return (
19
+ !(cds.env.runtime && cds.env.runtime.allow_upsert === false) &&
20
+ !(
21
+ req.target &&
22
+ req.target._isDraftEnabled &&
23
+ (!cds.env.fiori.lean_draft || (!req.data.IsActiveEntity && req.event === 'PATCH'))
24
+ )
25
+ )
19
26
  }
20
27
 
21
28
  const _infoForeignKeyInParent = (req, tx) => {
@@ -94,7 +101,7 @@ const _updateThenCreate = async (req, odataReq, odataRes, tx) => {
94
101
  result = await tx.dispatch(req)
95
102
  } catch (e) {
96
103
  const is404 = e.code === 404 || e.status === 404 || e.statusCode === 404
97
- if (is404 && _isUpsertAllowed(req.target)) {
104
+ if (is404 && _isUpsertAllowed(req)) {
98
105
  // PUT/ PATCH with if-match header means "only if already exists", i.e., no insert if not
99
106
  if (req.headers['if-match'])
100
107
  throw Object.assign(new Error('412'), { statusCode: 412 })
@@ -1,7 +1,8 @@
1
- const { alias2ref } = require('../../../common/utils/csn') // REVISIT: eliminate that
2
1
  const cds = require('../../../cds')
3
2
  const OData = require('./OData')
4
3
 
4
+ const { alias2ref } = require('../../../common/utils/csn')
5
+
5
6
  /**
6
7
  * This is the express handler for a specific OData endpoint.
7
8
  * Note: the same service can be served at different endpoints.
@@ -13,7 +14,9 @@ module.exports = srv => {
13
14
 
14
15
  function OkraAdapter(srv, model = srv.model) {
15
16
  const edm = cds.compile.to.edm(model, { service: srv.definition?.name || srv.name })
16
- alias2ref(srv, edm) // REVISIT: eliminate that -> done again and again -> search for _alias2ref
17
+
18
+ alias2ref(srv, edm)
19
+
17
20
  return new OData(edm, model, srv.options).addCDSServiceToChannel(srv)
18
21
  }
19
22
 
@@ -123,7 +123,8 @@ const _stringifyColumnsFromData = columns =>
123
123
  .map(key => `${key}(${_stringifyColumnsFromData(columns[key])})`)
124
124
  .join(',')
125
125
 
126
- const _listColumns = ({ columns, data, isUpsert, returnType, event, /* express */ _req, service }) => {
126
+ const _listColumns = ({ columns, data, isUpsert, returnType, event, /* express */ _req, service, propertyName }) => {
127
+ if (columns.length === 1 && propertyName) return `/${propertyName}`
127
128
  // query options ($select, $expand, etc) as strings
128
129
  const queryOptions = _req.query
129
130
  let columnsStr
@@ -114,15 +114,9 @@ const _getSearchableColumns = entity => {
114
114
  const defaultSearchFilteredColumns = searchableColumns.filter(column => column[defaultSearchElementTerm])
115
115
 
116
116
  if (defaultSearchFilteredColumns.length > 0) {
117
- if (!cds._deprecationWarningForDefaultSearchElement) {
118
- LOG._warn &&
119
- LOG.warn(
120
- 'Annotation "@Search.defaultSearchElement" is deprecated and will be removed in an upcoming release. ' +
121
- 'Use "@cds.search" instead.'
122
- )
123
- cds._deprecationWarningForDefaultSearchElement = true
124
- }
125
-
117
+ cds._logDeprecation(
118
+ 'Annotation "@Search.defaultSearchElement" is deprecated and will be removed. Use "@cds.search" instead.'
119
+ )
126
120
  return defaultSearchFilteredColumns.map(column => column.name)
127
121
  }
128
122
 
@@ -9,3 +9,16 @@ const { any, entity, Association } = cds.builtin.classes
9
9
  cds.extend(any).with(require('./common/aspects/any'))
10
10
  cds.extend(Association).with(require('./common/aspects/Association'))
11
11
  cds.extend(entity).with(require('./common/aspects/entity'))
12
+
13
+ /**
14
+ * Logs a deprecation warning once per process
15
+ *
16
+ * @param {string} msg the deprecation warning to be logged
17
+ */
18
+ cds._logDeprecation = function (msg) {
19
+ if (!cds._deprecations.has(msg)) {
20
+ cds._deprecations.add(msg)
21
+ cds.log().warn(msg)
22
+ }
23
+ }
24
+ Object.defineProperty(cds, '_deprecations', { value: new Set() })
@@ -118,6 +118,9 @@ const _parentKey = (element, key) => {
118
118
  const _findWhere = (data, where) => {
119
119
  return data.filter(entry => {
120
120
  return Object.keys(where).every(key => {
121
+ if (Buffer.isBuffer(entry[key]) && Buffer.isBuffer(where[key])) {
122
+ return Buffer.compare(entry[key], where[key]) === 0
123
+ }
121
124
  return where[key] === entry[key]
122
125
  })
123
126
  })
@@ -137,7 +137,7 @@ function _getStaticWhere(allBackLinks, entity1) {
137
137
 
138
138
  const _addToCQNs = (cqns, subCQN, element, model, level) => {
139
139
  // REVISIT:
140
- // The compiler generates foreign-key constraints (if features.assert_integrity_type = 'DB')
140
+ // The compiler generates foreign-key constraints (if features.assert_integrity === 'db')
141
141
  // and enables DELETE CASCADE. For these cases, the runtime doesn't need to delete compositions
142
142
  // manually, it's done by the database itself.
143
143
  // However, there are cases (unmanaged compositions), where this doesn't happen.
@@ -38,7 +38,7 @@ const _getFiltered = err => {
38
38
  return error
39
39
  }
40
40
 
41
- const _rewriteDBError = error => {
41
+ const _rewriteError = error => {
42
42
  let { code, message } = error
43
43
  code = String(code || 'null')
44
44
 
@@ -66,7 +66,7 @@ const _rewriteDBError = error => {
66
66
 
67
67
  const _normalize = (err, locale, formatterFn = _getFiltered) => {
68
68
  // REVISIT: code and message rewriting
69
- _rewriteDBError(err)
69
+ _rewriteError(err)
70
70
 
71
71
  // message (i18n)
72
72
  err.message = getErrorMessage(err, locale)
@@ -16,7 +16,7 @@ function handler(req) {
16
16
 
17
17
  // @read-only
18
18
  let entity = getAuthRelevantEntity(req, this.model, ['@readonly'])
19
- if (cds.env.fiori.lean_draft && (req.event === 'NEW' || req.event === 'UPDATE')) entity = entity?.actives
19
+ if (cds.env.fiori.lean_draft) entity = entity?.actives || entity
20
20
 
21
21
  if (!entity || !entity['@readonly']) return
22
22
  if (entity['@readonly'] && req.event in WRITE_EVENTS) req.reject(405, 'ENTITY_IS_READ_ONLY', [entity.name])
@@ -69,7 +69,7 @@ const _addNormalizedRestrictPerGrant = (grant, where, restrict, restricts, defin
69
69
 
70
70
  const _addNormalizedRestrict = (restrict, restricts, definition) => {
71
71
  const where = restrict.where
72
- ? restrict.where.replace(/\$user/g, '$user.id').replace(/\$user\.id\./g, '$user.')
72
+ ? (restrict.where['='] || restrict.where).replace(/\$user/g, '$user.id').replace(/\$user\.id\./g, '$user.')
73
73
  : undefined
74
74
 
75
75
  restrict.grant = Array.isArray(restrict.grant) ? restrict.grant : [restrict.grant || '*']
@@ -1,5 +1,4 @@
1
1
  const cds = require('../../cds')
2
- const LOG = cds.log('app')
3
2
  const { getAllKeys } = require('../../cds-services/adapter/odata-v4/odata-to-cqn/utils')
4
3
  const { DRAFT_COLUMNS_MAP } = require('../../common/constants/draft')
5
4
 
@@ -7,10 +6,10 @@ const _getStaticOrders = req => {
7
6
  const { target: entity, query } = req
8
7
  const defaultOrders = entity['@cds.default.order'] || entity['@odata.default.order'] || []
9
8
 
10
- if (!cds._deprecationWarningForDefaultSort && defaultOrders.length > 0) {
11
- LOG._warn &&
12
- LOG.warn('Annotations "@cds.default.order" and "@odata.default.order" are deprecated and will be removed.')
13
- cds._deprecationWarningForDefaultSort = true
9
+ if (defaultOrders.length > 0) {
10
+ cds._logDeprecation(
11
+ 'Annotations "@cds.default.order" and "@odata.default.order" are deprecated and will be removed.'
12
+ )
14
13
  }
15
14
 
16
15
  const ordersFromKeys = []
@@ -1,4 +1,5 @@
1
1
  const cds = require('../../cds')
2
+
2
3
  const resolveStructured = require('../../common/utils/resolveStructured')
3
4
 
4
5
  const getEtagElement = entity => {
@@ -91,7 +92,8 @@ const _initializeCache = (model, namespace) => {
91
92
  }
92
93
 
93
94
  const findCsnTargetFor = (edmName, model, namespace) => {
94
- const cache = model._edmToCSNNameMap || (model._edmToCSNNameMap = {})
95
+ const cache =
96
+ model._edmToCSNNameMap || Object.defineProperty(model, '_edmToCSNNameMap', { value: {} })._edmToCSNNameMap
95
97
  const edm2csnMap = cache[namespace] || (cache[namespace] = _initializeCache(model, namespace))
96
98
 
97
99
  if (edm2csnMap[edmName]) return edm2csnMap[edmName]
@@ -133,12 +135,6 @@ const _setAlias2ref = entity => {
133
135
  return entity
134
136
  }
135
137
 
136
- function _alias2RefRest(service) {
137
- for (const each of service.entities) {
138
- _setAlias2ref(each)
139
- }
140
- }
141
-
142
138
  const prefixForStruct = element => {
143
139
  const prefixes = []
144
140
  let parent = element.parent
@@ -149,19 +145,28 @@ const prefixForStruct = element => {
149
145
  return prefixes.length ? prefixes.reverse().join('_') + '_' : ''
150
146
  }
151
147
 
148
+ /*
149
+ * REVISIT:
150
+ * - which scenarios require this?
151
+ * - does it still work when serving multiple protocols?
152
+ * - can we cache it?
153
+ */
152
154
  function alias2ref(service, edm) {
153
155
  if (!edm) {
154
- return _alias2RefRest(service)
155
- }
156
- const defs = edm[service.definition.name]
157
- for (const each of service.entities) {
158
- const def = defs[each.name.replace(service.definition.name + '.', '').replace(/\./g, '_')]
159
- if (!def || !def.$Key || def.$Key.every(ele => typeof ele === 'string')) continue
160
- each._alias2ref = Object.defineProperty({}, '__2alias', { value: {}, configurable: true })
161
- for (const mapping of def.$Key.filter(ele => typeof ele !== 'string')) {
162
- for (const [key, value] of Object.entries(mapping)) {
163
- each._alias2ref[key] = value.split('/')
164
- each._alias2ref.__2alias[value] = key
156
+ // REST
157
+ for (const each of service.entities) _setAlias2ref(each)
158
+ } else {
159
+ // OData
160
+ const defs = edm[service.definition.name]
161
+ for (const each of service.entities) {
162
+ const def = defs[each.name.replace(service.definition.name + '.', '').replace(/\./g, '_')]
163
+ if (!def || !def.$Key || def.$Key.every(ele => typeof ele === 'string')) continue
164
+ each._alias2ref = Object.defineProperty({}, '__2alias', { value: {}, configurable: true })
165
+ for (const mapping of def.$Key.filter(ele => typeof ele !== 'string')) {
166
+ for (const [key, value] of Object.entries(mapping)) {
167
+ each._alias2ref[key] = value.split('/')
168
+ each._alias2ref.__2alias[value] = key
169
+ }
165
170
  }
166
171
  }
167
172
  }
@@ -1,5 +1,6 @@
1
1
  const cds = require('../../cds')
2
2
 
3
+ // REVISIT: This is a perfect example of the expensive-check-before-doing anti pattern
3
4
  const containsAnyRestrictions = srv => {
4
5
  const accessRestrictions = getAccessRestrictions(srv)
5
6
  if (accessRestrictions.length > 1 || accessRestrictions[0] !== 'any') return true
@@ -22,23 +23,13 @@ const containsAnyRestrictions = srv => {
22
23
  const getAccessRestrictions = srv => {
23
24
  let restrictions = srv.definition['@restrict'] || srv.definition['@requires']
24
25
  if (restrictions) {
25
- if (typeof restrictions === 'string') restrictions = [restrictions]
26
- else
27
- restrictions = restrictions
28
- .map(r => (typeof r === 'string' ? r : r.to))
29
- .reduce((acc, cur) => {
30
- Array.isArray(cur) ? acc.push(...cur) : acc.push(cur)
31
- return acc
32
- }, [])
26
+ if (typeof restrictions === 'string') return [restrictions]
27
+ return restrictions.map(r => r.to || r).flat()
33
28
  } else {
34
- const { restrict_all_services } = cds.env.requires.auth
35
- const in_prod = process.env.NODE_ENV === 'production'
36
- // REVISIT: cleanup during streamlined auth
37
- const is_mocked_auth = cds.env.requires.auth._kind === 'mocked'
38
- if (restrict_all_services === false || !in_prod || is_mocked_auth) restrictions = ['any']
39
- else restrictions = ['authenticated-user']
29
+ if (cds.env.requires.auth.restrict_all_services === false) return ['any']
30
+ if (process.env.NODE_ENV !== 'production') return ['any']
31
+ return ['authenticated-user']
40
32
  }
41
- return restrictions
42
33
  }
43
34
 
44
35
  module.exports = {
@@ -31,7 +31,8 @@ const _processComplexCategory = ({ row, key, val, category, req, element }) => {
31
31
 
32
32
  // propagate keys
33
33
  if (category === 'propagateForeignKeys') {
34
- propagateForeignKeys(key, row, element._foreignKeys, element.isComposition, { deleteAssocs: true })
34
+ if (req.event !== 'UPSERT')
35
+ propagateForeignKeys(key, row, element._foreignKeys, element.isComposition, { deleteAssocs: true })
35
36
  return
36
37
  }
37
38
 
@@ -175,7 +176,7 @@ const _pickDraft = element => {
175
176
  return { categories } // > no need to continue
176
177
  }
177
178
 
178
- if (element.default && !DRAFT_COLUMNS_MAP[element.name]) {
179
+ if (element.default && !DRAFT_COLUMNS_MAP[element.name] && !element.isAssociation) {
179
180
  categories.push({ category: 'default', args: element })
180
181
  }
181
182
 
@@ -73,7 +73,7 @@ const _shouldReadOverDraft = (req, definitions) => {
73
73
 
74
74
  // for $expand requests, read over the draft if the navigation target is non-draft-enabled
75
75
  // REVISIT: this is an interim workaround to be removed
76
- if (!req.target._isDraftEnabled && SELECT.columns && SELECT.columns.some(column => column.expand)) return true
76
+ if (!req.target._isDraftEnabled && SELECT.columns?.some(column => column.expand)) return true
77
77
 
78
78
  // read over the draft only for navigation scenarios
79
79
  if (fromRef.length === 1) return false
@@ -90,10 +90,7 @@ const _shouldReadOverDraft = (req, definitions) => {
90
90
  if (isActiveEntityRequested(firstFromRef.where)) return false
91
91
 
92
92
  const pathSegments = fromRef.map(path => (typeof path === 'string' ? path : path.id))
93
- const excludeAssoc = assoc => {
94
- if (assoc.name === 'DraftAdministrativeData' || assoc.name === 'SiblingEntity') return true
95
- return false
96
- }
93
+ const excludeAssoc = assoc => assoc.name === 'DraftAdministrativeData' || assoc.name === 'SiblingEntity'
97
94
 
98
95
  // Read over the draft only if:
99
96
  // - the navigation target is an association and
@@ -203,7 +203,43 @@ cds.ApplicationService.prototype.handle = async function (req) {
203
203
  }
204
204
 
205
205
  if (req.event === 'NEW' || req.event === 'CANCEL' || req.event === 'draftPrepare') {
206
- if (draftParams.IsActiveEntity) req.reject(501)
206
+ if (draftParams.IsActiveEntity && !cds.env.fiori.bypass_draft) req.reject(501)
207
+ if (req.data.IsActiveEntity === true && cds.env.fiori.bypass_draft) {
208
+ const containsDraftRoot =
209
+ this.model.entities[query.INSERT.into?.ref?.[0]?.id || query.INSERT.into?.ref?.[0] || query.INSERT.into][
210
+ '@Common.DraftRoot.ActivationAction'
211
+ ]
212
+
213
+ if (!containsDraftRoot) req.reject(403, 'DRAFT_MODIFICATION_ONLY_VIA_ROOT')
214
+
215
+ const isDirectAccess = typeof req.query.INSERT.into === 'string' || req.query.INSERT.into.ref?.length === 1
216
+ const data = Object.assign({}, req.data) // IsActiveEntity is not enumerable
217
+ const draftsRootRef =
218
+ typeof query.INSERT.into === 'string'
219
+ ? [req.target.drafts.name]
220
+ : _redirectRefToDrafts([query.INSERT.into.ref[0]], this.model)
221
+ let rootHasDraft
222
+
223
+ // children: check root entity has no draft
224
+ if (!isDirectAccess) {
225
+ rootHasDraft = await SELECT.one([1]).from({ ref: draftsRootRef })
226
+ }
227
+
228
+ // direct access and req.data contains keys: check if root has no draft with that keys
229
+ if (isDirectAccess && entity_keys(query._target).every(k => k in data)) {
230
+ const keyData = entity_keys(query._target).reduce((res, k) => {
231
+ res[k] = req.data[k]
232
+ return res
233
+ }, {})
234
+ rootHasDraft = await SELECT.one([1]).from({ ref: draftsRootRef }).where(keyData)
235
+ }
236
+
237
+ if (rootHasDraft) req.reject(409, 'DRAFT_ALREADY_EXISTS')
238
+
239
+ const cqn = INSERT.into(query.INSERT.into).entries(data)
240
+ await run(cqn, { event: 'CREATE' })
241
+ return { ...data, IsActiveEntity: true }
242
+ }
207
243
  req.target = req.target.drafts
208
244
 
209
245
  if (query.INSERT?.into) {
@@ -244,13 +280,16 @@ cds.ApplicationService.prototype.handle = async function (req) {
244
280
 
245
281
  if (req.event === 'draftActivate') {
246
282
  LOG.debug('activate draft')
283
+
247
284
  // It would be great if we'd have a SELECT ** to deeply expand the entity (along compositions), that should
248
285
  // be implemented in expand implementation.
249
286
  if (req.query.SELECT.from.ref.length > 1 || draftParams.IsActiveEntity !== false) {
250
287
  req.reject(400, 'Action "draftActivate" can only be called on the root draft entity')
251
288
  }
289
+
252
290
  const targetDraft = req.target.drafts
253
291
  const cols = expandStarStar(targetDraft)
292
+
254
293
  // Use `run` (since also etags might need to be checked)
255
294
  // REVISIT: Find a better approach (`etag` as part of CQN?)
256
295
  const draftRef = _redirectRefToDrafts(query.SELECT.from.ref, this.model)
@@ -265,13 +304,15 @@ cds.ApplicationService.prototype.handle = async function (req) {
265
304
  ])
266
305
  )
267
306
  if (!res) req.reject(_etagValidationType ? 412 : { code: 'DRAFT_NOT_EXISTING', status: 404 })
268
- if (res.DraftAdministrativeData?.InProcessByUser !== cds.context.user.id)
307
+ if (res.DraftAdministrativeData?.InProcessByUser !== cds.context.user.id) {
269
308
  req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER', [res.DraftAdministrativeData?.InProcessByUser])
309
+ }
270
310
  const DraftAdministrativeData_DraftUUID = res.DraftAdministrativeData_DraftUUID
271
311
  delete res.DraftAdministrativeData_DraftUUID
272
312
  delete res.DraftAdministrativeData
273
313
  const HasActiveEntity = res.HasActiveEntity
274
314
  delete res.HasActiveEntity
315
+
275
316
  // First run the handlers as they might need access to DraftAdministrativeData or the draft entities
276
317
  const result = await run(
277
318
  HasActiveEntity
@@ -299,6 +340,7 @@ cds.ApplicationService.prototype.handle = async function (req) {
299
340
  columns.push({ ref: ['DraftAdministrativeData'], expand: [{ ref: ['InProcessByUser'] }] })
300
341
  rootQuery.SELECT.columns = columns
301
342
  rootQuery.SELECT.one = true
343
+ rootQuery.SELECT.from = { ref: [query.SELECT.from.ref[0]] }
302
344
  const root = await cds.run(rootQuery)
303
345
  if (!root) req.reject(404)
304
346
  if (root.DraftAdministrativeData?.InProcessByUser !== cds.context.user.id) req.reject(403)
@@ -308,7 +350,8 @@ cds.ApplicationService.prototype.handle = async function (req) {
308
350
  }
309
351
 
310
352
  if (req.event === 'PATCH') {
311
- if (draftParams.IsActiveEntity || !('IsActiveEntity' in draftParams)) req.reject(501)
353
+ if (!('IsActiveEntity' in draftParams)) req.reject(501)
354
+
312
355
  if (draftParams.IsActiveEntity === false) {
313
356
  LOG.debug('patch draft')
314
357
  if (req.target?.name.endsWith('DraftAdministrativeData')) req.reject(405)
@@ -334,6 +377,28 @@ cds.ApplicationService.prototype.handle = async function (req) {
334
377
  req.data.IsActiveEntity = false
335
378
  return req.data
336
379
  }
380
+
381
+ LOG.debug('patch active')
382
+
383
+ if (!cds.env.fiori.bypass_draft) {
384
+ const msg =
385
+ !cds.profiles?.includes('production') &&
386
+ '`cds.env.fiori.bypass_draft` must be enabled to support updating active instances.'
387
+ return req.reject(403, msg)
388
+ }
389
+
390
+ const entityRef = query.UPDATE.entity.ref
391
+
392
+ if (!this.model.entities[entityRef[0].id]['@Common.DraftRoot.ActivationAction']) {
393
+ req.reject(403, 'DRAFT_MODIFICATION_ONLY_VIA_ROOT')
394
+ }
395
+
396
+ const draftsRef = _redirectRefToDrafts(entityRef, this.model)
397
+ const hasDraft = !!(await SELECT.one([1]).from({ ref: [draftsRef[0]] }))
398
+ if (hasDraft) req.reject(409, 'DRAFT_ALREADY_EXISTS')
399
+
400
+ await run(query)
401
+ return req.data
337
402
  }
338
403
 
339
404
  if (req.event === 'STREAM' && draftParams.IsActiveEntity === false) {
@@ -345,7 +410,6 @@ cds.ApplicationService.prototype.handle = async function (req) {
345
410
  }
346
411
 
347
412
  req.query = query
348
-
349
413
  return handle(req)
350
414
  }
351
415
 
@@ -1132,7 +1196,7 @@ async function onCancel(req) {
1132
1196
  if (processByUser && processByUser !== cds.context.user.id)
1133
1197
  req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER', [processByUser])
1134
1198
  }
1135
- const queries = !draft ? [] : [this.run(DELETE.from({ ref: req.query.DELETE.from.ref }))]
1199
+ const queries = !draft ? [] : [this.run(DELETE.from({ ref: req.query.DELETE.from.ref }), req.data)]
1136
1200
  if (draft && req.target['@Common.DraftRoot.ActivationAction'])
1137
1201
  // only for draft root
1138
1202
  queries.push(
@@ -94,7 +94,7 @@ class HanaDatabase extends DatabaseService {
94
94
  }
95
95
 
96
96
  _registerBeforeHandlers() {
97
- this.before(['CREATE', 'UPDATE'], '*', this._input) // > has to run before rewrite
97
+ this.before(['CREATE', 'UPDATE', 'UPSERT'], '*', this._input) // > has to run before rewrite
98
98
  this.before('READ', '*', search) // > has to run before rewrite
99
99
  this.before(['CREATE', 'READ', 'UPDATE', 'DELETE', 'UPSERT'], '*', this._rewrite)
100
100
 
@@ -39,7 +39,7 @@ class AMQPWebhookMessaging extends MessagingService {
39
39
  }
40
40
 
41
41
  startListening(opt = {}) {
42
- if (!this._listenToAll && !this.subscribedTopics.size) return
42
+ if (!this._listenToAll.value && !this.subscribedTopics.size) return
43
43
  if (!opt.doNotDeploy) {
44
44
  const management = this.getManagement()
45
45
  this.queued(management.createQueueAndSubscriptions.bind(management))()
@@ -1,15 +1,10 @@
1
1
  const cds = require('../cds')
2
- const LOG = cds.log()
3
-
4
- let logged
5
2
 
6
3
  module.exports = class Outbox extends cds.Service {
7
4
  constructor(...args) {
8
- if (!logged && LOG._warn) {
9
- // prettier-ignore
10
- LOG.warn('Internal class `OutboxService` is deprecated and will be removed. Services are outboxable via config or `cds.outboxed()`.')
11
- logged = true
12
- }
5
+ cds._logDeprecation(
6
+ 'Internal class `OutboxService` is deprecated and will be removed. Services are outboxable via config or `cds.outboxed()`.'
7
+ )
13
8
 
14
9
  super(...args)
15
10