@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.
- package/CHANGELOG.md +81 -1
- package/_i18n/messages_en_US_saptrc.properties +1 -1
- package/common.cds +5 -2
- package/lib/compile/cds-compile.js +1 -0
- package/lib/compile/for/assert.js +64 -0
- package/lib/compile/for/flows.js +194 -58
- package/lib/compile/for/lean_drafts.js +75 -7
- package/lib/compile/parse.js +1 -1
- package/lib/compile/to/csn.js +6 -2
- package/lib/compile/to/edm.js +1 -1
- package/lib/compile/to/yaml.js +8 -1
- package/lib/dbs/cds-deploy.js +2 -2
- package/lib/env/cds-env.js +14 -4
- package/lib/env/defaults.js +6 -1
- package/lib/i18n/localize.js +1 -1
- package/lib/index.js +7 -7
- package/lib/req/event.js +4 -0
- package/lib/req/validate.js +4 -1
- package/lib/srv/cds.Service.js +2 -1
- package/lib/srv/middlewares/auth/ias-auth.js +5 -7
- package/lib/srv/middlewares/auth/index.js +1 -1
- package/lib/srv/protocols/index.js +7 -6
- package/lib/srv/srv-handlers.js +7 -0
- package/libx/_runtime/common/Service.js +5 -1
- package/libx/_runtime/common/constants/events.js +1 -0
- package/libx/_runtime/common/generic/assert.js +220 -0
- package/libx/_runtime/common/generic/flows.js +168 -108
- package/libx/_runtime/common/generic/input.js +6 -4
- package/libx/_runtime/common/utils/cqn.js +0 -24
- package/libx/_runtime/common/utils/normalizeTimestamp.js +2 -2
- package/libx/_runtime/common/utils/resolveView.js +8 -2
- package/libx/_runtime/common/utils/templateProcessor.js +10 -1
- package/libx/_runtime/common/utils/templateProcessorPathSerializer.js +21 -9
- package/libx/_runtime/fiori/lean-draft.js +511 -379
- package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +39 -35
- package/libx/_runtime/messaging/enterprise-messaging.js +2 -2
- package/libx/_runtime/remote/Service.js +4 -5
- package/libx/_runtime/ucl/Service.js +111 -15
- package/libx/common/utils/streaming.js +1 -1
- package/libx/odata/middleware/batch.js +8 -6
- package/libx/odata/middleware/create.js +2 -2
- package/libx/odata/middleware/delete.js +2 -2
- package/libx/odata/middleware/metadata.js +18 -11
- package/libx/odata/middleware/read.js +2 -2
- package/libx/odata/middleware/service-document.js +1 -1
- package/libx/odata/middleware/update.js +1 -1
- package/libx/odata/parse/afterburner.js +46 -36
- package/libx/odata/parse/cqn2odata.js +2 -6
- package/libx/odata/parse/grammar.peggy +91 -13
- package/libx/odata/parse/parser.js +1 -1
- package/libx/odata/utils/index.js +2 -2
- package/libx/odata/utils/readAfterWrite.js +2 -0
- package/libx/queue/TaskRunner.js +26 -1
- package/libx/queue/index.js +11 -1
- package/package.json +1 -1
- 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: { ...
|
|
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
|
|
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
|
-
|
|
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
|
}
|
package/lib/compile/parse.js
CHANGED
|
@@ -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(
|
|
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
|
package/lib/compile/to/csn.js
CHANGED
|
@@ -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:
|
|
30
|
-
if (cds.env.features.
|
|
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) {
|
package/lib/compile/to/edm.js
CHANGED
|
@@ -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.
|
|
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
|
package/lib/compile/to/yaml.js
CHANGED
|
@@ -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 ||
|
|
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
|
+
}
|
package/lib/dbs/cds-deploy.js
CHANGED
|
@@ -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
|
|
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
|
|
package/lib/env/cds-env.js
CHANGED
|
@@ -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 (
|
|
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
|
|
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}
|
|
329
|
+
throw new Error(`[cds.env] - failed to add service bindings from ${bindingsSource}`, {cause: e});
|
|
320
330
|
}
|
|
321
331
|
}
|
|
322
332
|
}
|
package/lib/env/defaults.js
CHANGED
|
@@ -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: {
|
package/lib/i18n/localize.js
CHANGED
|
@@ -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
|
|
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
|
|
120
|
-
create (..._) { return
|
|
121
|
-
insert (..._) { return
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
delete (..._) { return
|
|
125
|
-
disconnect (..._) { return
|
|
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 }
|
package/lib/req/validate.js
CHANGED
|
@@ -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,
|
|
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
|
|
package/lib/srv/cds.Service.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
20
|
-
|
|
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
|
|
60
|
-
const { credentials: xsuaa_credentials, config: xsuaa_serviceConfig = {} } = cds.env.requires
|
|
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 = { ...
|
|
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 (
|
|
121
|
-
|
|
122
|
-
if (
|
|
123
|
-
if (path[0] !== '/') 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
|
|
package/lib/srv/srv-handlers.js
CHANGED
|
@@ -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)
|
|
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 })
|