@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
@@ -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
+ })
@@ -5,12 +5,10 @@ const { getFrom } = require('../../../../lib/compile/for/flows')
5
5
  const FLOW_STATUS = '@flow.status'
6
6
  const FROM = '@from'
7
7
  const TO = '@to'
8
- // backwards compat
9
- const FLOW_FROM = '@flow.from'
10
- const FLOW_TO = '@flow.to'
11
-
12
8
  const FLOW_PREVIOUS = '$flow.previous'
13
9
 
10
+ const $transitions_ = Symbol.for('transitions_')
11
+
14
12
  function buildAllowedCondition(action, statusElementName, statusEnum) {
15
13
  const fromList = getFrom(action)
16
14
  const conditions = fromList.map(from => {
@@ -20,154 +18,216 @@ function buildAllowedCondition(action, statusElementName, statusEnum) {
20
18
  return `(${conditions.join(' OR ')})`
21
19
  }
22
20
 
23
- async function isCurrentStatusInFrom(req, action, statusElementName, statusEnum) {
21
+ async function isCurrentStatusInFrom(subject, action, statusElementName, statusEnum) {
24
22
  const cond = buildAllowedCondition(action, statusElementName, statusEnum)
25
23
  const parsedXpr = cds.parse.expr(cond)
26
- const dbEntity = await SELECT.one.from(req.subject).where(parsedXpr)
24
+ const dbEntity = await SELECT.one.from(subject).where(parsedXpr)
27
25
  return dbEntity !== undefined
28
26
  }
29
27
 
30
- async function checkStatus(req, action, statusElementName, statusEnum) {
31
- const allowed = await isCurrentStatusInFrom(req, action, statusElementName, statusEnum)
28
+ async function checkStatus(subject, action, statusElementName, statusEnum) {
29
+ const allowed = await isCurrentStatusInFrom(subject, action, statusElementName, statusEnum)
32
30
  if (!allowed) {
33
31
  const from = getFrom(action)
34
32
  const fromValues = JSON.stringify(from.flatMap(el => Object.values(el)))
35
33
  cds.error({
36
- code: 409,
34
+ status: 409,
37
35
  message: from.length > 1 ? 'INVALID_FLOW_TRANSITION_MULTI' : 'INVALID_FLOW_TRANSITION_SINGLE',
38
36
  args: [action.name, statusElementName, fromValues]
39
37
  })
40
38
  }
41
39
  }
42
40
 
43
- const buildUpKeys = parentKeys => {
41
+ // REVISIT: what about renamed keys?
42
+ const buildUpKeys = async (entity, data, subject) => {
43
+ const parentKeys = Object.keys(entity.keys).filter(k => k !== 'IsActiveEntity')
44
+ // REVISIT: when do we not hava all keys?
45
+ const keyValues =
46
+ data && parentKeys.every(key => key in data) ? data : await SELECT.one.from(subject).columns(parentKeys)
44
47
  const upKeys = {}
45
- for (const key in parentKeys) {
46
- upKeys[`up__${key}`] = parentKeys[key]
48
+ for (let i = 0; i < parentKeys.length; i++) {
49
+ upKeys[`up__${parentKeys[i]}`] = keyValues[parentKeys[i]]
47
50
  }
48
51
  return upKeys
49
52
  }
50
53
 
51
- const updateFlowHistory = async (req, toValue, upKeys, changes, isPrevious) => {
52
- if (cds.env.features.flows_history_stack && isPrevious) {
53
- await DELETE.from(req.target.compositions['transitions_'].target).where({
54
- timestamp: changes[changes.length - 1].timestamp
55
- })
54
+ const resolveTo = (action, statusEnum) => {
55
+ let to = action[TO]
56
+ to = to['#'] ? (statusEnum[to['#']].val ?? statusEnum[to['#']]['$path'].at(-1)) : to
57
+ return to
58
+ }
59
+
60
+ const handleTransition = async (entity, data, subject, to) => {
61
+ const isPrevious = to['='] === FLOW_PREVIOUS
62
+ if (isPrevious) {
63
+ const upKeys = await buildUpKeys(entity, data, subject)
64
+ const previous = await SELECT.one
65
+ .from(entity[$transitions_].target)
66
+ .where({ ...upKeys })
67
+ .orderBy('timestamp desc')
68
+ .limit(1, 1)
69
+ if (!previous)
70
+ cds.error({ status: 409, message: 'No change has been made yet, cannot transition to previous status.' })
71
+ to = previous.status
72
+ }
73
+ return to
74
+ }
75
+
76
+ const getStatusInfo = statusElement => {
77
+ let statusEnum, statusElementName
78
+ if (statusElement.enum) {
79
+ statusEnum = statusElement.enum
80
+ statusElementName = statusElement.name
81
+ } else if (statusElement?._target?.elements['code']) {
82
+ statusEnum = statusElement._target.elements['code'].enum
83
+ statusElementName = statusElement.name + '_code'
56
84
  } else {
57
- await INSERT.into(req.target.compositions['transitions_'].target).entries({
58
- ...upKeys,
59
- status: toValue
85
+ cds.error({
86
+ status: 409,
87
+ message: `Status element in ${statusElement.parent.name} must be an enum or target an entity with an enum named "code"`
60
88
  })
61
89
  }
90
+ return { statusEnum, statusElementName }
62
91
  }
63
92
 
64
- const buildToKey = (action, statusEnum) => {
65
- const to = action[TO] ?? action[FLOW_TO]
66
- const toKey = to['#'] ? (statusEnum[to['#']].val ?? statusEnum[to['#']]['$path'].at(-1)) : to
67
- return toKey
93
+ const from_factory = (entity, action, { statusElementName, statusEnum }) => {
94
+ async function handle_flow_from(req) {
95
+ const subject = cds.clone(req.subject)
96
+ if (entity.name.endsWith('.drafts')) subject.ref[0].id = entity.name
97
+
98
+ await checkStatus(subject, action, statusElementName, statusEnum)
99
+ }
100
+ handle_flow_from._initial = true
101
+ return handle_flow_from
68
102
  }
69
103
 
70
- const handleStatusTransitionWithHistory = async (req, statusElementName, toKey, service) => {
71
- let upKeys, changes
72
- upKeys = buildUpKeys(req.params[0])
73
- changes = await SELECT.from(req.target.compositions['transitions_'].target)
74
- .where({ ...upKeys })
75
- .orderBy('timestamp asc')
76
- const isPrevious = toKey['='] === FLOW_PREVIOUS
77
- if (isPrevious) {
78
- if (changes.length <= 1)
79
- return cds.error({ code: 409, message: 'No change has been made yet, cannot transition to previous status.' })
80
- toKey = changes[changes.length - 2].status
104
+ const to___factory = (entity, action, { statusElementName, statusEnum }) => {
105
+ return async function handle_flow_to(req, next) {
106
+ const res = await next()
107
+
108
+ let subject = cds.clone(req.subject)
109
+ if (entity.name.endsWith('.drafts')) subject.ref[0].id = entity.name
110
+
111
+ // REVISIT: this only happens on CREATE, where req.subject is a collection
112
+ // -> could be avoided if setting the status would be done via req.data
113
+ if (!subject.ref[0].id) {
114
+ const keys = Object.keys(entity.keys).reduce((acc, cur) => {
115
+ acc[cur] = res[cur]
116
+ return acc
117
+ }, {})
118
+ subject = SELECT.from(entity.name, keys).SELECT.from
119
+ }
120
+
121
+ let to = resolveTo(action, statusEnum)
122
+
123
+ if (Object.prototype.hasOwnProperty.call(entity, $transitions_)) {
124
+ to = await handleTransition(entity, req.data, subject, to)
125
+ }
126
+
127
+ await UPDATE(subject).with({ [statusElementName]: to })
128
+
129
+ // REVISIT: for stack, we now need to delete the last to transitions
130
+ if (cds.env.features.flows_history_stack && resolveTo(action, statusEnum)['='] === FLOW_PREVIOUS) {
131
+ const upKeys = await buildUpKeys(entity, req.data, req.subject)
132
+ const timestamps = SELECT('timestamp')
133
+ .from(entity[$transitions_].target)
134
+ .where({ ...upKeys })
135
+ .orderBy('timestamp desc')
136
+ .limit(2)
137
+ await DELETE.from(entity[$transitions_].target)
138
+ .where({ ...upKeys })
139
+ .where(`timestamp in`, timestamps)
140
+ }
141
+
142
+ return res
81
143
  }
82
- await service.run(UPDATE(req.subject).with({ [statusElementName]: toKey }))
83
- await updateFlowHistory(req, toKey, upKeys, changes, isPrevious)
84
144
  }
85
145
 
86
146
  /**
87
147
  * handler registration
88
148
  */
89
149
  module.exports = cds.service.impl(function () {
90
- const entry = []
91
- const exit = []
150
+ const b4 = []
151
+ const on = []
152
+ const after = []
92
153
 
93
154
  for (const entity of this.entities) {
94
155
  if (!entity.actions || !entity.elements) continue
95
156
 
96
- const fromActions = []
97
- const toActions = []
98
- for (const action of entity.actions) {
99
- if (action[FROM] || action[FLOW_FROM]) fromActions.push(action)
100
- if (action[TO] || action[FLOW_TO]) toActions.push(action)
101
- }
102
- if (fromActions.length === 0 && toActions.length === 0) continue
103
-
104
- let statusElement = Object.values(entity.elements).find(el => el[FLOW_STATUS])
105
- if (!statusElement) {
106
- cds.error({
107
- code: 409,
108
- message: `Entity ${entity.name} does not have a status element, but its actions have registered @flow annotations.`
157
+ const statusElement = Object.values(entity.elements).find(el => el[FLOW_STATUS])
158
+ if (!statusElement) continue
159
+
160
+ const statusInfo = getStatusInfo(statusElement)
161
+
162
+ // determine and cache target for transitions recording, if any
163
+ let base = entity
164
+ while (base.__proto__.kind === 'entity') base = base.__proto__
165
+ if (base.compositions?.transitions_) {
166
+ entity[$transitions_] = base.compositions.transitions_
167
+ // track changes on db level
168
+ cds.connect.to('db').then(db => {
169
+ db.after(['CREATE', 'UPDATE', 'UPSERT'], entity, async (res, req) => {
170
+ if ((res.affectedRows ?? res) !== 1) return
171
+ if (!(statusInfo.statusElementName in req.data)) return
172
+ const status = req.data[statusInfo.statusElementName]
173
+ const upKeys = await buildUpKeys(entity, req.data, req.subject)
174
+ const last = await SELECT.one.from(entity[$transitions_].target).orderBy('timestamp desc').where(upKeys)
175
+ if (last?.status !== status) await UPSERT.into(entity[$transitions_].target).entries({ ...upKeys, status })
176
+ })
109
177
  })
110
178
  }
111
179
 
112
- let statusEnum, statusElementName
113
- if (statusElement.enum) {
114
- statusEnum = statusElement.enum
115
- statusElementName = statusElement.name
116
- } else if (statusElement?._target?.elements['code']) {
117
- statusEnum = statusElement._target.elements['code'].enum
118
- statusElementName = statusElement.name + '_code'
119
- } else {
120
- cds.error({
121
- code: 409,
122
- message: `Status element in entity ${entity.name} is not an enum and does not have a valid target with code enum.`
123
- })
180
+ // register handlers
181
+ for (const action of entity.actions) {
182
+ const to__ = action[TO]
183
+ const from = action[FROM]
184
+
185
+ // REVISIT: for CRUD and Draft, we could set status in before handlers (on db level) to save roundtrips
186
+ switch (action.name) {
187
+ // CRUD
188
+ case 'CREATE':
189
+ if (to__) on.push([action.name, entity, to___factory(entity, action, statusInfo)])
190
+ break
191
+ case 'READ':
192
+ // nothing to do
193
+ break
194
+ case 'UPDATE':
195
+ if (from) b4.push([action.name, entity, from_factory(entity, action, statusInfo)])
196
+ if (to__) on.push([action.name, entity, to___factory(entity, action, statusInfo)])
197
+ break
198
+ case 'DELETE':
199
+ if (from) b4.push([action.name, entity, from_factory(entity, action, statusInfo)])
200
+ break
201
+ // Draft
202
+ case 'NEW':
203
+ if (to__) on.push(['CREATE', entity.drafts, to___factory(entity.drafts, action, statusInfo)])
204
+ break
205
+ case 'PATCH':
206
+ if (from) b4.push(['UPDATE', entity.drafts, from_factory(entity.drafts, action, statusInfo)])
207
+ if (to__) on.push(['UPDATE', entity.drafts, to___factory(entity.drafts, action, statusInfo)])
208
+ break
209
+ case 'SAVE':
210
+ if (from) b4.push([action.name, entity.drafts, from_factory(entity.drafts, action, statusInfo)])
211
+ if (to__) on.push([action.name, entity.drafts, to___factory(entity, action, statusInfo)])
212
+ break
213
+ case 'EDIT':
214
+ if (from) b4.push([action.name, entity, from_factory(entity, action, statusInfo)])
215
+ if (to__) on.push([action.name, entity, to___factory(entity.drafts, action, statusInfo)])
216
+ break
217
+ case 'DISCARD':
218
+ if (from) b4.push([action.name, entity.drafts, from_factory(entity.drafts, action, statusInfo)])
219
+ break
220
+ // custom actions
221
+ default:
222
+ if (from) b4.push([action.name, entity, from_factory(entity, action, statusInfo)])
223
+ if (to__) on.push([action.name, entity, to___factory(entity, action, statusInfo)])
224
+ }
124
225
  }
125
-
126
- entry.push({ events: fromActions, entity, statusElementName, statusEnum })
127
- exit.push({ events: toActions, entity, statusElementName, statusEnum })
128
226
  }
129
227
 
130
228
  this.prepend(function () {
131
- for (const each of entry) {
132
- this.before(
133
- each.events,
134
- each.entity,
135
- Object.assign(
136
- async function handle_entry_state(req) {
137
- const action = req.target.actions[req.event]
138
- await checkStatus(req, action, each.statusElementName, each.statusEnum)
139
- },
140
- { _initial: true }
141
- )
142
- )
143
- }
144
-
145
- for (const each of exit) {
146
- async function handle_after_create(res, req) {
147
- const parentKeys = Object.keys(req.target.keys)
148
- const entry = {}
149
- for (let i = 0; i < parentKeys.length; i++) {
150
- entry[`up__${parentKeys[i]}`] = req.data[parentKeys[i]]
151
- }
152
- await INSERT.into(req.target.compositions['transitions_'].target).entries({
153
- ...entry,
154
- status: res[each.statusElementName]
155
- })
156
- }
157
- if ('transitions_' in (each.entity.compositions ?? {})) this.after('CREATE', each.entity, handle_after_create)
158
-
159
- async function handle_exit_state(req, next) {
160
- const res = await next()
161
- const action = req.target.actions[req.event]
162
- let toKey = buildToKey(action, each.statusEnum)
163
- if ('transitions_' in (req.target.compositions ?? {})) {
164
- await handleStatusTransitionWithHistory(req, each.statusElementName, toKey, this)
165
- } else {
166
- await this.run(UPDATE(req.subject).with({ [each.statusElementName]: toKey }))
167
- }
168
- return res
169
- }
170
- this.on(each.events, each.entity, handle_exit_state)
171
- }
229
+ for (const each of b4) this.before(...each)
230
+ for (const each of on) this.on(...each)
231
+ for (const each of after) this.after(...each)
172
232
  })
173
233
  })
@@ -327,6 +327,10 @@ const _pick = element => {
327
327
  if (categories.length) return { categories }
328
328
  }
329
329
 
330
+ // List of validation error codes that should cause an early rejection of the request
331
+ // > If one of these codes is found, the request will be rejected right after validation
332
+ const EARLY_REJECT_CODES = { ASSERT_DATA_TYPE: 1, ASSERT_MANDATORY: 1, ASSERT_NOT_NULL: 1 }
333
+
330
334
  async function validate_input(req) {
331
335
  if (!req.target || req.target._unresolved) return // Validation requires resolved targets
332
336
  if (req.event === 'CREATE' && req.target.isDraft) return // already handled in `NEW`, no need in `EDIT`
@@ -357,8 +361,7 @@ async function validate_input(req) {
357
361
  const errs = cds.validate(req.data, req.target, assertOptions)
358
362
  if (errs) {
359
363
  errs.forEach(err => req.error(err))
360
- // data types are incorrect -> stop further processing which could rely on data type
361
- if (errs.some(e => e.message === 'ASSERT_DATA_TYPE')) req.reject()
364
+ if (errs.some(e => e.message in EARLY_REJECT_CODES)) req.reject()
362
365
  else return
363
366
  }
364
367
 
@@ -406,8 +409,7 @@ function validate_action(req) {
406
409
  let errs = cds.validate(data, operation, assertOptions)
407
410
  if (errs) {
408
411
  errs.forEach(err => req.error(err))
409
- // data types are incorrect -> stop further processing which could rely on data type
410
- if (errs.some(e => e.message === 'ASSERT_DATA_TYPE')) req.reject()
412
+ if (errs.some(e => e.message in EARLY_REJECT_CODES)) req.reject()
411
413
  else return
412
414
  }
413
415
 
@@ -2,28 +2,6 @@ const cds = require('../../cds')
2
2
  const { SELECT } = cds.ql
3
3
  const { setEntityContained } = require('./csn')
4
4
 
5
- const getEntityNameFromDeleteCQN = cqn => {
6
- let from
7
- if (cqn && cqn.DELETE && cqn.DELETE.from) {
8
- if (typeof cqn.DELETE.from === 'string') {
9
- from = cqn.DELETE.from
10
- } else if (cqn.DELETE.from.name) {
11
- from = cqn.DELETE.from.name
12
- } else if (cqn.DELETE.from.ref && cqn.DELETE.from.ref.length === 1) {
13
- from = cqn.DELETE.from.ref[0]
14
- }
15
- }
16
- return from
17
- }
18
-
19
- const getEntityNameFromUpdateCQN = cqn => {
20
- return (
21
- (cqn.UPDATE.entity.ref && cqn.UPDATE.entity.ref[0] && (cqn.UPDATE.entity.ref[0].id || cqn.UPDATE.entity.ref[0])) ||
22
- cqn.UPDATE.entity.name ||
23
- cqn.UPDATE.entity
24
- )
25
- }
26
-
27
5
  // scope: simple wheres à la "[{ ref: ['foo'] }, '=', { val: 'bar' }, 'and', ... ]"
28
6
  function where2obj(where, target = null, data = {}) {
29
7
  for (let i = 0; i < where.length; ) {
@@ -89,8 +67,6 @@ const resolveFromSelect = query => {
89
67
  }
90
68
 
91
69
  module.exports = {
92
- getEntityNameFromDeleteCQN,
93
- getEntityNameFromUpdateCQN,
94
70
  where2obj,
95
71
  targetFromPath,
96
72
  resolveFromSelect
@@ -11,7 +11,7 @@ module.exports = value => {
11
11
  if (typeof value === 'number') value = new Date(value).toISOString()
12
12
  if (typeof value !== 'string') {
13
13
  const msg = `Value "${value}" is not a valid Timestamp`
14
- throw Object.assign(new Error(msg), { statusCode: 400 })
14
+ cds.error({ status: 400, message: msg })
15
15
  }
16
16
 
17
17
  const decimalPointIndex = _lengthIfNotFoundIndex(value.lastIndexOf('.'), value.length)
@@ -22,7 +22,7 @@ module.exports = value => {
22
22
  let dt = new Date(value.slice(0, dateEndIndex) + tz)
23
23
  if (isNaN(dt)) {
24
24
  const msg = `Value "${value}" is not a valid Timestamp`
25
- throw Object.assign(new Error(msg), { statusCode: 400 })
25
+ cds.error({ status: 400, message: msg })
26
26
  }
27
27
  const dateNoMillisNoTZ = dt.toISOString().slice(0, 19)
28
28
  const normalizedFractionalDigits = value
@@ -473,8 +473,14 @@ const _newDelete = (query, transitions, options) => {
473
473
  : targetName
474
474
 
475
475
  if (newDelete.where) {
476
- const from = typeof query.DELETE.from === 'string' ? query.DELETE.from : query.DELETE.from.ref[0]
477
- newDelete.where = _newWhere(newDelete.where, targetTransition, from, query.DELETE.from.as, undefined, options)
476
+ newDelete.where = _newWhere(
477
+ newDelete.where,
478
+ targetTransition,
479
+ query.DELETE.from.ref[0],
480
+ query.DELETE.from.as,
481
+ undefined,
482
+ options
483
+ )
478
484
  }
479
485
 
480
486
  return newDelete