@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
@@ -1,24 +1,25 @@
1
- const last = /\w+$/
2
1
 
3
- exports.singular4 = (dn,stripped) => {
2
+ this.singular4 = (dn,stripped) => {
4
3
  let n = dn.name || dn; if (stripped) n = n.match(last)[0]
5
4
  return dn['@singular'] || (
6
- /.*species|news$/i.test(n) ? n :
7
- /.*ess$/.test(n) ? n : // Address
8
- /.*ees$/.test(n) ? n.slice(0, -1) : // Employees --> Employee
9
- /.*[sz]es$/.test(n) ? n.slice(0, -2) :
10
- /.*[^aeiou]ies$/.test(n) ? n.slice(0, -3) + 'y' : // Deliveries --> Delivery
11
- /.*s$/.test(n) ? n.slice(0, -1) :
5
+ /species|news$/i.test(n) ? n :
6
+ /ess$/.test(n) ? n : // Address
7
+ /ees$/.test(n) ? n.slice(0, -1) : // Employees --> Employee
8
+ /[sz]es$/.test(n) ? n.slice(0, -2) :
9
+ /[^aeiou]ies$/.test(n) ? n.slice(0, -3) + 'y' : // Deliveries --> Delivery
10
+ /s$/.test(n) ? n.slice(0, -1) :
12
11
  n
13
12
  )
14
13
  }
15
14
 
16
- exports.plural4 = (dn,stripped) => {
15
+ this.plural4 = (dn,stripped) => {
17
16
  let n = dn.name || dn; if (stripped) n = n.match(last)[0]
18
17
  return dn['@plural'] || (
19
- /.*analysis|status|species|news$/i.test(n) ? n :
20
- /.*[^aeiou]y$/.test(n) ? n.slice(0,-1) + 'ies' :
21
- /.*(s|x|z|ch|sh)$/.test(n) ? n + 'es' :
18
+ /analysis|status|species|sheep|news$/i.test(n) ? n :
19
+ /[^aeiou]y$/.test(n) ? n.slice(0,-1) + 'ies' :
20
+ /(s|x|z|ch|sh)$/.test(n) ? n + 'es' :
22
21
  n + 's'
23
22
  )
24
23
  }
24
+
25
+ const last = /\w+$/
package/lib/utils/tar.js CHANGED
@@ -15,16 +15,37 @@ const win = path => {
15
15
  if (Array.isArray(path)) return path.map(el => win(el))
16
16
  }
17
17
 
18
- // Copy files to temp dir on Windows and pack temp dir.
18
+ async function copyDir(src, dest) {
19
+ if ((await fs.promises.stat(src)).isDirectory()) {
20
+ const entries = await fs.promises.readdir(src)
21
+ return Promise.all(entries.map(async each => copyDir(path.join(src, each), path.join(dest, each))))
22
+ } else {
23
+ await fs.promises.mkdir(path.dirname(dest), { recursive: true })
24
+ return fs.promises.copyFile(src, dest)
25
+ }
26
+ }
27
+
28
+ // Copy resources containing files and folders to temp dir on Windows and pack temp dir.
19
29
  // cli tar has a size limit on Windows.
20
- const createTemp = async (root, files) => {
21
- const temp = await fs.promises.mkdtemp(`${fs.realpathSync(require('os').tmpdir())}${path.sep}tar-`)
22
- for (const file of files) {
23
- const fname = path.relative(root, file)
24
- const destination = path.join(temp, fname)
25
- const dirname = path.dirname(destination)
26
- if (!await exists(dirname)) await fs.promises.mkdir(dirname, { recursive: true })
27
- await fs.promises.copyFile(file, destination)
30
+ const createTemp = async (root, resources) => {
31
+ // Asynchronously copies the entire content from src to dest.
32
+ const temp = await fs.promises.mkdtemp(`${fs.realpathSync(require('os').tmpdir())}${path.sep}tar-`)
33
+ for (let resource of resources) {
34
+ const destination = path.join(temp, path.relative(root, resource))
35
+ if ((await fs.promises.stat(resource)).isFile()) {
36
+ const dirName = path.dirname(destination)
37
+ if (!await exists(dirName)) {
38
+ await fs.promises.mkdir(dirName, { recursive: true })
39
+ }
40
+ await fs.promises.copyFile(resource, destination)
41
+ } else {
42
+ if (fs.promises.cp) {
43
+ await fs.promises.cp(resource, destination, { recursive: true })
44
+ } else {
45
+ // node < 16
46
+ await copyDir(resource, destination)
47
+ }
48
+ }
28
49
  }
29
50
 
30
51
  return temp
@@ -58,8 +79,9 @@ exports.create = async (dir='.', ...args) => {
58
79
 
59
80
  if (typeof dir === 'string') dir = _resolve(dir)
60
81
  if (Array.isArray(dir)) [ dir, ...args ] = [ cds.root, dir, ...args ]
61
-
82
+
62
83
  let c, temp
84
+ args = args.filter(el => el)
63
85
  if (process.platform === 'win32') {
64
86
  const spawnDir = (dir, args) => {
65
87
  if (args.some(arg => arg === '-f')) return spawn ('tar', ['c', '-C', win(dir), ...win(args)])
@@ -76,7 +98,7 @@ exports.create = async (dir='.', ...args) => {
76
98
  } else {
77
99
  if (Array.isArray(args[0])) args.push (...args.shift().map (f => path.isAbsolute(f) ? path.relative(dir,f) : f))
78
100
  else args.push('.')
79
-
101
+
80
102
  c = spawn ('tar', ['c', '-C', dir, ...args])
81
103
  }
82
104
 
@@ -88,8 +110,16 @@ exports.create = async (dir='.', ...args) => {
88
110
  * @example const buffer = await tar.c('src/dir')
89
111
  */
90
112
  then (r,e) {
91
- const bb=[]; c.stdout.on('data', b => bb.push(b))
92
- c.on('close', ()=>r(Buffer.concat(bb)))
113
+ const bb=[]
114
+ const eb=[]
115
+ c.stdout.on('data', b => bb.push(b))
116
+ c.stderr.on('data', b => eb.push(b))
117
+ c.on('close', (code) => {
118
+ if (code === 0) {
119
+ return r(Buffer.concat(bb))
120
+ }
121
+ e(new Error('tar: ' + Buffer.concat(eb)))
122
+ })
93
123
  c.on('error', e)
94
124
  if (process.platform === 'win32') {
95
125
  c.on('close', async () => temp && exists(temp) && await rimraf(temp))
@@ -162,10 +162,10 @@ class ODataRequest extends cds.Request {
162
162
  * super
163
163
  */
164
164
  const { user } = req
165
-
165
+ const tenant = req.tenant || user?.tenant
166
166
  // REVISIT: public API for query options (express style req.query already in use)?
167
167
  const _queryOptions = odataReq.getQueryOptions()
168
- super({ event, target, data, query, user, method, headers, req, res, _queryOptions })
168
+ super({ event, target, data, query, user, method, headers, req, res, _queryOptions, tenant })
169
169
  }
170
170
 
171
171
  /*
@@ -82,7 +82,7 @@ const action = service => {
82
82
  await tx.rollback(e).catch(() => {})
83
83
  }
84
84
  } finally {
85
- req.messages && odataRes.setHeader('sap-messages', getSapMessages(req.messages, req._.req))
85
+ req.messages && odataRes.setHeader('sap-messages', getSapMessages(req.messages, req.http.req))
86
86
 
87
87
  if (err) next(err)
88
88
  else next(null, toODataResult(result, req))
@@ -68,7 +68,7 @@ const create = service => {
68
68
  await tx.rollback(e).catch(() => {})
69
69
  }
70
70
  } finally {
71
- req.messages && odataRes.setHeader('sap-messages', getSapMessages(req.messages, req._.req))
71
+ req.messages && odataRes.setHeader('sap-messages', getSapMessages(req.messages, req.http.req))
72
72
 
73
73
  if (err) next(err)
74
74
  else next(null, toODataResult(result, req))
@@ -49,7 +49,7 @@ const del = service => {
49
49
  await tx.rollback(e).catch(() => {})
50
50
  }
51
51
  } finally {
52
- req.messages && odataRes.setHeader('sap-messages', getSapMessages(req.messages, req._.req))
52
+ req.messages && odataRes.setHeader('sap-messages', getSapMessages(req.messages, req.http.req))
53
53
 
54
54
  if (err) next(err)
55
55
  else next(null, null)
@@ -20,20 +20,6 @@ const metadata = service => {
20
20
 
21
21
  try {
22
22
  const { 'cds.xt.ModelProviderService': mps } = cds.services
23
- // REVISIT: The following block should replaced with the one commented bellow after the next release of cds-mtxs.
24
- // Currently lkg tests fail w/o the try-catch.
25
- let edmx
26
- try {
27
- if (mps) edmx = await mps.getEdmx({ tenant, model: service.model, service: service.definition.name, locale })
28
- // eslint-disable-next-line no-empty
29
- } catch (_) {}
30
- if (!edmx)
31
- edmx = cds.localize(
32
- service.model,
33
- locale,
34
- cds.compile.to.edmx(service.model, { service: service.definition.name })
35
- )
36
- /*
37
23
  let edmx = mps
38
24
  ? await mps.getEdmx({ tenant, model: service.model, service: service.definition.name, locale })
39
25
  : cds.localize(
@@ -41,7 +27,7 @@ const metadata = service => {
41
27
  locale,
42
28
  // REVISIT: we could cache this in model._cached
43
29
  cds.compile.to.edmx(service.model, { service: service.definition.name })
44
- ) */
30
+ )
45
31
  return next(null, toODataResult(edmx))
46
32
  } catch (e) {
47
33
  if (LOG._error) {
@@ -491,7 +491,7 @@ const read = service => {
491
491
  await tx.rollback(e).catch(() => {})
492
492
  }
493
493
  } finally {
494
- req.messages && odataRes.setHeader('sap-messages', getSapMessages(req.messages, req._.req))
494
+ req.messages && odataRes.setHeader('sap-messages', getSapMessages(req.messages, req.http.req))
495
495
 
496
496
  if (err) next(err)
497
497
  else next(null, result, additional)
@@ -179,7 +179,7 @@ const update = service => {
179
179
  await tx.rollback(e).catch(() => {})
180
180
  }
181
181
  } finally {
182
- req.messages && odataRes.setHeader('sap-messages', getSapMessages(req.messages, req._.req))
182
+ req.messages && odataRes.setHeader('sap-messages', getSapMessages(req.messages, req.http.req))
183
183
 
184
184
  if (err) next(err)
185
185
  else if (primitive && result) {
@@ -37,7 +37,7 @@ UriSyntaxError.Message = {
37
37
  KEY_VALUE_NOT_FOUND: "No '%s' value found for key '%s'",
38
38
  PREVIOUS_TYPE_HAS_NO_MEDIA: "Previous segment type '%s' does not have a media resource",
39
39
  MUST_BE_COUNT_OR_BOUND_OPERATION: "Expected current segment '%s' to be '$count' or a bound operation",
40
- MUST_BE_COUNT_OR_REF_OR_BOUND_OPERATION: "Expected current segment '%s' to be '$count', '$ref', or a bound operation",
40
+ MUST_BE_COUNT_OR_REF_OR_BOUND_OPERATION: "Expected current segment '%s' to be '$count', '$ref', a bound operation or a key value with a proper type",
41
41
 
42
42
  ALIAS_NOT_FOUND: "Parameter alias '%s' not found",
43
43
  WRONG_ALIAS_VALUE: "Wrong value for parameter alias '%s'",
@@ -143,7 +143,12 @@ class UriParser {
143
143
  * @returns {UriInfo} the result of parsing
144
144
  */
145
145
  parseRelativeUri (uri, queryOptions) {
146
- let uriPathSegments = uri.split('/').map(decodeURIComponent)
146
+ let uriPathSegments
147
+ try {
148
+ uriPathSegments = uri.split('/').map(decodeURIComponent)
149
+ } catch (error) {
150
+ throw new UriSyntaxError('wrong percent encoding in uri: ' + uri)
151
+ }
147
152
 
148
153
  let uriInfo = new UriInfo()
149
154
 
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const { AsyncResource } = require('node:async_hooks')
3
+ const { AsyncResource } = require('async_hooks')
4
4
  const Transform = require('stream').Transform
5
5
 
6
6
  /**
@@ -24,18 +24,6 @@ class ConditionalRequestValidator {
24
24
  if (method !== HttpMethods.GET && !ifMatch && !ifNoneMatch) throw new PreconditionRequiredError()
25
25
  return
26
26
  }
27
-
28
- if (ifMatch || ifNoneMatch) {
29
- // Careless clients send this also for DELETE and POST, other careless clients send the star in double-quotes.
30
- if ([HttpMethods.POST].includes(method)) {
31
- if (
32
- (ifMatch && ifMatch.trim() !== '*' && ifMatch.trim() !== '"*"') ||
33
- (ifNoneMatch && ifNoneMatch.trim() !== '*' && ifNoneMatch.trim() !== '"*"')
34
- ) {
35
- throw new PreconditionFailedError()
36
- }
37
- }
38
- }
39
27
  }
40
28
 
41
29
  /**
@@ -18,12 +18,6 @@ const _getEntitySets = (edm, namespace) => {
18
18
  return entities
19
19
  }
20
20
 
21
- const _getConcurrent = (namespace, element, csn) => {
22
- // autoexposed entities now used . in csn and _ in edm
23
- const e = findCsnTargetFor(element, csn, namespace)
24
- return !!e._etag
25
- }
26
-
27
21
  const oDataConfiguration = (edm, csn) => {
28
22
  let namespace
29
23
  for (const prop in edm) {
@@ -44,7 +38,7 @@ const oDataConfiguration = (edm, csn) => {
44
38
 
45
39
  configuration[entitySet] = {
46
40
  maxPageSize: getMaxPageSize(e),
47
- isConcurrent: _getConcurrent(namespace, entitySet, csn)
41
+ isConcurrent: !!e._etag
48
42
  }
49
43
 
50
44
  // custom aggregates
@@ -208,6 +208,9 @@ const _processCategory = (req, category, elementInfo, options, previousResult) =
208
208
  localizeAfterDraftActivate(row, key, req.locale)
209
209
  break
210
210
 
211
+ case '@cds.Boolean':
212
+ if (row[key] != null) row[key] = !!row[key]
213
+
211
214
  // no default
212
215
  }
213
216
  }
@@ -270,6 +273,7 @@ const _pick = options => (element, target) => {
270
273
 
271
274
  if (element['@odata.etag']) categories.push('@odata.etag')
272
275
  if (element._type === 'cds.Decimal') categories.push('@cds.Decimal')
276
+ if (cds.db?.kind === 'better-sqlite' && element._type === 'cds.Boolean') categories.push('@cds.Boolean')
273
277
 
274
278
  categories.push(..._assocs(element, target))
275
279
 
@@ -50,7 +50,29 @@ class ApplicationService extends cds.Service {
50
50
  }
51
51
 
52
52
  registerFioriHandlers() {
53
- return require('../../fiori/generic').impl.call(this)
53
+ if (cds.env.features.lean_draft) {
54
+ const {
55
+ onNewDraft,
56
+ onDraftPrepare,
57
+ onDraftActivate,
58
+ onPatch,
59
+ onDraftEdit,
60
+ onDelete
61
+ } = require('../../fiori/lean-draft')
62
+ const LOG = cds.log('fiori|drafts')
63
+
64
+ for (let each of this.entities)
65
+ if (each.drafts) {
66
+ LOG.debug('serving drafts for', { entity: each.name })
67
+ this.on('NEW', each, onNewDraft)
68
+ this.on('PATCH', each, onPatch)
69
+ this.on('EDIT', each, onDraftEdit)
70
+ this.on('draftPrepare', each, onDraftPrepare)
71
+ this.on('draftActivate', each, onDraftActivate)
72
+ this.on('draftActivate', each, onDraftActivate)
73
+ this.on(['CANCEL', 'DELETE'], each, onDelete)
74
+ }
75
+ } else return require('../../fiori/generic').impl.call(this)
54
76
  }
55
77
 
56
78
  registerCrudHandlers() {
@@ -259,46 +259,6 @@ const _checkFormatElement = (element, value, errors, key, pathSegments) => {
259
259
  }
260
260
  }
261
261
 
262
- // check for forbidden deep operations for association
263
- const checkIfAssocDeep = (element, value, req) => {
264
- if (!value) return
265
-
266
- if (element.on) {
267
- req.error(
268
- assertError(
269
- element.is2one
270
- ? { code: ASSERT_DEEP_ASSOCIATION, args: ['unmanaged to-one', element.name] }
271
- : { code: ASSERT_DEEP_ASSOCIATION, args: ['to-many', element.name] },
272
- element,
273
- value
274
- )
275
- )
276
-
277
- return
278
- }
279
-
280
- if (element.is2one) {
281
- // managed to one
282
- Object.keys(value).forEach(prop => {
283
- if (typeof value[prop] !== 'object') {
284
- const foreignKey = element._foreignKeys.find(fk => fk.childElement.name === prop)
285
- if (foreignKey) return
286
-
287
- const key = element.keys.find(element => element.ref[0] === prop)
288
- if (key) return
289
-
290
- const err = assertError(
291
- { code: ASSERT_DEEP_ASSOCIATION, args: ['managed to-one', element.name] },
292
- element,
293
- value
294
- )
295
- err.target += `.${prop}`
296
- req.error(err)
297
- }
298
- })
299
- }
300
- }
301
-
302
262
  /**
303
263
  * @param {import('../../types/api').InputConstraints} constraints
304
264
  */
@@ -407,7 +367,6 @@ module.exports = {
407
367
  checkInputConstraints,
408
368
  checkKeys,
409
369
  assertError,
410
- checkIfAssocDeep,
411
370
  checkStaticElementByKey,
412
371
  assertNotNullError,
413
372
  assertTargets
@@ -283,7 +283,11 @@ const _selectDeepUpdateData = async args => {
283
283
 
284
284
  // if a view has an orderBy with renamed field, we need to resolve it
285
285
  const _resolveOrderBy = (orderBy, transitions) => {
286
- if (orderBy && transitions.length > 0) orderBy.map(el => (el.ref[0] = transitions[0].mapping.get(el.ref[0]).ref[0]))
286
+ // no resolved entity found
287
+ if (!transitions?.length) return
288
+ // if there are no renamed fields, no need to resolve
289
+ if (!transitions[0].mapping.size) return
290
+ if (orderBy) orderBy.map(el => (el.ref[0] = transitions[0].mapping.get(el.ref[0]).ref[0]))
287
291
  }
288
292
 
289
293
  /*
@@ -9,9 +9,9 @@ const reject = (req, reason = null) => {
9
9
  // unauthorized or forbidden?
10
10
  if (req.user._is_anonymous) {
11
11
  // REVISIT: challenges handling should be done in protocol adapter (i.e., express error middleware)
12
- // REVISIT: improve `req._.req` check if this is an HTTP request
13
- if (req._.req && req.user._challenges && req.user._challenges.length > 0) {
14
- req._.res.set('WWW-Authenticate', req.user._challenges.join(';'))
12
+ // REVISIT: improve `req.http.req` check if this is an HTTP request
13
+ if (req.http?.req && req.user._challenges && req.user._challenges.length > 0) {
14
+ req.http.res.set('WWW-Authenticate', req.user._challenges.join(';'))
15
15
  }
16
16
 
17
17
  // REVISIT: security log in else case?
@@ -26,7 +26,7 @@ const _targetEntityDoesNotExist = async req => {
26
26
 
27
27
  exports.impl = cds.service.impl(function () {
28
28
  // eslint-disable-next-line complexity
29
- this.on(['CREATE', 'READ', 'UPDATE', 'DELETE'], '*', async function (req) {
29
+ this.on(['CREATE', 'READ', 'UPDATE', 'DELETE', 'UPSERT'], '*', async function (req) {
30
30
  if (typeof req.query !== 'string' && req.target && req.target._hasPersistenceSkip) {
31
31
  throw getError({
32
32
  code: 501,
@@ -16,6 +16,7 @@ const { checkInputConstraints, assertTargets } = require('../../cds-services/uti
16
16
  const getTemplate = require('../utils/template')
17
17
  const templateProcessor = require('../utils/templateProcessor')
18
18
  const { getDataFromCQN, setDataFromCQN } = require('../utils/data')
19
+ const getRowUUIDGeneratorFn = require('../utils/rowUUIDGenerator')
19
20
 
20
21
  const _shouldSuppressErrorPropagation = (event, value) => {
21
22
  return (
@@ -34,24 +35,6 @@ const _getSimpleCategory = category => {
34
35
  return category
35
36
  }
36
37
 
37
- const _rowKeysGenerator = eventName => {
38
- if (eventName === 'UPDATE') return
39
- return (keyNames, row, template) => {
40
- for (const keyName of keyNames) {
41
- if (Object.prototype.hasOwnProperty.call(row, keyName)) {
42
- continue
43
- }
44
-
45
- const elementInfo = template.elements.get(keyName)
46
- const plain = elementInfo && elementInfo.picked && elementInfo.picked.plain
47
- if (!plain || !plain.categories) continue
48
- if (plain.categories.includes('uuid')) {
49
- row[keyName] = cds.utils.uuid()
50
- }
51
- }
52
- }
53
- }
54
-
55
38
  const _isDraftCoreComputed = (req, element, event) =>
56
39
  cds.env.features.preserve_computed !== false &&
57
40
  req._ &&
@@ -65,10 +48,7 @@ const _isStreamingProperty = (elements, row, property) =>
65
48
  )
66
49
 
67
50
  const _getMediaTypeValue = req =>
68
- req._.req &&
69
- req._.req.headers['content-type'] &&
70
- !req._.req.headers['content-type'].match(/json|multipart/i) &&
71
- req._.req.headers['content-type']
51
+ !req.http?.req?.headers?.['content-type'].match(/json|multipart/i) && req.http?.req?.headers?.['content-type']
72
52
 
73
53
  const _preProcessAssertTarget = (assocInfo, assertMap) => {
74
54
  const { element: assoc, row } = assocInfo
@@ -265,7 +245,7 @@ async function commonGenericInput(req) {
265
245
  }
266
246
 
267
247
  const pathOptions = {
268
- rowKeysGenerator: _rowKeysGenerator(req.event),
248
+ rowUUIDGenerator: getRowUUIDGeneratorFn(req.event),
269
249
  includeKeyValues: true,
270
250
  pathSegments: []
271
251
  }
@@ -276,7 +256,7 @@ async function commonGenericInput(req) {
276
256
  if (pathSegment) pathOptions.pathSegments.push(pathSegment)
277
257
 
278
258
  if (keys && 'IsActiveEntity' in keys) {
279
- pathOptions.extraKeys = { IsActiveEntity: keys.IsActiveEntity }
259
+ pathOptions.draftKeys = { IsActiveEntity: keys.IsActiveEntity }
280
260
  }
281
261
  }
282
262
 
@@ -1,23 +1,24 @@
1
1
  const cds = require('../../cds')
2
- const { getDefaultPageSize, getMaxPageSize } = require('../utils/page')
2
+ const { getPageSize } = require('../utils/page')
3
3
 
4
4
  const commonGenericPaging = function (req) {
5
5
  // only if http request
6
- if (!(req.http?.req || req._.req)) return
6
+ if (!req.http?.req) return
7
7
 
8
8
  // target === null if view with parameters
9
- if (!req.target || !req.query.SELECT || req.query.SELECT.one) return
9
+ if (!req.target || !req.query?.SELECT || req.query.SELECT.one) return
10
10
 
11
11
  _addPaging(req.query, req.target)
12
12
  }
13
13
 
14
- const _addPaging = function (query, target) {
15
- let { rows, offset } = query.SELECT.limit || {}
16
- rows = rows && 'val' in rows ? rows.val : getDefaultPageSize(target)
17
- offset = offset && 'val' in offset ? offset.val : 0
18
- query.limit(...[Math.min(rows, getMaxPageSize(target)), offset])
14
+ const _addPaging = function ({ SELECT }, target) {
15
+ const { rows } = SELECT.limit || (SELECT.limit = {})
16
+ const conf = getPageSize(target)
17
+ SELECT.limit.rows = {
18
+ val: !rows ? conf.default : Math.min(rows.val ?? rows, conf.max)
19
+ }
19
20
  //Handle nested limits
20
- if (query.SELECT.from.SELECT) _addPaging(query.SELECT.from, target)
21
+ if (SELECT.from.SELECT?.limit) _addPaging(SELECT.from, target)
21
22
  }
22
23
 
23
24
  /**
@@ -804,6 +804,33 @@ const _convertSelect = (query, model, _options) => {
804
804
  return query
805
805
  }
806
806
 
807
+ const _convertUpsert = (query, model) => {
808
+ // resolve path expression
809
+ const resolvedIntoClause = _convertPathExpressionForInsert(query.UPSERT.into, model)
810
+
811
+ const target = model.definitions[resolvedIntoClause]
812
+ if (!target) {
813
+ // if there is no target, just return original query, as a copy is not deep anyways and all the sub items of query.UPSERT are referenced only anyways
814
+ return query
815
+ }
816
+
817
+ // overwrite only .into, foreign keys are already set
818
+ // 'a' added as placeholder since its overwritten by Object.assign below
819
+ const upsert = UPSERT.into('a')
820
+
821
+ // REVISIT flatten structured types, currently its done in SQL builder
822
+
823
+ // We add all previous properties ot the newly created query.
824
+ // Reason is to not lose the query API functionality
825
+ Object.assign(upsert.UPSERT, query.UPSERT, { into: { ref: [resolvedIntoClause], as: query.UPSERT.into.as } })
826
+
827
+ const resolved = resolveView(upsert, model, cds.db)
828
+ // required for deplyoing of extensions, not used anywhere else except UpsertBuilder
829
+ resolved._target = resolved.UPSERT?._transitions?.[0].target || query._target
830
+ // resolved._target = query._target
831
+ return resolved
832
+ }
833
+
807
834
  const _convertInsert = (query, model) => {
808
835
  // resolve path expression
809
836
  const resolvedIntoClause = _convertPathExpressionForInsert(query.INSERT.into, model)
@@ -950,6 +977,10 @@ const cqn2cqn4sql = (query, model, options = { suppressSearch: false }) => {
950
977
  return _convertInsert(query, model)
951
978
  }
952
979
 
980
+ if (query.UPSERT) {
981
+ return _convertUpsert(query, model)
982
+ }
983
+
953
984
  if (query.DELETE) {
954
985
  return _convertDelete(query, model, options)
955
986
  }
@@ -129,21 +129,22 @@ const _findCsnTarget = (edmName, model, namespace) => {
129
129
  return target
130
130
  }
131
131
 
132
+ const _initializeCache = (model, namespace) => {
133
+ const cache = {}
134
+ for (const name in model.definitions) {
135
+ // do no cache entities within different namespace
136
+ if (!name.startsWith(`${namespace}.`)) continue
137
+ // cut off namespace and underscoreify entity name (OData does not allow dots)
138
+ cache[name.replace(new RegExp(`^${namespace}\\.`), '').replace(/\./g, '_')] = model.definitions[name]
139
+ }
140
+ return cache
141
+ }
142
+
132
143
  const findCsnTargetFor = (edmName, model, namespace) => {
133
- const cache =
134
- model._edmToCSNNameMap || Object.defineProperty(model, '_edmToCSNNameMap', { value: {} })._edmToCSNNameMap
135
- const edm2csnMap =
136
- cache[namespace] ||
137
- Object.defineProperty(cache, namespace, {
138
- get() {
139
- const _ = {}
140
- for (const name in model.definitions) {
141
- if (!name.startsWith(`${namespace}.`)) continue
142
- _[name.replace(new RegExp(`^${namespace}\\.`), '').replace(/\./g, '_')] = model.definitions[name]
143
- }
144
- return _
145
- }
146
- })[namespace]
144
+ const cache = model._edmToCSNNameMap || (model._edmToCSNNameMap = {})
145
+ const edm2csnMap = cache[namespace] || (cache[namespace] = _initializeCache(model, namespace))
146
+
147
+ if (edm2csnMap[edmName]) return edm2csnMap[edmName]
147
148
 
148
149
  const target = _findCsnTarget(edmName, model, namespace)
149
150
 
@@ -226,7 +227,12 @@ function getDraftTreeRoot(entity, model) {
226
227
  for (const k in model.definitions) {
227
228
  const e = model.definitions[k]
228
229
  if (e.kind !== 'entity' || !e.compositions) continue
229
- for (const c in e.compositions) if (e.compositions[c].target === current.name) parents.push(e)
230
+ for (const c in e.compositions)
231
+ if (
232
+ e.compositions[c].target === current.name ||
233
+ e.compositions[c].target === current.name.replace(/\.drafts/, '')
234
+ )
235
+ parents.push(e)
230
236
  }
231
237
  if (parents.length > 1 && parents.some(p => p !== parents[0])) {
232
238
  // > unable to determine single parent
@@ -19,7 +19,8 @@ const ensureUnlocalized = table => {
19
19
  return _table
20
20
  }
21
21
 
22
- const ensureDraftsSuffix = name => (name.endsWith('_drafts') ? name : `${ensureUnlocalized(name)}_drafts`)
22
+ const ensureDraftsSuffix = name =>
23
+ name.endsWith('_drafts') || name.endsWith('.drafts') ? name : `${ensureUnlocalized(name)}_drafts`
23
24
 
24
25
  const ensureNoDraftsSuffix = name => name.replace(/_drafts$/g, '')
25
26