@sap/cds 6.0.3 → 6.1.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 (131) hide show
  1. package/CHANGELOG.md +165 -18
  2. package/apis/cds.d.ts +11 -7
  3. package/apis/log.d.ts +46 -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 +14 -34
  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 +12 -9
  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 +60 -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/deploy/to-hana/cfUtil.js +7 -2
  19. package/bin/deploy/to-hana/hana.js +20 -25
  20. package/bin/deploy/to-hana/hdiDeployUtil.js +13 -2
  21. package/bin/serve.js +7 -4
  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} +9 -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 +614 -0
  34. package/lib/index.js +19 -16
  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/Query.js +9 -3
  38. package/lib/ql/SELECT.js +2 -2
  39. package/lib/ql/{index.js → cds-ql.js} +0 -9
  40. package/lib/req/context.js +49 -17
  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 +1 -1
  47. package/lib/{serve/Service-api.js → srv/srv-api.js} +22 -6
  48. package/lib/{serve/Service-dispatch.js → srv/srv-dispatch.js} +13 -8
  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 +207 -0
  52. package/lib/{serve/Transaction.js → srv/srv-tx.js} +57 -40
  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 -145
  56. package/lib/utils/jest.js +43 -0
  57. package/lib/utils/resources/index.js +15 -25
  58. package/lib/utils/resources/tar.js +18 -41
  59. package/libx/_runtime/auth/index.js +14 -11
  60. package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +7 -19
  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 +1 -4
  63. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +1 -4
  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 +1 -1
  69. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +1 -4
  70. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/PrimitiveValueDecoder.js +0 -2
  71. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/deserializer/DeserializerFactory.js +3 -1
  72. package/libx/_runtime/cds-services/adapter/odata-v4/to.js +38 -4
  73. package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +1 -3
  74. package/libx/_runtime/cds-services/services/utils/differ.js +4 -0
  75. package/libx/_runtime/cds-services/util/errors.js +1 -29
  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/binary.js +3 -4
  79. package/libx/_runtime/common/utils/cqn2cqn4sql.js +0 -1
  80. package/libx/_runtime/common/utils/entityFromCqn.js +8 -5
  81. package/libx/_runtime/common/utils/resolveView.js +1 -1
  82. package/libx/_runtime/common/utils/template.js +1 -1
  83. package/libx/_runtime/db/Service.js +2 -14
  84. package/libx/_runtime/db/expand/expandCQNToJoin.js +28 -25
  85. package/libx/_runtime/db/expand/rawToExpanded.js +7 -6
  86. package/libx/_runtime/db/generic/input.js +8 -1
  87. package/libx/_runtime/db/sql-builder/SelectBuilder.js +37 -18
  88. package/libx/_runtime/extensibility/activate.js +47 -47
  89. package/libx/_runtime/extensibility/add.js +22 -13
  90. package/libx/_runtime/extensibility/addExtension.js +17 -13
  91. package/libx/_runtime/extensibility/defaults.js +25 -30
  92. package/libx/_runtime/extensibility/handler/transformREAD.js +20 -18
  93. package/libx/_runtime/extensibility/linter/allowlist_checker.js +373 -0
  94. package/libx/_runtime/extensibility/linter/annotations_checker.js +113 -0
  95. package/libx/_runtime/extensibility/linter/checker_base.js +20 -0
  96. package/libx/_runtime/extensibility/linter/namespace_checker.js +180 -0
  97. package/libx/_runtime/extensibility/linter.js +32 -0
  98. package/libx/_runtime/extensibility/push.js +77 -20
  99. package/libx/_runtime/extensibility/service.js +29 -12
  100. package/libx/_runtime/extensibility/token.js +56 -0
  101. package/libx/_runtime/extensibility/utils.js +8 -6
  102. package/libx/_runtime/extensibility/validation.js +6 -9
  103. package/libx/_runtime/fiori/generic/new.js +0 -11
  104. package/libx/_runtime/hana/Service.js +0 -1
  105. package/libx/_runtime/hana/conversion.js +12 -1
  106. package/libx/_runtime/hana/customBuilder/CustomFunctionBuilder.js +4 -3
  107. package/libx/_runtime/hana/customBuilder/CustomSelectBuilder.js +5 -0
  108. package/libx/_runtime/hana/pool.js +6 -10
  109. package/libx/_runtime/hana/search2Contains.js +0 -5
  110. package/libx/_runtime/hana/search2cqn4sql.js +1 -0
  111. package/libx/_runtime/messaging/outbox/utils.js +1 -1
  112. package/libx/_runtime/messaging/service.js +11 -6
  113. package/libx/_runtime/remote/utils/data.js +5 -0
  114. package/libx/_runtime/sqlite/Service.js +7 -6
  115. package/libx/_runtime/sqlite/execute.js +41 -28
  116. package/libx/odata/afterburner.js +79 -2
  117. package/libx/odata/cqn2odata.js +9 -7
  118. package/libx/odata/grammar.pegjs +157 -76
  119. package/libx/odata/index.js +9 -3
  120. package/libx/odata/parser.js +1 -1
  121. package/libx/odata/utils.js +39 -5
  122. package/libx/rest/RestAdapter.js +3 -7
  123. package/libx/rest/middleware/delete.js +4 -5
  124. package/libx/rest/middleware/parse.js +3 -2
  125. package/package.json +3 -3
  126. package/server.js +1 -1
  127. package/srv/extensibility-service.cds +6 -3
  128. package/srv/model-provider.cds +3 -1
  129. package/srv/model-provider.js +84 -104
  130. package/srv/mtx.js +7 -1
  131. 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(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(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
+ if (tag) {
89
+ await DELETE.from('cds.xt.Extensions').where({ tag })
90
+ }
91
+
92
+ LOG.info(`validating extension '${tag}' ...`)
93
+ // validation
94
+ const { 'cds.xt.ModelProviderService': mps } = cds.services
95
+ // REVISIT: Isn't that also done during activate?
96
+ const csn = await mps.getCsn(tenant, Object.keys(cds.context.features || {}))
97
+ try {
98
+ cds.extend(csn).with(extCsn)
99
+ } catch (err) {
100
+ return req.reject(400, getCompilerError(err.messages))
101
+ }
102
+ await linter(extCsn, csn, files, req)
48
103
 
104
+ // insert and activate extension
49
105
  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
- )
106
+ await INSERT.into('cds.xt.Extensions').entries({
107
+ ID,
108
+ csn: JSON.stringify(extCsn),
109
+ sources,
110
+ activated: 'database',
111
+ tag
56
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
+ }
@@ -8,7 +8,7 @@ const EXT_BACK_PACK = 'extensions__'
8
8
 
9
9
  const getTargetRead = req => {
10
10
  let name = ''
11
- if (req.query.SELECT.from.join) {
11
+ if (req.query.SELECT.from.join && req.query.SELECT.from.args) {
12
12
  // join
13
13
  name = req.query.SELECT.from.args.find(arg => arg.ref && arg.ref[0] !== 'DRAFT.DraftAdministativeData').ref[0]
14
14
  } else if (req.target.name.SET) {
@@ -63,15 +63,17 @@ const hasExtendedEntity = (req, model) => {
63
63
  return true
64
64
  }
65
65
 
66
- if (req.query.SELECT.from.join) {
66
+ if (req.query.SELECT.from.join && req.query.SELECT.from.args) {
67
67
  return _hasExtendedEntityArgs(req.query.SELECT.from.args, model)
68
68
  }
69
69
 
70
- if (req.target.name.SET) {
71
- return isExtendedEntity(req.target.name.SET.args[0]._target.name, model)
72
- }
70
+ if (req.target) {
71
+ if (req.target.name.SET) {
72
+ return isExtendedEntity(req.target.name.SET.args[0]._target.name, model)
73
+ }
73
74
 
74
- return isExtendedEntity(req.target.name, model)
75
+ return isExtendedEntity(req.target.name, model)
76
+ }
75
77
  }
76
78
 
77
79
  const getExtendedFields = (entityName, model) => {
@@ -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
  }
@@ -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
@@ -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)
@@ -10,13 +10,9 @@ const getError = require('../common/error')
10
10
  function multiTenantInstanceManager(config = cds.env.requires.db) {
11
11
  const { credentials } = config
12
12
 
13
- if (
14
- !credentials ||
15
- typeof credentials !== 'object' ||
16
- !(credentials.get_managed_instance_url || credentials.sm_url)
17
- ) {
18
- throw Object.assign(new Error('No or malformed db credentials'), { credentials: credentials })
19
- }
13
+ if (!credentials) throw Object.assign(new Error('No database credentials provided'))
14
+ else if (typeof credentials !== 'object' || !(credentials.get_managed_instance_url || credentials.sm_url))
15
+ throw Object.assign(new Error('Malformed database credentials provided'))
20
16
 
21
17
  // new instance manager
22
18
  return new Promise((resolve, reject) => {
@@ -53,9 +49,9 @@ function multiTenantInstanceManager(config = cds.env.requires.db) {
53
49
  function singleTenantInstanceManager(config = cds.env.requires.db) {
54
50
  const { credentials } = config
55
51
 
56
- if (!credentials || typeof credentials !== 'object' || !credentials.host) {
57
- throw Object.assign(new Error('No or malformed db credentials'), { credentials })
58
- }
52
+ if (!credentials) throw Object.assign(new Error('No database credentials provided'))
53
+ else if (typeof credentials !== 'object' || !credentials.host)
54
+ throw Object.assign(new Error('Malformed database credentials provided'))
59
55
 
60
56
  // mock instance manager
61
57
  return {
@@ -51,11 +51,6 @@ const search2Contains = (cqnSearchPhrase, columns) => {
51
51
 
52
52
  const expressionArgs = [{ list: columns }, { val: searchString }]
53
53
 
54
- // REVISIT: Mark the expression args with a `_$search` flag, as the `CONTAINS`
55
- // predicate is not fully supported in the CustomFunctionBuilder class.
56
- // The `_$search` property is enumerable: false (default), writable: false (default)
57
- Object.defineProperty(expressionArgs, '_$search', { value: true })
58
-
59
54
  const expression = {
60
55
  func: 'contains',
61
56
  args: expressionArgs
@@ -58,6 +58,7 @@ const search2cqn4sql = (query, entity, options) => {
58
58
 
59
59
  if (useContains) {
60
60
  expression = search2Contains(cqnSearchPhrase, columns2Search)
61
+ Object.defineProperty(query.SELECT, '_$searchUsingContains', { value: true, enumerable: true })
61
62
  } else {
62
63
  // No CONTAINS optimization possible. The search implementation for localized
63
64
  // texts falls back to the LIKE predicate.
@@ -191,7 +191,7 @@ const _createMessage = (name, msg, context) => {
191
191
  const writeInOutbox = async (name, msg, context) => {
192
192
  const outboxMsg = _createMessage(name, msg, context)
193
193
  const messagesEntity = _getMessagesEntity()
194
- return INSERT.into(messagesEntity).entries(outboxMsg)
194
+ return cds.tx(context).run(INSERT.into(messagesEntity).entries(outboxMsg))
195
195
  }
196
196
 
197
197
  module.exports = { processMessages, registerMessageProcessor, writeInOutbox, hasPersistentOutbox, isUnrecoverable }
@@ -1,6 +1,7 @@
1
1
  const cds = require('../cds')
2
2
  const queued = require('./common-utils/queued')
3
3
  const OutboxService = require('./Outbox')
4
+ const ExtendedModels = require('../../../lib/srv/srv-models')
4
5
 
5
6
  const appId = require('./common-utils/appId')
6
7
 
@@ -76,9 +77,17 @@ class MessagingService extends OutboxService {
76
77
  return super.init()
77
78
  }
78
79
 
79
- emit(event, data, headers) {
80
+ async emit(event, data, headers) {
80
81
  const _msg = typeof event === 'object' ? event : { event, data, headers }
81
- const msg = _msg instanceof cds.Event ? _msg : new cds.Event(this.message4(_msg))
82
+ if (_msg instanceof cds.Event) return super.emit(_msg)
83
+ if (_msg.inbound && !cds.context) {
84
+ cds.context = { tenant: _msg.tenant, user: cds.User.privileged }
85
+ if (cds.model) {
86
+ const ctx = cds.context
87
+ ctx.model = await ExtendedModels.model4(ctx.tenant, ctx.features)
88
+ }
89
+ }
90
+ const msg = new cds.Event(this.message4(_msg))
82
91
  return super.emit(msg)
83
92
  }
84
93
 
@@ -112,10 +121,6 @@ class MessagingService extends OutboxService {
112
121
 
113
122
  message4(msg) {
114
123
  const _msg = { ...msg }
115
- if (msg.inbound && !cds.context) {
116
- // REVISIT: why are all inbound messages executed with privileged user?
117
- cds.context = { tenant: msg.tenant, user: cds.User.privileged }
118
- }
119
124
  _msg.event = _warnAndStripTopicPrefix(_msg.event, this.LOG)
120
125
  if (!_msg.headers) _msg.headers = {}
121
126
  if (!_msg.inbound) {
@@ -114,6 +114,7 @@ const _convertValue = (ieee754Compatible, exponentialDecimals) => (value, elemen
114
114
 
115
115
  return value
116
116
  }
117
+ const _PT = ([hh, mm, ss]) => `PT${hh}H${mm}M${ss}S`
117
118
 
118
119
  const _convertPayloadValue = (value, element) => {
119
120
  const type = _elementType(element)
@@ -121,6 +122,10 @@ const _convertPayloadValue = (value, element) => {
121
122
  // see https://www.odata.org/documentation/odata-version-2-0/json-format/
122
123
  if (value == null) return value
123
124
  switch (type) {
125
+ case 'cds.Time':
126
+ return value.match(/^(PT)([H,M,S,0-9])*$/) ? value : _PT(value.split(':'))
127
+ case 'cds.Decimal':
128
+ return typeof value === 'string' ? value : new String(value)
124
129
  case 'cds.Date':
125
130
  case 'cds.DateTime':
126
131
  return `/Date(${new Date(value).getTime()})/`
@@ -70,7 +70,6 @@ module.exports = class SQLiteDatabase extends DatabaseService {
70
70
  }
71
71
 
72
72
  _registerBeforeHandlers() {
73
- this._ensureModel && this.before('*', this._ensureModel)
74
73
  this.before(['CREATE', 'UPDATE'], '*', this._input) // > has to run before rewrite
75
74
  this.before(['CREATE', 'READ', 'UPDATE', 'DELETE'], '*', this._rewrite)
76
75
 
@@ -188,11 +187,13 @@ module.exports = class SQLiteDatabase extends DatabaseService {
188
187
  return
189
188
  }
190
189
 
191
- const tx = this.transaction()
192
- await tx.run(dropViews)
193
- await tx.run(dropTables)
194
- await tx.run(createEntities)
195
- await tx.commit()
190
+ await this.run(async tx => {
191
+ // This starts a new transaction if called from CLI, while joining
192
+ // existing root tx, e.g. when called from DeploymenrService
193
+ await tx.run(dropViews)
194
+ await tx.run(dropTables)
195
+ await tx.run(createEntities)
196
+ })
196
197
 
197
198
  return true
198
199
  }