@cap-js/audit-logging 0.8.1 → 0.8.3

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 CHANGED
@@ -4,11 +4,23 @@ All notable changes to this project will be documented in this file.
4
4
  This project adheres to [Semantic Versioning](http://semver.org/).
5
5
  The format is based on [Keep a Changelog](http://keepachangelog.com/).
6
6
 
7
+ ## Version 0.8.3 - 2025-04-09
8
+
9
+ - Preperation for `@sap/cds^9`
10
+
11
+ ## Version 0.8.2 - 2024-11-27
12
+
13
+ ### Fixed
14
+
15
+ - Erroneous modification log for non-updated key properties
16
+ - Error during non-modifying queries on database level
17
+ - Specify charset UTF-8 for requests to SAP Audit Log Service
18
+
7
19
  ## Version 0.8.1 - 2024-09-13
8
20
 
9
21
  ### Fixed
10
22
 
11
- - Support for @sap/cds^8.2
23
+ - Support for `@sap/cds^8.2`
12
24
  - Reduce clutter in error raised for outbound requests
13
25
 
14
26
  ## Version 0.8.0 - 2024-05-24
@@ -31,7 +43,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/).
31
43
 
32
44
  ### Added
33
45
 
34
- - Support for @sap/cds^7.5
46
+ - Support for `@sap/cds^7.5`
35
47
 
36
48
  ### Fixed
37
49
 
@@ -0,0 +1,208 @@
1
+ let initializing = false
2
+
3
+ class Relation {
4
+ constructor(csn, path = []) {
5
+ if (!initializing) throw new Error(`Do not new a relation, use 'Relation.to()' instead`)
6
+ Object.defineProperty(this, 'csn', { get: () => csn })
7
+ Object.defineProperty(this, 'path', {
8
+ get: () => path,
9
+ set: _ => {
10
+ path = _
11
+ }
12
+ })
13
+ if (csn.target) Object.defineProperty(this, 'target', { get: () => csn.target })
14
+ initializing = false
15
+ }
16
+
17
+ static to(from, name) {
18
+ initializing = true
19
+ if (!name) return new Relation(from)
20
+ return from._elements[name] && new Relation(from._elements[name], [...from.path, name])
21
+ }
22
+
23
+ _has(prop) {
24
+ return Reflect.has(this, prop)
25
+ }
26
+
27
+ get _elements() {
28
+ if (this.csn.elements) return this.csn.elements
29
+ if (this.csn._target && this.csn._target.elements) return this.csn._target.elements
30
+ // if (csn.targetAspect) relation.elements = model.definitions[csn.targetAspect].elements
31
+ // if (csn.kind = 'type') relation.elements = model.definitions[csn.type].element
32
+ return {}
33
+ }
34
+
35
+ join(fromAlias = '', toAlias = '') {
36
+ return _getOnCond(this.csn, this.path, { select: fromAlias, join: toAlias })
37
+ }
38
+ }
39
+
40
+ const exposeRelation = relation => Object.defineProperty({}, '_', { get: () => relation })
41
+
42
+ const relationHandler = relation => ({
43
+ get: (target, name) => {
44
+ const path = name.split(',')
45
+ const prop = path.join('_')
46
+ if (!target[prop]) {
47
+ if (path.length === 1) {
48
+ // REVISIT: property 'join' must not be used in CSN to make this working
49
+ if (relation._has(prop)) return relation[prop]
50
+ const newRelation = Relation.to(relation, prop)
51
+ if (newRelation) {
52
+ target[prop] = new Proxy(exposeRelation(newRelation), relationHandler(newRelation))
53
+ }
54
+
55
+ return target[prop]
56
+ }
57
+
58
+ target[prop] = path.reduce((relation, value) => relation[value] || relation.csn._relations[value], relation)
59
+ target[prop].path = path
60
+ }
61
+
62
+ return target[prop]
63
+ }
64
+ })
65
+
66
+ module.exports = {
67
+ Relation,
68
+ exposeRelation,
69
+ relationHandler
70
+ }
71
+
72
+ //
73
+ // ----- utils
74
+ //
75
+
76
+ const _prefixForStruct = element => {
77
+ const prefixes = []
78
+ let parent = element.parent
79
+ while (parent && parent.kind !== 'entity') {
80
+ prefixes.push(parent.name)
81
+ parent = parent.parent
82
+ }
83
+ return prefixes.length ? prefixes.reverse().join('_') + '_' : ''
84
+ }
85
+
86
+ const _toRef = (alias, column) => {
87
+ if (Array.isArray(column)) column = column.join('_')
88
+ return { ref: alias ? [alias, column] : [column] }
89
+ }
90
+
91
+ const _adaptRefs = (onCond, path, { select, join }) => {
92
+ const _adaptEl = el => {
93
+ const ref = el.ref
94
+
95
+ if (ref) {
96
+ if (ref[0] === path.join('_') && ref[1]) {
97
+ return _toRef(select, ref.slice(1))
98
+ }
99
+
100
+ // no alias for special $user of canonical localized association
101
+ if (ref[0] === '$user' && path[0] === 'localized') {
102
+ return _toRef(undefined, ref.slice(0))
103
+ }
104
+
105
+ return _toRef(join, ref.slice(0))
106
+ }
107
+
108
+ if (el.xpr) return { xpr: el.xpr.map(_adaptEl) }
109
+ return el
110
+ }
111
+
112
+ return onCond.map(_adaptEl)
113
+ }
114
+
115
+ const _replace$selfAndAliasOnCond = (xpr, csnElement, aliases, path) => {
116
+ const selfIndex = xpr.findIndex(({ ref }) => ref?.[0] === '$self')
117
+ if (selfIndex != -1) {
118
+ let backLinkIndex
119
+ if (xpr[selfIndex + 1] && xpr[selfIndex + 1] === '=') backLinkIndex = selfIndex + 2
120
+ if (xpr[selfIndex - 1] && xpr[selfIndex - 1] === '=') backLinkIndex = selfIndex - 2
121
+ if (backLinkIndex != null) {
122
+ const ref = xpr[backLinkIndex].ref
123
+ const backlinkName = ref[ref.length - 1]
124
+ const mutOnCond = _newOnConditions(csnElement._backlink, [backlinkName], {
125
+ select: aliases.join,
126
+ join: aliases.select
127
+ })
128
+
129
+ xpr.splice(Math.min(backLinkIndex, selfIndex), 3, ...mutOnCond)
130
+ }
131
+ }
132
+
133
+ for (let i = 0; i < xpr.length; i++) {
134
+ const element = xpr[i]
135
+ if (element.xpr) {
136
+ _replace$selfAndAliasOnCond(element.xpr, csnElement, aliases, path)
137
+ continue
138
+ }
139
+
140
+ if (element.ref) {
141
+ if (element.ref[0] === path.join('_') && element.ref[1]) {
142
+ element.ref = _toRef(aliases.select, element.ref.slice(1)).ref
143
+ continue
144
+ }
145
+
146
+ // no alias for special $user of canonical localized association
147
+ if (element.ref[0] === '$user' && path[0] === 'localized') {
148
+ element.ref = _toRef(undefined, element.ref.slice(0)).ref
149
+ continue
150
+ }
151
+ //no alias for special $now variable
152
+ if (element.ref[0] === '$now') {
153
+ continue
154
+ }
155
+
156
+ if (element.ref[0] === aliases.join || element.ref[0] === aliases.select) {
157
+ // nothing todo here, as already right alias
158
+ continue
159
+ }
160
+
161
+ element.ref = _toRef(aliases.join, element.ref.slice(0)).ref
162
+ }
163
+ }
164
+ }
165
+
166
+ const _args = (csnElement, path, aliases) => {
167
+ const onCond = csnElement.on
168
+ if (!onCond || onCond.length === 0) return []
169
+ if (onCond.length < 3 && !onCond[0]?.xpr) return onCond
170
+ if (!csnElement._isSelfManaged) return _adaptRefs(onCond, path, aliases)
171
+
172
+ const onCondCopy = JSON.parse(JSON.stringify(onCond))
173
+ _replace$selfAndAliasOnCond(onCondCopy, csnElement, aliases, path)
174
+
175
+ return onCondCopy
176
+ }
177
+
178
+ // this is only for 2one managed w/o on-conditions, i.e. no static values are possible
179
+ const _foreignToOn = (csnElement, path, { select, join }) => {
180
+ const on = []
181
+
182
+ for (const key of csnElement._foreignKeys) {
183
+ if (on.length !== 0) {
184
+ on.push('and')
185
+ }
186
+
187
+ const prefixChild = _prefixForStruct(key.childElement)
188
+ const ref1 = _toRef(select, prefixChild + key.childElement.name)
189
+ const structPrefix = path.length > 1 ? path.slice(0, -1) : []
190
+ const ref2 = _toRef(join, [...structPrefix, key.parentElement.name])
191
+ on.push(ref1, '=', ref2)
192
+ }
193
+
194
+ return on
195
+ }
196
+
197
+ const _newOnConditions = (csnElement, path, aliases) => {
198
+ if (csnElement.keys) {
199
+ return _foreignToOn(csnElement, path, aliases)
200
+ }
201
+
202
+ return _args(csnElement, path, aliases)
203
+ }
204
+
205
+ const _getOnCond = (csnElement, path = [], aliases = { select: '', join: '' }) => {
206
+ const onCond = _newOnConditions(csnElement, path, aliases)
207
+ return [{ xpr: onCond }]
208
+ }
@@ -76,7 +76,7 @@ const addDiffToCtx = async function (req) {
76
76
  if (!_audit.diffs) _audit.diffs = new Map()
77
77
 
78
78
  // get diff
79
- let diff = await req.diff()
79
+ let diff = (await req.diff()) || {}
80
80
  diff = _getDataWithAppliedTransitions(diff, req)
81
81
 
82
82
  // add keys, if necessary
@@ -88,9 +88,9 @@ const addDiffToCtx = async function (req) {
88
88
  }
89
89
  addDiffToCtx._initial = true
90
90
 
91
- const _getOldAndNew = (action, row, key) => {
91
+ const _getOldAndNew = (action, row, key, entity) => {
92
92
  let oldValue = action === 'Create' ? null : row._old && row._old[key]
93
- if (oldValue === undefined) oldValue = null
93
+ if (oldValue === undefined) oldValue = action === 'Update' && key in entity.keys ? row[key] : null
94
94
  else if (Array.isArray(oldValue)) oldValue = JSON.stringify(oldValue)
95
95
  let newValue = action === 'Delete' ? null : row[key]
96
96
  if (newValue === undefined) newValue = null
@@ -98,9 +98,9 @@ const _getOldAndNew = (action, row, key) => {
98
98
  return { oldValue, newValue }
99
99
  }
100
100
 
101
- const _addAttribute = (log, action, row, key) => {
101
+ const _addAttribute = (log, action, row, key, entity) => {
102
102
  if (!log.attributes.find(ele => ele.name === key)) {
103
- const { oldValue, newValue } = _getOldAndNew(action, row, key)
103
+ const { oldValue, newValue } = _getOldAndNew(action, row, key, entity)
104
104
  if (oldValue !== newValue) {
105
105
  const attr = { name: key }
106
106
  if (action !== 'Create') attr.old = oldValue
@@ -139,7 +139,7 @@ const _processorFnModification = (modificationLogs, model, req, beforeWrite) =>
139
139
  } else if (category === 'DataSubjectID') {
140
140
  addDataSubject(modificationLog, row, key, entity)
141
141
  } else if (category === 'IsPotentiallyPersonal' || category === 'IsPotentiallySensitive') {
142
- _addAttribute(modificationLog, action, row, key)
142
+ _addAttribute(modificationLog, action, row, key, entity)
143
143
  // do not log the value of a sensitive attribute
144
144
  if (element['@PersonalData.IsPotentiallySensitive']) _maskAttribute(modificationLog.attributes, key)
145
145
  }
package/lib/utils.js CHANGED
@@ -1,5 +1,7 @@
1
1
  const cds = require('@sap/cds')
2
2
 
3
+ const { Relation, exposeRelation, relationHandler } = require('./_relation')
4
+
3
5
  const WRITE = { CREATE: 1, UPDATE: 1, DELETE: 1 }
4
6
 
5
7
  const $hasPersonalData = Symbol('@cap-js/audit-logging:hasPersonalData')
@@ -9,7 +11,7 @@ const $visitedUp = Symbol('@cap-js/audit-logging:visitedUp')
9
11
  const $visitedDown = Symbol('@cap-js/audit-logging:visitedDown')
10
12
 
11
13
  const hasPersonalData = entity => {
12
- if (!entity.own($hasPersonalData)) {
14
+ if (entity.own($hasPersonalData) == null) {
13
15
  if (!entity['@PersonalData.EntitySemantics']) entity.set($hasPersonalData, false)
14
16
  else {
15
17
  // default role to entity name
@@ -121,7 +123,22 @@ const _buildSubSelect = (model, { entity, relative, element, next }, row, previo
121
123
  const targetAlias = _alias(element._target)
122
124
  const relativeAlias = _alias(relative)
123
125
 
124
- childCqn.where(relative._relations[element.name].join(targetAlias, relativeAlias))
126
+ if (!('_relations' in relative)) {
127
+ const newRelation = Relation.to(relative)
128
+ relative._relations = new Proxy(exposeRelation(newRelation), relationHandler(newRelation))
129
+ }
130
+
131
+ let w = relative._relations[element.name].join(targetAlias, relativeAlias)
132
+
133
+ // REVISIT: rewrite to path expression, if alias for relative is already used in subselect to avoid sql error
134
+ if (previousCqn?._aliases.has(relativeAlias)) {
135
+ let t
136
+ for (const a in entity.associations) if (entity.associations[a].target === relative.name) t = entity.associations[a]
137
+ if (t && w[0]?.xpr) for (const ele of w[0].xpr) if (ele.ref?.[0] === relativeAlias) ele.ref.splice(0, 1, as, t.name)
138
+ }
139
+ childCqn._aliases = new Set(previousCqn ? [...previousCqn._aliases.values(), as] : [as])
140
+
141
+ childCqn.where(w)
125
142
 
126
143
  if (previousCqn) childCqn.where('exists', previousCqn)
127
144
  else childCqn.where(_addKeysToWhere(keys, row, as))
@@ -147,22 +164,24 @@ const _getDataSubjectIdQuery = ({ dataSubjectEntity, subs }, row, model) => {
147
164
  }
148
165
 
149
166
  const _getUps = (entity, model) => {
150
- if (entity.own($parents)) return entity[$parents]
151
- const ups = []
152
- for (const def of Object.values(model.definitions)) {
153
- if (def.kind !== 'entity' || !def.associations) continue
154
- for (const element of Object.values(def.associations)) {
155
- if (element.target !== entity.name || element._isBacklink || element.name === 'SiblingEntity') continue
156
- ups.push(element)
167
+ if (entity.own($parents) == null) {
168
+ const ups = []
169
+ for (const def of Object.values(model.definitions)) {
170
+ if (def.kind !== 'entity' || !def.associations) continue
171
+ for (const element of Object.values(def.associations)) {
172
+ if (element.target !== entity.name || element._isBacklink || element.name === 'SiblingEntity') continue
173
+ ups.push(element)
174
+ }
157
175
  }
176
+ entity.set($parents, ups)
158
177
  }
159
- return entity.set($parents, ups)
178
+ return entity.own($parents)
160
179
  }
161
180
 
162
181
  const _getDataSubjectUp = (root, model, entity, prev, next, result) => {
163
182
  for (const element of _getUps(entity, model)) {
164
183
  // cycle detection
165
- if (!element.own($visitedUp)) element.set($visitedUp, new Set())
184
+ if (element.own($visitedUp) == null) element.set($visitedUp, new Set())
166
185
  if (element.own($visitedUp).has(root)) continue
167
186
  element.own($visitedUp).add(root)
168
187
 
@@ -192,7 +211,7 @@ const _getDataSubjectDown = (root, entity, prev, next) => {
192
211
  }
193
212
  for (const element of associations) {
194
213
  // cycle detection
195
- if (!element.own($visitedDown)) element.set($visitedDown, new Set())
214
+ if (element.own($visitedDown) == null) element.set($visitedDown, new Set())
196
215
  if (element.own($visitedDown).has(root)) continue
197
216
  element.own($visitedDown).add(root)
198
217
 
@@ -204,12 +223,14 @@ const _getDataSubjectDown = (root, entity, prev, next) => {
204
223
  }
205
224
 
206
225
  const getDataSubject = (entity, model) => {
207
- if (entity.own($dataSubject)) return entity[$dataSubject]
208
- // entities with EntitySemantics 'DataSubjectDetails' or 'Other' must not necessarily
209
- // be always below or always above 'DataSubject' entity in CSN tree
210
- let dataSubjectInfo = _getDataSubjectUp(entity.name, model, entity)
211
- if (!dataSubjectInfo) dataSubjectInfo = _getDataSubjectDown(entity.name, entity)
212
- return entity.set($dataSubject, dataSubjectInfo)
226
+ if (entity.own($dataSubject) == null) {
227
+ // entities with EntitySemantics 'DataSubjectDetails' or 'Other' must not necessarily
228
+ // be always below or always above 'DataSubject' entity in CSN tree
229
+ let dataSubjectInfo = _getDataSubjectUp(entity.name, model, entity)
230
+ if (!dataSubjectInfo) dataSubjectInfo = _getDataSubjectDown(entity.name, entity)
231
+ entity.set($dataSubject, dataSubjectInfo)
232
+ }
233
+ return entity.own($dataSubject)
213
234
  }
214
235
 
215
236
  const _getDataSubjectsMap = req => {
@@ -235,20 +256,29 @@ const addDataSubjectForDetailsEntity = (row, log, req, entity, model) => {
235
256
  else map.set(role, _getDataSubjectIdQuery(dataSubjectInfo, row, model))
236
257
  }
237
258
 
238
- const resolveDataSubjects = async (logs, req) => {
259
+ const resolveDataSubjects = (logs, req) => {
260
+ const ps = []
261
+
239
262
  const map = _getDataSubjectsMap(req)
263
+
240
264
  for (const each of Object.values(logs)) {
241
265
  if (each.data_subject.id instanceof cds.ql.Query) {
242
266
  const q = each.data_subject.id
243
- if (map.has(q)) {
244
- each.data_subject.id = map.get(q)
245
- } else {
246
- const res = await q
247
- map.set(q, res)
248
- each.data_subject.id = res
267
+ if (!map.has(q)) {
268
+ const p = cds.run(q).then(res => map.set(q, res))
269
+ map.set(q, p)
270
+ ps.push(p)
249
271
  }
250
272
  }
251
273
  }
274
+
275
+ return Promise.all(ps).then(() => {
276
+ for (const each of Object.values(logs)) {
277
+ if (each.data_subject.id instanceof cds.ql.Query) {
278
+ each.data_subject.id = map.get(each.data_subject.id)
279
+ }
280
+ }
281
+ })
252
282
  }
253
283
 
254
284
  module.exports = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap-js/audit-logging",
3
- "version": "0.8.1",
3
+ "version": "0.8.3",
4
4
  "description": "CDS plugin providing integration to the SAP Audit Log service as well as out-of-the-box personal data-related audit logging based on annotations.",
5
5
  "repository": "cap-js/audit-logging",
6
6
  "author": "SAP SE (https://www.sap.com)",
package/srv/log2restv2.js CHANGED
@@ -80,7 +80,7 @@ module.exports = class AuditLog2RESTv2 extends AuditLogService {
80
80
  }
81
81
 
82
82
  async _send(data, path) {
83
- const headers = { 'content-type': 'application/json' }
83
+ const headers = { 'content-type': 'application/json;charset=utf-8' }
84
84
  if (this._vcap) {
85
85
  headers.XS_AUDIT_ORG = this._vcap.organization_name
86
86
  headers.XS_AUDIT_SPACE = this._vcap.space_name