@sap/cds 5.6.2 → 5.7.2

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 (183) hide show
  1. package/CHANGELOG.md +133 -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 +7 -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 +13 -4
  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 +4 -2
  38. package/lib/log/index.js +2 -2
  39. package/lib/ql/Whereable.js +1 -0
  40. package/lib/req/cds-context.js +79 -0
  41. package/lib/req/context.js +5 -77
  42. package/lib/req/request.js +1 -1
  43. package/lib/serve/Service-api.js +8 -4
  44. package/lib/serve/Service-dispatch.js +0 -7
  45. package/lib/serve/Service-methods.js +6 -8
  46. package/lib/serve/Transaction.js +35 -30
  47. package/lib/serve/adapters.js +1 -4
  48. package/lib/utils/axios.js +1 -1
  49. package/libx/_runtime/audit/Service.js +44 -20
  50. package/libx/_runtime/audit/generic/personal/access.js +16 -11
  51. package/libx/_runtime/audit/generic/personal/modification.js +5 -5
  52. package/libx/_runtime/audit/generic/personal/utils.js +46 -37
  53. package/libx/_runtime/{common/auth → auth}/index.js +21 -7
  54. package/libx/_runtime/{common/auth → auth}/strategies/JWT.js +2 -2
  55. package/libx/_runtime/{common/auth → auth}/strategies/basic.js +2 -2
  56. package/libx/_runtime/{common/auth → auth}/strategies/dummy.js +1 -1
  57. package/libx/_runtime/{common/auth → auth}/strategies/mock.js +2 -2
  58. package/libx/_runtime/{common/auth → auth}/strategies/utils/uaa.js +1 -1
  59. package/libx/_runtime/{common/auth → auth}/strategies/utils/xssec.js +0 -0
  60. package/libx/_runtime/{common/auth → auth}/strategies/xsuaa.js +2 -2
  61. package/libx/_runtime/cds-services/adapter/odata-v4/Dispatcher.js +7 -2
  62. package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +0 -7
  63. package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +0 -8
  64. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +3 -4
  65. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +6 -7
  66. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/delete.js +3 -4
  67. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +2 -11
  68. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +16 -6
  69. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +26 -65
  70. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +0 -7
  71. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +3 -66
  72. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/ExpressionToCQN.js +26 -0
  73. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +5 -5
  74. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/utils.js +2 -2
  75. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriParser.js +13 -10
  76. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/invocation/ConditionalRequestControlCommand.js +0 -7
  77. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/validator/ConditionalRequestValidator.js +0 -8
  78. package/libx/_runtime/cds-services/adapter/odata-v4/utils/stream.js +54 -76
  79. package/libx/_runtime/cds-services/adapter/rest/RestRequest.js +0 -7
  80. package/libx/_runtime/cds-services/adapter/rest/handlers/create.js +3 -6
  81. package/libx/_runtime/cds-services/adapter/rest/handlers/delete.js +3 -6
  82. package/libx/_runtime/cds-services/adapter/rest/handlers/operation.js +3 -6
  83. package/libx/_runtime/cds-services/adapter/rest/handlers/read.js +3 -6
  84. package/libx/_runtime/cds-services/adapter/rest/handlers/update.js +3 -6
  85. package/libx/_runtime/cds-services/adapter/rest/rest-to-cqn/index.js +2 -2
  86. package/libx/_runtime/cds-services/adapter/rest/utils/parse-url.js +8 -4
  87. package/libx/_runtime/cds-services/services/Service.js +0 -6
  88. package/libx/_runtime/cds-services/services/utils/columns.js +10 -3
  89. package/libx/_runtime/cds-services/services/utils/compareJson.js +4 -7
  90. package/libx/_runtime/cds-services/services/utils/differ.js +4 -1
  91. package/libx/_runtime/cds-services/services/utils/handlerUtils.js +1 -41
  92. package/libx/_runtime/cds-services/util/assert.js +1 -262
  93. package/libx/_runtime/cds.js +6 -9
  94. package/libx/_runtime/common/aspects/entity.js +1 -1
  95. package/libx/_runtime/common/composition/delete.js +4 -2
  96. package/libx/_runtime/common/composition/update.js +22 -35
  97. package/libx/_runtime/common/composition/utils.js +3 -7
  98. package/libx/_runtime/common/error/standardError.js +11 -0
  99. package/libx/_runtime/common/generic/auth.js +63 -33
  100. package/libx/_runtime/common/generic/crud.js +11 -23
  101. package/libx/_runtime/common/generic/input.js +20 -0
  102. package/libx/_runtime/common/generic/paging.js +2 -2
  103. package/libx/_runtime/common/generic/put.js +4 -10
  104. package/libx/_runtime/common/generic/sorting.js +12 -30
  105. package/libx/_runtime/common/perf/index.js +24 -0
  106. package/libx/_runtime/common/utils/cqn.js +58 -1
  107. package/libx/_runtime/common/utils/cqn2cqn4sql.js +297 -121
  108. package/libx/_runtime/common/utils/csn.js +38 -56
  109. package/libx/_runtime/common/utils/entityFromCqn.js +6 -6
  110. package/libx/_runtime/common/utils/resolveView.js +4 -5
  111. package/libx/_runtime/common/utils/rewriteAsterisks.js +46 -5
  112. package/libx/_runtime/common/utils/search2cqn4sql.js +21 -9
  113. package/libx/_runtime/common/utils/structured.js +35 -25
  114. package/libx/_runtime/db/Service.js +0 -6
  115. package/libx/_runtime/db/expand/expand-v2.js +130 -0
  116. package/libx/_runtime/db/expand/expandCQNToJoin.js +38 -52
  117. package/libx/_runtime/db/expand/index.js +3 -1
  118. package/libx/_runtime/db/generic/arrayed.js +3 -1
  119. package/libx/_runtime/db/generic/input.js +52 -10
  120. package/libx/_runtime/db/generic/integrity.js +367 -26
  121. package/libx/_runtime/db/generic/virtual.js +51 -13
  122. package/libx/_runtime/db/query/update.js +9 -3
  123. package/libx/_runtime/db/sql-builder/ExpressionBuilder.js +8 -9
  124. package/libx/_runtime/{common → db}/utils/propagateForeignKeys.js +11 -14
  125. package/libx/_runtime/fiori/generic/activate.js +1 -0
  126. package/libx/_runtime/fiori/generic/before.js +2 -1
  127. package/libx/_runtime/fiori/generic/edit.js +1 -0
  128. package/libx/_runtime/fiori/generic/patch.js +1 -1
  129. package/libx/_runtime/fiori/generic/read.js +155 -57
  130. package/libx/_runtime/fiori/uiflex/handler/transformRESULT.js +0 -4
  131. package/libx/_runtime/fiori/uiflex/index.js +1 -1
  132. package/libx/_runtime/fiori/uiflex/{extensibility/index.js → service.js} +6 -4
  133. package/libx/_runtime/fiori/utils/delete.js +7 -1
  134. package/libx/_runtime/hana/Service.js +1 -8
  135. package/libx/_runtime/hana/customBuilder/CustomSelectBuilder.js +5 -14
  136. package/libx/_runtime/hana/execute.js +10 -4
  137. package/libx/_runtime/hana/pool.js +55 -45
  138. package/libx/_runtime/hana/search.js +7 -6
  139. package/libx/_runtime/hana/search2cqn4sql.js +8 -5
  140. package/libx/_runtime/hana/searchToContains.js +3 -1
  141. package/libx/_runtime/index.js +5 -5
  142. package/libx/_runtime/messaging/AMQPWebhookMessaging.js +3 -3
  143. package/libx/_runtime/messaging/Outbox.js +53 -0
  144. package/libx/_runtime/messaging/common-utils/AMQPClient.js +17 -10
  145. package/libx/_runtime/messaging/common-utils/connections.js +14 -9
  146. package/libx/_runtime/messaging/common-utils/waitingTime.js +2 -0
  147. package/libx/_runtime/messaging/enterprise-messaging-shared.js +2 -3
  148. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +2 -2
  149. package/libx/_runtime/messaging/enterprise-messaging.js +21 -15
  150. package/libx/_runtime/messaging/file-based.js +5 -5
  151. package/libx/_runtime/messaging/message-queuing.js +2 -3
  152. package/libx/_runtime/messaging/outbox/OutboxRunner.js +75 -0
  153. package/libx/_runtime/messaging/outbox/utils.js +192 -0
  154. package/libx/_runtime/messaging/service.js +16 -30
  155. package/libx/_runtime/remote/Service.js +15 -0
  156. package/libx/_runtime/remote/utils/client.js +15 -3
  157. package/libx/_runtime/remote/utils/{dataConversion.js → data.js} +12 -2
  158. package/libx/_runtime/sqlite/Service.js +7 -10
  159. package/libx/_runtime/sqlite/customBuilder/CustomExpressionBuilder.js +19 -0
  160. package/libx/_runtime/sqlite/execute.js +18 -12
  161. package/libx/_runtime/types/api.js +2 -1
  162. package/libx/gql/resolvers/parse/ast2cqn/columns.js +1 -1
  163. package/libx/odata/{odata2cqn/afterburner.js → afterburner.js} +28 -16
  164. package/libx/odata/{cqn2odata/index.js → cqn2odata.js} +1 -1
  165. package/libx/odata/{odata2cqn/grammar.pegjs → grammar.pegjs} +182 -118
  166. package/libx/odata/index.js +18 -15
  167. package/libx/odata/parser.js +1 -0
  168. package/libx/odata/utils.js +57 -0
  169. package/libx/rest/RestAdapter.js +2 -6
  170. package/libx/rest/utils/data.js +1 -6
  171. package/package.json +4 -3
  172. package/server.js +4 -5
  173. package/srv/audit-log.cds +87 -0
  174. package/{libx/_runtime/fiori/uiflex/extensibility/index.cds → srv/flex.cds} +0 -0
  175. package/srv/flex.js +1 -0
  176. package/srv/outbox.cds +11 -0
  177. package/srv/outbox.js +0 -0
  178. package/libx/_runtime/cds-services/adapter/perf/performance.js +0 -104
  179. package/libx/_runtime/cds-services/adapter/perf/performanceMeasurement.js +0 -33
  180. package/libx/odata/odata2cqn/index.js +0 -3
  181. package/libx/odata/odata2cqn/parser.js +0 -1
  182. package/libx/odata/readme.md +0 -1
  183. package/libx/odata/utils/index.js +0 -64
@@ -0,0 +1,192 @@
1
+ const cds = require('../../cds')
2
+ const waitingTime = require('../common-utils/waitingTime')
3
+ const OutboxRunner = require('./OutboxRunner')
4
+ const { isStandardError } = require('../../common/error/standardError')
5
+ const LOG = cds.log('persistent-outbox')
6
+ const _safeJSONParse = string => {
7
+ try {
8
+ return string && JSON.parse(string)
9
+ } catch (e) {}
10
+ }
11
+
12
+ const outboxRunner = new OutboxRunner()
13
+ const cdsUser = 'cds.internal.user'
14
+ const messageProcessorRegistered = Symbol('message processor registered')
15
+
16
+ const _getMessagesEntity = () => {
17
+ const messagesDbName = 'cds.outbox.Messages'
18
+ const messagesEntity = cds.model.definitions[messagesDbName]
19
+ if (!messagesEntity)
20
+ throw new Error(`The entity '${messagesDbName}' is missing but needed for the persistent outbox.`)
21
+ return messagesEntity
22
+ }
23
+
24
+ // REVISIT: Is there a better way?
25
+ const _isProviderTenant = tenant =>
26
+ cds.requires.uaa && cds.requires.uaa.credentials && cds.requires.uaa.credentials.identityzoneid === tenant
27
+
28
+ const hasPersistentOutbox = (srv, tenant) => {
29
+ if (!cds.requires.outbox || cds.requires.outbox.kind !== 'persistent-outbox') return false
30
+ if (srv.options && srv.options.outbox && srv.options.outbox.kind && srv.options.outbox.kind !== 'persistent-outbox')
31
+ return false
32
+ if (cds._mtxEnabled && tenant && _isProviderTenant(tenant)) return false // no persistence for provider account
33
+ return true
34
+ }
35
+
36
+ const isUnrecoverable = (service, error) => {
37
+ let unrecoverable = service.isUnrecoverableError && service.isUnrecoverableError(error)
38
+ if (unrecoverable === undefined) unrecoverable = error.unrecoverable
39
+ return unrecoverable || isStandardError(error)
40
+ }
41
+
42
+ const _processSingleMessage = async (service, message, succeededMessages) => {
43
+ const msg = _safeJSONParse(message.msg)
44
+ const userId = msg[cdsUser]
45
+ delete msg[cdsUser]
46
+ if (!msg) return message.ID
47
+ // Promise resolve is necessary because we want to set `cds.context` only
48
+ // inside this call
49
+ return Promise.resolve().then(async () => {
50
+ if (userId) cds.context.user = new cds.User.Privileged(userId)
51
+ try {
52
+ service._emitImmediate && (await service._emitImmediate(msg))
53
+ succeededMessages.push(message.ID)
54
+ } catch (e) {
55
+ const failedMessage = {
56
+ event: msg.event,
57
+ ID: message.ID,
58
+ attempts: message.attempts,
59
+ error: e,
60
+ unrecoverable: isUnrecoverable(service, e)
61
+ }
62
+ throw Object.assign(new Error('processing failed'), { failedMessage })
63
+ }
64
+ })
65
+ }
66
+
67
+ // Note: This function can also run for each tenant on startup
68
+ const processMessages = async (service, tenant, _opts = {}) => {
69
+ const opts = Object.assign({ attempt: 0 }, _opts)
70
+ const name = service.name
71
+ const messagesEntity = _getMessagesEntity()
72
+
73
+ outboxRunner.run({ name, tenant }, () => {
74
+ let letAppCrash = false
75
+ const config = tenant ? { tenant, user: new cds.User.Privileged() } : { user: new cds.User.Privileged() }
76
+ const spawn = cds.spawn(async () => {
77
+ let messages
78
+ try {
79
+ const messagesQuery = SELECT.from(messagesEntity)
80
+ .where({ target: name })
81
+ .orderBy('timestamp')
82
+ .limit(opts.chunkSize)
83
+ .forUpdate()
84
+ if (opts.maxAttempts) messagesQuery.where({ attempts: { '<': opts.maxAttempts } })
85
+ messages = await messagesQuery
86
+ } catch (e) {
87
+ // could potentially be a timeout
88
+ const _waitingTime = waitingTime(opts.attempt)
89
+ LOG._error &&
90
+ LOG.error(
91
+ 'Outbox SELECT FOR UPDATE failed',
92
+ opts.attempt > 0
93
+ ? ''
94
+ : {
95
+ cause: e
96
+ },
97
+ `Retrying in ${Math.round(_waitingTime / 1000)} s`
98
+ )
99
+ outboxRunner.schedule(
100
+ {
101
+ name,
102
+ tenant,
103
+ waitingTime: _waitingTime
104
+ },
105
+ () => processMessages(service, tenant, { ...opts, attempt: opts.attempt + 1 })
106
+ )
107
+ return
108
+ }
109
+ const messagesToBeDeleted = []
110
+ let error
111
+ for (const m of messages) {
112
+ try {
113
+ await _processSingleMessage(service, m, messagesToBeDeleted)
114
+ } catch (e) {
115
+ error = e
116
+ break
117
+ }
118
+ }
119
+ if (error && error.failedMessage.unrecoverable) {
120
+ // opts.crashOnError is not official!!!
121
+ if (opts.crashOnError !== false) letAppCrash = true
122
+ messagesToBeDeleted.push(error.failedMessage.ID)
123
+ }
124
+ if (messagesToBeDeleted.length) await DELETE.from(messagesEntity).where('ID in', messagesToBeDeleted)
125
+ if (error) {
126
+ if (!error.failedMessage.unrecoverable) {
127
+ const _waitingTime = waitingTime(error.failedMessage.attempts)
128
+ const info = { service: name, event: error.failedMessage.event }
129
+ if (error.failedMessage.attempts > 0) info.cause = error.failedMessage.error
130
+ LOG._error && LOG.error('Emit failed', info, `Retrying in ${Math.round(_waitingTime / 1000)} s`)
131
+ await UPDATE(messagesEntity)
132
+ .where({ ID: error.failedMessage.ID })
133
+ .set({ attempts: { '+=': 1 } })
134
+ outboxRunner.schedule(
135
+ {
136
+ name,
137
+ tenant,
138
+ waitingTime: _waitingTime
139
+ },
140
+ () => processMessages(service, tenant, opts)
141
+ )
142
+ } else {
143
+ LOG._error &&
144
+ LOG.error(
145
+ 'Emit failed',
146
+ { service: name, event: error.failedMessage.event, cause: error.failedMessage.error },
147
+ 'Unrecoverable, outbox entry deleted'
148
+ )
149
+ }
150
+ } else {
151
+ outboxRunner.success({ name, tenant })
152
+ if (messages.length === opts.chunkSize) {
153
+ processMessages(service, tenant, opts) // We only processed max. opts.chunkSize, so there might be more
154
+ } else {
155
+ LOG._trace && LOG.trace(`All persistent-outbox messages processed for service '${service.name}'`)
156
+ }
157
+ }
158
+ }, config)
159
+ spawn.on('done', () => {
160
+ if (letAppCrash) process.exit(1)
161
+ outboxRunner.end({ name, tenant }, () => processMessages(service, tenant, opts))
162
+ })
163
+ })
164
+ }
165
+
166
+ const registerMessageProcessor = (name, context) => {
167
+ const registry = context[messageProcessorRegistered] || (context[messageProcessorRegistered] = new Set())
168
+ if (!registry.has(name)) {
169
+ registry.add(name)
170
+ return true
171
+ }
172
+ return false
173
+ }
174
+
175
+ const _createMessage = (name, msg, context) => {
176
+ const _msg = { ...msg, [cdsUser]: context.user.id }
177
+ const outboxMsg = {
178
+ ID: cds.utils.uuid(),
179
+ target: name,
180
+ timestamp: new Date().toISOString(), // needs to be different for each emit
181
+ msg: JSON.stringify(_msg)
182
+ }
183
+ return outboxMsg
184
+ }
185
+
186
+ const writeInOutbox = async (name, msg, context) => {
187
+ const outboxMsg = _createMessage(name, msg, context)
188
+ const messagesEntity = _getMessagesEntity()
189
+ return INSERT.into(messagesEntity).entries(outboxMsg)
190
+ }
191
+
192
+ module.exports = { processMessages, registerMessageProcessor, writeInOutbox, hasPersistentOutbox, isUnrecoverable }
@@ -1,6 +1,7 @@
1
1
  const cds = require('../cds')
2
2
  const LOG = cds.log('messaging')
3
3
  const queued = require('./common-utils/queued')
4
+ const OutboxService = require('./Outbox')
4
5
 
5
6
  const _topic = declared => declared['@topic'] || declared.name
6
7
 
@@ -18,7 +19,7 @@ const _warnAndStripTopicPrefix = event => {
18
19
  }
19
20
 
20
21
  // There's currently no mechanism to detect mocked services, this is the best we can do.
21
- class MessagingService extends cds.Service {
22
+ class MessagingService extends OutboxService {
22
23
  init() {
23
24
  // enables queued async operations (without awaiting)
24
25
  this.queued = queued()
@@ -56,22 +57,6 @@ class MessagingService extends cds.Service {
56
57
  })
57
58
  }
58
59
 
59
- // if outbox is switched on, decorate the emit method to actually do
60
- // the emit only when the request succeeded
61
- if (this.options.outbox) {
62
- const { emit } = this
63
- this.emit = function (...args) {
64
- const context = this.context || cds.context
65
- if (context && typeof context.on === 'function')
66
- return context.on('succeeded', () =>
67
- emit.call(this, ...args).catch(e => {
68
- LOG._error && LOG.error(e)
69
- })
70
- )
71
- return emit.call(this, ...args)
72
- }
73
- }
74
-
75
60
  const { on } = this
76
61
  this.on = function (...args) {
77
62
  if (Array.isArray(args[0])) {
@@ -80,6 +65,7 @@ class MessagingService extends cds.Service {
80
65
  }
81
66
  return on.call(this, ...args)
82
67
  }
68
+ return super.init()
83
69
  }
84
70
 
85
71
  emit(event, data, headers) {
@@ -114,22 +100,22 @@ class MessagingService extends cds.Service {
114
100
  }
115
101
  }
116
102
 
117
- message4(event, data, headers) {
118
- const msg = typeof event === 'object' ? { ...event } : { event, data, headers }
119
- msg.event = _warnAndStripTopicPrefix(msg.event)
120
- if (!msg.headers) msg.headers = {}
121
- if (!msg.inbound) {
122
- msg.headers = { ...msg.headers } // don't change the original object
123
- this.prepareHeaders(msg.headers, msg.event)
124
- msg.event = this.prepareTopic(msg.event, false)
103
+ message4(msg) {
104
+ const _msg = { ...msg }
105
+ _msg.event = _warnAndStripTopicPrefix(_msg.event)
106
+ if (!_msg.headers) _msg.headers = {}
107
+ if (!_msg.inbound) {
108
+ _msg.headers = { ..._msg.headers } // don't change the original object
109
+ this.prepareHeaders(_msg.headers, _msg.event)
110
+ _msg.event = this.prepareTopic(_msg.event, false)
125
111
  } else if (this.subscribedTopics) {
126
112
  const subscribedEvent =
127
- this.subscribedTopics.get(msg.event) ||
128
- (this.wildcarded && this.subscribedTopics.get(this.wildcarded(msg.event)))
129
- if (!subscribedEvent) throw new Error(`No handler for incoming message with topic '${msg.event}' found.`)
130
- msg.event = subscribedEvent
113
+ this.subscribedTopics.get(_msg.event) ||
114
+ (this.wildcarded && this.subscribedTopics.get(this.wildcarded(_msg.event)))
115
+ if (!subscribedEvent) throw new Error(`No handler for incoming message with topic '${_msg.event}' found.`)
116
+ _msg.event = subscribedEvent
131
117
  }
132
- return msg
118
+ return _msg
133
119
  }
134
120
  }
135
121
 
@@ -185,6 +185,21 @@ class RemoteService extends cds.Service {
185
185
  })
186
186
  }
187
187
 
188
+ // FIXME: This is a dirty hack for this situation:
189
+ // - This PR has cds.Service.model setter to always consistently apply cds.compile.for.odata, also for RemoteServices, which wasn't the case before
190
+ // - because of that tests/_runtime/remote/__tests__/integration/odata.test.js fails, which relies on the former behavoir of RemoteServices
191
+ // NOTE: that test would never have worked for RemoteServices bootstrapped from single cds.model, which is always cds.compiled.for.odata
192
+ // REVISIT: should become obsolete with Universal CSN
193
+ set model(m) {
194
+ const fn = cds.compile.for.odata
195
+ try {
196
+ cds.compile.for.odata = m => m
197
+ super.model = m
198
+ } finally {
199
+ cds.compile.for.odata = fn
200
+ }
201
+ }
202
+
188
203
  // Overload .handle in order to resolve projections up to a definition that is known by the remote service instance.
189
204
  // Result is post processed according to the inverse projection in order to reflect the correct result of the original query.
190
205
  async handle(req) {
@@ -1,9 +1,11 @@
1
1
  const cds = require('../../cds')
2
2
  const LOG = cds.log('remote')
3
3
 
4
+ const SANITIZE_VALUES = process.env.NODE_ENV === 'production' && cds.env.log.sanitize_values !== false
5
+
4
6
  const cdsLocale = require('../../../../lib/req/locale')
5
7
 
6
- const { convertV2ResponseData } = require('./dataConversion')
8
+ const { convertV2ResponseData, deepSanitize } = require('./data')
7
9
 
8
10
  let _cloudSdkCore
9
11
 
@@ -16,6 +18,12 @@ const PPPD = {
16
18
 
17
19
  const KINDS_SUPPORTING_BATCH = { odata: 1, 'odata-v2': 1, 'odata-v4': 1 }
18
20
 
21
+ const _sanitizeHeaders = headers => {
22
+ if (headers && headers.authorization) headers.authorization = headers.authorization.split(' ')[0] + ' ...'
23
+
24
+ return headers
25
+ }
26
+
19
27
  const _executeHttpRequest = async ({ requestConfig, destination, destinationOptions, jwt }) => {
20
28
  const { getDestination, executeHttpRequest } = cloudSdkCore()
21
29
 
@@ -41,6 +49,11 @@ const _executeHttpRequest = async ({ requestConfig, destination, destinationOpti
41
49
  requestOptions = { fetchCsrfToken: true }
42
50
  }
43
51
 
52
+ LOG._debug &&
53
+ LOG.debug(`${requestConfig.method} ${destination.url}${requestConfig.url}`, {
54
+ headers: _sanitizeHeaders({ ...requestConfig.headers }),
55
+ data: requestConfig.data && SANITIZE_VALUES ? deepSanitize(requestConfig.data) : requestConfig.data
56
+ })
44
57
  return executeHttpRequest(destination, requestConfig, requestOptions)
45
58
  }
46
59
 
@@ -194,8 +207,7 @@ const _getSanitizedError = (e, reqOptions, suppressRemoteResponseBody) => {
194
207
  if (correlationId) e.correlationId = correlationId
195
208
 
196
209
  // sanitize authorization
197
- if (e.request.headers && e.request.headers.authorization)
198
- e.request.headers.authorization = e.request.headers.authorization.split(' ')[0] + ' ...'
210
+ _sanitizeHeaders(e.request.headers)
199
211
 
200
212
  // delete functions and complex objects in config
201
213
  for (const k in e) if (typeof e[k] === 'function') delete e[k]
@@ -49,7 +49,6 @@ const _getConvertRecordFn = (target, ieee754Compatible) => record => {
49
49
  return record
50
50
  }
51
51
 
52
- // eslint-disable-next-line complexity
53
52
  const _convertValue = (value, type, ieee754Compatible) => {
54
53
  if (value == null) {
55
54
  return value
@@ -121,6 +120,17 @@ const convertV2ResponseData = (data, target, ieee754Compatible) => {
121
120
  return _convertData(data, target, ieee754Compatible)
122
121
  }
123
122
 
123
+ const deepSanitize = arg => {
124
+ if (Array.isArray(arg)) return arg.map(deepSanitize)
125
+ if (typeof arg === 'object')
126
+ return Object.keys(arg).reduce((acc, cur) => {
127
+ acc[cur] = deepSanitize(arg[cur])
128
+ return acc
129
+ }, {})
130
+ return '***'
131
+ }
132
+
124
133
  module.exports = {
125
- convertV2ResponseData
134
+ convertV2ResponseData,
135
+ deepSanitize
126
136
  }
@@ -45,12 +45,6 @@ module.exports = class SQLiteDatabase extends DatabaseService {
45
45
  this.dbcs = new Map()
46
46
  }
47
47
 
48
- set model(csn) {
49
- const m = csn && 'definitions' in csn ? cds.linked(cds.compile.for.odata(csn)) : csn
50
- cds.alpha_localized(m)
51
- super.model = m
52
- }
53
-
54
48
  init() {
55
49
  this._registerBeforeHandlers()
56
50
  this._registerOnHandlers()
@@ -114,8 +108,12 @@ module.exports = class SQLiteDatabase extends DatabaseService {
114
108
  * connection
115
109
  */
116
110
  async acquire(arg) {
117
- // REVISIT: remove fallback arg.user.tenant with cds^6
118
- const tenant = (typeof arg === 'string' ? arg : arg.tenant || (arg.user && arg.user.tenant)) || 'anonymous'
111
+ // in non-multi-tenant scenarios, the default db should be returned regardless of arg
112
+ let tenant = 'anonymous'
113
+ if (this.options.multiTenant && arg) {
114
+ if (typeof arg === 'string') tenant = arg
115
+ else tenant = arg.tenant || (arg.user && arg.user.tenant) || tenant // > REVISIT: remove fallback arg.user.tenant with cds^6
116
+ }
119
117
 
120
118
  let dbc = this.dbcs.get(tenant)
121
119
  if (!dbc) {
@@ -129,9 +127,8 @@ module.exports = class SQLiteDatabase extends DatabaseService {
129
127
  dbc = await _new(dbUrl)
130
128
 
131
129
  dbc._queued = []
132
- dbc._tenant = tenant
133
130
 
134
- if (cds.env.features._foreign_key_constraints) {
131
+ if (cds.env.features._db_foreign_key_constraints) {
135
132
  await new Promise((resolve, reject) => {
136
133
  dbc.exec('PRAGMA foreign_keys = ON', err => {
137
134
  if (err) reject(err)
@@ -18,6 +18,25 @@ class CustomExpressionBuilder extends ExpressionBuilder {
18
18
  Object.defineProperty(this, 'FunctionBuilder', { value: FunctionBuilder })
19
19
  return FunctionBuilder
20
20
  }
21
+
22
+ _addListToOutputObj(list) {
23
+ this._outputObj.sql.push('(')
24
+
25
+ // sqlite requires "VALUES" keyword for list of list of values
26
+ if (list[0] && list[0].list && list[0].list[0] && 'val' in list[0].list[0]) {
27
+ this._outputObj.sql.push('VALUES')
28
+ }
29
+
30
+ for (let i = 0, len = list.length; i < len; i++) {
31
+ this._expressionElementToSQL(list[i])
32
+
33
+ if (len > 1 && i + 1 < len) {
34
+ this._outputObj.sql.push(',')
35
+ }
36
+ }
37
+
38
+ this._outputObj.sql.push(')')
39
+ }
21
40
  }
22
41
 
23
42
  module.exports = CustomExpressionBuilder
@@ -2,7 +2,7 @@ const { SQLITE_TYPE_CONVERSION_MAP } = require('./conversion')
2
2
  const CustomBuilder = require('./customBuilder')
3
3
  const { sqlFactory } = require('../db/sql-builder/')
4
4
  const { getPostProcessMapper, postProcess } = require('../db/data-conversion/post-processing')
5
- const { createJoinCQNFromExpanded, hasExpand, rawToExpanded } = require('../db/expand')
5
+ const { createJoinCQNFromExpanded, hasExpand, rawToExpanded, expandV2 } = require('../db/expand')
6
6
  const { Readable } = require('stream')
7
7
 
8
8
  const cds = require('../cds')
@@ -10,6 +10,8 @@ const LOG = cds.log('sqlite|db|sql')
10
10
  // && {_debug:true, debug(sql){ cds._debug && console.log(sql+';\n') }} //> please keep that for debugging stakeholder tests
11
11
  const { inspect } = require('util')
12
12
 
13
+ const SANITIZE_VALUES = process.env.NODE_ENV === 'production' && cds.env.log.sanitize_values !== false
14
+
13
15
  /*
14
16
  * capture stack trace on the way to exec to know origin
15
17
  * -> very expensive
@@ -32,20 +34,21 @@ const _colored = {
32
34
  COMMIT: '\x1b[1m\x1b[32mCOMMIT\x1b[0m',
33
35
  ROLLBACK: '\x1b[1m\x1b[91mROLLBACK\x1b[0m'
34
36
  }
35
- const _augmented = (err, sql, o) => {
37
+ const _augmented = (err, sql, values, o) => {
36
38
  err.query = sql
39
+ if (values) err.values = SANITIZE_VALUES ? ['***'] : values
37
40
  err.message += ' in: \n' + sql
38
41
  if (o) err.stack = err.message + o.stack.slice(5)
39
42
  return err
40
43
  }
41
44
 
42
45
  function _executeSimpleSQL(dbc, sql, values) {
43
- LOG._debug && LOG.debug(_colored[sql] || sql, values || '')
46
+ LOG._debug && LOG.debug(_colored[sql] || sql, Array.isArray(values) ? (SANITIZE_VALUES ? ['***'] : values) : '')
44
47
 
45
48
  return new Promise((resolve, reject) => {
46
49
  const o = _captureStack()
47
50
  dbc.run(sql, values, function (err) {
48
- if (err) return reject(_augmented(err, sql, o))
51
+ if (err) return reject(_augmented(err, sql, values, o))
49
52
 
50
53
  resolve(this.changes)
51
54
  })
@@ -53,12 +56,12 @@ function _executeSimpleSQL(dbc, sql, values) {
53
56
  }
54
57
 
55
58
  function executeSelectSQL(dbc, sql, values, isOne, postMapper) {
56
- LOG._debug && LOG.debug(sql, values)
59
+ LOG._debug && LOG.debug(sql, SANITIZE_VALUES ? ['***'] : values)
57
60
 
58
61
  return new Promise((resolve, reject) => {
59
62
  const o = _captureStack()
60
63
  dbc[isOne ? 'get' : 'all'](sql, values, (err, result) => {
61
- if (err) return reject(_augmented(err, sql, o))
64
+ if (err) return reject(_augmented(err, sql, values, o))
62
65
 
63
66
  // REVISIT
64
67
  // .get returns undefined if nothing in db
@@ -96,6 +99,10 @@ function _processExpand(model, dbc, cqn, user, locale, txTimestamp) {
96
99
 
97
100
  function executeSelectCQN(model, dbc, query, user, locale, txTimestamp) {
98
101
  if (hasExpand(query)) {
102
+ // expand: '**' or '*3' is handled by new impl
103
+ if (query.SELECT.columns.some(c => c.expand && typeof c.expand === 'string' && /^\*{1}[\d|*]+/.test(c.expand))) {
104
+ return expandV2(model, dbc, query, user, locale, txTimestamp, executeSelectCQN)
105
+ }
99
106
  return _processExpand(model, dbc, query, user, locale, txTimestamp)
100
107
  }
101
108
  const { sql, values = [] } = sqlFactory(
@@ -138,10 +145,10 @@ const _executeBulkInsertSQL = (dbc, sql, values) =>
138
145
  return reject(new Error(`Cannot execute SQL statement. Invalid values provided: ${inspect(values)}`))
139
146
  }
140
147
 
141
- LOG._debug && LOG.debug(sql, values)
148
+ LOG._debug && LOG.debug(sql, SANITIZE_VALUES ? ['***'] : values)
142
149
  const o = _captureStack()
143
150
  const stmt = dbc.prepare(sql, err => {
144
- if (err) return reject(_augmented(err, sql, o))
151
+ if (err) return reject(_augmented(err, sql, values, o))
145
152
 
146
153
  if (!Array.isArray(values[0])) values = [values]
147
154
 
@@ -155,11 +162,10 @@ const _executeBulkInsertSQL = (dbc, sql, values) =>
155
162
  i++
156
163
  stmt.run(each, function (err) {
157
164
  if (err) {
158
- err.values = each
159
165
  if (!isFinalized) {
160
166
  isFinalized = true
161
167
  stmt.finalize()
162
- return reject(_augmented(err, sql, o))
168
+ return reject(_augmented(err, sql, each, o))
163
169
  }
164
170
  }
165
171
 
@@ -209,12 +215,12 @@ function executeInsertSQL(dbc, sql, values, query) {
209
215
  }
210
216
  }
211
217
 
212
- LOG._debug && LOG.debug(sql, values)
218
+ LOG._debug && LOG.debug(sql, SANITIZE_VALUES ? ['***'] : values)
213
219
 
214
220
  return new Promise((resolve, reject) => {
215
221
  const o = _captureStack()
216
222
  dbc.run(sql, values, function (err) {
217
- if (err) return reject(_augmented(err, sql, o))
223
+ if (err) return reject(_augmented(err, sql, values, o))
218
224
 
219
225
  // InsertResult needs an object per row with its values
220
226
  if (query && values.length > 0) {
@@ -101,7 +101,8 @@
101
101
 
102
102
  /**
103
103
  * @typedef {object} cqn2cqn4sqlOptions
104
- * @property {boolean} suppressSearch=false Indicates whether the search handler is called.
104
+ * @property {boolean} [suppressSearch=false] Indicates whether the search handler is called.
105
+ * @property {import('../db/Service')} [service]
105
106
  */
106
107
 
107
108
  module.exports = {}
@@ -19,7 +19,7 @@ const astToColumns = selections => {
19
19
 
20
20
  const filter = getArgumentByName(selection.arguments, ARGUMENT.FILTER)
21
21
  if (filter) {
22
- column.where = astToWhere(filter).xpr
22
+ column.where = astToWhere(filter)
23
23
  }
24
24
 
25
25
  const orderBy = getArgumentByName(selection.arguments, ARGUMENT.ORDER_BY)