@sap/cds 6.0.4 → 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 (120) hide show
  1. package/CHANGELOG.md +128 -18
  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/deploy/to-hana/cfUtil.js +7 -2
  19. package/bin/serve.js +7 -4
  20. package/lib/compile/{index.js → cds-compile.js} +0 -0
  21. package/lib/compile/extend.js +15 -5
  22. package/lib/compile/minify.js +1 -15
  23. package/lib/compile/parse.js +1 -1
  24. package/lib/compile/resolve.js +2 -2
  25. package/lib/compile/to/srvinfo.js +6 -4
  26. package/lib/{deploy.js → dbs/cds-deploy.js} +7 -6
  27. package/lib/env/{index.js → cds-env.js} +1 -17
  28. package/lib/env/{requires.js → cds-requires.js} +24 -3
  29. package/lib/env/defaults.js +7 -1
  30. package/lib/env/schemas/cds-package.json +11 -0
  31. package/lib/env/schemas/cds-rc.json +605 -0
  32. package/lib/index.js +19 -16
  33. package/lib/log/{errors.js → cds-error.js} +1 -1
  34. package/lib/log/{index.js → cds-log.js} +0 -0
  35. package/lib/ql/SELECT.js +1 -1
  36. package/lib/ql/{index.js → cds-ql.js} +0 -0
  37. package/lib/req/context.js +35 -7
  38. package/lib/req/locale.js +5 -1
  39. package/lib/{serve → srv}/adapters.js +23 -19
  40. package/lib/{connect → srv}/bindings.js +0 -0
  41. package/lib/{connect/index.js → srv/cds-connect.js} +1 -1
  42. package/lib/{serve/index.js → srv/cds-serve.js} +0 -0
  43. package/lib/{serve → srv}/factory.js +1 -1
  44. package/lib/{serve/Service-api.js → srv/srv-api.js} +14 -6
  45. package/lib/{serve/Service-dispatch.js → srv/srv-dispatch.js} +3 -2
  46. package/lib/{serve/Service-handlers.js → srv/srv-handlers.js} +10 -0
  47. package/lib/{serve/Service-methods.js → srv/srv-methods.js} +10 -8
  48. package/lib/srv/srv-models.js +206 -0
  49. package/lib/{serve/Transaction.js → srv/srv-tx.js} +6 -1
  50. package/lib/utils/{tests.js → cds-test.js} +2 -2
  51. package/lib/utils/cds-utils.js +146 -0
  52. package/lib/utils/index.js +2 -145
  53. package/lib/utils/jest.js +43 -0
  54. package/lib/utils/resources/index.js +14 -24
  55. package/lib/utils/resources/tar.js +18 -41
  56. package/libx/_runtime/auth/index.js +13 -10
  57. package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +7 -19
  58. package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +1 -4
  59. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +1 -4
  60. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +1 -4
  61. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/delete.js +1 -4
  62. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +2 -2
  63. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +6 -19
  64. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +1 -4
  65. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +1 -1
  66. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +1 -4
  67. package/libx/_runtime/cds-services/adapter/odata-v4/to.js +38 -4
  68. package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +1 -3
  69. package/libx/_runtime/cds-services/services/utils/differ.js +4 -0
  70. package/libx/_runtime/cds-services/util/errors.js +1 -29
  71. package/libx/_runtime/common/i18n/messages.properties +2 -1
  72. package/libx/_runtime/common/perf/index.js +10 -15
  73. package/libx/_runtime/common/utils/cqn2cqn4sql.js +0 -1
  74. package/libx/_runtime/common/utils/entityFromCqn.js +8 -5
  75. package/libx/_runtime/common/utils/template.js +1 -1
  76. package/libx/_runtime/db/Service.js +2 -14
  77. package/libx/_runtime/db/expand/expandCQNToJoin.js +28 -25
  78. package/libx/_runtime/db/generic/input.js +4 -0
  79. package/libx/_runtime/db/sql-builder/SelectBuilder.js +37 -18
  80. package/libx/_runtime/extensibility/activate.js +47 -47
  81. package/libx/_runtime/extensibility/add.js +19 -13
  82. package/libx/_runtime/extensibility/addExtension.js +17 -13
  83. package/libx/_runtime/extensibility/defaults.js +25 -30
  84. package/libx/_runtime/extensibility/linter/allowlist_checker.js +373 -0
  85. package/libx/_runtime/extensibility/linter/annotations_checker.js +113 -0
  86. package/libx/_runtime/extensibility/linter/checker_base.js +20 -0
  87. package/libx/_runtime/extensibility/linter/namespace_checker.js +180 -0
  88. package/libx/_runtime/extensibility/linter.js +32 -0
  89. package/libx/_runtime/extensibility/push.js +78 -21
  90. package/libx/_runtime/extensibility/service.js +29 -12
  91. package/libx/_runtime/extensibility/token.js +56 -0
  92. package/libx/_runtime/extensibility/validation.js +6 -9
  93. package/libx/_runtime/fiori/generic/new.js +0 -11
  94. package/libx/_runtime/hana/Service.js +0 -1
  95. package/libx/_runtime/hana/conversion.js +12 -1
  96. package/libx/_runtime/hana/customBuilder/CustomFunctionBuilder.js +4 -3
  97. package/libx/_runtime/hana/customBuilder/CustomSelectBuilder.js +5 -0
  98. package/libx/_runtime/hana/pool.js +6 -10
  99. package/libx/_runtime/hana/search2Contains.js +0 -5
  100. package/libx/_runtime/hana/search2cqn4sql.js +1 -0
  101. package/libx/_runtime/messaging/outbox/utils.js +1 -1
  102. package/libx/_runtime/messaging/service.js +11 -6
  103. package/libx/_runtime/remote/utils/data.js +5 -0
  104. package/libx/_runtime/sqlite/Service.js +0 -1
  105. package/libx/odata/afterburner.js +79 -2
  106. package/libx/odata/cqn2odata.js +9 -7
  107. package/libx/odata/grammar.pegjs +157 -76
  108. package/libx/odata/index.js +9 -3
  109. package/libx/odata/parser.js +1 -1
  110. package/libx/odata/utils.js +39 -5
  111. package/libx/rest/RestAdapter.js +1 -2
  112. package/libx/rest/middleware/delete.js +4 -5
  113. package/libx/rest/middleware/parse.js +3 -2
  114. package/package.json +3 -3
  115. package/server.js +1 -1
  116. package/srv/extensibility-service.cds +6 -3
  117. package/srv/model-provider.cds +3 -1
  118. package/srv/model-provider.js +84 -104
  119. package/srv/mtx.js +7 -1
  120. package/libx/_runtime/cds-services/adapter/odata-v4/Dispatcher.js +0 -240
@@ -0,0 +1,180 @@
1
+ const Checker = require('./checker_base')
2
+
3
+ class NamespaceChecker extends Checker {
4
+ static async check(extensionCsn, fullCsn, compileDir, mtxConfig) {
5
+ let elementPrefixes = mtxConfig['element-prefix']
6
+ let namespaceBlocklist = mtxConfig['namespace-blocklist'] || mtxConfig['namespace-blacklist']
7
+ const warnings = []
8
+
9
+ if (elementPrefixes) {
10
+ if (!Array.isArray(elementPrefixes)) {
11
+ elementPrefixes = [elementPrefixes]
12
+ }
13
+
14
+ if (extensionCsn.extensions) {
15
+ // forall switches back to definitions if extensions are undefined
16
+ extensionCsn.forall(
17
+ () => true,
18
+ (element, name, parent) => {
19
+ element.name = name // TODO check if bug
20
+ this._checkElement(element, parent, elementPrefixes, compileDir, warnings)
21
+ },
22
+ extensionCsn.extensions
23
+ )
24
+ }
25
+
26
+ extensionCsn.forall(
27
+ element => {
28
+ return ['entity', 'function', 'action'].includes(element.kind)
29
+ },
30
+ entity => {
31
+ this._checkEntity(entity, extensionCsn, fullCsn, elementPrefixes, compileDir, warnings)
32
+ }
33
+ )
34
+ }
35
+
36
+ if (namespaceBlocklist) {
37
+ if (!Array.isArray(namespaceBlocklist)) {
38
+ namespaceBlocklist = [namespaceBlocklist]
39
+ }
40
+
41
+ extensionCsn.forall('service', service => {
42
+ this._checkNamespace(service, namespaceBlocklist, compileDir, warnings)
43
+ })
44
+
45
+ extensionCsn.forall(
46
+ element => {
47
+ return ['aspect', 'entity', 'type'].includes(element.kind)
48
+ },
49
+ entity => {
50
+ if (entity._unresolved) return // skip unresolved entities
51
+ this._checkNamespace(entity, namespaceBlocklist, compileDir, warnings)
52
+ }
53
+ )
54
+ }
55
+
56
+ return warnings
57
+ }
58
+
59
+ static _checkElement(element, parent, elementPrefixes, compileDir, warnings) {
60
+ if (elementPrefixes.length < 1) {
61
+ return
62
+ }
63
+
64
+ if (!parent) {
65
+ return
66
+ }
67
+
68
+ for (const elementPrefix of elementPrefixes) {
69
+ if (!parent.extend || element.name.startsWith(elementPrefix)) {
70
+ return
71
+ }
72
+ }
73
+
74
+ warnings.push(this._createPrefixWarning(element, compileDir, elementPrefixes))
75
+ }
76
+
77
+ static _checkEntity(element, reflectedCsn, reflectedFullCsn, elementPrefixes, compileDir, warnings) {
78
+ if (elementPrefixes.length < 1) {
79
+ return
80
+ }
81
+
82
+ if (!this._hasEnclosingEntity(reflectedCsn, element)) {
83
+ return
84
+ }
85
+
86
+ const parent = this._getEnclosingEntity(reflectedCsn, element)
87
+
88
+ // parent exists in extension
89
+ if (parent) {
90
+ return
91
+ }
92
+
93
+ // check full csn for parent - if it exists, continue
94
+ const parentFromFullCsn = this._getEnclosingEntity(reflectedFullCsn, element)
95
+ if (!parentFromFullCsn) {
96
+ return
97
+ }
98
+
99
+ // checks nested element - TODO determine real parent and split off parent name
100
+ const nestedElementName = this._getNestedEntityName(element) // ,parent
101
+
102
+ for (const elementPrefix of elementPrefixes) {
103
+ if (nestedElementName.startsWith(elementPrefix)) {
104
+ return
105
+ }
106
+ }
107
+
108
+ warnings.push(this._createPrefixWarning(element, compileDir, elementPrefixes))
109
+ }
110
+
111
+ static _hasEnclosingEntity(reflectedCsn, element) {
112
+ const plainEntityName = element.name.replace(reflectedCsn.namespace + '.', '')
113
+ const splitEntityName = plainEntityName.split('.')
114
+ if (splitEntityName.length > 1) {
115
+ return true
116
+ }
117
+ return false
118
+ }
119
+
120
+ static _getEnclosingEntity(reflectedCsn, element) {
121
+ const splitEntityName = element.name.split('.')
122
+ if (splitEntityName.length > 1) {
123
+ splitEntityName.pop()
124
+ return reflectedCsn.definitions[splitEntityName.join('.')]
125
+ }
126
+ return null
127
+ }
128
+
129
+ static _getNestedEntityName(element) {
130
+ const splitEntityName = element.name.split('.')
131
+ splitEntityName.shift()
132
+ return splitEntityName.join('.')
133
+ }
134
+
135
+ static _checkNamespace(element, namespaceBlacklist, compileDir, warnings) {
136
+ for (const namespace of namespaceBlacklist) {
137
+ if (element.name.startsWith(namespace)) {
138
+ warnings.push(this._createNamespaceWarning(element, compileDir, namespace))
139
+ }
140
+ }
141
+ }
142
+
143
+ static _createPrefixWarning(element, compileDir, prefixRule) {
144
+ const originFile = this._localizeFile(element.$location.file, compileDir)
145
+
146
+ return (
147
+ 'Element ' +
148
+ element.name +
149
+ ' from ' +
150
+ originFile +
151
+ ' (line:' +
152
+ element.$location.line +
153
+ ', col:' +
154
+ element.$location.col +
155
+ ')' +
156
+ ' does not adhere to prefix rule: ' +
157
+ prefixRule
158
+ )
159
+ }
160
+
161
+ static _createNamespaceWarning(element, compileDir, namespace) {
162
+ const originFile = this._localizeFile(element.$location.file, compileDir)
163
+
164
+ return (
165
+ 'Element ' +
166
+ element.name +
167
+ ' from ' +
168
+ originFile +
169
+ ' (line:' +
170
+ element.$location.line +
171
+ ', col:' +
172
+ element.$location.col +
173
+ ')' +
174
+ ' uses a forbidden namespace: ' +
175
+ namespace
176
+ )
177
+ }
178
+ }
179
+
180
+ module.exports = NamespaceChecker
@@ -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
  }
@@ -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 }