@sap/cds 9.4.5 → 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 +71 -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 +3 -0
- 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/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 +24 -25
- package/libx/odata/parse/cqn2odata.js +2 -6
- package/libx/odata/parse/grammar.peggy +90 -12
- 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
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
|
|
@@ -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 })
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
const cds = require('@sap/cds')
|
|
2
|
+
const getTemplate = require('../utils/template')
|
|
3
|
+
const templatePathSerializer = require('../utils/templateProcessorPathSerializer')
|
|
4
|
+
|
|
5
|
+
const $has_asserts = Symbol.for('has_asserts')
|
|
6
|
+
|
|
7
|
+
const { compileUpdatedDraftMessages } = require('../../fiori/lean-draft')
|
|
8
|
+
|
|
9
|
+
const _serialize = obj =>
|
|
10
|
+
JSON.stringify(
|
|
11
|
+
Object.keys(obj)
|
|
12
|
+
.sort()
|
|
13
|
+
.reduce((a, k) => ((a[k] = obj[k]), a), {})
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
const _bufferReviver = (key, value) => {
|
|
17
|
+
if (value && typeof value === 'object' && value.type === 'Buffer' && Array.isArray(value.data)) {
|
|
18
|
+
return Buffer.from(value.data)
|
|
19
|
+
}
|
|
20
|
+
return value
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
module.exports = cds.service.impl(async function () {
|
|
24
|
+
this.after(['INSERT', 'UPSERT', 'UPDATE'], async (res, req) => {
|
|
25
|
+
if (!($has_asserts in req.target)) {
|
|
26
|
+
let has_asserts = false
|
|
27
|
+
for (const each in req.target.elements) {
|
|
28
|
+
const element = req.target.elements[each]
|
|
29
|
+
if (element['@assert']) {
|
|
30
|
+
has_asserts = true
|
|
31
|
+
break
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
req.target[$has_asserts] = has_asserts
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!cds.context?.tx || !req.target[$has_asserts]) return
|
|
38
|
+
|
|
39
|
+
const IS_DRAFT_ENTITY = req.target.isDraft
|
|
40
|
+
|
|
41
|
+
if (req.event === 'CREATE' && IS_DRAFT_ENTITY) return
|
|
42
|
+
|
|
43
|
+
let touched
|
|
44
|
+
if (cds.env.features.assert_touched_only !== false && IS_DRAFT_ENTITY && req.event === 'UPDATE')
|
|
45
|
+
touched = Object.keys(res).filter(k => !(k in req.target.keys))
|
|
46
|
+
|
|
47
|
+
const template = getTemplate('assert', this, req.target, {
|
|
48
|
+
pick: element => element['@assert'],
|
|
49
|
+
ignore: element => element.isAssociation && !element.isComposition
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
if (!cds.context.tx.changes) {
|
|
53
|
+
cds.context.tx.changes = {}
|
|
54
|
+
|
|
55
|
+
req.before('commit', async function () {
|
|
56
|
+
const { changes } = cds.context.tx
|
|
57
|
+
|
|
58
|
+
const errors = []
|
|
59
|
+
|
|
60
|
+
for (const [entityName, serializedChanges] of Object.entries(changes)) {
|
|
61
|
+
if (!serializedChanges.size) continue
|
|
62
|
+
|
|
63
|
+
const deserializedChanges = Array.from(serializedChanges).map(([k, v]) => [JSON.parse(k, _bufferReviver), v])
|
|
64
|
+
|
|
65
|
+
const entity = cds.model.definitions[entityName]
|
|
66
|
+
const IS_DRAFT_ENTITY = entity.isDraft
|
|
67
|
+
|
|
68
|
+
// Cache assert query on entity
|
|
69
|
+
if (!Object.hasOwn(entity, 'assert')) {
|
|
70
|
+
const asserts = []
|
|
71
|
+
|
|
72
|
+
for (const element of Object.values(entity.elements)) {
|
|
73
|
+
if (element._foreignKey4) continue
|
|
74
|
+
if (element.isAssociation && !element.isComposition) continue
|
|
75
|
+
|
|
76
|
+
const assert = element['@assert']
|
|
77
|
+
if (!assert) continue
|
|
78
|
+
|
|
79
|
+
// replace $self with $main
|
|
80
|
+
const xpr = JSON.parse(JSON.stringify(assert.xpr).replace(/\$self/g, '$main'))
|
|
81
|
+
|
|
82
|
+
asserts.push({ xpr, as: '@assert:' + element.name })
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
entity.assert = cds.ql.SELECT([...Object.keys(entity.keys), ...asserts]).from(entity)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const query = cds.ql.clone(entity.assert)
|
|
89
|
+
|
|
90
|
+
// Select only rows with changes
|
|
91
|
+
const keyNames = Object.keys(entity.keys).filter(
|
|
92
|
+
k => !entity.keys[k].virtual && !entity.keys[k].isAssociation
|
|
93
|
+
)
|
|
94
|
+
const keyMap = Object.fromEntries(keyNames.map(k => [k, true]))
|
|
95
|
+
|
|
96
|
+
query.where([
|
|
97
|
+
{ list: keyNames.map(k => ({ ref: [k] })) },
|
|
98
|
+
'in',
|
|
99
|
+
{ list: deserializedChanges.map(([keyKV]) => ({ list: keyNames.map(k => ({ val: keyKV[k] })) })) }
|
|
100
|
+
])
|
|
101
|
+
|
|
102
|
+
const results = await query
|
|
103
|
+
|
|
104
|
+
for (const row of results) {
|
|
105
|
+
const keyColumns = Object.fromEntries(Object.entries(row).filter(([k]) => k in keyMap))
|
|
106
|
+
const { touched, req, pathSegmentsInfo } = serializedChanges.get(_serialize(keyColumns))
|
|
107
|
+
const failedColumns = Object.entries(row)
|
|
108
|
+
.filter(([k, v]) => v !== null && !(k in keyMap))
|
|
109
|
+
.map(([k, v]) => [k.replace(/^@assert:/, ''), v])
|
|
110
|
+
|
|
111
|
+
if (failedColumns.length === 0) continue
|
|
112
|
+
|
|
113
|
+
const failedAsserts = failedColumns.map(([element, message]) => {
|
|
114
|
+
const error = {
|
|
115
|
+
code: 'ASSERT',
|
|
116
|
+
target: element,
|
|
117
|
+
numericSeverity: 4,
|
|
118
|
+
'@Common.numericSeverity': 4
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// if error function was used in @assert expression -> use its output
|
|
122
|
+
try {
|
|
123
|
+
// Depending on DB, function result may be JavaScript Object or JSON String
|
|
124
|
+
const parsed = typeof message === 'string' ? JSON.parse(message) : message
|
|
125
|
+
Object.assign(error, parsed)
|
|
126
|
+
if (Array.isArray(error.targets)) {
|
|
127
|
+
const target = error.targets.at(0)
|
|
128
|
+
const additionalTargets = error.targets.slice(1)
|
|
129
|
+
if (target) error.target = target
|
|
130
|
+
if (additionalTargets.length) error.additionalTargets = additionalTargets
|
|
131
|
+
}
|
|
132
|
+
delete error.targets
|
|
133
|
+
} catch {
|
|
134
|
+
error.message = message
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return error
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
if (IS_DRAFT_ENTITY) {
|
|
141
|
+
const draft = await SELECT.one
|
|
142
|
+
.from({ ref: [req.subject.ref[0]] })
|
|
143
|
+
.columns('DraftAdministrativeData_DraftUUID', 'DraftAdministrativeData.DraftMessages')
|
|
144
|
+
const persistedMessages = draft.DraftAdministrativeData_DraftMessages || []
|
|
145
|
+
|
|
146
|
+
// keep all messages that have targets that were touched in this change
|
|
147
|
+
const newMessages = touched
|
|
148
|
+
? failedAsserts.filter(a => {
|
|
149
|
+
const targets = [a.target].concat(a.additionalTargets || [])
|
|
150
|
+
return touched.some(t => targets.includes(t))
|
|
151
|
+
})
|
|
152
|
+
: failedAsserts
|
|
153
|
+
|
|
154
|
+
const nextDraftMessages = compileUpdatedDraftMessages(
|
|
155
|
+
newMessages,
|
|
156
|
+
persistedMessages,
|
|
157
|
+
req.data,
|
|
158
|
+
req.subject.ref
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
await UPDATE('DRAFT.DraftAdministrativeData')
|
|
162
|
+
.set({ DraftMessages: nextDraftMessages })
|
|
163
|
+
.where({ DraftUUID: draft.DraftAdministrativeData_DraftUUID })
|
|
164
|
+
} else {
|
|
165
|
+
const isDraftAction = req._.event?.startsWith('draft')
|
|
166
|
+
const prefix = templatePathSerializer('', pathSegmentsInfo)
|
|
167
|
+
failedAsserts.forEach(err => {
|
|
168
|
+
err.target = (isDraftAction ? 'in/' : '') + prefix + err.target
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
errors.push(...failedAsserts)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (errors.length) {
|
|
177
|
+
if (errors.length === 1) throw errors[0]
|
|
178
|
+
const err = new cds.error('MULTIPLE_ERRORS', { details: errors })
|
|
179
|
+
delete err.stack
|
|
180
|
+
throw err
|
|
181
|
+
}
|
|
182
|
+
})
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const templateProcessOptions = {
|
|
186
|
+
pathSegmentsInfo: [],
|
|
187
|
+
includeKeyValues: true
|
|
188
|
+
}
|
|
189
|
+
if (req._.event?.startsWith('draft')) {
|
|
190
|
+
const IsActiveEntity = req.data.IsActiveEntity || false
|
|
191
|
+
templateProcessOptions.draftKeys = { IsActiveEntity }
|
|
192
|
+
}
|
|
193
|
+
// Collect entity keys and their values of changed rows
|
|
194
|
+
template.process(
|
|
195
|
+
req.data,
|
|
196
|
+
elementInfo => {
|
|
197
|
+
const { row, target, pathSegmentsInfo } = elementInfo
|
|
198
|
+
const targetName = target.name
|
|
199
|
+
|
|
200
|
+
cds.context.tx.changes[targetName] ??= new Map()
|
|
201
|
+
|
|
202
|
+
const keys = {}
|
|
203
|
+
for (const key in target.keys) {
|
|
204
|
+
if (key === 'IsActiveEntity') continue
|
|
205
|
+
if (!(key in row)) continue
|
|
206
|
+
keys[key] = row[key]
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (!Object.keys(keys).length) return
|
|
210
|
+
|
|
211
|
+
const serialized = _serialize(keys)
|
|
212
|
+
const changes = cds.context.tx.changes[targetName]
|
|
213
|
+
if (changes.has(serialized)) return
|
|
214
|
+
|
|
215
|
+
changes.set(serialized, { touched, req, pathSegmentsInfo: [...pathSegmentsInfo] })
|
|
216
|
+
},
|
|
217
|
+
templateProcessOptions
|
|
218
|
+
)
|
|
219
|
+
})
|
|
220
|
+
})
|