@sap/cds 6.0.2 → 6.1.0

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 (134) hide show
  1. package/CHANGELOG.md +153 -19
  2. package/apis/cds.d.ts +11 -7
  3. package/apis/log.d.ts +48 -0
  4. package/apis/ql.d.ts +72 -15
  5. package/bin/build/buildTaskHandler.js +5 -2
  6. package/bin/build/constants.js +4 -1
  7. package/bin/build/provider/buildTaskHandlerEdmx.js +11 -39
  8. package/bin/build/provider/buildTaskHandlerFeatureToggles.js +13 -32
  9. package/bin/build/provider/buildTaskHandlerInternal.js +56 -4
  10. package/bin/build/provider/buildTaskProviderInternal.js +22 -14
  11. package/bin/build/provider/hana/index.js +8 -7
  12. package/bin/build/provider/java/index.js +18 -8
  13. package/bin/build/provider/mtx/index.js +7 -4
  14. package/bin/build/provider/mtx/resourcesTarBuilder.js +64 -35
  15. package/bin/build/provider/mtx-extension/index.js +57 -0
  16. package/bin/build/provider/mtx-sidecar/index.js +46 -18
  17. package/bin/build/provider/nodejs/index.js +34 -13
  18. package/bin/build/util.js +6 -4
  19. package/bin/deploy/to-hana/cfUtil.js +7 -2
  20. package/bin/deploy/to-hana/hana.js +6 -3
  21. package/bin/serve.js +8 -13
  22. package/lib/compile/{index.js → cds-compile.js} +0 -0
  23. package/lib/compile/extend.js +15 -5
  24. package/lib/compile/minify.js +1 -15
  25. package/lib/compile/parse.js +1 -1
  26. package/lib/compile/resolve.js +2 -2
  27. package/lib/compile/to/srvinfo.js +6 -4
  28. package/lib/{deploy.js → dbs/cds-deploy.js} +8 -8
  29. package/lib/env/{index.js → cds-env.js} +1 -17
  30. package/lib/env/{requires.js → cds-requires.js} +24 -3
  31. package/lib/env/defaults.js +7 -1
  32. package/lib/env/schemas/cds-package.json +11 -0
  33. package/lib/env/schemas/cds-rc.json +605 -0
  34. package/lib/index.js +20 -17
  35. package/lib/log/{errors.js → cds-error.js} +1 -1
  36. package/lib/log/{index.js → cds-log.js} +0 -0
  37. package/lib/ql/SELECT.js +1 -1
  38. package/lib/ql/{index.js → cds-ql.js} +0 -0
  39. package/lib/req/cds-context.js +1 -1
  40. package/lib/req/context.js +35 -7
  41. package/lib/req/locale.js +5 -1
  42. package/lib/{serve → srv}/adapters.js +23 -19
  43. package/lib/{connect → srv}/bindings.js +0 -0
  44. package/lib/{connect/index.js → srv/cds-connect.js} +1 -1
  45. package/lib/{serve/index.js → srv/cds-serve.js} +1 -1
  46. package/lib/{serve → srv}/factory.js +2 -3
  47. package/lib/{serve/Service-api.js → srv/srv-api.js} +14 -6
  48. package/lib/{serve/Service-dispatch.js → srv/srv-dispatch.js} +3 -2
  49. package/lib/{serve/Service-handlers.js → srv/srv-handlers.js} +10 -0
  50. package/lib/{serve/Service-methods.js → srv/srv-methods.js} +10 -8
  51. package/lib/srv/srv-models.js +206 -0
  52. package/lib/{serve/Transaction.js → srv/srv-tx.js} +6 -1
  53. package/lib/utils/{tests.js → cds-test.js} +2 -2
  54. package/lib/utils/cds-utils.js +146 -0
  55. package/lib/utils/index.js +2 -136
  56. package/lib/utils/jest.js +43 -0
  57. package/lib/utils/resources/index.js +14 -24
  58. package/lib/utils/resources/tar.js +18 -41
  59. package/libx/_runtime/auth/index.js +13 -10
  60. package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +9 -20
  61. package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +1 -4
  62. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +19 -7
  63. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +8 -11
  64. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/delete.js +1 -4
  65. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +2 -2
  66. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +6 -19
  67. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +1 -4
  68. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +2 -2
  69. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +8 -10
  70. package/libx/_runtime/cds-services/adapter/odata-v4/to.js +38 -4
  71. package/libx/_runtime/cds-services/adapter/odata-v4/utils/handlerUtils.js +2 -6
  72. package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +8 -5
  73. package/libx/_runtime/cds-services/services/utils/differ.js +4 -0
  74. package/libx/_runtime/cds-services/util/errors.js +1 -29
  75. package/libx/_runtime/common/constants/events.js +1 -3
  76. package/libx/_runtime/common/i18n/messages.properties +2 -1
  77. package/libx/_runtime/common/perf/index.js +10 -15
  78. package/libx/_runtime/common/utils/cqn2cqn4sql.js +0 -1
  79. package/libx/_runtime/common/utils/entityFromCqn.js +8 -5
  80. package/libx/_runtime/common/utils/template.js +1 -1
  81. package/libx/_runtime/db/Service.js +2 -14
  82. package/libx/_runtime/db/expand/expandCQNToJoin.js +28 -25
  83. package/libx/_runtime/db/generic/input.js +4 -0
  84. package/libx/_runtime/db/sql-builder/SelectBuilder.js +37 -18
  85. package/libx/_runtime/extensibility/activate.js +47 -47
  86. package/libx/_runtime/extensibility/add.js +19 -13
  87. package/libx/_runtime/extensibility/addExtension.js +17 -13
  88. package/libx/_runtime/extensibility/defaults.js +25 -30
  89. package/libx/_runtime/extensibility/linter/allowlist_checker.js +373 -0
  90. package/libx/_runtime/extensibility/linter/annotations_checker.js +113 -0
  91. package/libx/_runtime/extensibility/linter/checker_base.js +20 -0
  92. package/libx/_runtime/extensibility/linter/namespace_checker.js +180 -0
  93. package/libx/_runtime/extensibility/linter.js +32 -0
  94. package/libx/_runtime/extensibility/push.js +78 -21
  95. package/libx/_runtime/extensibility/service.js +29 -12
  96. package/libx/_runtime/extensibility/token.js +56 -0
  97. package/libx/_runtime/extensibility/validation.js +6 -9
  98. package/libx/_runtime/fiori/generic/activate.js +0 -4
  99. package/libx/_runtime/fiori/generic/edit.js +1 -9
  100. package/libx/_runtime/fiori/generic/new.js +3 -28
  101. package/libx/_runtime/fiori/generic/patch.js +6 -7
  102. package/libx/_runtime/fiori/generic/prepare.js +11 -18
  103. package/libx/_runtime/fiori/generic/read.js +11 -1
  104. package/libx/_runtime/fiori/utils/handler.js +0 -17
  105. package/libx/_runtime/hana/Service.js +0 -1
  106. package/libx/_runtime/hana/conversion.js +12 -1
  107. package/libx/_runtime/hana/customBuilder/CustomFunctionBuilder.js +4 -3
  108. package/libx/_runtime/hana/customBuilder/CustomSelectBuilder.js +5 -0
  109. package/libx/_runtime/hana/pool.js +6 -10
  110. package/libx/_runtime/hana/search2Contains.js +0 -5
  111. package/libx/_runtime/hana/search2cqn4sql.js +1 -0
  112. package/libx/_runtime/messaging/AMQPWebhookMessaging.js +18 -19
  113. package/libx/_runtime/messaging/file-based.js +1 -0
  114. package/libx/_runtime/messaging/outbox/utils.js +1 -1
  115. package/libx/_runtime/messaging/service.js +11 -6
  116. package/libx/_runtime/remote/utils/client.js +6 -2
  117. package/libx/_runtime/remote/utils/data.js +5 -0
  118. package/libx/_runtime/sqlite/Service.js +0 -1
  119. package/libx/odata/afterburner.js +79 -2
  120. package/libx/odata/cqn2odata.js +9 -7
  121. package/libx/odata/grammar.pegjs +161 -77
  122. package/libx/odata/index.js +9 -3
  123. package/libx/odata/parser.js +1 -1
  124. package/libx/odata/utils.js +39 -5
  125. package/libx/rest/RestAdapter.js +1 -2
  126. package/libx/rest/middleware/delete.js +4 -5
  127. package/libx/rest/middleware/parse.js +3 -2
  128. package/package.json +3 -3
  129. package/server.js +1 -1
  130. package/srv/extensibility-service.cds +6 -3
  131. package/srv/model-provider.cds +3 -1
  132. package/srv/model-provider.js +84 -104
  133. package/srv/mtx.js +7 -1
  134. package/libx/_runtime/cds-services/adapter/odata-v4/Dispatcher.js +0 -240
@@ -0,0 +1,32 @@
1
+ const cds = require('../cds')
2
+
3
+ const NamespaceChecker = require('./linter/namespace_checker')
4
+ const AnnotationsChecker = require('./linter/annotations_checker')
5
+ const AllowlistChecker = require('./linter/allowlist_checker')
6
+
7
+ const LINTER_OPTIONS = ['element-prefix', 'extension-allowlist', 'namespace-blocklist']
8
+ const LEGACY_OPTIONS = ['entity-whitelist', 'service-whitelist', 'namespace-blacklist']
9
+
10
+ const linter = async (extCsn, fullCsn, extensionFilenames, req) => {
11
+ const conf = cds.env.requires['cds.xt.ExtensibilityService'] || cds.env.mtx
12
+ const compat = cds.env.mtx
13
+ const linter_options = {}
14
+ let x
15
+ for (let p of LINTER_OPTIONS) if ((x = conf[p] || compat[p])) linter_options[p] = x // eslint-disable-line no-cond-assign
16
+ for (let p of LEGACY_OPTIONS) if ((x = compat[p])) linter_options[p] = x // eslint-disable-line no-cond-assign
17
+ if (!Object.keys(linter_options).length) return
18
+
19
+ const reflectedCsn = cds.reflect(extCsn)
20
+ const compileBaseDir = global.cds.root
21
+ const warnings = await Promise.all([
22
+ NamespaceChecker.check(reflectedCsn, fullCsn, compileBaseDir, linter_options),
23
+ AnnotationsChecker.check(reflectedCsn, extensionFilenames, compileBaseDir, linter_options),
24
+ AllowlistChecker.check(reflectedCsn, fullCsn, extensionFilenames, compileBaseDir, linter_options)
25
+ ])
26
+ const linterWarnings = [].concat.apply([], warnings) // REVISIT: What are we doing here?
27
+ if (linterWarnings.length > 0) {
28
+ req.reject(422, linterWarnings[0]) // REVISIT: Why are we returning the first warning only?
29
+ }
30
+ }
31
+
32
+ module.exports = linter
@@ -3,29 +3,29 @@ const path = require('path')
3
3
  const cds = require('../cds')
4
4
 
5
5
  const activate = require('./activate')
6
- const { validateExtension } = require('./validation')
7
- const { collectFiles, getCompilerError, exists } = require('./utils')
6
+ const { collectFiles, getCompilerError } = require('./utils')
8
7
  const { packTarArchive, unpackTarArchive } = require('../../../lib/utils/resources')
8
+ const linter = require('./linter')
9
9
 
10
10
  const TEMP_DIR = fs.realpathSync(require('os').tmpdir())
11
+ const LOG = cds.log('mtx')
11
12
 
12
13
  const _compileProject = async function (extension, req) {
13
- let csn, root
14
+ let csn, root, files
14
15
  try {
15
16
  root = await fs.promises.mkdtemp(`${TEMP_DIR}${path.sep}extension-`)
16
17
  await unpackTarArchive(extension, root)
17
- csn = await cds.compile(collectFiles(root, ['.cds', '.json']), { flavor: 'parsed' })
18
+ files = collectFiles(root, ['.cds', '.csn']) // REVISIT: don't we have exactly one ext.csn file for all extensions?
19
+ csn = await cds.compile(files, { flavor: 'parsed' })
18
20
  if (csn.requires) delete csn.requires
19
21
  } catch (err) {
20
22
  if (err.messages) req.reject(400, getCompilerError(err.messages))
21
23
  else throw err
22
24
  } finally {
23
- if (await exists(root)) {
24
- await (fs.promises.rm || fs.promises.rmdir)(root, { recursive: true, force: true })
25
- }
25
+ ;(fs.promises.rm || fs.promises.rmdir)(root, { recursive: true, force: true }).catch(() => {})
26
26
  }
27
27
 
28
- return csn
28
+ return { csn, files }
29
29
  }
30
30
 
31
31
  const base = async function (req) {
@@ -38,24 +38,81 @@ const base = async function (req) {
38
38
  return packTarArchive([...cdsFiles, ...csvFiles, ...i18nFiles], cds.root)
39
39
  }
40
40
 
41
+ // const _copyFile = async function (file, dir) {
42
+ // const destination = path.join(dir, path.relative(cds.root, file))
43
+ // const dirname = path.dirname(destination)
44
+ // if (!(await exists(dirname))) await fs.promises.mkdir(dirname, { recursive: true })
45
+ // await fs.promises.copyFile(file, destination)
46
+ // }
47
+
48
+ const pull = async function (req) {
49
+ LOG.info(`pulling latest model for tenant '${req.tenant}'`)
50
+ const { 'cds.xt.ModelProviderService': mps } = cds.services
51
+ const csn = await mps.getCsn({
52
+ tenant: req.tenant,
53
+ toggles: Object.keys(cds.context.features || {}), // with all enabled feature extensions
54
+ base: true, // without any custom extensions
55
+ flavor: 'xtended'
56
+ })
57
+ // const csvObj = await cds.deploy.resources()
58
+ // const csvFiles = Object.keys(csvObj).filter(f => f.startsWith(cds.root) && !f.includes('node_modules'))
59
+ // const i18nFiles = collectFiles(cds.root, ['.properties'])
60
+
61
+ req._.res?.set('content-type', 'application/octet-stream; charset=binary')
62
+
63
+ let temp, tgz
64
+ try {
65
+ temp = await fs.promises.mkdtemp(`${TEMP_DIR}${path.sep}extension-`)
66
+ await fs.promises.writeFile(path.join(temp, 'index.csn'), cds.compile.to.json(csn))
67
+ // for (const file of csvFiles) await _copyFile(file, temp)
68
+ // for (const file of i18nFiles) await _copyFile(file, temp)
69
+ tgz = await packTarArchive(temp)
70
+ } finally {
71
+ ;(fs.promises.rm || fs.promises.rmdir)(temp, { recursive: true, force: true }).catch(() => {})
72
+ }
73
+
74
+ return tgz
75
+ }
76
+
41
77
  const push = async function (req) {
42
- let { extension } = req.data
43
- if (!extension || !extension.data) req.reject(400, 'Missing extension')
44
- const csn = await _compileProject(extension.data, req)
45
- if (!csn) req.reject(400, 'Missing or bad extension')
78
+ let { extension, tag } = req.data
79
+ if (!extension) req.reject(400, 'Missing extension')
80
+ const sources = typeof extension === 'string' ? Buffer.from(extension, 'base64') : extension
81
+ const { csn: extCsn, files } = await _compileProject(sources, req)
82
+ if (!extCsn) req.reject(400, 'Missing or bad extension')
83
+ if (!tag) tag = null
46
84
  const tenant = req.tenant
47
- await validateExtension(csn, tenant, req)
85
+ if (tenant) cds.context = { tenant }
86
+
87
+ // remove current extension with tag
88
+ let currentExt
89
+ if (tag) {
90
+ currentExt = await cds.db.run(SELECT.from('cds.xt.Extensions').where({ tag }))
91
+ if (currentExt.length) await cds.db.run(DELETE.from('cds.xt.Extensions').where({ tag }))
92
+ }
48
93
 
94
+ LOG.info(`validating extension '${tag}' ...`)
95
+ // validation
96
+ const { 'cds.xt.ModelProviderService': mps } = cds.services
97
+ const csn = await mps.getCsn(tenant, Object.keys(cds.context.features || {}))
98
+ try {
99
+ cds.extend(csn).with(extCsn)
100
+ } catch (err) {
101
+ if (currentExt && currentExt.length) {
102
+ await cds.db.run(INSERT.into('cds.xt.Extensions').entries(currentExt)) // REVISIT: why did we eagerly delete that at all above?
103
+ }
104
+ return req.reject(400, getCompilerError(err.messages))
105
+ }
106
+ await linter(extCsn, csn, files, req)
107
+
108
+ // insert and activate extension
49
109
  const ID = cds.utils.uuid()
50
- await cds.tx({ tenant }, async tx => {
51
- await tx.run(
52
- INSERT.into('cds.xt.Extensions').entries([
53
- { ID, csn: JSON.stringify(csn), sources: extension.data, activated: 'database' }
54
- ])
55
- )
56
- })
110
+ await cds.db.run(
111
+ INSERT.into('cds.xt.Extensions').entries([{ ID, csn: JSON.stringify(extCsn), sources, activated: 'database', tag }])
112
+ )
57
113
 
114
+ LOG.info(`activating extension '${tag}' ...`)
58
115
  await activate(ID, null, tenant)
59
116
  }
60
117
 
61
- module.exports = { base, push }
118
+ module.exports = { base, push, pull }
@@ -2,20 +2,37 @@ const cds = require('../cds')
2
2
 
3
3
  const addExtension = require('./addExtension')
4
4
  const { add, promote } = require('./add')
5
- const { base, push } = require('./push')
5
+ const { base, push, pull } = require('./push')
6
+ const { token } = require('./token')
6
7
  const { transformExtendedFieldsCREATE, transformExtendedFieldsUPDATE } = require('./handler/transformWRITE')
7
8
  const { transformExtendedFieldsREAD } = require('./handler/transformREAD')
8
9
  const { transformExtendedFieldsRESULT } = require('./handler/transformRESULT')
9
10
 
10
- module.exports = async function () {
11
- this.on('addExtension', addExtension)
12
- this.on('add', add)
13
- this.on('promote', promote)
14
- this.on('base', base)
15
- this.on('push', push)
16
- cds.db
17
- .before('CREATE', transformExtendedFieldsCREATE)
18
- .before('UPDATE', transformExtendedFieldsUPDATE)
19
- .before('READ', transformExtendedFieldsREAD)
20
- .after('READ', transformExtendedFieldsRESULT)
11
+ module.exports = class ExtensibilityService extends cds.ApplicationService {
12
+ init() {
13
+ this.on('addExtension', addExtension)
14
+ this.on('add', add)
15
+ this.on('promote', promote)
16
+ this.on('base', base)
17
+ this.on('push', push)
18
+ this.on('pull', pull)
19
+
20
+ cds.on('served', () => cds.app.get('/-/cds/login/token', token))
21
+
22
+ const { 'cds.xt.ModelProviderService': mps } = cds.services
23
+ // REVISIT: mps._in_sidecar -> revisit options
24
+ if (!mps?._in_sidecar)
25
+ cds.db
26
+ .before('CREATE', transformExtendedFieldsCREATE)
27
+ .before('UPDATE', transformExtendedFieldsUPDATE)
28
+ .before('READ', transformExtendedFieldsREAD)
29
+ .after('READ', transformExtendedFieldsRESULT)
30
+
31
+ return super.init()
32
+ }
33
+
34
+ // REVISIT: Do we want to keep this?
35
+ get isExtensible() {
36
+ return false
37
+ }
21
38
  }
@@ -0,0 +1,56 @@
1
+ const { URL } = require('url')
2
+ const cds = require('../../../lib')
3
+ const LOG = cds.log()
4
+
5
+ module.exports = {
6
+ async token(request, response) {
7
+ if (request.method === 'HEAD') {
8
+ response.status(204).send()
9
+ return
10
+ }
11
+
12
+ const { passcode, refresh_token, subdomain, clientid, clientsecret } = request.query
13
+ const { credentials } = cds.env.requires.auth
14
+ if (!credentials) {
15
+ cds.error(
16
+ 'No auth credentials defined. The application is likely not bound to an authentication service instance.'
17
+ )
18
+ }
19
+
20
+ const parsedUrl = new URL(credentials.url)
21
+ parsedUrl.hostname = subdomain + '.' + parsedUrl.hostname.split('.').slice(1).join('.')
22
+
23
+ LOG.info(`Get auth token using URL ${parsedUrl}`)
24
+
25
+ if (clientid) {
26
+ LOG.info(`Using clientid/clientsecret from API call with clientid ${clientid}`)
27
+ }
28
+
29
+ const username = clientid ? clientid : credentials.clientid
30
+ const password = clientid ? clientsecret : credentials.clientsecret
31
+ const { xsappname } = cds.env.requires.auth?.credentials ?? cds.env.requires.uaa?.credentials ?? {}
32
+ const path =
33
+ (refresh_token
34
+ ? `oauth/token?grant_type=refresh_token&refresh_token=${refresh_token}`
35
+ : `oauth/token?grant_type=password&passcode=${encodeURIComponent(passcode)}`) +
36
+ `&scope=${encodeURIComponent(xsappname + '.cds.ExtensionDeveloper')}`
37
+
38
+ try {
39
+ const { data } = await require('axios').post(
40
+ parsedUrl + path,
41
+ { 'Content-Type': 'application/json' },
42
+ { auth: { username, password } }
43
+ )
44
+ response.send(data)
45
+ } catch (error) {
46
+ error.message = `Authentication failed with root cause '${error.message}'. Passcode URL: https://${parsedUrl.hostname}/passcode`
47
+ const {
48
+ constructor: { name },
49
+ message
50
+ } = error
51
+ const status = name in { JwtRequestError: 1, IncompleteJwtResponseError: 1 } ? 401 : error.response.status ?? 500
52
+ LOG.error(message)
53
+ response.status(status).send({ message, status })
54
+ }
55
+ }
56
+ }
@@ -2,12 +2,12 @@ const cds = require('../cds')
2
2
 
3
3
  const { getCompilerError } = require('./utils')
4
4
 
5
- const validateCsn = (csn, req) => {
5
+ const validateCsn = (csn, appCsn, req) => {
6
6
  if (!csn) req.reject(400, 'Missing extension')
7
7
  if (!csn.extensions) return
8
8
 
9
9
  csn.extensions.forEach(extension => {
10
- if (!extension.extend || !cds.model.definitions[extension.extend]) {
10
+ if (!extension.extend || !appCsn.definitions[extension.extend]) {
11
11
  req.reject(400, 'Invalid extension. Parameter "extend" missing or malformed')
12
12
  }
13
13
 
@@ -17,7 +17,7 @@ const validateCsn = (csn, req) => {
17
17
  })
18
18
  }
19
19
 
20
- const validateExtensionFields = (csn, req) => {
20
+ const validateExtensionFields = (csn, appCsn, req) => {
21
21
  if (!csn.extensions) return
22
22
 
23
23
  csn.extensions.forEach(extension => {
@@ -27,7 +27,7 @@ const validateExtensionFields = (csn, req) => {
27
27
  req.reject(400, `Invalid extension. Bad element name "${name}"`)
28
28
  }
29
29
 
30
- if (Object.keys(cds.model.definitions[extension.extend].elements).includes(name)) {
30
+ if (Object.keys(appCsn.definitions[extension.extend].elements).includes(name)) {
31
31
  req.reject(400, `Invalid extension. Element "${name}" already exists`)
32
32
  }
33
33
  })
@@ -35,12 +35,9 @@ const validateExtensionFields = (csn, req) => {
35
35
  })
36
36
  }
37
37
 
38
- const validateExtension = async (ext, tenant, req) => {
38
+ const validateExtension = (ext, csn, req) => {
39
39
  try {
40
- const { 'cds.xt.ModelProviderService': mps } = cds.services
41
- const csn = await mps.getCsn(tenant, ['*'])
42
- const extCsn = cds.compile.to.json(ext)
43
- await cds.compile.to.csn({ 'base.csn': JSON.stringify(csn), 'ext.csn': extCsn })
40
+ cds.extend(csn).with(ext)
44
41
  } catch (err) {
45
42
  req.reject(400, getCompilerError(err.messages))
46
43
  }
@@ -11,7 +11,6 @@ const {
11
11
  const { readAndDeleteKeywords, isActiveEntityRequested, getKeyData } = require('../utils/where')
12
12
  const { isDraftRootEntity } = require('../../fiori/utils/csn')
13
13
  const { getColumns } = require('../../cds-services/services/utils/columns')
14
- const { setStatusCodeAndHeader, getKeyProperty } = require('../utils/handler')
15
14
 
16
15
  const { DRAFT_COLUMNS_MAP } = require('../../common/constants/draft')
17
16
 
@@ -175,9 +174,6 @@ const _handler = async function (req) {
175
174
  })
176
175
  ])
177
176
 
178
- const k = getKeyProperty(req.target.keys)
179
- setStatusCodeAndHeader(req._.odataRes, { [k]: result[k] }, req.target.name.replace(`${this.name}.`, ''), true)
180
-
181
177
  return result
182
178
  }
183
179
 
@@ -4,14 +4,7 @@ const { INSERT, SELECT, DELETE } = cds.ql
4
4
  const { getCompositionTree } = require('../../common/composition')
5
5
  const { getColumns } = require('../../cds-services/services/utils/columns')
6
6
  const { getTransition } = require('../../common/utils/resolveView')
7
- const {
8
- draftIsLocked,
9
- ensureDraftsSuffix,
10
- ensureNoDraftsSuffix,
11
- getSubCQNs,
12
- setStatusCodeAndHeader,
13
- filterKeys
14
- } = require('../utils/handler')
7
+ const { draftIsLocked, ensureDraftsSuffix, ensureNoDraftsSuffix, getSubCQNs, filterKeys } = require('../utils/handler')
15
8
  const { isActiveEntityRequested, getKeyData } = require('../utils/where')
16
9
 
17
10
  const _getDraftColumns = draftUUID => ({
@@ -163,7 +156,6 @@ const _handler = async function (req) {
163
156
  }
164
157
 
165
158
  await Promise.all(insertCQNs.map(CQN => dbtx.run(CQN)))
166
- setStatusCodeAndHeader(req._.odataRes, rootWhere, req.target.name.replace(`${this.name}.`, ''), false)
167
159
  return results[0][0]
168
160
  }
169
161
 
@@ -4,7 +4,7 @@ const { INSERT, SELECT, UPDATE } = cds.ql
4
4
  const onDraftActivate = require('./activate')._handler
5
5
  const { isNavigationToMany } = require('../utils/req')
6
6
  const { getKeysCondition } = require('../utils/where')
7
- const { removeDraftUUIDIfNecessary, ensureDraftsSuffix } = require('../utils/handler')
7
+ const { ensureDraftsSuffix } = require('../utils/handler')
8
8
  const { DRAFT_COLUMNS_MAP } = require('../../common/constants/draft')
9
9
 
10
10
  const _getUpdateDraftAdminCQN = ({ user, timestamp }, draftUUID) => {
@@ -56,17 +56,6 @@ const _handler = async function (req, next) {
56
56
  return onDraftActivate(req, next)
57
57
  }
58
58
 
59
- // fill default values
60
- const elements = req.target.elements
61
- for (const column in elements) {
62
- const col = elements[column]
63
- if (col.default !== undefined && !(column in DRAFT_COLUMNS_MAP)) {
64
- if ('val' in col.default) req.data[col.name] = col.default.val
65
- else if ('ref' in col.default) req.data[col.name] = col.default.ref[0]
66
- else req.data[col.name] = col.default
67
- }
68
- }
69
-
70
59
  const navigationToMany = isNavigationToMany(req)
71
60
 
72
61
  const adminDataCQN = navigationToMany
@@ -74,26 +63,12 @@ const _handler = async function (req, next) {
74
63
  : _getInsertDraftAdminCQN(req, req.data.DraftAdministrativeData_DraftUUID)
75
64
  const insertDataCQN = _getInsertDataCQN(req, req.data.DraftAdministrativeData_DraftUUID)
76
65
 
77
- // read data as on db and return
78
- const columns = Object.keys(req.target.elements)
79
- .map(e => req.target.elements[e])
80
- .filter(e => !e.isAssociation)
81
- .map(e => e.name)
82
- const readInsertDataCQN = SELECT.from(insertDataCQN.INSERT.into).columns(columns)
83
- readInsertDataCQN.where(getKeysCondition(req.target, req.data))
84
-
85
66
  const dbtx = cds.tx(req)
86
67
 
87
68
  await Promise.all([dbtx.run(adminDataCQN), dbtx.run(insertDataCQN)])
88
69
 
89
- const result = await dbtx.run(readInsertDataCQN)
90
- if (result.length === 0) {
91
- req.reject(404)
92
- }
93
-
94
- removeDraftUUIDIfNecessary(req)(result[0])
95
-
96
- return result[0]
70
+ req._.readAfterWrite = true
71
+ return { ...req.data, IsActiveEntity: false }
97
72
  }
98
73
 
99
74
  module.exports = cds.service.impl(function (srv, entity) {
@@ -29,7 +29,7 @@ const _getSelectCQN = (model, { target: { name } }, keysCondition, checkUser = t
29
29
  }
30
30
 
31
31
  // REVISIT: support navigation to one
32
- return SELECT.from(draftName)
32
+ return SELECT.one(draftName)
33
33
  .columns(columns)
34
34
  .join('DRAFT.DraftAdministrativeData')
35
35
  .on([
@@ -71,19 +71,18 @@ const _handler = async function (req) {
71
71
  let result = await dbtx.run(_getSelectCQN(this.model, req, keysCondition))
72
72
 
73
73
  // Potential timeout scenario supported
74
- if (result[0].draftAdmin_inProcessByUser && result[0].draftAdmin_inProcessByUser !== req.user.id) {
74
+ if (result.draftAdmin_inProcessByUser && result.draftAdmin_inProcessByUser !== req.user.id) {
75
75
  // REVISIT: security log?
76
76
  req.reject(403)
77
77
  }
78
78
 
79
79
  const updateDraftCQN = _getUpdateDraftCQN(req, keysCondition)
80
- const updateDraftAdminCQN = getUpdateDraftAdminCQN(req, result[0].DraftAdministrativeData_DraftUUID)
80
+ const updateDraftAdminCQN = getUpdateDraftAdminCQN(req, result.DraftAdministrativeData_DraftUUID)
81
81
 
82
82
  await Promise.all([dbtx.run(updateDraftCQN), dbtx.run(updateDraftAdminCQN)])
83
- result = await dbtx.run(_getSelectCQN(this.model, req, keysCondition, false))
84
- if (result.length === 0) req.reject(404)
85
- removeDraftUUIDIfNecessary(req)(result[0])
86
- return result[0]
83
+ req._.readAfterWrite = true
84
+
85
+ return { ...req.data, IsActiveEntity: false }
87
86
  }
88
87
 
89
88
  module.exports = cds.service.impl(function (srv, entity) {
@@ -5,8 +5,6 @@ const { isActiveEntityRequested } = require('../utils/where')
5
5
  const { ensureDraftsSuffix, ensureNoDraftsSuffix } = require('../utils/handler')
6
6
  const { getColumns } = require('../../cds-services/services/utils/columns')
7
7
 
8
- const { DRAFT_COLUMNS_CQN } = require('../../common/constants/draft')
9
-
10
8
  /**
11
9
  * Generic Handler for PreparationAction requests.
12
10
  * In case of success it returns the prepared draft entry.
@@ -19,15 +17,14 @@ const _handler = async function (req) {
19
17
  }
20
18
 
21
19
  const target = ensureDraftsSuffix(req.target.name)
22
- const columns = [
23
- ...getColumns(this.model.definitions[ensureNoDraftsSuffix(req.target.name)], {
24
- removeIgnore: true,
25
- filterVirtual: true
26
- }).map(obj => obj.name),
27
- ...DRAFT_COLUMNS_CQN.filter(column => column.ref[0] !== 'DraftAdministrativeData_DraftUUID')
28
- ]
20
+ const columns = getColumns(this.model.definitions[ensureNoDraftsSuffix(req.target.name)], {
21
+ keysOnly: true,
22
+ removeIgnore: true,
23
+ filterVirtual: true,
24
+ onlyNames: true
25
+ })
29
26
  columns.push({ ref: ['DRAFT.DraftAdministrativeData', 'inProcessByUser'], as: 'draftAdmin_inProcessByUser' })
30
- const select = SELECT.from(target)
27
+ const select = SELECT.one(target)
31
28
  .columns(columns)
32
29
  .join('DRAFT.DraftAdministrativeData')
33
30
  .on([
@@ -36,18 +33,14 @@ const _handler = async function (req) {
36
33
  { ref: ['DRAFT.DraftAdministrativeData', 'DraftUUID'] }
37
34
  ])
38
35
  .where(req.query.SELECT.from.ref[0].where)
39
-
40
36
  const result = await cds.tx(req).run(select)
41
-
42
- if (result.length === 0) req.reject(404)
43
-
44
- if (result[0].draftAdmin_inProcessByUser !== req.user.id) {
37
+ if (!result) req.reject(404)
38
+ if (result.draftAdmin_inProcessByUser !== req.user.id) {
45
39
  // REVISIT: security log?
46
40
  req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER')
47
41
  }
48
- delete result[0].draftAdmin_inProcessByUser
49
-
50
- return result[0]
42
+ delete result.draftAdmin_inProcessByUser
43
+ return result
51
44
  }
52
45
 
53
46
  module.exports = cds.service.impl(function (srv, entity) {
@@ -120,6 +120,11 @@ const DRAFT_COLUMNS_CASTED = [
120
120
  }
121
121
  ]
122
122
 
123
+ const DRAFT_COLUMNS_CASTED_WITH_DRAFTADMIN_UUID = [
124
+ ...DRAFT_COLUMNS_CASTED,
125
+ { ref: ['DraftAdministrativeData_DraftUUID'] }
126
+ ]
127
+
123
128
  // default draft values for active entities
124
129
  const _getDefaultDraftProperties = ({ hasDraft, isActive = true, withDraftUUID = true }) => {
125
130
  const columns = [
@@ -442,7 +447,10 @@ const _activeWithoutDraft = (req, draftWhere, columns) => {
442
447
 
443
448
  const _draftOfWhichIAmOwner = (req, draftWhere, columns) => {
444
449
  const { table, name } = _getTableName(req, true)
445
- const outerMostColumns = _getOuterMostColumns(addColumnAlias(columns, name), DRAFT_COLUMNS_CASTED)
450
+ const outerMostColumns = _getOuterMostColumns(
451
+ addColumnAlias(columns, name),
452
+ DRAFT_COLUMNS_CASTED_WITH_DRAFTADMIN_UUID
453
+ )
446
454
 
447
455
  const cqn = SELECT.from(table)
448
456
  .columns(...outerMostColumns)
@@ -1277,6 +1285,8 @@ const _handler = async function (req) {
1277
1285
  const query4sql = cqn2cqn4sql(req.query, this.model, { _4fiori: true })
1278
1286
 
1279
1287
  // Clone the request. Do not clone with Object.assign as that would skip all non-enumerable properties.
1288
+ // REVISIT: query4sql.clone() doesn't really clone the original query, hence _generateCQN will heavily modify
1289
+ // it, e.g. IsActiveEntity is stripped. This is a problem for subsequent handlers which rely on this information.
1280
1290
  const reqClone = { __proto__: req, query: query4sql.clone() }
1281
1291
  // Clone draft restrictions to the cloned query.
1282
1292
  reqClone.query._draftRestrictions = query._draftRestrictions
@@ -183,15 +183,6 @@ const _aliased = (arr, columns, alias) =>
183
183
  })
184
184
 
185
185
  // Only works for root entity, otherwise the relative position needs to be adapted
186
- const setStatusCodeAndHeader = (response, keys, entityName, isActiveEntity) => {
187
- response.setStatusCode(201)
188
-
189
- const keysString = Object.keys(keys)
190
- .map(key => `${key}=${keys[key]}`)
191
- .join(',')
192
- response.setHeader('location', `../${entityName}(${keysString},IsActiveEntity=${isActiveEntity})`)
193
- }
194
-
195
186
  const removeDraftUUIDIfNecessary = req =>
196
187
  req._.req && req._.req.headers && req._.req.headers['x-cds-odata-version'] === 'v2'
197
188
  ? () => {}
@@ -255,12 +246,6 @@ const draftIsLocked = lastChangedAt => {
255
246
  return DRAFT_CANCEL_TIMEOUT_IN_MS > Date.now() - Date.parse(lastChangedAt)
256
247
  }
257
248
 
258
- const getKeyProperty = keys => {
259
- return Object.keys(keys).find(k => {
260
- return k !== 'IsActiveEntity' && !keys[k]._isAssociationStrict
261
- })
262
- }
263
-
264
249
  const filterKeys = keys => {
265
250
  return Object.keys(keys).filter(key => {
266
251
  return key !== 'IsActiveEntity' && !keys[key]._isAssociationStrict
@@ -273,7 +258,6 @@ module.exports = {
273
258
  getUpdateDraftAdminCQN,
274
259
  getEnrichedCQN,
275
260
  removeDraftUUIDIfNecessary,
276
- setStatusCodeAndHeader,
277
261
  isDraftActivateAction,
278
262
  ensureDraftsSuffix,
279
263
  ensureNoDraftsSuffix,
@@ -282,7 +266,6 @@ module.exports = {
282
266
  proxifyToNoDraftsName,
283
267
  addColumnAlias,
284
268
  replaceRefWithDraft,
285
- getKeyProperty,
286
269
  filterKeys,
287
270
  getDeleteDraftAdminCqn,
288
271
  getCompositionTargets
@@ -91,7 +91,6 @@ class HanaDatabase extends DatabaseService {
91
91
  }
92
92
 
93
93
  _registerBeforeHandlers() {
94
- this._ensureModel && this.before('*', this._ensureModel)
95
94
  this.before(['CREATE', 'UPDATE'], '*', this._input) // > has to run before rewrite
96
95
  this.before('READ', '*', search) // > has to run before rewrite
97
96
  this.before(['CREATE', 'READ', 'UPDATE', 'DELETE'], '*', this._rewrite)
@@ -1,5 +1,8 @@
1
1
  const cds = require('../cds')
2
2
 
3
+ const driver = require('./driver')
4
+ const isHdb = driver?.name === 'hdb'
5
+
3
6
  const convertToBoolean = boolean => {
4
7
  if (boolean === null) {
5
8
  return null
@@ -35,7 +38,15 @@ const convertToISONoMillis = element => {
35
38
 
36
39
  const convertToString = element => {
37
40
  if (element) {
38
- return element instanceof Buffer ? Buffer.from(element, 'base64').toString() : element
41
+ if (element instanceof Buffer) {
42
+ if (isHdb && driver.iconv) {
43
+ return driver.iconv.decode(element, 'cesu8')
44
+ }
45
+
46
+ return Buffer.from(element, 'base64').toString()
47
+ }
48
+
49
+ return element
39
50
  }
40
51
 
41
52
  return null
@@ -21,12 +21,13 @@ class CustomFunctionBuilder extends FunctionBuilder {
21
21
 
22
22
  _handleContains(args) {
23
23
  // fuzzy search has three arguments, must not be converted to like expressions
24
- if (args.length > 2 || args._$search) {
24
+ if (args.length > 2 || this._options.$searchUsingContains) {
25
25
  this._outputObj.sql.push('CONTAINS')
26
26
  this._addFunctionArgs(args, true)
27
- } else {
28
- super._handleContains(args)
27
+ return
29
28
  }
29
+
30
+ super._handleContains(args)
30
31
  }
31
32
  }
32
33
 
@@ -28,6 +28,11 @@ class CustomSelectBuilder extends SelectBuilder {
28
28
  return SelectBuilder
29
29
  }
30
30
 
31
+ getDefaultOptions() {
32
+ const options = { $searchUsingContains: !!this._obj.SELECT._$searchUsingContains }
33
+ return { ...super.getDefaultOptions(), ...options }
34
+ }
35
+
31
36
  _val(obj) {
32
37
  if (typeof obj.val === 'boolean') return { sql: obj.val ? 'true' : 'false', values: [] }
33
38
  return super._val(obj)