@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.
- package/CHANGELOG.md +165 -18
- package/apis/cds.d.ts +11 -7
- package/apis/log.d.ts +46 -0
- package/apis/ql.d.ts +72 -15
- package/bin/build/buildTaskHandler.js +5 -2
- package/bin/build/constants.js +4 -1
- package/bin/build/provider/buildTaskHandlerEdmx.js +11 -39
- package/bin/build/provider/buildTaskHandlerFeatureToggles.js +14 -34
- package/bin/build/provider/buildTaskHandlerInternal.js +56 -4
- package/bin/build/provider/buildTaskProviderInternal.js +22 -14
- package/bin/build/provider/hana/index.js +12 -9
- package/bin/build/provider/java/index.js +18 -8
- package/bin/build/provider/mtx/index.js +7 -4
- package/bin/build/provider/mtx/resourcesTarBuilder.js +60 -35
- package/bin/build/provider/mtx-extension/index.js +57 -0
- package/bin/build/provider/mtx-sidecar/index.js +46 -18
- package/bin/build/provider/nodejs/index.js +34 -13
- package/bin/deploy/to-hana/cfUtil.js +7 -2
- package/bin/deploy/to-hana/hana.js +20 -25
- package/bin/deploy/to-hana/hdiDeployUtil.js +13 -2
- package/bin/serve.js +7 -4
- package/lib/compile/{index.js → cds-compile.js} +0 -0
- package/lib/compile/extend.js +15 -5
- package/lib/compile/minify.js +1 -15
- package/lib/compile/parse.js +1 -1
- package/lib/compile/resolve.js +2 -2
- package/lib/compile/to/srvinfo.js +6 -4
- package/lib/{deploy.js → dbs/cds-deploy.js} +9 -8
- package/lib/env/{index.js → cds-env.js} +1 -17
- package/lib/env/{requires.js → cds-requires.js} +24 -3
- package/lib/env/defaults.js +7 -1
- package/lib/env/schemas/cds-package.json +11 -0
- package/lib/env/schemas/cds-rc.json +614 -0
- package/lib/index.js +19 -16
- package/lib/log/{errors.js → cds-error.js} +1 -1
- package/lib/log/{index.js → cds-log.js} +0 -0
- package/lib/ql/Query.js +9 -3
- package/lib/ql/SELECT.js +2 -2
- package/lib/ql/{index.js → cds-ql.js} +0 -9
- package/lib/req/context.js +49 -17
- package/lib/req/locale.js +5 -1
- package/lib/{serve → srv}/adapters.js +23 -19
- package/lib/{connect → srv}/bindings.js +0 -0
- package/lib/{connect/index.js → srv/cds-connect.js} +1 -1
- package/lib/{serve/index.js → srv/cds-serve.js} +1 -1
- package/lib/{serve → srv}/factory.js +1 -1
- package/lib/{serve/Service-api.js → srv/srv-api.js} +22 -6
- package/lib/{serve/Service-dispatch.js → srv/srv-dispatch.js} +13 -8
- package/lib/{serve/Service-handlers.js → srv/srv-handlers.js} +10 -0
- package/lib/{serve/Service-methods.js → srv/srv-methods.js} +10 -8
- package/lib/srv/srv-models.js +207 -0
- package/lib/{serve/Transaction.js → srv/srv-tx.js} +57 -40
- package/lib/utils/{tests.js → cds-test.js} +2 -2
- package/lib/utils/cds-utils.js +146 -0
- package/lib/utils/index.js +2 -145
- package/lib/utils/jest.js +43 -0
- package/lib/utils/resources/index.js +15 -25
- package/lib/utils/resources/tar.js +18 -41
- package/libx/_runtime/auth/index.js +14 -11
- package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +7 -19
- package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +1 -4
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +1 -4
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +1 -4
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/delete.js +1 -4
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +2 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +6 -19
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +1 -4
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +1 -4
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/PrimitiveValueDecoder.js +0 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/deserializer/DeserializerFactory.js +3 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/to.js +38 -4
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +1 -3
- package/libx/_runtime/cds-services/services/utils/differ.js +4 -0
- package/libx/_runtime/cds-services/util/errors.js +1 -29
- package/libx/_runtime/common/i18n/messages.properties +2 -1
- package/libx/_runtime/common/perf/index.js +10 -15
- package/libx/_runtime/common/utils/binary.js +3 -4
- package/libx/_runtime/common/utils/cqn2cqn4sql.js +0 -1
- package/libx/_runtime/common/utils/entityFromCqn.js +8 -5
- package/libx/_runtime/common/utils/resolveView.js +1 -1
- package/libx/_runtime/common/utils/template.js +1 -1
- package/libx/_runtime/db/Service.js +2 -14
- package/libx/_runtime/db/expand/expandCQNToJoin.js +28 -25
- package/libx/_runtime/db/expand/rawToExpanded.js +7 -6
- package/libx/_runtime/db/generic/input.js +8 -1
- package/libx/_runtime/db/sql-builder/SelectBuilder.js +37 -18
- package/libx/_runtime/extensibility/activate.js +47 -47
- package/libx/_runtime/extensibility/add.js +22 -13
- package/libx/_runtime/extensibility/addExtension.js +17 -13
- package/libx/_runtime/extensibility/defaults.js +25 -30
- package/libx/_runtime/extensibility/handler/transformREAD.js +20 -18
- package/libx/_runtime/extensibility/linter/allowlist_checker.js +373 -0
- package/libx/_runtime/extensibility/linter/annotations_checker.js +113 -0
- package/libx/_runtime/extensibility/linter/checker_base.js +20 -0
- package/libx/_runtime/extensibility/linter/namespace_checker.js +180 -0
- package/libx/_runtime/extensibility/linter.js +32 -0
- package/libx/_runtime/extensibility/push.js +77 -20
- package/libx/_runtime/extensibility/service.js +29 -12
- package/libx/_runtime/extensibility/token.js +56 -0
- package/libx/_runtime/extensibility/utils.js +8 -6
- package/libx/_runtime/extensibility/validation.js +6 -9
- package/libx/_runtime/fiori/generic/new.js +0 -11
- package/libx/_runtime/hana/Service.js +0 -1
- package/libx/_runtime/hana/conversion.js +12 -1
- package/libx/_runtime/hana/customBuilder/CustomFunctionBuilder.js +4 -3
- package/libx/_runtime/hana/customBuilder/CustomSelectBuilder.js +5 -0
- package/libx/_runtime/hana/pool.js +6 -10
- package/libx/_runtime/hana/search2Contains.js +0 -5
- package/libx/_runtime/hana/search2cqn4sql.js +1 -0
- package/libx/_runtime/messaging/outbox/utils.js +1 -1
- package/libx/_runtime/messaging/service.js +11 -6
- package/libx/_runtime/remote/utils/data.js +5 -0
- package/libx/_runtime/sqlite/Service.js +7 -6
- package/libx/_runtime/sqlite/execute.js +41 -28
- package/libx/odata/afterburner.js +79 -2
- package/libx/odata/cqn2odata.js +9 -7
- package/libx/odata/grammar.pegjs +157 -76
- package/libx/odata/index.js +9 -3
- package/libx/odata/parser.js +1 -1
- package/libx/odata/utils.js +39 -5
- package/libx/rest/RestAdapter.js +3 -7
- package/libx/rest/middleware/delete.js +4 -5
- package/libx/rest/middleware/parse.js +3 -2
- package/package.json +3 -3
- package/server.js +1 -1
- package/srv/extensibility-service.cds +6 -3
- package/srv/model-provider.cds +3 -1
- package/srv/model-provider.js +84 -104
- package/srv/mtx.js +7 -1
- 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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
44
|
-
const
|
|
45
|
-
|
|
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
|
-
|
|
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.
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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 =
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
.
|
|
18
|
-
|
|
19
|
-
.
|
|
20
|
-
|
|
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
|
|
71
|
-
|
|
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
|
-
|
|
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 || !
|
|
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(
|
|
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 =
|
|
38
|
+
const validateExtension = (ext, csn, req) => {
|
|
39
39
|
try {
|
|
40
|
-
|
|
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
|
-
|
|
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 ||
|
|
24
|
+
if (args.length > 2 || this._options.$searchUsingContains) {
|
|
25
25
|
this._outputObj.sql.push('CONTAINS')
|
|
26
26
|
this._addFunctionArgs(args, true)
|
|
27
|
-
|
|
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
|
-
|
|
15
|
-
|
|
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
|
|
57
|
-
|
|
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
|
-
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
}
|