@sap/cds 9.4.4 → 9.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 (56) hide show
  1. package/CHANGELOG.md +81 -1
  2. package/_i18n/messages_en_US_saptrc.properties +1 -1
  3. package/common.cds +5 -2
  4. package/lib/compile/cds-compile.js +1 -0
  5. package/lib/compile/for/assert.js +64 -0
  6. package/lib/compile/for/flows.js +194 -58
  7. package/lib/compile/for/lean_drafts.js +75 -7
  8. package/lib/compile/parse.js +1 -1
  9. package/lib/compile/to/csn.js +6 -2
  10. package/lib/compile/to/edm.js +1 -1
  11. package/lib/compile/to/yaml.js +8 -1
  12. package/lib/dbs/cds-deploy.js +2 -2
  13. package/lib/env/cds-env.js +14 -4
  14. package/lib/env/defaults.js +6 -1
  15. package/lib/i18n/localize.js +1 -1
  16. package/lib/index.js +7 -7
  17. package/lib/req/event.js +4 -0
  18. package/lib/req/validate.js +4 -1
  19. package/lib/srv/cds.Service.js +2 -1
  20. package/lib/srv/middlewares/auth/ias-auth.js +5 -7
  21. package/lib/srv/middlewares/auth/index.js +1 -1
  22. package/lib/srv/protocols/index.js +7 -6
  23. package/lib/srv/srv-handlers.js +7 -0
  24. package/libx/_runtime/common/Service.js +5 -1
  25. package/libx/_runtime/common/constants/events.js +1 -0
  26. package/libx/_runtime/common/generic/assert.js +220 -0
  27. package/libx/_runtime/common/generic/flows.js +168 -108
  28. package/libx/_runtime/common/generic/input.js +6 -4
  29. package/libx/_runtime/common/utils/cqn.js +0 -24
  30. package/libx/_runtime/common/utils/normalizeTimestamp.js +2 -2
  31. package/libx/_runtime/common/utils/resolveView.js +8 -2
  32. package/libx/_runtime/common/utils/templateProcessor.js +10 -1
  33. package/libx/_runtime/common/utils/templateProcessorPathSerializer.js +21 -9
  34. package/libx/_runtime/fiori/lean-draft.js +511 -379
  35. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +39 -35
  36. package/libx/_runtime/messaging/enterprise-messaging.js +2 -2
  37. package/libx/_runtime/remote/Service.js +4 -5
  38. package/libx/_runtime/ucl/Service.js +111 -15
  39. package/libx/common/utils/streaming.js +1 -1
  40. package/libx/odata/middleware/batch.js +8 -6
  41. package/libx/odata/middleware/create.js +2 -2
  42. package/libx/odata/middleware/delete.js +2 -2
  43. package/libx/odata/middleware/metadata.js +18 -11
  44. package/libx/odata/middleware/read.js +2 -2
  45. package/libx/odata/middleware/service-document.js +1 -1
  46. package/libx/odata/middleware/update.js +1 -1
  47. package/libx/odata/parse/afterburner.js +46 -36
  48. package/libx/odata/parse/cqn2odata.js +2 -6
  49. package/libx/odata/parse/grammar.peggy +91 -13
  50. package/libx/odata/parse/parser.js +1 -1
  51. package/libx/odata/utils/index.js +2 -2
  52. package/libx/odata/utils/readAfterWrite.js +2 -0
  53. package/libx/queue/TaskRunner.js +26 -1
  54. package/libx/queue/index.js +11 -1
  55. package/package.json +1 -1
  56. package/srv/ucl-service.cds +2 -0
@@ -19,7 +19,6 @@ function _isCompositionBacklink(e) {
19
19
  }
20
20
  }
21
21
 
22
-
23
22
  // NOTE: Keep outside of the function to avoid calling the parser repeatedly
24
23
  const { Draft } = cds.linked(`
25
24
  entity ActiveEntity { key ID: UUID; }
@@ -42,9 +41,20 @@ const { Draft } = cds.linked(`
42
41
  `).definitions
43
42
 
44
43
  function DraftEntity4(active, name = active.name + '.drafts') {
44
+ // skip compositions with @odata.draft.enabled: false
45
+ const active_elements = {}
46
+ for (const each in active.elements) {
47
+ const element = active.elements[each]
48
+ if (element.isComposition && element['@odata.draft.enabled'] === false) {
49
+ // exclude, i.e., do nothing
50
+ } else {
51
+ active_elements[each] = element
52
+ }
53
+ }
54
+
45
55
  const draft = Object.create(active, {
46
56
  name: { value: name }, // REVISIT: lots of things break if we do that!
47
- elements: { value: { ...active.elements, ...Draft.elements }, enumerable: true },
57
+ elements: { value: { ...active_elements, ...Draft.elements }, enumerable: true },
48
58
  actives: { value: active },
49
59
  query: { value: undefined }, // to not inherit that from active
50
60
  // drafts: { value: undefined }, // to not inherit that from active -> doesn't work yet as the coding in lean-draft.js uses .drafts to identify both active and draft entities
@@ -58,6 +68,51 @@ function DraftEntity4(active, name = active.name + '.drafts') {
58
68
  return draft
59
69
  }
60
70
 
71
+ function addNewActionAnnotation(def) {
72
+ // Skip if a new action was defined manually
73
+ if (def.own('@Common.DraftRoot.NewAction')) return
74
+
75
+ // Skip for non draft roots
76
+ if (!def.own('@Common.DraftRoot.ActivationAction')) return
77
+
78
+ // TODO: This is perhaps THE ugliest way to automatically add a 'draftNew' action:
79
+ // TODO: > Instead, this should happen in cds-compiler/lib/transfrom/draft/odata.js
80
+ // TODO: > Within generateDrafts -> generateDraftForOData
81
+ // TODO: > Unfortunately, the 'createAction' utility does not currently allow creating collection bound actions
82
+
83
+ def['@Common.DraftRoot.NewAction'] = `${def._service.name}.draftNew`
84
+
85
+ // TODO: Find a better way than this:
86
+ // TODO: > By rewriting `draftNew` into a `NEW` req in draftHandle, action input validation is skipped
87
+ // TODO: > This causes issues if the action has parameters derived from key fields that should be mandatory
88
+ // TODO: > This will bubble up a NOT NULL CONSTRAINT error instead of raising a proper client error
89
+ // TODO: > This behavior also occurs for regular custom actions
90
+
91
+ // Format a list of cds action parameters, based on the entities key fields
92
+ // > E.g.: [ 'dayKey: Integer', 'nameKey: String', ...]
93
+ // > UUID keys are skipped as they are generated
94
+ const idParameters = Object.values(def.keys)
95
+ .filter(el => el.key && !el.virtual && el._type !== 'cds.UUID') // TODO: Ignore @UI.Hidden keys?
96
+ .map(el => `${el.name}: ${el._type}`)
97
+
98
+ // Use cds.linked to create a valid action definition
99
+ const { draftNew } = cds.linked(`
100
+ service Service {
101
+ entity ActiveEntity { } actions {
102
+ action draftNew(in: many $self, ${idParameters.join(', ')}) returns ActiveEntity;
103
+ }
104
+ }
105
+ `).definitions['Service.ActiveEntity'].actions
106
+
107
+ draftNew.name = 'draftNew'
108
+ draftNew.returns = Object.create(def)
109
+ draftNew.returns.type = def.name
110
+ draftNew.parent = { name: def.name}
111
+ delete draftNew['$location']
112
+
113
+ def.actions['draftNew'] = draftNew
114
+ }
115
+
61
116
  module.exports = function cds_compile_for_lean_drafts(csn) {
62
117
  function _redirect(assoc, target) {
63
118
  assoc.target = target.name
@@ -78,7 +133,7 @@ module.exports = function cds_compile_for_lean_drafts(csn) {
78
133
  if (d) return d
79
134
  // We need to construct a fake draft entity definition
80
135
  // We cannot use new cds.entity because runtime aspects would be missing
81
- const draft = new DraftEntity4 (active, _draftEntity)
136
+ const draft = new DraftEntity4(active, _draftEntity)
82
137
  Object.defineProperty(model.definitions, _draftEntity, { value: draft })
83
138
  Object.defineProperty(active, 'drafts', { value: draft })
84
139
 
@@ -123,6 +178,7 @@ module.exports = function cds_compile_for_lean_drafts(csn) {
123
178
  let _2manies
124
179
  for (const each in draft.elements) {
125
180
  const e = draft.elements[each]
181
+
126
182
  // add @odata.draft.enclosed to filtered compositions
127
183
  if (e.$enclosed) {
128
184
  e['@odata.draft.enclosed'] = true
@@ -130,6 +186,7 @@ module.exports = function cds_compile_for_lean_drafts(csn) {
130
186
  _2manies ??= Object.keys(draft.elements).map(k => draft.elements[k]).filter(c => c.isComposition && c.is2many)
131
187
  if (_2manies.find(c => c.name !== e.name && c.target.replace(/\.drafts$/, '') === e.target)) e['@odata.draft.enclosed'] = true
132
188
  }
189
+
133
190
  const newEl = Object.create(e)
134
191
  if (
135
192
  e.isComposition ||
@@ -139,19 +196,21 @@ module.exports = function cds_compile_for_lean_drafts(csn) {
139
196
  if (e._target['@odata.draft.enabled'] === false) continue // happens for texts if @fiori.draft.enabled is not set
140
197
  _redirect(newEl, addDraftEntity(e._target, model))
141
198
  }
199
+
142
200
  if (e.name === 'DraftAdministrativeData') {
143
201
  // redirect to DraftAdministrativeData service entity
144
202
  if (active._service?.entities.DraftAdministrativeData) _redirect(newEl, active._service.entities.DraftAdministrativeData)
145
203
  }
146
- Object.defineProperty (newEl,'parent',{value:draft,enumerable:false, configurable: true, writable: true})
204
+
205
+ Object.defineProperty(newEl, 'parent', { value: draft, enumerable: false, configurable: true, writable: true })
147
206
 
148
207
  for (const key in newEl) {
149
208
  if (
150
209
  key === '@mandatory' ||
151
- key === '@Common.FieldControl' && newEl[key]?.['#'] === 'Mandatory' ||
210
+ (key === '@Common.FieldControl' && newEl[key]?.['#'] === 'Mandatory') ||
152
211
  // key === '@Core.Immutable': Not allowed via UI anyway -> okay to cleanse them in PATCH
153
212
  // REVISIT: Remove feature flag dependency: If active, validation errors will be degraded to messages and stored in draft admin data
154
- (!active._service?.entities.DraftAdministrativeData.elements.DraftMessages && key.startsWith('@assert')) ||
213
+ (!active._service?.entities.DraftAdministrativeData.elements.DraftMessages && key.startsWith('@assert')) ||
155
214
  key.startsWith('@PersonalData')
156
215
  )
157
216
  newEl[key] = undefined
@@ -182,12 +241,18 @@ module.exports = function cds_compile_for_lean_drafts(csn) {
182
241
 
183
242
  for (const name in csn.definitions) {
184
243
  const def = csn.definitions[name]
244
+
245
+ // Do nothing for entities that are not draft-enabled
185
246
  if (!_isDraft(def) || def['@cds.external']) continue
247
+
248
+ // Mark elements as virtual as required
186
249
  def.elements.IsActiveEntity.virtual = true
187
250
  def.elements.HasDraftEntity.virtual = true
188
251
  def.elements.HasActiveEntity.virtual = true
189
- if (def.elements.DraftAdministrativeData_DraftUUID) def.elements.DraftAdministrativeData_DraftUUID.virtual = true
190
252
  def.elements.DraftAdministrativeData.virtual = true
253
+ if (def.elements.DraftAdministrativeData_DraftUUID) def.elements.DraftAdministrativeData_DraftUUID.virtual = true
254
+
255
+ // For Hierarchies: Exclude recursive compoisitions from draft tree
191
256
  if (def.elements.LimitedDescendantCount) {
192
257
  // for hierarchies: make sure recursive compositions are not part of the draft tree
193
258
  for (const c in def.compositions) {
@@ -195,7 +260,10 @@ module.exports = function cds_compile_for_lean_drafts(csn) {
195
260
  if (comp.target === def.name) comp['@odata.draft.ignore'] = true
196
261
  }
197
262
  }
263
+
198
264
  // will insert drafts entities, so that others can use `.drafts` even without incoming draft requests
199
265
  addDraftEntity(def, csn)
266
+
267
+ if (cds.env.fiori.draft_new_action) addNewActionAnnotation(def)
200
268
  }
201
269
  }
@@ -77,7 +77,7 @@ exports.ttl = (parse, strings, ...values) => {
77
77
  // }
78
78
 
79
79
  let cql = values.reduce ((cql,v,i) => {
80
- if (Array.isArray(v) && strings[i].match(/ in $/i)) values[i] = { list: v.map(cxn4) }
80
+ if (Array.isArray(v) && strings[i].match(/\sin\s*$/i)) values[i] = { list: v.map(cxn4) }
81
81
  return cql + strings[i] + (v instanceof cds.entity ? v.name : ':'+i)
82
82
  },'') + strings.at(-1)
83
83
  const cqn = parse (cql) //; cqn.$params = values
@@ -26,8 +26,12 @@ function cds_compile_to_csn (model, options, _flavor) {
26
26
  else return _finalize (cdsc.compileSources(model,o)) //> compile CDL sources
27
27
 
28
28
  function _finalize (csn) {
29
- // REVISIT: experimental implementation to automatically add aspect FlowHistory
30
- if (cds.env.features.history_for_flows) cds.compile.for.flows(csn)
29
+ // REVISIT: should move into compiler
30
+ if (cds.env.features.compile_for_assert) cds.compile.for.assert(csn)
31
+
32
+ // REVISIT: should move into compiler
33
+ csn = cds.compile.for.flows(csn)
34
+
31
35
  if (o.min) csn = cds.minify(csn)
32
36
  // REVISIT: experimental implementation to detect external APIs
33
37
  for (let each in csn.definitions) {
@@ -41,7 +41,7 @@ function cds_compile_to_edmx (csn,_o) {
41
41
  let result
42
42
  const next = () => {
43
43
  if (!result) {
44
- if (cds.env.features.compile_for_flows) enhanceCSNwithFlowAnnotations4FE(csn)
44
+ if (cds.env.features.annotate_for_flows) enhanceCSNwithFlowAnnotations4FE(csn)
45
45
  result = o.service === 'all' ? _many('.xml', cdsc.to.edmx.all(csn, o)) : cdsc.to.edmx(csn, o)
46
46
  }
47
47
  return result
@@ -29,9 +29,16 @@ module.exports = function _2yaml (object, {limit=111}={}) {
29
29
  if (typeof o === 'string') {
30
30
  if (o.indexOf('\n')>=0) return '|'+'\n'+indent+ o.replace(/\n/g,'\n'+indent)
31
31
  let s = o.trim()
32
- return !s || /^[\^@#:,=!<>*+-/|]/.test(s) || /:\s/.test(o) ? '"'+ o.replace(/\\/g,'\\\\') +'"' : s
32
+ return !s || _needs_quoting(s) ? '"'+ o.replace(/\\/g,'\\\\') +'"' : s
33
33
  }
34
34
  if (typeof o === 'function') return
35
35
  else return o
36
36
  }
37
37
  }
38
+
39
+
40
+ const _needs_quoting = s => {
41
+ if (/^[\^@#:,=!<>*+\-/|]/.test(s)) return true
42
+ if (/[{},[\]]/.test(s)) return true
43
+ if (/:\s/.test(s)) return true
44
+ }
@@ -113,8 +113,8 @@ deploy.schema = async function (db, csn = db.model, o) {
113
113
  if (!drops.length && !creas.length) return !o.dry
114
114
 
115
115
  if (schema_log) {
116
- schema_log.log(); for (let each of drops) schema_log.log(each)
117
- schema_log.log(); for (let each of creas) schema_log.log(each, '\n')
116
+ schema_log.log(); for (let each of drops) { schema_log.log(each) }
117
+ schema_log.log(); for (let each of creas) { schema_log.log(each); schema_log.log(); }
118
118
  }
119
119
  if (o.dry) return
120
120
 
@@ -293,16 +293,26 @@ class Config {
293
293
  }
294
294
  }
295
295
 
296
- _add_cloud_service_bindings({ VCAP_SERVICES, SERVICE_BINDING_ROOT }) {
296
+ _add_cloud_service_bindings({ VCAP_SERVICES, VCAP_SERVICES_FILE_PATH, SERVICE_BINDING_ROOT }) {
297
297
  let bindings, bindingsSource
298
298
 
299
299
  if (!this.requires) return
300
- if (VCAP_SERVICES && !(this.features && this.features.vcaps == false)) {
300
+ if (this.features?.vcaps === false) return
301
+
302
+ if (VCAP_SERVICES_FILE_PATH) {
303
+ try {
304
+ bindings = JSON.parse (fs.readFileSync(VCAP_SERVICES_FILE_PATH,'utf-8'))
305
+ bindingsSource = VCAP_SERVICES_FILE_PATH
306
+ } catch(e) {
307
+ throw new Error ('[cds.env] - failed to read/parse VCAP_SERVICES_FILE_PATH', {cause: e})
308
+ }
309
+ }
310
+ else if (VCAP_SERVICES) {
301
311
  try {
302
312
  bindings = JSON.parse(VCAP_SERVICES)
303
313
  bindingsSource = 'process.env.VCAP_SERVICES'
304
314
  } catch(e) {
305
- throw new Error ('[cds.env] - failed to parse VCAP_SERVICES:\n '+ e.message)
315
+ throw new Error ('[cds.env] - failed to parse VCAP_SERVICES', {cause: e})
306
316
  }
307
317
  }
308
318
 
@@ -316,7 +326,7 @@ class Config {
316
326
  const any = this._add_vcap_services_to(bindings)
317
327
  if (any) this._sources.push(bindingsSource)
318
328
  } catch(e) {
319
- throw new Error(`[cds.env] - failed to add service bindings from ${bindingsSource}:\n ${e.message}`);
329
+ throw new Error(`[cds.env] - failed to add service bindings from ${bindingsSource}`, {cause: e});
320
330
  }
321
331
  }
322
332
  }
@@ -27,6 +27,7 @@ module.exports = {
27
27
  'odata-v2' : { path: '/odata/v2' },
28
28
  'rest' : { path: '/rest' },
29
29
  'hcql' : { path: '/hcql' },
30
+ 'data.product' : null, // data products are not http-served protocols
30
31
  },
31
32
 
32
33
  features: {
@@ -46,6 +47,9 @@ module.exports = {
46
47
  precise_timestamps: false,
47
48
  ieee754compatible: undefined,
48
49
  consistent_params: true, //> remove with cds^10
50
+ annotate_for_flows: true,
51
+ history_for_flows: true,
52
+ compile_for_assert: undefined,
49
53
  // compat for db
50
54
  get string_decimals() { return this.ieee754compatible }
51
55
  },
@@ -57,7 +61,8 @@ module.exports = {
57
61
  wrap_multiple_errors: true,
58
62
  draft_lock_timeout: true,
59
63
  draft_deletion_timeout: true,
60
- draft_messages: true
64
+ draft_messages: true,
65
+ draft_new_action: false
61
66
  },
62
67
 
63
68
  ql: {
@@ -85,7 +85,7 @@ exports.edmx = edmx => {
85
85
 
86
86
  exports.json = json => {
87
87
  if (typeof json === 'object') json = JSON.stringify(json)
88
- const _json_replacer = s => s?.replace(/"/g, '\\"')
88
+ const _json_replacer = s => s && JSON.stringify(s).slice(1,-1)
89
89
  return localize(json) .using (_json_replacer)
90
90
  }
91
91
 
package/lib/index.js CHANGED
@@ -116,13 +116,13 @@ const cds = exports = module.exports = global.cds = new class cds extends EventE
116
116
  tx (..._) { return (this.db || this.txs).tx(..._) }
117
117
  run (..._) { return (this.db || typeof _[0] === 'function' && this.txs || this.error._no_primary_db).run(..._) }
118
118
  foreach (..._) { return (this.db || this.error._no_primary_db).foreach(..._) }
119
- read (..._) { return (this.db || this.error._no_primary_db).read(..._) }
120
- create (..._) { return (this.db || this.error._no_primary_db).create(..._) }
121
- insert (..._) { return (this.db || this.error._no_primary_db).insert(..._) }
122
- update (..._) { return (this.db || this.error._no_primary_db).update(..._) }
123
- upsert (..._) { return (this.db || this.error._no_primary_db).upsert(..._) }
124
- delete (..._) { return (this.db || this.error._no_primary_db).delete(..._) }
125
- disconnect (..._) { return (this.db || this.error._no_primary_db).disconnect(..._) }
119
+ read (..._) { return this.ql.SELECT(..._) }
120
+ create (..._) { return this.ql.INSERT.into(..._) }
121
+ insert (..._) { return this.ql.INSERT(..._) }
122
+ upsert (..._) { return this.ql.UPSERT(..._) }
123
+ update (..._) { return this.ql.UPDATE.entity(..._) }
124
+ delete (..._) { return this.ql.DELETE.from(..._) }
125
+ disconnect (..._) { return this.db?.disconnect(..._) }
126
126
 
127
127
  // Deprecated stuff to be removed in upcomming releases...
128
128
  /** @deprecated */ get lazified() { return this.lazify }
package/lib/req/event.js CHANGED
@@ -8,3 +8,7 @@ class EventMessage extends EventContext {}
8
8
 
9
9
  module.exports = exports = EventMessage
10
10
  exports.Context = EventContext
11
+
12
+ exports.CRUD_EVENTS = { CREATE: 1, READ: 1, UPDATE: 1, DELETE: 1 }
13
+ exports.DRAFT_EVENTS = { NEW: 1, SAVE: 1, PATCH: 1, DISCARD: 1, EDIT: 1 }
14
+ exports.WELL_KNOWN_EVENTS = { ...exports.CRUD_EVENTS, ...exports.DRAFT_EVENTS }
@@ -1,5 +1,7 @@
1
1
  const cds = require('..')
2
2
 
3
+ const { WELL_KNOWN_EVENTS } = require('./event')
4
+
3
5
  /** Validates given input data against a request target definition.
4
6
  * @param {entity} target the linked definition to check against, usually an entity definition
5
7
  * @returns {Error[]|undefined} an array of errors or undefined if no errors occurred
@@ -118,7 +120,7 @@ const $any = class any {
118
120
  min.val !== undefined && max.val !== undefined ? (v,p,ctx) => v == null || min.val < v && v < max.val || ctx.error ('ASSERT_RANGE', p, this.name, this['@assert.range.message'], v, '>'+min.val, '<'+max.val) :
119
121
  min.val !== undefined ? (v,p,ctx) => v == null || min.val < v && v <= max || ctx.error ('ASSERT_RANGE', p, this.name, this['@assert.range.message'], v, '>'+min.val, max) :
120
122
  max.val !== undefined ? (v,p,ctx) => v == null || min <= v && v < max.val || ctx.error ('ASSERT_RANGE', p, this.name, this['@assert.range.message'], v, min, '<'+max.val) :
121
- (v,p,ctx) => v == null || min <= v && v <= max || ctx.error ('ASSERT_RANGE', p, this.name, null, v, min, max)
123
+ (v,p,ctx) => v == null || min <= v && v <= max || ctx.error ('ASSERT_RANGE', p, this.name, this['@assert.range.message'], v, min, max)
122
124
  )
123
125
  }
124
126
  if (this['@assert.enum'] || this['@assert.range'] && this.enum) {
@@ -243,6 +245,7 @@ class entity extends struct {
243
245
  /** Actions are struct-like, with their parameters as elements to validate. */
244
246
  class action extends struct {
245
247
  validate (data, path, ctx) {
248
+ if (this.name in WELL_KNOWN_EVENTS) return
246
249
  super.validate (data, path, ctx, this.params || {})
247
250
  }
248
251
 
@@ -175,7 +175,8 @@ class Service extends ReflectionAPI {
175
175
  this._resolve.transitions = (query, abortCondition, skipForbiddenViewCheck) => {
176
176
  const target = query && typeof query === 'object' ? cds.infer.target(query) || query?._target : undefined
177
177
  const _tx = typeof tx === 'function' ? cds.context?.tx : this
178
- return getTransition(target, _tx, skipForbiddenViewCheck, undefined, {
178
+ const event = query?.INSERT ? 'INSERT' : query?.UPDATE ? 'UPDATE' : query?.DELETE ? 'DELETE' : undefined
179
+ return getTransition(target, _tx, skipForbiddenViewCheck, event, {
179
180
  abort: abortCondition ?? (this.isDatabaseService ? this.resolve._abortDB : _defaultAbort(this))
180
181
  })
181
182
  }
@@ -12,14 +12,12 @@ const {
12
12
 
13
13
  module.exports = function ias_auth(config) {
14
14
  // cds.env.requires.auth.known_claims is not an official config!
15
- const { kind, credentials, config: serviceConfig = {}, known_claims = KNOWN_CLAIMS } = config
15
+ const { kind, credentials, config: serviceConfig = {}, known_claims = KNOWN_CLAIMS, xsuaa = 'xsuaa' } = config
16
16
  const skipped_attrs = known_claims.reduce((a, x) => ((a[x] = 1), a), {})
17
17
 
18
18
  if (!credentials)
19
- throw new Error(
20
- `Authentication kind "${kind}" configured, but no IAS instance bound to application. ` +
21
- 'Either bind an IAS instance, or switch to an authentication kind that does not require a binding.'
22
- )
19
+ cds.error(`Authentication kind "${kind}" configured, but no IAS instance bound to application. ` +
20
+ 'Either bind an IAS instance, or switch to an authentication kind that does not require a binding.')
23
21
 
24
22
  // enable signature cache by default
25
23
  serviceConfig.validation ??= {}
@@ -56,8 +54,8 @@ module.exports = function ias_auth(config) {
56
54
  // xsuaa fallback allows to also accept XSUAA tokens during migration to IAS
57
55
  // automatically enabled if xsuaa credentials are available
58
56
  let xsuaa_service, xsuaa_user_factory
59
- if (cds.env.requires.xsuaa?.credentials) {
60
- const { credentials: xsuaa_credentials, config: xsuaa_serviceConfig = {} } = cds.env.requires.xsuaa
57
+ if (cds.env.requires[xsuaa]?.credentials) {
58
+ const { credentials: xsuaa_credentials, config: xsuaa_serviceConfig = {} } = cds.env.requires[xsuaa]
61
59
  xsuaa_service = new XsuaaService(xsuaa_credentials, xsuaa_serviceConfig)
62
60
  const get_xsuaa_user_factory = require('./jwt-auth')._get_user_factory
63
61
  xsuaa_user_factory = get_xsuaa_user_factory(xsuaa_credentials, xsuaa_credentials.xsappname, 'xsuaa')
@@ -17,7 +17,7 @@ for (let b in _builtin) _builtin[b+'-auth'] = _builtin[b]
17
17
  module.exports = function auth_factory (o) {
18
18
 
19
19
  // prepare options
20
- const options = { ...o, ...cds.requires.auth }
20
+ const options = { ...cds.requires.auth, ...o }
21
21
  let { kind, impl } = options
22
22
 
23
23
  // if no impl is given, it's a built-in strategy
@@ -29,6 +29,7 @@ class Protocols {
29
29
  if (typeof p === 'string') p = { path:p }
30
30
  if (merge) p = { ...protocols[kind], ...p }
31
31
  if (!p.impl) p.impl = './'+kind
32
+ if (!p.path) return p
32
33
  if (!p.path.startsWith('/')) p.path = '/'+p.path
33
34
  if (p.path.endsWith('/')) p.path = p.path.slice(0,-1)
34
35
  return p
@@ -108,7 +109,7 @@ class Protocols {
108
109
  else {
109
110
  annos=[]; for (let kind in this) {
110
111
  let path = def['@'+kind] || def['@protocol.'+kind]
111
- if (path) annos.push ({ kind, path })
112
+ if (path) annos.push ({ kind, path: typeof path === 'string' ? path : undefined })
112
113
  }
113
114
  }
114
115
  // no annotations at all -> use default protocol
@@ -116,11 +117,11 @@ class Protocols {
116
117
 
117
118
  // canonicalize to { kind, path } objects
118
119
  const endpoints = annos.map (each => {
119
- let { kind = each['='] || each, path } = each
120
- if (!(kind in this))
121
- return cds.log('adapters').warn ('ignoring unknown protocol:', kind)
122
- if (typeof path !== 'string') path = o?.at || o?.path || def['@path'] || _slugified(srv.name)
123
- if (path[0] !== '/') path = this[kind].path + '/' + path // prefix with protocol path
120
+ let { kind = each['='] || each, path } = each, protocol = this[kind]
121
+ if (protocol == undefined) return cds.log('adapters') .warn ('ignoring unknown protocol:', kind)
122
+ if (protocol.path == undefined) return // a pseudo-protocol, not served to http, e.g. data.product
123
+ if (!path) path = o?.at || o?.path || def['@path'] || _slugified(srv.name)
124
+ if (path[0] !== '/') path = protocol.path + '/' + path // prefix with protocol path
124
125
  return { kind, path }
125
126
  }) .filter (e => e) //> skipping unknown protocols
126
127
 
@@ -108,6 +108,13 @@ class EventHandlers {
108
108
 
109
109
  // Finally register with a filter function to match requests to be handled
110
110
  const handlers = event === 'error' ? this._error : handler._initial ? this._initial : this[phase] // REVISIT: remove _initial handlers
111
+
112
+ // REVISIT: remove compat flag with cds^10
113
+ if (!cds.env.features.async_handler_compat && (phase === 'before' && !handler._initial || phase === 'after') && handler.constructor.name !== 'AsyncFunction') {
114
+ const originalHandler = handler
115
+ handler = async function (...args) { return originalHandler.call(this, ...args) }
116
+ }
117
+
111
118
  handlers.push (new EventHandler (phase, event, path, handler))
112
119
 
113
120
  if (phase === 'on') cds.emit('subscribe',srv,event) //> inform messaging service
@@ -59,6 +59,10 @@ class ApplicationService extends cds.Service {
59
59
  return require('./generic/flows')
60
60
  }
61
61
 
62
+ static get handle_assert() {
63
+ return require('./generic/assert')
64
+ }
65
+
62
66
  // Overload .handle in order to resolve projections up to a definition that is known by the remote service instance.
63
67
  // Result is post processed according to the inverse projection in order to reflect the correct result of the original query.
64
68
  async handle(req) {
@@ -66,7 +70,7 @@ class ApplicationService extends cds.Service {
66
70
  if (!this._requires_resolving?.(req)) return super.handle(req)
67
71
  // rewrite the query to a target entity served by this service...
68
72
  const query = this.resolve(req.query)
69
- if (!query) throw new Error(`Target ${req.target.name} cannot be resolved for service ${this.name}`)
73
+ if (!query) cds.error`Target ${req.target.name} cannot be resolved for service ${this.name}`
70
74
  const target = query._target || req.target
71
75
  // we need to provide target explicitly because it's cached within ensure_target
72
76
  const _req = new cds.Request({ query, target, _resolved: true })
@@ -1,3 +1,4 @@
1
+ // REVISIT: merge with cds/lib/req/event.js
1
2
  exports.MOD_EVENTS = { UPDATE: 1, DELETE: 1, EDIT: 1 }
2
3
  exports.WRITE_EVENTS = { CREATE: 1, NEW: 1, PATCH: 1, CANCEL: 1, ...exports.MOD_EVENTS }
3
4
  exports.CRUD_EVENTS = { READ: 1, ...exports.WRITE_EVENTS }