@sap/cds 6.3.2 → 6.4.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 (128) hide show
  1. package/CHANGELOG.md +95 -0
  2. package/apis/cds.d.ts +3 -1
  3. package/apis/core.d.ts +118 -90
  4. package/apis/cqn.d.ts +11 -2
  5. package/apis/internal/inference.d.ts +7 -2
  6. package/apis/ql.d.ts +49 -11
  7. package/apis/serve.d.ts +8 -1
  8. package/apis/services.d.ts +311 -305
  9. package/bin/build/buildTaskEngine.js +28 -36
  10. package/bin/build/buildTaskFactory.js +32 -81
  11. package/bin/build/buildTaskHandler.js +3 -2
  12. package/bin/build/buildTaskProvider.js +2 -2
  13. package/bin/build/buildTaskProviderFactory.js +5 -14
  14. package/bin/build/constants.js +0 -1
  15. package/bin/build/provider/buildTaskHandlerEdmx.js +7 -6
  16. package/bin/build/provider/buildTaskHandlerFeatureToggles.js +6 -5
  17. package/bin/build/provider/buildTaskHandlerInternal.js +9 -30
  18. package/bin/build/provider/buildTaskProviderInternal.js +70 -58
  19. package/bin/build/provider/fiori/index.js +6 -5
  20. package/bin/build/provider/hana/2migration.js +20 -3
  21. package/bin/build/provider/hana/2tabledata.js +1 -0
  22. package/bin/build/provider/hana/index.js +40 -17
  23. package/bin/build/provider/java/index.js +10 -10
  24. package/bin/build/provider/mtx/index.js +25 -16
  25. package/bin/build/provider/mtx/resourcesTarBuilder.js +22 -27
  26. package/bin/build/provider/mtx-extension/index.js +3 -2
  27. package/bin/build/provider/mtx-sidecar/index.js +16 -15
  28. package/bin/build/provider/nodejs/index.js +14 -56
  29. package/bin/build/util.js +56 -16
  30. package/bin/deploy/to-hana/cfUtil.js +2 -0
  31. package/bin/deploy/to-hana/gitUtil.js +1 -1
  32. package/bin/deploy/to-hana/hana.js +45 -38
  33. package/bin/deploy/to-hana/hdiDeployUtil.js +17 -12
  34. package/bin/deploy/to-hana/mtaUtil.js +13 -14
  35. package/bin/mtx/in-cds.js +3 -1
  36. package/bin/serve.js +1 -1
  37. package/bin/version.js +2 -1
  38. package/lib/auth/index.js +17 -15
  39. package/lib/compile/cds-compile.js +1 -0
  40. package/lib/compile/cdsc.js +1 -0
  41. package/lib/compile/etc/_localized.js +2 -2
  42. package/lib/compile/for/lean_drafts.js +83 -0
  43. package/lib/compile/for/nodejs.js +1 -0
  44. package/lib/compile/minify.js +2 -1
  45. package/lib/compile/to/gql.js +1 -1
  46. package/lib/compile/to/sql.js +11 -1
  47. package/lib/core/entities.js +1 -1
  48. package/lib/core/index.js +9 -9
  49. package/lib/core/infer.js +1 -0
  50. package/lib/dbs/cds-deploy.js +97 -41
  51. package/lib/env/cds-env.js +9 -10
  52. package/lib/env/cds-requires.js +8 -2
  53. package/lib/env/defaults.js +0 -4
  54. package/lib/env/schemas/cds-rc.json +38 -0
  55. package/lib/ql/SELECT.js +10 -4
  56. package/lib/srv/bindings.js +1 -1
  57. package/lib/srv/factory.js +1 -1
  58. package/lib/srv/middlewares/cds-context.js +0 -2
  59. package/lib/srv/middlewares/ctx-auth.js +11 -0
  60. package/lib/srv/middlewares/ctx-model.js +22 -20
  61. package/lib/srv/middlewares/index.js +7 -9
  62. package/lib/srv/protocols/_legacy.js +4 -0
  63. package/lib/srv/protocols/graphql.js +2 -2
  64. package/lib/srv/protocols/index.js +7 -3
  65. package/lib/srv/srv-api.js +1 -0
  66. package/lib/srv/srv-methods.js +1 -1
  67. package/lib/utils/cds-utils.js +11 -0
  68. package/lib/utils/data.js +2 -2
  69. package/lib/utils/inflect.js +13 -12
  70. package/lib/utils/tar.js +43 -13
  71. package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +2 -2
  72. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +1 -1
  73. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +1 -1
  74. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/delete.js +1 -1
  75. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +1 -15
  76. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +1 -1
  77. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +1 -1
  78. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/errors/UriSyntaxError.js +1 -1
  79. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriParser.js +6 -1
  80. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/utils/BufferedWriter.js +1 -1
  81. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/validator/ConditionalRequestValidator.js +0 -12
  82. package/libx/_runtime/cds-services/adapter/odata-v4/utils/oDataConfiguration.js +1 -7
  83. package/libx/_runtime/cds-services/adapter/odata-v4/utils/result.js +4 -0
  84. package/libx/_runtime/cds-services/services/Service.js +23 -1
  85. package/libx/_runtime/cds-services/util/assert.js +0 -41
  86. package/libx/_runtime/common/composition/data.js +5 -1
  87. package/libx/_runtime/common/generic/auth/utils.js +3 -3
  88. package/libx/_runtime/common/generic/crud.js +1 -1
  89. package/libx/_runtime/common/generic/input.js +4 -24
  90. package/libx/_runtime/common/generic/paging.js +10 -9
  91. package/libx/_runtime/common/utils/cqn2cqn4sql.js +31 -0
  92. package/libx/_runtime/common/utils/csn.js +21 -15
  93. package/libx/_runtime/common/utils/draft.js +2 -1
  94. package/libx/_runtime/common/utils/resolveView.js +27 -4
  95. package/libx/_runtime/common/utils/rewriteAsterisks.js +3 -1
  96. package/libx/_runtime/common/utils/rowUUIDGenerator.js +21 -0
  97. package/libx/_runtime/common/utils/templateProcessor.js +12 -15
  98. package/libx/_runtime/common/utils/templateProcessorPathSerializer.js +23 -0
  99. package/libx/_runtime/db/expand/expandCQNToJoin.js +29 -12
  100. package/libx/_runtime/db/generic/input.js +7 -13
  101. package/libx/_runtime/db/sql-builder/InsertBuilder.js +5 -1
  102. package/libx/_runtime/db/sql-builder/UpsertBuilder.js +24 -0
  103. package/libx/_runtime/db/sql-builder/annotations.js +6 -3
  104. package/libx/_runtime/db/sql-builder/index.js +2 -0
  105. package/libx/_runtime/db/sql-builder/sqlFactory.js +9 -0
  106. package/libx/_runtime/db/utils/columns.js +4 -2
  107. package/libx/_runtime/fiori/generic/read.js +1 -12
  108. package/libx/_runtime/fiori/lean-draft.js +657 -0
  109. package/libx/_runtime/fiori/utils/handler.js +1 -1
  110. package/libx/_runtime/hana/Service.js +1 -1
  111. package/libx/_runtime/hana/execute.js +5 -5
  112. package/libx/_runtime/hana/pool.js +16 -1
  113. package/libx/_runtime/messaging/enterprise-messaging-utils/getTenantInfo.js +2 -1
  114. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +1 -1
  115. package/libx/_runtime/messaging/enterprise-messaging.js +2 -3
  116. package/libx/_runtime/messaging/outbox/utils.js +109 -70
  117. package/libx/_runtime/messaging/service.js +16 -7
  118. package/libx/_runtime/remote/Service.js +15 -2
  119. package/libx/_runtime/remote/utils/client.js +41 -11
  120. package/libx/_runtime/sqlite/Service.js +4 -1
  121. package/libx/_runtime/sqlite/convertDraftAdminPathExpression.js +56 -0
  122. package/libx/_runtime/sqlite/customBuilder/CustomUpsertBuilder.js +41 -0
  123. package/libx/_runtime/sqlite/customBuilder/index.js +5 -0
  124. package/libx/_runtime/sqlite/execute.js +1 -1
  125. package/libx/_runtime/types/api.js +2 -2
  126. package/libx/rest/RestAdapter.js +15 -13
  127. package/package.json +1 -1
  128. package/server.js +2 -19
@@ -7,6 +7,17 @@ const hana = require('./driver')
7
7
  const _require = require('../common/utils/require')
8
8
  const getError = require('../common/error')
9
9
 
10
+ function multiTenantServiceManager() {
11
+ try {
12
+ // Make sure cds-mtxs APIs are loaded
13
+ require('@sap/cds-mtxs/lib') // eslint-disable-line cds/no-missing-dependencies
14
+ } catch (e) {
15
+ if (e.code === 'MODULE_NOT_FOUND') return null
16
+ else throw e
17
+ }
18
+ return cds.env.requires['cds.xt.DeploymentService']?.['old-instance-manager'] ? null : cds.xt?.serviceManager
19
+ }
20
+
10
21
  function multiTenantInstanceManager(config = cds.env.requires.db) {
11
22
  const { credentials } = config
12
23
 
@@ -63,10 +74,14 @@ async function credentials4(tenant, db) {
63
74
  if (!db._instance_manager) {
64
75
  const opts = db.options && db.options.credentials ? db.options : undefined
65
76
  db._instance_manager = cds.requires.multitenancy
66
- ? await multiTenantInstanceManager(opts)
77
+ ? multiTenantServiceManager() ?? (await multiTenantInstanceManager(opts))
67
78
  : singleTenantInstanceManager(opts)
68
79
  }
69
80
 
81
+ if (cds.xt?.serviceManager && !cds.env.requires['cds.xt.DeploymentService']?.['old-instance-manager']) {
82
+ return (await db._instance_manager.get(tenant)).credentials
83
+ }
84
+
70
85
  return new Promise((resolve, reject) => {
71
86
  db._instance_manager.get(tenant, (err, res) => {
72
87
  if (err) return reject(err)
@@ -4,12 +4,13 @@ const _transform = o => ({ subdomain: o.subscribedSubdomain, tenant: o.subscribe
4
4
  const getTenantInfo = async tenant => {
5
5
  const provisioningServiceName = cds.mtx ? 'ProvisioningService' : 'cds.xt.SaasProvisioningService'
6
6
  const primaryKey = cds.mtx ? 'ID' : 'subscribedTenantId'
7
+ const path = cds.mtx ? 'tenant' : '/tenant' // HACK, otherwise the API doesn't work
7
8
 
8
9
  const provisioning = await cds.connect.to(provisioningServiceName)
9
10
  const tx = provisioning.tx({ user: new cds.User.Privileged() })
10
11
  try {
11
12
  const result = tenant
12
- ? _transform(await tx.get(`tenant`, { [primaryKey]: tenant }))
13
+ ? _transform(await tx.get(path, { [primaryKey]: tenant }))
13
14
  : (await tx.read('tenant')).map(o => _transform(o))
14
15
  await tx.commit()
15
16
  return result
@@ -72,7 +72,7 @@ class EndpointRegistry {
72
72
  const tenantId = getTenant(authInfo)
73
73
  const other = authInfo
74
74
  ? {
75
- _: { req: { authInfo, headers: {}, query: {} } }, // for messaging to retrieve subdomain
75
+ _: { req, res }, // For `cds.context.http`
76
76
  tenant: tenantId
77
77
  }
78
78
  : {}
@@ -33,8 +33,7 @@ const _multitenancyEnabled = () => cds.requires.multitenancy || _oldMtx()
33
33
  // REVISIT: It's bad to have to rely on the subdomain.
34
34
  // For all interactions where we perform the token exchange ourselves,
35
35
  // we will be able to use the zoneId instead of the subdomain.
36
- const _subdomainFromContext = context =>
37
- context && context._ && context._.req && context._.req.authInfo && context._.req.authInfo.getSubdomain()
36
+ const _subdomainFromContext = context => context?.http.req?.authInfo?.getSubdomain()
38
37
 
39
38
  class EnterpriseMessaging extends AMQPWebhookMessaging {
40
39
  init() {
@@ -102,7 +101,6 @@ class EnterpriseMessaging extends AMQPWebhookMessaging {
102
101
  return res
103
102
  })
104
103
  deploymentSrv.on('unsubscribe', async (req, next) => {
105
- const res = await next()
106
104
  const { tenant } = req.data
107
105
  let subdomain
108
106
  try {
@@ -112,6 +110,7 @@ class EnterpriseMessaging extends AMQPWebhookMessaging {
112
110
  this.LOG.error("'unsubscribe' is not yet implemented for @sap/cds-mtxs")
113
111
  throw e
114
112
  }
113
+ const res = await next()
115
114
  try {
116
115
  const management = await this.getManagement(subdomain).waitUntilReady()
117
116
  await management.undeploy()
@@ -42,29 +42,48 @@ const isUnrecoverable = (service, error) => {
42
42
  return unrecoverable || isStandardError(error)
43
43
  }
44
44
 
45
- const _processSingleMessage = async (service, message, succeededMessages) => {
46
- const msg = _safeJSONParse(message.msg)
47
- if (!msg) return
48
- const userId = msg[cdsUser]
49
- delete msg[cdsUser]
50
- // Promise resolve is necessary because we want to set `cds.context` only
51
- // inside this call
52
- return Promise.resolve().then(async () => {
53
- if (userId) cds.context = { user: new cds.User.Privileged(userId) }
45
+ const processDefault = async (messages, { toBeDeleted, toBeUpdated, options, service }) => {
46
+ /** throws if an emit failed due to a programming error
47
+ * returns false if an emit failed due to temporary issues **/
48
+ const run = async ({ ID, process }) => {
54
49
  try {
55
- service._emitImmediate && (await service._emitImmediate(msg))
56
- succeededMessages.push(message.ID)
50
+ await process()
51
+ toBeDeleted.push(ID)
57
52
  } catch (e) {
58
- const failedMessage = {
59
- event: msg.event,
60
- ID: message.ID,
61
- attempts: message.attempts,
62
- error: e,
63
- unrecoverable: isUnrecoverable(service, e)
53
+ if (isStandardError(e)) {
54
+ LOG.error(`${service.name}: Programming error detected:`, e)
55
+ toBeDeleted.push(ID)
56
+ throw new Error(`${service.name}: Programming error detected.`)
57
+ }
58
+ if (e.unrecoverable) {
59
+ LOG.error(`${service.name}: Unrecoverable error:`, e)
60
+ if (options.maxAttempts) {
61
+ const _msg = { ID, attempts: options.maxAttempts }
62
+ if (options.storeLastError !== false) _msg.lastError = e
63
+ toBeUpdated.push(_msg)
64
+ } else toBeDeleted.push(ID)
65
+ } else {
66
+ LOG.error(`${service.name}: Emit failed:`, e)
67
+ const _msg = { ID }
68
+ if (options.storeLastError !== false) _msg.lastError = e
69
+ toBeUpdated.push(_msg)
70
+ return false
64
71
  }
65
- throw Object.assign(new Error('processing failed'), { failedMessage })
66
72
  }
67
- })
73
+ }
74
+ if (options.parallel) {
75
+ const first = messages.next()?.value // First try to see if message can be emitted
76
+ if (first && (await run(first)) === false) return // No need to process the rest if the emit failed
77
+ const res = await Promise.allSettled([...messages].map(run))
78
+ const errors = res.filter(r => r.status === 'rejected').map(r => r.reason)
79
+ if (errors.length) {
80
+ throw new Error(`${service.name}: Programming errors detected.`)
81
+ }
82
+ } else {
83
+ for (const msg of messages) {
84
+ if ((await run(msg)) === false) break
85
+ }
86
+ }
68
87
  }
69
88
 
70
89
  // Note: This function can also run for each tenant on startup
@@ -89,15 +108,7 @@ const processMessages = async (service, tenant, _opts = {}) => {
89
108
  } catch (e) {
90
109
  // could potentially be a timeout
91
110
  const _waitingTime = waitingTime(opts.attempt)
92
- LOG.error(
93
- 'Outbox SELECT FOR UPDATE failed',
94
- opts.attempt > 0
95
- ? ''
96
- : {
97
- cause: e
98
- },
99
- `Retrying in ${Math.round(_waitingTime / 1000)} s`
100
- )
111
+ LOG.error(`${name}: Message retrieval failed`, e, `Retrying in ${Math.round(_waitingTime / 1000)} s`)
101
112
  outboxRunner.schedule(
102
113
  {
103
114
  name,
@@ -108,57 +119,85 @@ const processMessages = async (service, tenant, _opts = {}) => {
108
119
  )
109
120
  return
110
121
  }
111
- const messagesToBeDeleted = []
112
- let error
113
- for (const m of messages) {
114
- try {
115
- await _processSingleMessage(service, m, messagesToBeDeleted)
116
- } catch (e) {
117
- error = e
118
- break
122
+ let currMaxAttempts = 0
123
+ const messagesGen = function* () {
124
+ for (const _message of messages) {
125
+ const msg = _safeJSONParse(_message.msg)
126
+ const userId = msg[cdsUser]
127
+ delete msg[cdsUser]
128
+ currMaxAttempts = Math.max(_message.attempts || 0, currMaxAttempts)
129
+ const user = new cds.User.Privileged(userId)
130
+ if (!msg) continue
131
+ const res = {
132
+ process: () =>
133
+ Promise.resolve().then(async () => {
134
+ if (userId) cds.context = { user }
135
+ try {
136
+ return service._emitImmediate && (await service._emitImmediate(msg))
137
+ } catch (e) {
138
+ if (isUnrecoverable(service, e)) e.unrecoverable = true
139
+ throw e
140
+ }
141
+ }),
142
+ ID: _message.ID,
143
+ msg,
144
+ user,
145
+ opts
146
+ }
147
+ yield res
119
148
  }
120
149
  }
121
- if (error && error.failedMessage.unrecoverable) {
122
- // opts.crashOnError is not official!!!
150
+
151
+ const process = service.outbox?.process || this.process || processDefault
152
+ const toBeDeleted = []
153
+ const toBeUpdated = []
154
+ try {
155
+ await process(messagesGen(), {
156
+ toBeDeleted,
157
+ toBeUpdated,
158
+ service,
159
+ emit: service._emitImmediate.bind(service),
160
+ options: opts
161
+ })
162
+ } catch (e) {
123
163
  if (opts.crashOnError !== false) letAppCrash = true
124
- messagesToBeDeleted.push(error.failedMessage.ID)
125
164
  }
126
- if (messagesToBeDeleted.length) await DELETE.from(messagesEntity).where('ID in', messagesToBeDeleted)
127
- if (error) {
128
- if (!error.failedMessage.unrecoverable) {
129
- const _waitingTime = waitingTime(error.failedMessage.attempts)
130
- const info = { service: name, event: error.failedMessage.event }
131
- if (error.failedMessage.attempts > 0) info.cause = error.failedMessage.error
132
- LOG.error('Emit failed', info, `Retrying in ${Math.round(_waitingTime / 1000)} s`)
165
+
166
+ const queries = []
167
+ const _waitingTime = waitingTime(currMaxAttempts)
168
+ if (toBeDeleted.length) queries.push(DELETE.from(messagesEntity).where('ID in', toBeDeleted))
169
+ if (toBeUpdated.length) {
170
+ for (const toBeUpdatedMsg of toBeUpdated) {
171
+ if (toBeDeleted.includes(toBeUpdatedMsg.ID)) continue
133
172
  const data = {
134
173
  attempts: { '+=': 1 }
135
174
  }
136
- if (opts.storeLastError) {
137
- data.lastError = util.inspect(error.failedMessage.error)
138
- }
139
- await UPDATE(messagesEntity).where({ ID: error.failedMessage.ID }).set(data)
140
- outboxRunner.schedule(
141
- {
142
- name,
143
- tenant,
144
- waitingTime: _waitingTime
145
- },
146
- () => processMessages(service, tenant, opts)
147
- )
148
- } else {
149
- LOG.error(
150
- 'Emit failed',
151
- { service: name, event: error.failedMessage.event, cause: error.failedMessage.error },
152
- 'Unrecoverable, outbox entry deleted'
153
- )
175
+ Object.assign(data, toBeUpdatedMsg)
176
+ if (data.lastError && typeof data.lastError !== 'string') data.lastError = util.inspect(data.lastError)
177
+ queries.push(UPDATE(messagesEntity).where({ ID: toBeUpdatedMsg.ID }).set(data))
154
178
  }
179
+ }
180
+
181
+ await Promise.all(queries)
182
+
183
+ if (letAppCrash) return
184
+
185
+ if (toBeUpdated.length) {
186
+ LOG.error(`${name}: Some messages could not be emitted, retrying in ${Math.round(_waitingTime / 1000)} s`)
187
+ return outboxRunner.schedule(
188
+ {
189
+ name,
190
+ tenant,
191
+ waitingTime: _waitingTime
192
+ },
193
+ () => processMessages(service, tenant, opts)
194
+ )
195
+ }
196
+ outboxRunner.success({ name, tenant })
197
+ if (toBeDeleted.length === opts.chunkSize) {
198
+ processMessages(service, tenant, opts) // We only processed max. opts.chunkSize, so there might be more
155
199
  } else {
156
- outboxRunner.success({ name, tenant })
157
- if (messages.length === opts.chunkSize) {
158
- processMessages(service, tenant, opts) // We only processed max. opts.chunkSize, so there might be more
159
- } else {
160
- LOG._trace && LOG.trace(`All persistent-outbox messages processed for service '${service.name}'`)
161
- }
200
+ LOG._trace && LOG.trace(`${name}: All messages processed`)
162
201
  }
163
202
  }, config)
164
203
  spawn.on('done', () => {
@@ -4,6 +4,7 @@ const OutboxService = require('./Outbox')
4
4
  const ExtendedModels = require('../../../lib/srv/srv-models')
5
5
 
6
6
  const appId = require('./common-utils/appId')
7
+ const { context } = require('../../../lib/core/classes')
7
8
 
8
9
  const _topic = declared => declared['@topic'] || declared.name
9
10
 
@@ -81,14 +82,22 @@ class MessagingService extends OutboxService {
81
82
  const _msg = typeof event === 'object' ? event : { event, data, headers }
82
83
  if (_msg instanceof cds.Event) return super.emit(_msg)
83
84
  if (_msg.inbound && !cds.context) {
84
- return cds._context.run({ tenant: _msg.tenant, user: cds.User.privileged }, async () => {
85
- if (cds.model) {
86
- const ctx = cds.context
87
- ctx.model = await ExtendedModels.model4(ctx.tenant, ctx.features)
85
+ return cds._context.run(
86
+ {
87
+ tenant: _msg.tenant,
88
+ user: cds.User.privileged,
89
+ ...(_msg._?.req && { req: _msg._.req }),
90
+ ...(_msg._?.res && { res: _msg._.res })
91
+ },
92
+ async () => {
93
+ if (cds.model) {
94
+ const ctx = cds.context
95
+ ctx.model = await ExtendedModels.model4(ctx.tenant, ctx.features)
96
+ }
97
+ const msg = new cds.Event(this.message4(_msg))
98
+ return super.emit(msg)
88
99
  }
89
- const msg = new cds.Event(this.message4(_msg))
90
- return super.emit(msg)
91
- })
100
+ )
92
101
  }
93
102
  const msg = new cds.Event(this.message4(_msg))
94
103
  return super.emit(msg)
@@ -164,7 +164,7 @@ const resolvedTargetOfQuery = q => {
164
164
  const transitions = (typeof q === 'object' && (q.SELECT || q.INSERT || q.UPDATE || q.DELETE)._transitions) || []
165
165
  return transitions.length && [transitions.length - 1].target
166
166
  }
167
-
167
+ let logged
168
168
  class RemoteService extends cds.Service {
169
169
  init() {
170
170
  if (!this.options.credentials) {
@@ -178,6 +178,17 @@ class RemoteService extends cds.Service {
178
178
  getDestination((this.definition && this.definition.name) || this.datasource, this.options.credentials)
179
179
  this.requestTimeout = this.options.credentials.requestTimeout
180
180
  if (this.requestTimeout == null) this.requestTimeout = 60000
181
+ if (cds.env.features.fetch_csrf && !logged) {
182
+ // for logging once for all remote services
183
+ logged = true
184
+ LOG._warn &&
185
+ LOG.warn(
186
+ 'Configuration option "cds.env.features.fetch_csrf" is deprecated.\n Please use "csrf"/"csrfInBatch" as described in https://cap.cloud.sap/docs/node.js/remote-services'
187
+ )
188
+ }
189
+ // REVISIT: remove cds.env.features.fetch_csrf in next major ^7
190
+ this.csrf = cds.env.features.fetch_csrf || this.options.csrf
191
+ this.csrfInBatch = this.options.csrfInBatch
181
192
  this.path = this.options.credentials.path
182
193
  this.kind = getKind(this.options) // TODO: Simplify
183
194
 
@@ -210,7 +221,9 @@ class RemoteService extends cds.Service {
210
221
  this.kind,
211
222
  resolvedTarget,
212
223
  returnType,
213
- this.destinationOptions
224
+ this.destinationOptions,
225
+ this.csrf,
226
+ this.csrfInBatch
214
227
  )
215
228
 
216
229
  // hidden compat flag in order to suppress logging response body of failed request
@@ -22,7 +22,7 @@ const _sanitizeHeaders = headers => {
22
22
  return headers
23
23
  }
24
24
 
25
- const _executeHttpRequest = async ({ requestConfig, destination, destinationOptions, jwt }) => {
25
+ const _executeHttpRequest = async ({ requestConfig, destination, destinationOptions, jwt, csrf, csrfInBatch }) => {
26
26
  const { executeHttpRequestWithOrigin } = cloudSdk()
27
27
  const destinationName = typeof destination === 'string' && destination
28
28
 
@@ -44,11 +44,7 @@ const _executeHttpRequest = async ({ requestConfig, destination, destinationOpti
44
44
 
45
45
  let requestOptions
46
46
  if (PPPD[requestConfig.method]) {
47
- // For GET requests, one doesn't need to fetch CSRF tokens.
48
- // Once we support batch requests (other than autoBatched GET requests),
49
- // we must check the respective subrequests.
50
- const csrfRequired = requestConfig._autoBatch ? false : cds.env.features.fetch_csrf === true
51
- requestOptions = { fetchCsrfToken: csrfRequired }
47
+ requestOptions = { fetchCsrfToken: requestConfig._autoBatch ? csrfInBatch === true : csrf === true }
52
48
  }
53
49
 
54
50
  LOG._debug &&
@@ -282,11 +278,28 @@ const _getSanitizedError = (e, reqOptions, options = { suppressRemoteResponseBod
282
278
  // eslint-disable-next-line complexity
283
279
  const run = async (
284
280
  requestConfig,
285
- { destination, jwt, kind, resolvedTarget, returnType, suppressRemoteResponseBody, destinationOptions }
281
+ {
282
+ destination,
283
+ jwt,
284
+ kind,
285
+ resolvedTarget,
286
+ returnType,
287
+ suppressRemoteResponseBody,
288
+ destinationOptions,
289
+ csrf,
290
+ csrfInBatch
291
+ }
286
292
  ) => {
287
293
  let response
288
294
  try {
289
- response = await _executeHttpRequest({ requestConfig, destination, destinationOptions, jwt })
295
+ response = await _executeHttpRequest({
296
+ requestConfig,
297
+ destination,
298
+ destinationOptions,
299
+ jwt,
300
+ csrf,
301
+ csrfInBatch
302
+ })
290
303
  } catch (e) {
291
304
  // > axios received status >= 400 -> gateway error
292
305
  const msg = e?.response?.data?.error?.message?.value ?? e?.response?.data?.error?.message ?? e.message
@@ -466,7 +479,7 @@ const getReqOptions = (req, query, service) => {
466
479
 
467
480
  // forward all dwc-* headers
468
481
  if (service.options.forward_dwc_headers) {
469
- const originalHeaders = (req.context && req.context._ && req.context._.req && req.context._.req.headers) || {}
482
+ const originalHeaders = req.context?.http.req.headers || {}
470
483
  for (const k in originalHeaders) if (k.match(/^dwc-/)) reqOptions.headers[k] = originalHeaders[k]
471
484
  }
472
485
 
@@ -516,9 +529,26 @@ const getReqOptions = (req, query, service) => {
516
529
  return reqOptions
517
530
  }
518
531
 
519
- const getAdditionalOptions = (req, destination, kind, resolvedTarget, returnType, destinationOptions) => {
532
+ const getAdditionalOptions = (
533
+ req,
534
+ destination,
535
+ kind,
536
+ resolvedTarget,
537
+ returnType,
538
+ destinationOptions,
539
+ csrf,
540
+ csrfInBatch
541
+ ) => {
520
542
  const jwt = getJwt(req)
521
- const additionalOptions = { destination, kind, resolvedTarget, returnType, destinationOptions }
543
+ const additionalOptions = {
544
+ destination,
545
+ kind,
546
+ resolvedTarget,
547
+ returnType,
548
+ destinationOptions,
549
+ csrf,
550
+ csrfInBatch
551
+ }
522
552
  if (jwt) additionalOptions.jwt = jwt
523
553
  return additionalOptions
524
554
  }
@@ -9,6 +9,7 @@ let _sqlite
9
9
  */
10
10
  const localized = require('./localized')
11
11
  const convertAssocToOneManaged = require('./convertAssocToOneManaged')
12
+ const convertDraftAdminPathExpression = require('./convertDraftAdminPathExpression')
12
13
 
13
14
  /*
14
15
  * sqlite-specific execution
@@ -72,8 +73,10 @@ module.exports = class SQLiteDatabase extends DatabaseService {
72
73
 
73
74
  _registerBeforeHandlers() {
74
75
  this.before(['CREATE', 'UPDATE'], '*', this._input) // > has to run before rewrite
75
- this.before(['CREATE', 'READ', 'UPDATE', 'DELETE'], '*', this._rewrite)
76
+ this.before(['CREATE', 'READ', 'UPDATE', 'DELETE', 'UPSERT'], '*', this._rewrite)
76
77
 
78
+ if (cds.env.features.lean_draft && cds.db?.kind !== 'better-sqlite')
79
+ this.before('READ', '*', convertDraftAdminPathExpression)
77
80
  this.before('READ', '*', convertAssocToOneManaged)
78
81
  this.before('READ', '*', localized) // > has to run after rewrite
79
82
  this.before('READ', '*', this._virtual)
@@ -0,0 +1,56 @@
1
+ const cds = require('../cds')
2
+
3
+ function sqliteConvertDraftAdminPathExpression(req) {
4
+ if (
5
+ !req.query?.SELECT ||
6
+ !req.query?._target?.name?.endsWith('.drafts') ||
7
+ req.query?.SELECT?.from?.args?.some(a => a.ref?.[0] === 'DRAFT_DraftAdministrativeData')
8
+ )
9
+ return
10
+ let hasDraftAdminPathExpression = false
11
+
12
+ const alias = req.query.SELECT.from.as
13
+
14
+ const _modifyCols = cols => {
15
+ return cols.map(col => {
16
+ if (col.ref?.length > 1 && col.ref[0] === 'DraftAdministrativeData') {
17
+ hasDraftAdminPathExpression = true
18
+ const newCol = { ...col }
19
+ newCol.ref = [...col.ref]
20
+ newCol.ref[0] = 'filterAdmin'
21
+ return newCol
22
+ } else if (col.ref?.length > 1 && alias && col.ref[0] === alias && col.ref[1] === 'DraftAdministrativeData') {
23
+ hasDraftAdminPathExpression = true
24
+ const newCol = { ...col }
25
+ newCol.ref = [...col.ref]
26
+ newCol.ref.shift()
27
+ newCol.ref[0] = 'filterAdmin'
28
+ return newCol
29
+ }
30
+ if (col.expand) {
31
+ const newCol = { ...col }
32
+ newCol.expand = _modifyCols(col.expand)
33
+ return newCol
34
+ }
35
+ return col
36
+ })
37
+ }
38
+
39
+ const clone = cds.ql.clone(req.query)
40
+
41
+ if (clone.SELECT.columns) clone.SELECT.columns = _modifyCols(req.query.SELECT.columns)
42
+ if (clone.SELECT.where) clone.SELECT.where = _modifyCols(req.query.SELECT.where)
43
+
44
+ if (hasDraftAdminPathExpression) {
45
+ clone
46
+ .join('DRAFT_DraftAdministrativeData', 'filterAdmin')
47
+ .on([
48
+ { ref: alias ? [alias, 'DraftAdministrativeData_DraftUUID'] : ['DraftAdministrativeData_DraftUUID'] },
49
+ '=',
50
+ { ref: ['filterAdmin', 'DraftUUID'] }
51
+ ])
52
+ req.query = clone
53
+ }
54
+ }
55
+ sqliteConvertDraftAdminPathExpression._initial = true
56
+ module.exports = sqliteConvertDraftAdminPathExpression
@@ -0,0 +1,41 @@
1
+ const InsertBuilder = require('../../db/sql-builder').InsertBuilder
2
+ const getAnnotatedColumns = require('../../db/sql-builder/annotations')
3
+
4
+ class CustomUpsertBuilder extends InsertBuilder {
5
+ annotatedColumns(entityName, csn) {
6
+ const { updateAnnotatedColumns } = getAnnotatedColumns(entityName, csn)
7
+
8
+ if (updateAnnotatedColumns?.size) {
9
+ this.managedCols = Array.from(updateAnnotatedColumns.keys())
10
+ }
11
+
12
+ return { insertAnnotatedColumns: updateAnnotatedColumns }
13
+ }
14
+
15
+ // REVISIT: We need to copy over the implementation for annotation handling
16
+ build() {
17
+ this._obj = { INSERT: this._obj.UPSERT, _target: this._obj._target }
18
+ super.build()
19
+ const csnKeys =
20
+ (this._obj._target ? this._obj._target.keys : this._csn.definitions[this._obj.INSERT.into].keys) || {}
21
+ const keys = Object.keys(csnKeys).filter(k => !csnKeys[k].isAssociation)
22
+ const updates = []
23
+ const columns = this._obj.INSERT.columns || Object.keys(this._obj.INSERT.entries[0])
24
+ if (this.managedCols) {
25
+ columns.push(...this.managedCols)
26
+ }
27
+
28
+ columns.forEach(col => {
29
+ const col_ = col.replace(/\./g, '_')
30
+ if (!keys.includes(col_)) updates.push(`${col_}=excluded.${col_}`)
31
+ })
32
+ const conflict = updates.length
33
+ ? ` ON CONFLICT(${keys}) DO UPDATE SET ` + updates.join(', ')
34
+ : ` ON CONFLICT(${keys}) DO NOTHING`
35
+
36
+ this._outputObj.sql = this._outputObj.sql + conflict
37
+ return this._outputObj
38
+ }
39
+ }
40
+
41
+ module.exports = CustomUpsertBuilder
@@ -23,6 +23,11 @@ const dependencies = {
23
23
  const CustomUpdateBuilder = require('./CustomUpdateBuilder')
24
24
  Object.defineProperty(dependencies, 'UpdateBuilder', { value: CustomUpdateBuilder })
25
25
  return CustomUpdateBuilder
26
+ },
27
+ get UpsertBuilder() {
28
+ const CustomUpsertBuilder = require('./CustomUpsertBuilder')
29
+ Object.defineProperty(dependencies, 'UpsertBuilder', { value: CustomUpsertBuilder })
30
+ return CustomUpsertBuilder
26
31
  }
27
32
  }
28
33
 
@@ -206,7 +206,7 @@ function executePlainSQL(dbc, sql, values, isOne, postMapper) {
206
206
  return executeSelectSQL(dbc, sql, values, isOne, postMapper)
207
207
  }
208
208
 
209
- if (/^\s*insert/i.test(sql)) {
209
+ if (/^\s*insert/i.test(sql) || /^\s*upsert/i.test(sql)) {
210
210
  return executeInsertSQL(dbc, sql, values)
211
211
  }
212
212
 
@@ -48,8 +48,8 @@
48
48
 
49
49
  /**
50
50
  * @typedef {object} TemplateProcessorPathOptions
51
- * @property {object} [extraKeys]
52
- * @property {function} [rowKeysGenerator]
51
+ * @property {object} [draftKeys]
52
+ * @property {function} [rowUUIDGenerator]
53
53
  * @property {string[]} [segments=[]] - Path segments to relate the error message.
54
54
  * @property {boolean} [includeKeyValues=false] Indicates whether the key values are included in the path segments
55
55
  * The path segments are used to build the error target (a relative resource path)