@sap/cds 5.6.4 → 5.7.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 (176) hide show
  1. package/CHANGELOG.md +102 -0
  2. package/_i18n/i18n_fr.properties +4 -4
  3. package/apis/cds.d.ts +7 -10
  4. package/apis/connect.d.ts +3 -3
  5. package/apis/core.d.ts +2 -4
  6. package/apis/models.d.ts +2 -3
  7. package/apis/ql.d.ts +0 -1
  8. package/apis/services.d.ts +3 -3
  9. package/bin/build/buildTaskFactory.js +16 -10
  10. package/bin/build/buildTaskProviderFactory.js +3 -3
  11. package/bin/build/constants.js +2 -1
  12. package/bin/build/provider/buildTaskProviderInternal.js +14 -14
  13. package/bin/build/provider/hana/2migration.js +2 -3
  14. package/bin/build/provider/hana/index.js +34 -0
  15. package/bin/build/provider/hana/migrationtable.js +90 -22
  16. package/bin/build/provider/hana/template/undeploy.json +5 -0
  17. package/bin/build/provider/node-cf/index.js +9 -2
  18. package/bin/serve.js +16 -18
  19. package/lib/compile/cdsc.js +15 -5
  20. package/lib/compile/etc/_localized.js +4 -4
  21. package/lib/compile/extend.js +8 -0
  22. package/lib/compile/index.js +3 -1
  23. package/lib/compile/minify.js +61 -0
  24. package/lib/compile/resolve.js +4 -1
  25. package/lib/compile/to/gql.js +9 -0
  26. package/lib/compile/to/sql.js +26 -30
  27. package/lib/connect/index.js +1 -1
  28. package/lib/core/entities.js +0 -3
  29. package/lib/core/infer.js +1 -0
  30. package/lib/core/reflect.js +0 -34
  31. package/lib/deploy.js +25 -17
  32. package/lib/env/defaults.js +3 -1
  33. package/lib/env/index.js +8 -3
  34. package/lib/env/presets.js +38 -0
  35. package/lib/env/requires.js +16 -11
  36. package/lib/index.js +13 -11
  37. package/lib/log/format/kibana.js +3 -1
  38. package/lib/log/index.js +2 -2
  39. package/lib/req/cds-context.js +79 -0
  40. package/lib/req/context.js +5 -77
  41. package/lib/req/request.js +1 -1
  42. package/lib/serve/Service-api.js +8 -4
  43. package/lib/serve/Service-dispatch.js +0 -7
  44. package/lib/serve/Service-methods.js +6 -8
  45. package/lib/serve/Transaction.js +35 -30
  46. package/lib/serve/adapters.js +1 -4
  47. package/lib/utils/axios.js +1 -1
  48. package/libx/_runtime/audit/Service.js +44 -20
  49. package/libx/_runtime/audit/generic/personal/access.js +16 -11
  50. package/libx/_runtime/audit/generic/personal/modification.js +5 -5
  51. package/libx/_runtime/audit/generic/personal/utils.js +46 -37
  52. package/libx/_runtime/{common/auth → auth}/index.js +21 -7
  53. package/libx/_runtime/{common/auth → auth}/strategies/JWT.js +2 -2
  54. package/libx/_runtime/{common/auth → auth}/strategies/basic.js +2 -2
  55. package/libx/_runtime/{common/auth → auth}/strategies/dummy.js +1 -1
  56. package/libx/_runtime/{common/auth → auth}/strategies/mock.js +2 -2
  57. package/libx/_runtime/{common/auth → auth}/strategies/utils/uaa.js +1 -1
  58. package/libx/_runtime/{common/auth → auth}/strategies/utils/xssec.js +0 -0
  59. package/libx/_runtime/{common/auth → auth}/strategies/xsuaa.js +2 -2
  60. package/libx/_runtime/cds-services/adapter/odata-v4/Dispatcher.js +7 -2
  61. package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +0 -7
  62. package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +0 -8
  63. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +3 -4
  64. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +6 -7
  65. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/delete.js +3 -4
  66. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +2 -11
  67. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +16 -6
  68. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +26 -65
  69. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +0 -7
  70. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +3 -66
  71. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/ExpressionToCQN.js +24 -0
  72. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/utils.js +2 -2
  73. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriParser.js +13 -10
  74. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/invocation/ConditionalRequestControlCommand.js +0 -7
  75. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/validator/ConditionalRequestValidator.js +0 -8
  76. package/libx/_runtime/cds-services/adapter/odata-v4/utils/stream.js +54 -76
  77. package/libx/_runtime/cds-services/adapter/rest/RestRequest.js +0 -7
  78. package/libx/_runtime/cds-services/adapter/rest/handlers/create.js +3 -6
  79. package/libx/_runtime/cds-services/adapter/rest/handlers/delete.js +3 -6
  80. package/libx/_runtime/cds-services/adapter/rest/handlers/operation.js +3 -6
  81. package/libx/_runtime/cds-services/adapter/rest/handlers/read.js +3 -6
  82. package/libx/_runtime/cds-services/adapter/rest/handlers/update.js +3 -6
  83. package/libx/_runtime/cds-services/adapter/rest/utils/parse-url.js +8 -4
  84. package/libx/_runtime/cds-services/services/Service.js +0 -6
  85. package/libx/_runtime/cds-services/services/utils/columns.js +10 -3
  86. package/libx/_runtime/cds-services/services/utils/compareJson.js +3 -6
  87. package/libx/_runtime/cds-services/services/utils/differ.js +4 -1
  88. package/libx/_runtime/cds-services/services/utils/handlerUtils.js +1 -41
  89. package/libx/_runtime/cds-services/util/assert.js +1 -262
  90. package/libx/_runtime/cds.js +6 -9
  91. package/libx/_runtime/common/aspects/entity.js +1 -1
  92. package/libx/_runtime/common/composition/delete.js +4 -2
  93. package/libx/_runtime/common/composition/update.js +22 -38
  94. package/libx/_runtime/common/composition/utils.js +3 -7
  95. package/libx/_runtime/common/error/standardError.js +11 -0
  96. package/libx/_runtime/common/generic/auth.js +61 -30
  97. package/libx/_runtime/common/generic/crud.js +11 -23
  98. package/libx/_runtime/common/generic/input.js +20 -0
  99. package/libx/_runtime/common/generic/put.js +4 -10
  100. package/libx/_runtime/common/generic/sorting.js +12 -30
  101. package/libx/_runtime/common/perf/index.js +24 -0
  102. package/libx/_runtime/common/utils/cqn.js +58 -1
  103. package/libx/_runtime/common/utils/cqn2cqn4sql.js +289 -114
  104. package/libx/_runtime/common/utils/csn.js +38 -56
  105. package/libx/_runtime/common/utils/entityFromCqn.js +6 -6
  106. package/libx/_runtime/common/utils/resolveView.js +4 -5
  107. package/libx/_runtime/common/utils/rewriteAsterisks.js +46 -5
  108. package/libx/_runtime/common/utils/search2cqn4sql.js +21 -9
  109. package/libx/_runtime/common/utils/structured.js +35 -25
  110. package/libx/_runtime/db/Service.js +0 -6
  111. package/libx/_runtime/db/expand/expand-v2.js +130 -0
  112. package/libx/_runtime/db/expand/expandCQNToJoin.js +38 -52
  113. package/libx/_runtime/db/expand/index.js +3 -1
  114. package/libx/_runtime/db/generic/input.js +52 -10
  115. package/libx/_runtime/db/generic/integrity.js +367 -26
  116. package/libx/_runtime/db/generic/virtual.js +51 -13
  117. package/libx/_runtime/db/query/update.js +9 -3
  118. package/libx/_runtime/db/sql-builder/ExpressionBuilder.js +8 -9
  119. package/libx/_runtime/{common → db}/utils/propagateForeignKeys.js +11 -14
  120. package/libx/_runtime/fiori/generic/activate.js +1 -0
  121. package/libx/_runtime/fiori/generic/before.js +2 -1
  122. package/libx/_runtime/fiori/generic/edit.js +1 -0
  123. package/libx/_runtime/fiori/generic/patch.js +1 -1
  124. package/libx/_runtime/fiori/generic/read.js +123 -57
  125. package/libx/_runtime/fiori/uiflex/index.js +1 -1
  126. package/libx/_runtime/fiori/uiflex/{extensibility/index.js → service.js} +3 -3
  127. package/libx/_runtime/fiori/utils/delete.js +7 -1
  128. package/libx/_runtime/hana/Service.js +1 -8
  129. package/libx/_runtime/hana/customBuilder/CustomSelectBuilder.js +5 -14
  130. package/libx/_runtime/hana/execute.js +10 -4
  131. package/libx/_runtime/hana/pool.js +55 -45
  132. package/libx/_runtime/hana/search.js +7 -6
  133. package/libx/_runtime/hana/search2cqn4sql.js +8 -5
  134. package/libx/_runtime/hana/searchToContains.js +3 -1
  135. package/libx/_runtime/index.js +5 -5
  136. package/libx/_runtime/messaging/AMQPWebhookMessaging.js +3 -3
  137. package/libx/_runtime/messaging/Outbox.js +53 -0
  138. package/libx/_runtime/messaging/common-utils/AMQPClient.js +17 -10
  139. package/libx/_runtime/messaging/common-utils/connections.js +14 -9
  140. package/libx/_runtime/messaging/common-utils/waitingTime.js +2 -0
  141. package/libx/_runtime/messaging/enterprise-messaging-shared.js +2 -3
  142. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +2 -2
  143. package/libx/_runtime/messaging/enterprise-messaging.js +21 -15
  144. package/libx/_runtime/messaging/file-based.js +5 -5
  145. package/libx/_runtime/messaging/message-queuing.js +2 -3
  146. package/libx/_runtime/messaging/outbox/OutboxRunner.js +75 -0
  147. package/libx/_runtime/messaging/outbox/utils.js +192 -0
  148. package/libx/_runtime/messaging/service.js +16 -30
  149. package/libx/_runtime/remote/Service.js +15 -0
  150. package/libx/_runtime/remote/utils/client.js +15 -3
  151. package/libx/_runtime/remote/utils/{dataConversion.js → data.js} +12 -2
  152. package/libx/_runtime/sqlite/Service.js +7 -10
  153. package/libx/_runtime/sqlite/customBuilder/CustomExpressionBuilder.js +19 -0
  154. package/libx/_runtime/sqlite/execute.js +18 -12
  155. package/libx/_runtime/types/api.js +2 -1
  156. package/libx/odata/{odata2cqn/afterburner.js → afterburner.js} +19 -15
  157. package/libx/odata/{cqn2odata/index.js → cqn2odata.js} +1 -1
  158. package/libx/odata/{odata2cqn/grammar.pegjs → grammar.pegjs} +171 -130
  159. package/libx/odata/index.js +16 -14
  160. package/libx/odata/parser.js +1 -0
  161. package/libx/odata/utils.js +57 -0
  162. package/libx/rest/RestAdapter.js +2 -6
  163. package/libx/rest/utils/data.js +1 -6
  164. package/package.json +4 -3
  165. package/server.js +4 -5
  166. package/srv/audit-log.cds +87 -0
  167. package/{libx/_runtime/fiori/uiflex/extensibility/index.cds → srv/flex.cds} +0 -0
  168. package/srv/flex.js +1 -0
  169. package/srv/outbox.cds +11 -0
  170. package/srv/outbox.js +0 -0
  171. package/libx/_runtime/cds-services/adapter/perf/performance.js +0 -104
  172. package/libx/_runtime/cds-services/adapter/perf/performanceMeasurement.js +0 -33
  173. package/libx/odata/odata2cqn/index.js +0 -3
  174. package/libx/odata/odata2cqn/parser.js +0 -1
  175. package/libx/odata/readme.md +0 -1
  176. package/libx/odata/utils/index.js +0 -64
@@ -1,39 +1,38 @@
1
- const cds = require('../index'), { Context } = cds.Request, { cds_tx_protection } = cds.env.features
2
- const _context = Symbol()
1
+ const cds = require('../index'), { EventContext } = cds, { cds_tx_protection } = cds.env.features
3
2
 
4
3
  /**
5
4
  * This is the implementation of the `srv.tx(req)` method. It constructs
6
5
  * a new Transaction as a derivate of the `srv` (i.e. {__proto__:srv})
7
6
  * @returns { Transaction & import('./Service-api') }
7
+ * @param { EventContext } ctx
8
8
  */
9
- module.exports = function tx (req,fn) { const srv = this
10
- if (srv.context) return srv
11
- if (!req) {
12
- // called as srv.tx() -> new root transaction
13
- return RootTransaction.for (srv, Context.new())
14
- }
15
- if (typeof req === 'function') [ req, fn ] = [ Context.new(), req ]
9
+ module.exports = function tx (ctx,fn) { const srv = this
10
+
11
+ if (srv.context) return srv // srv.tx().tx() -> idempotent
12
+
13
+ // Last arg may be a function -> srv.tx (tx => { ... })
14
+ if (typeof ctx === 'function') [ ctx, fn ] = [ undefined, ctx ]
16
15
  if (typeof fn === 'function') {
17
- // auto-committed transaction, i.e. cds.tx (tx => {...})
18
- const tx = srv.tx(req)
16
+ const tx = srv.tx(ctx)
19
17
  return Promise.resolve(tx).then(fn) .then (tx.commit,tx.rollback)
20
18
  }
21
- if (req instanceof Context) {
22
- // called for a nested req -> nested tx
23
- if (req.context !== req) return NestedTransaction.for (srv, req.context)
24
- // called for a req with a root tx -> nested tx
25
- if (req._tx) return NestedTransaction.for (srv, req)
26
- // called for a top-level req -> root tx
27
- else return RootTransaction.for (srv, req)
19
+
20
+ // This is for compatibility with cds.tx(req)
21
+ if (ctx instanceof EventContext) {
22
+ if (ctx.context !== ctx) return NestedTransaction.for (srv, ctx.context)
23
+ if (ctx._tx) return NestedTransaction.for (srv, ctx)
24
+ else return RootTransaction.for (srv, ctx)
28
25
  }
29
- if (req[_context]) {
30
- // called again for an arbitrary context object -> see below
31
- return NestedTransaction.for (srv, req[_context])
32
- } else {
33
- // called first time for an arbitrary context object
34
- const root = Context.new(req); Object.defineProperty (req, _context, {value:root})
35
- return RootTransaction.for (srv, root)
26
+
27
+ // `ctx` is a plain context object or undefined
28
+ if (ctx) { // REVISIT: This is for compatibility with AFC only
29
+ if (ctx._txed_before) return NestedTransaction.for (srv, ctx._txed_before)
30
+ else {
31
+ Object.defineProperty(ctx, '_txed_before', { value: EventContext.for(ctx) }) // > must be non-enumerable
32
+ return RootTransaction.for (srv, ctx._txed_before)
33
+ }
36
34
  }
35
+ return RootTransaction.for (srv, EventContext.for(ctx))
37
36
  }
38
37
 
39
38
 
@@ -59,8 +58,8 @@ class Transaction {
59
58
  }
60
59
 
61
60
  /**
62
- * In addition to srv.commit, resets the transaction to initial state,
63
- * in order to re-start on subsequently dispatched events.
61
+ * In addition to srv.commit, sets the transaction to committed state,
62
+ * in order to prevent continuous use without explicit reopen (i.e., begin).
64
63
  */
65
64
  async commit (res) {
66
65
  if (this.ready) { //> nothing to do if no transaction started at all
@@ -71,10 +70,13 @@ class Transaction {
71
70
  }
72
71
 
73
72
  /**
74
- * In addition to srv.rollback, resets the transaction to initial state,
75
- * in order to re-start on subsequently dispatched events.
73
+ * In addition to srv.rollback, sets the transaction to rolled back state,
74
+ * in order to prevent continuous use without explicit reopen (i.e., begin).
76
75
  */
77
76
  async rollback (err) {
77
+ // nothing to do if transaction already rolled back or committed (committed occurs if error thrown in on succeeded handler)
78
+ if (this.ready === 'rolled back' || this.ready === 'committed') return
79
+
78
80
  /*
79
81
  * srv.on('error', function (err, req) { ... })
80
82
  * synchroneous modification of passed error only
@@ -103,7 +105,7 @@ class RootTransaction extends Transaction {
103
105
 
104
106
  /**
105
107
  * In addition to srv.commit, ensures all nested transactions
106
- * are informed by emitting 'succeesed' event to them all.
108
+ * are informed by emitting 'succeeded' event to them all.
107
109
  */
108
110
  async commit (res) {
109
111
  if (cds_tx_protection) this.context._done = 'committed'
@@ -122,6 +124,9 @@ class RootTransaction extends Transaction {
122
124
  * are informed by emitting 'failed' event to them all.
123
125
  */
124
126
  async rollback (err) {
127
+ // nothing to do if transaction already rolled back (we need to check here as well to not emit failed twice)
128
+ if (this.ready === 'rolled back') return
129
+
125
130
  if (cds_tx_protection) this.context._done = 'rolled back'
126
131
  try {
127
132
  await this.context.emit ('failed',err)
@@ -32,10 +32,7 @@ class ProtocolAdapter {
32
32
  */
33
33
  in (app) {
34
34
  const srv = this.service
35
- if (!app._perf_measured) {
36
- lib.performanceMeasurement (app)
37
- app._perf_measured = true
38
- }
35
+ lib.perf (app)
39
36
  lib.auth (srv, app, srv.options)
40
37
  app.use (srv.path+'/webapp/', (_,res)=> res.sendStatus(404))
41
38
  app.use (srv.path, this)
@@ -45,7 +45,7 @@ const _error = (e) => {
45
45
  }
46
46
  if (!e.response) throw e
47
47
  if (!e.response.data) throw e
48
- if (!e.response.data.error) throw new Error(e.message + '\n\n' + e.response.data)
48
+ if (!e.response.data.error) throw new Error(e.message + '\n\n' + JSON.stringify(e.response.data, null, 2))
49
49
  const { code, message } = e.response.data.error
50
50
  throw new Error (code && code !== 'null' ? `${code} - ${message}` : message)
51
51
  }
@@ -1,5 +1,5 @@
1
1
  const cds = require('../cds')
2
- const LOG = cds.log('audit-log')
2
+ const OutboxService = require('../messaging/Outbox')
3
3
 
4
4
  const v2utils = require('./utils/v2')
5
5
 
@@ -10,10 +10,17 @@ const _getTenantAndUser = () => ({
10
10
  tenant: (cds.context && cds.context.tenant) || ANONYMOUS
11
11
  })
12
12
 
13
- module.exports = class AuditLogService extends cds.MessagingService {
13
+ module.exports = class AuditLogService extends OutboxService {
14
14
  async init() {
15
- // call MessagingService's init, which handles outboxing
15
+ // call OutboxService's init, which handles outboxing
16
16
  await super.init()
17
+
18
+ // register handlers
19
+ this.on('dataAccessLog', this.dataAccessLog)
20
+ this.on('dataModificationLog', this.dataModificationLog)
21
+ this.on('securityLog', this.securityLog)
22
+ this.on('configChangeLog', this.configChangeLog)
23
+
17
24
  // connect to audit log service
18
25
  // REVISIT for GA: throw error on connect issue instead of warn and this.ready?
19
26
  this.alc = await v2utils.connect(this.options.credentials)
@@ -25,20 +32,25 @@ module.exports = class AuditLogService extends cds.MessagingService {
25
32
  if (!this.options.outbox) return this.send(event, data)
26
33
 
27
34
  if (this.ready && this[event]) {
28
- // best effort until persistent outbox -> only log the failures
29
35
  try {
30
- await this[event](data)
36
+ // this will open a new tx -> preserve user
37
+ await super.send(new cds.Request({ method: event, data, user: cds.context.user }))
31
38
  } catch (e) {
32
- LOG._warn && LOG.warn(e)
39
+ if (e.code === 'ERR_ASSERTION') {
40
+ e.unrecoverable = true
41
+ }
42
+ throw e
33
43
  }
34
44
  }
35
45
  }
36
46
 
37
47
  async send(event, data) {
38
- if (this.ready && this[event]) return this[event](data)
48
+ if (this.ready && this[event]) return super.send(event, data)
39
49
  }
40
50
 
41
- async dataAccessLog({ accesses }) {
51
+ async dataAccessLog(arg) {
52
+ const accesses = arg.accesses || arg.data.accesses
53
+
42
54
  if (!this.ready) throw new Error('AuditLogService not connected')
43
55
 
44
56
  const { tenant, user } = _getTenantAndUser()
@@ -61,7 +73,9 @@ module.exports = class AuditLogService extends cds.MessagingService {
61
73
  }
62
74
 
63
75
  // REVISIT: modification.action not used in auditlog v2
64
- async dataModificationLog({ modifications }) {
76
+ async dataModificationLog(arg) {
77
+ const modifications = arg.modifications || arg.data.modifications
78
+
65
79
  if (!this.ready) throw new Error('AuditLogService not connected')
66
80
 
67
81
  const { tenant, user } = _getTenantAndUser()
@@ -83,16 +97,19 @@ module.exports = class AuditLogService extends cds.MessagingService {
83
97
  }
84
98
  }
85
99
 
86
- async securityLog({ action, data }) {
100
+ async securityLog(arg) {
101
+ let { action, data } = arg
102
+ if (arg.data && arg.data.action) {
103
+ action = arg.data.action
104
+ data = arg.data.data
105
+ }
106
+
87
107
  if (!this.ready) throw new Error('AuditLogService not connected')
88
108
 
89
- // cds.context not always set on auth-related errors -> try to extract from data
90
- let user, tenant
91
- if (cds.context) {
92
- const tenantAndUser = _getTenantAndUser()
93
- tenant = tenantAndUser.tenant
94
- user = tenantAndUser.user
95
- } else {
109
+ let { tenant, user } = _getTenantAndUser()
110
+
111
+ // cds.context may not be proper on auth-related errors -> try to extract from data
112
+ if (tenant === ANONYMOUS && user === ANONYMOUS) {
96
113
  try {
97
114
  const parsed = JSON.parse(data)
98
115
  if (parsed.tenant) {
@@ -106,8 +123,6 @@ module.exports = class AuditLogService extends cds.MessagingService {
106
123
  data = JSON.stringify(parsed)
107
124
  } catch (e) {}
108
125
  }
109
- if (!tenant) tenant = ANONYMOUS
110
- if (!user) user = ANONYMOUS
111
126
 
112
127
  // build the log
113
128
  const entry = v2utils.buildSecurityLog(this.alc, action, data, tenant, user)
@@ -117,7 +132,16 @@ module.exports = class AuditLogService extends cds.MessagingService {
117
132
  }
118
133
 
119
134
  // REVISIT: action and success not used in auditlog v2
120
- async configChangeLog({ action, success, configurations }) {
135
+ async configChangeLog(arg) {
136
+ let { action, success, configurations } = arg
137
+ if (arg.data) {
138
+ // eslint-disable-next-line no-unused-vars
139
+ action = arg.data.action
140
+ // eslint-disable-next-line no-unused-vars
141
+ success = arg.data.success
142
+ configurations = arg.data.configurations
143
+ }
144
+
121
145
  if (!this.ready) throw new Error('AuditLogService not connected')
122
146
 
123
147
  const { tenant, user } = _getTenantAndUser()
@@ -17,6 +17,8 @@ let als
17
17
 
18
18
  const _processorFnAccess = (accessLogs, model, req) => {
19
19
  return ({ row, key, element, plain }) => {
20
+ if (row.IsActiveEntity === false) return
21
+
20
22
  const entity = getRootEntity(element)
21
23
 
22
24
  // create or augment log entry
@@ -28,7 +30,7 @@ const _processorFnAccess = (accessLogs, model, req) => {
28
30
  addObjectID(accessLog, row, key)
29
31
  } else if (category === 'DataSubjectID') {
30
32
  addDataSubject(accessLog, row, key, entity)
31
- } else if (category === 'IsPotentiallySensitive') {
33
+ } else if (category === 'IsPotentiallySensitive' && key in row) {
32
34
  // add attribute
33
35
  if (!accessLog.attributes.find(ele => ele.name === key)) accessLog.attributes.push({ name: key })
34
36
  // REVISIT: attribute vs. attachment?
@@ -37,10 +39,11 @@ const _processorFnAccess = (accessLogs, model, req) => {
37
39
 
38
40
  // add promise to determine data subject if a DataSubjectDetails entity
39
41
  if (
40
- element.parent['@PersonalData.EntitySemantics'] === 'DataSubjectDetails' &&
42
+ (entity['@PersonalData.EntitySemantics'] === 'DataSubjectDetails' ||
43
+ entity['@PersonalData.EntitySemantics'] === 'Other') &&
41
44
  accessLog.dataSubject.id.length === 0 // > id still an array -> promise not yet set
42
45
  ) {
43
- addDataSubjectForDetailsEntity(row, accessLog, req, entity, model, element)
46
+ addDataSubjectForDetailsEntity(row, accessLog, req, entity, model)
44
47
  }
45
48
  }
46
49
  }
@@ -55,11 +58,13 @@ const _getDataAccessLogs = (data, req, tx) => {
55
58
 
56
59
  const accessLogs = {}
57
60
 
58
- const processFn = _processorFnAccess(accessLogs, tx.model, req)
59
- const data_ = Array.isArray(data) ? data : [data]
60
- data_.forEach(row => {
61
- templateProcessor({ processFn, row, template })
62
- })
61
+ if (typeof data === 'object' && data !== null) {
62
+ const processFn = _processorFnAccess(accessLogs, tx.model, req)
63
+ const data_ = Array.isArray(data) ? data : [data]
64
+ data_.forEach(row => {
65
+ templateProcessor({ processFn, row, template })
66
+ })
67
+ }
63
68
 
64
69
  return accessLogs
65
70
  }
@@ -69,9 +74,9 @@ const auditAccessHandler = async function (data, req) {
69
74
  if (!als.ready) return
70
75
 
71
76
  const accessLogs = _getDataAccessLogs(data, req, this)
72
- const accesses = Object.keys(accessLogs).map(k => accessLogs[k])
73
-
74
- await resolveDataSubjectPromises(accesses)
77
+ // REVISIT: a function called resolveDataSubjectPromises should not also convert an object to an array
78
+ let accesses = await resolveDataSubjectPromises(accessLogs)
79
+ accesses = accesses.filter(ele => ele.attributes.length)
75
80
 
76
81
  if (accesses.length) await als.emit('dataAccessLog', { accesses })
77
82
  }
@@ -73,10 +73,11 @@ const _processorFnModification = (modificationLogs, model, req, beforeWrite) =>
73
73
 
74
74
  // add promise to determine data subject if a DataSubjectDetails entity
75
75
  if (
76
- element.parent['@PersonalData.EntitySemantics'] === 'DataSubjectDetails' &&
76
+ (entity['@PersonalData.EntitySemantics'] === 'DataSubjectDetails' ||
77
+ entity['@PersonalData.EntitySemantics'] === 'Other') &&
77
78
  modificationLog.dataSubject.id.length === 0 // > id still an array -> promise not yet set
78
79
  ) {
79
- addDataSubjectForDetailsEntity(row, modificationLog, req, entity, model, element)
80
+ addDataSubjectForDetailsEntity(row, modificationLog, req, entity, model)
80
81
  }
81
82
  }
82
83
 
@@ -107,7 +108,7 @@ const _calcModificationLogsHandler = async function (req, beforeWrite, that) {
107
108
 
108
109
  // execute the data subject promises before going along to on phase
109
110
  // guarantees that the reads are executed before the data is modified
110
- await Promise.all(Object.keys(modificationLogs).map(k => modificationLogs[k].dataSubject.id))
111
+ await resolveDataSubjectPromises(modificationLogs)
111
112
  }
112
113
 
113
114
  const calcModificationLogsHandler4Before = function (req) {
@@ -125,9 +126,8 @@ const emitModificationHandler = async function (_, req) {
125
126
  const modificationLogs = req.context._audit.modificationLogs.get(req.query)
126
127
  const modifications = Object.keys(modificationLogs)
127
128
  .map(k => modificationLogs[k])
128
- .filter(ele => ele.attributes.length)
129
+ .filter(log => log.attributes.length)
129
130
 
130
- await resolveDataSubjectPromises(modifications)
131
131
  await als.emit('dataModificationLog', { modifications })
132
132
  }
133
133
 
@@ -7,7 +7,7 @@ const ASPECTS = { CREATE: '_auditCreate', READ: '_auditRead', UPDATE: '_auditUpd
7
7
 
8
8
  const getMapKeyForCurrentRequest = req => {
9
9
  // running in srv or db layer? -> srv's req.query used as key of diff and logs maps at req.context
10
- return req._tx.constructor.name.match(/Database$/i) ? req._.query : req.query
10
+ return req._tx instanceof cds.DatabaseService ? req._.query : req.query
11
11
  }
12
12
 
13
13
  const getRootEntity = element => {
@@ -22,11 +22,9 @@ const getPick = event => {
22
22
  const categories = []
23
23
  if (!element.isAssociation && element.key) categories.push('ObjectID')
24
24
  if (
25
+ !element.isAssociation &&
25
26
  element['@PersonalData.FieldSemantics'] === 'DataSubjectID' &&
26
- // item element of arrayed element has no parent, but
27
- // at the moment annotation on item level is not supported
28
- element.parent &&
29
- element.parent['@PersonalData.EntitySemantics'] === 'DataSubject'
27
+ target['@PersonalData.EntitySemantics'] === 'DataSubject'
30
28
  )
31
29
  categories.push('DataSubjectID')
32
30
  if (event in WRITE && element['@PersonalData.IsPotentiallyPersonal']) categories.push('IsPotentiallyPersonal')
@@ -57,7 +55,7 @@ const createLogEntry = (logs, entity, row) => {
57
55
  }
58
56
 
59
57
  const addObjectID = (log, row, key) => {
60
- if (!log.dataObject.id.find(ele => ele.keyName === key))
58
+ if (!log.dataObject.id.find(ele => ele.keyName === key) && key !== 'IsActiveEntity')
61
59
  log.dataObject.id.push({ keyName: key, value: String(row[key]) })
62
60
  }
63
61
 
@@ -69,36 +67,42 @@ const addDataSubject = (log, row, key, entity) => {
69
67
  }
70
68
  }
71
69
 
72
- const _addKeysToWhere = (child, row) => {
73
- const keysWithValue = []
74
- Object.keys(child.keys).forEach(el => {
75
- if (keysWithValue.length > 0) keysWithValue.push('and')
76
- keysWithValue.push({ ref: [child.name, el] }, '=', {
77
- val: row[el]
78
- })
79
- })
80
- return keysWithValue
81
- }
82
-
83
- const _buildSubSelect = (model, child, { element, up }, row, previousCqn) => {
84
- const entity = element.parent
85
- const childCqn = SELECT.from(child.name)
86
- .columns(Object.keys(child.keys))
87
- .where(entity._relations[element.name].join(child.name, entity.name))
70
+ const _addKeysToWhere = (entity, row) =>
71
+ Object.values(entity.keys)
72
+ .filter(key => !key.isAssociation && key.name !== 'IsActiveEntity')
73
+ .reduce((keys, key) => {
74
+ if (keys.length) keys.push('and')
75
+ keys.push({ ref: [entity.name, key.name] }, '=', { val: row[key.name] })
76
+ return keys
77
+ }, [])
78
+
79
+ const _keyColumns = entity =>
80
+ Object.values(entity.keys)
81
+ .filter(key => !key.isAssociation && key.name !== 'IsActiveEntity')
82
+ .map(key => key.name)
83
+
84
+ const _buildSubSelect = (model, { entity, relative, element, next }, row, previousCqn) => {
85
+ // relative is a parent or an entity itself
86
+ const childCqn = SELECT.from(entity.name)
87
+ .columns(_keyColumns(entity))
88
+ .where(relative._relations[element.name].join(element._target.name, relative.name))
88
89
  if (previousCqn) {
89
90
  childCqn.where('exists', previousCqn)
90
91
  } else {
91
- childCqn.where(_addKeysToWhere(child, row))
92
+ childCqn.where(_addKeysToWhere(entity, row))
92
93
  }
93
- if (up) return _buildSubSelect(model, entity, up, {}, childCqn)
94
+ if (next) return _buildSubSelect(model, next, {}, childCqn)
94
95
  return childCqn
95
96
  }
96
97
 
97
- const _getDataSubjectIdPromise = (child, dataSubjectInfo, row, req, model) => {
98
- const root = dataSubjectInfo.entity
99
- const cqn = SELECT.from(root.name)
100
- .columns(Object.keys(root.keys))
101
- .where(['exists', _buildSubSelect(model, child, dataSubjectInfo.up, row)])
98
+ const _getDataSubjectIdPromise = ({ dataSubjectEntity, subs }, row, req, model) => {
99
+ const cqn = SELECT.from(dataSubjectEntity.name)
100
+ .columns(_keyColumns(dataSubjectEntity))
101
+ .where(['exists', _buildSubSelect(model, subs[0], row)])
102
+ // entity reused in different branches => must check all
103
+ for (let i = 1; i < subs.length; i++) {
104
+ cqn.or(['exists', _buildSubSelect(model, subs[i], row)])
105
+ }
102
106
  return cds
103
107
  .tx(req)
104
108
  .run(cqn)
@@ -109,12 +113,12 @@ const _getDataSubjectIdPromise = (child, dataSubjectInfo, row, req, model) => {
109
113
  })
110
114
  }
111
115
 
112
- const addDataSubjectForDetailsEntity = (row, log, req, entity, model, element) => {
116
+ const addDataSubjectForDetailsEntity = (row, log, req, entity, model) => {
113
117
  const role = entity['@PersonalData.DataSubjectRole']
114
118
 
115
- const dataSubjectInfo = getDataSubject(entity, model, role, element)
119
+ const dataSubjectInfo = getDataSubject(entity, model, role)
116
120
 
117
- log.dataSubject.type = dataSubjectInfo.entity.name
121
+ log.dataSubject.type = dataSubjectInfo.dataSubjectEntity.name
118
122
 
119
123
  /*
120
124
  * for each req (cf. $batch with atomicity) and data subject role (e.g., customer vs supplier),
@@ -125,13 +129,18 @@ const addDataSubjectForDetailsEntity = (row, log, req, entity, model, element) =
125
129
  if (!req.context._audit.dataSubjects.has(mapKey)) req.context._audit.dataSubjects.set(mapKey, new Map())
126
130
  const map = req.context._audit.dataSubjects.get(mapKey)
127
131
  if (map.has(role)) log.dataSubject.id = map.get(role)
128
- else map.set(role, _getDataSubjectIdPromise(entity, dataSubjectInfo, row, req, model))
132
+ // REVISIT by downward lookups row might already contain ID - some potential to optimize
133
+ else map.set(role, _getDataSubjectIdPromise(dataSubjectInfo, row, req, model))
129
134
  }
130
135
 
131
- const resolveDataSubjectPromises = async logs => {
132
- const dataSubjPromise = logs.filter(el => el.dataSubject.id instanceof Promise)
133
- const res = await Promise.all(dataSubjPromise.map(el => el.dataSubject.id))
134
- dataSubjPromise.forEach((el, i) => (el.dataSubject.id = res[i]))
136
+ const resolveDataSubjectPromises = log => {
137
+ const logs = Object.values(log)
138
+ return Promise.all(logs.map(log => log.dataSubject.id)).then(IDs =>
139
+ logs.map((log, i) => {
140
+ log.dataSubject.id = IDs[i]
141
+ return log
142
+ })
143
+ )
135
144
  }
136
145
 
137
146
  module.exports = {
@@ -1,8 +1,8 @@
1
- const cds = require('../../cds')
1
+ const cds = require('../cds')
2
2
  const LOG = cds.log('app')
3
3
 
4
- const _require = require('../utils/require')
5
- const { UNAUTHORIZED } = require('../utils/auth')
4
+ const _require = require('../common/utils/require')
5
+ const { UNAUTHORIZED } = require('../common/utils/auth')
6
6
 
7
7
  let passport
8
8
 
@@ -105,7 +105,24 @@ module.exports = (srv, app, options) => {
105
105
  if (config.impl) {
106
106
  // > custom middleware
107
107
  app.use(srv.path, _require(cds.resolve(config.impl)))
108
+ return
109
+ }
110
+
111
+ config.strategy = Array.isArray(config.strategy) ? config.strategy : [config.strategy]
108
112
 
113
+ if (config.strategy.length === 1 && config.strategy[0] in { dummy: 1, mock: 1 }) {
114
+ // > without passport
115
+ const impl =
116
+ config.strategy[0] === 'dummy'
117
+ ? new (require('./strategies/dummy'))()
118
+ : new (require('./strategies/mock'))(config.users, `mock_${srv.name}`)
119
+ app.use(srv.path, (req, res, next) => {
120
+ let user, challenge
121
+ impl.success = arg => (user = arg)
122
+ impl.fail = arg => (challenge = arg)
123
+ impl.authenticate(req)
124
+ _callback(req, res, next, undefined, user, [challenge])
125
+ })
109
126
  return
110
127
  }
111
128
 
@@ -113,10 +130,7 @@ module.exports = (srv, app, options) => {
113
130
  passport = passport || _require('passport')
114
131
 
115
132
  // initialize strategies
116
- config.strategy = Array.isArray(config.strategy) ? config.strategy : [config.strategy]
117
- for (const strategy of config.strategy) {
118
- _initializeStrategy(strategy, config, srv)
119
- }
133
+ for (const strategy of config.strategy) _initializeStrategy(strategy, config, srv)
120
134
 
121
135
  // authenticate
122
136
  app.use(srv.path, passport.initialize())
@@ -1,6 +1,6 @@
1
- const cds = require('../../../cds')
1
+ const cds = require('../../cds')
2
2
 
3
- const _require = require('../../utils/require')
3
+ const _require = require('../../common/utils/require')
4
4
  const uaaUtils = require('./utils/uaa')
5
5
  const xssecUtils = require('./utils/xssec')
6
6
 
@@ -1,6 +1,6 @@
1
- const cds = require('../../../cds')
1
+ const cds = require('../../cds')
2
2
 
3
- const _require = require('../../utils/require')
3
+ const _require = require('../../common/utils/require')
4
4
 
5
5
  // use _require for a better error message
6
6
  const { BasicStrategy: BS } = _require('passport-http')
@@ -1,4 +1,4 @@
1
- const cds = require('../../../cds')
1
+ const cds = require('../../cds')
2
2
 
3
3
  class DummyStrategy {
4
4
  constructor() {
@@ -1,4 +1,4 @@
1
- const cds = require('../../../cds')
1
+ const cds = require('../../cds')
2
2
 
3
3
  const CHALLENGE = 'Basic realm="Users"'
4
4
 
@@ -56,7 +56,7 @@ class MockStrategy {
56
56
 
57
57
  const [scheme, base64] = authorization.split(' ')
58
58
  if (!scheme || scheme.toLowerCase() !== 'basic') return this.fail(CHALLENGE)
59
- if (!base64) return this.fail(400)
59
+ if (!base64) return this.fail(CHALLENGE)
60
60
 
61
61
  const [id, password] = Buffer.from(base64, 'base64').toString().split(':')
62
62
 
@@ -1,4 +1,4 @@
1
- const cds = require('../../../../cds')
1
+ const cds = require('../../../cds')
2
2
 
3
3
  const getCredentials = uaa => {
4
4
  uaa =
@@ -1,6 +1,6 @@
1
- const cds = require('../../../cds')
1
+ const cds = require('../../cds')
2
2
 
3
- const _require = require('../../utils/require')
3
+ const _require = require('../../common/utils/require')
4
4
  const uaaUtils = require('./utils/uaa')
5
5
  const xssecUtils = require('./utils/xssec')
6
6
 
@@ -5,6 +5,8 @@ const OData = require('./OData')
5
5
 
6
6
  const { alias2ref } = require('../../../common/utils/csn')
7
7
 
8
+ const { normalizeError } = require('../../../common/error/frontend')
9
+
8
10
  function _createNewService(name, csn, defaultOptions) {
9
11
  const reflectedModel = cds.linked(cds.compile.for.odata(csn))
10
12
  const options = Object.assign({}, defaultOptions, { reflectedModel })
@@ -117,8 +119,11 @@ class Dispatcher {
117
119
  e.message = 'Unable to get service from service map due to error: ' + e.message
118
120
  LOG.error(e)
119
121
  }
120
- // REVISIT: use i18n
121
- return res.status(500).send({ error: { code: 'null', message: 'Internal Server Error' } })
122
+ // clear map entry
123
+ this._extMap.delete(hash)
124
+ // return 503 to client
125
+ const { error } = normalizeError(Object.assign(e, { statusCode: 503 }), req)
126
+ return res.status(503).send({ error })
122
127
  }
123
128
 
124
129
  // invoke extended service, if exists
@@ -229,13 +229,6 @@ class OData {
229
229
  this._odataService.use(ACTION_EXECUTE_HANDLER, _action(cdsService))
230
230
  }
231
231
 
232
- // _startPerfMeasurementOData (req) {
233
- // if (req.performanceMeasurement) {
234
- // const uuid = req.performanceMeasurement.uuid
235
- // req.performanceMeasurement.performance.mark(`${uuid} ODataIn Start`)
236
- // }
237
- // }
238
-
239
232
  /**
240
233
  * Process request.
241
234
  *