@sap/cds 6.0.2 → 6.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +153 -19
- package/apis/cds.d.ts +11 -7
- package/apis/log.d.ts +48 -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 +13 -32
- package/bin/build/provider/buildTaskHandlerInternal.js +56 -4
- package/bin/build/provider/buildTaskProviderInternal.js +22 -14
- package/bin/build/provider/hana/index.js +8 -7
- 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 +64 -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/build/util.js +6 -4
- package/bin/deploy/to-hana/cfUtil.js +7 -2
- package/bin/deploy/to-hana/hana.js +6 -3
- package/bin/serve.js +8 -13
- 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} +8 -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 +605 -0
- package/lib/index.js +20 -17
- package/lib/log/{errors.js → cds-error.js} +1 -1
- package/lib/log/{index.js → cds-log.js} +0 -0
- package/lib/ql/SELECT.js +1 -1
- package/lib/ql/{index.js → cds-ql.js} +0 -0
- package/lib/req/cds-context.js +1 -1
- package/lib/req/context.js +35 -7
- 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 +2 -3
- package/lib/{serve/Service-api.js → srv/srv-api.js} +14 -6
- package/lib/{serve/Service-dispatch.js → srv/srv-dispatch.js} +3 -2
- 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 +206 -0
- package/lib/{serve/Transaction.js → srv/srv-tx.js} +6 -1
- package/lib/utils/{tests.js → cds-test.js} +2 -2
- package/lib/utils/cds-utils.js +146 -0
- package/lib/utils/index.js +2 -136
- package/lib/utils/jest.js +43 -0
- package/lib/utils/resources/index.js +14 -24
- package/lib/utils/resources/tar.js +18 -41
- package/libx/_runtime/auth/index.js +13 -10
- package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +9 -20
- package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +1 -4
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +19 -7
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +8 -11
- 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 +2 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +8 -10
- package/libx/_runtime/cds-services/adapter/odata-v4/to.js +38 -4
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/handlerUtils.js +2 -6
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +8 -5
- 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/constants/events.js +1 -3
- package/libx/_runtime/common/i18n/messages.properties +2 -1
- package/libx/_runtime/common/perf/index.js +10 -15
- package/libx/_runtime/common/utils/cqn2cqn4sql.js +0 -1
- package/libx/_runtime/common/utils/entityFromCqn.js +8 -5
- 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/generic/input.js +4 -0
- package/libx/_runtime/db/sql-builder/SelectBuilder.js +37 -18
- package/libx/_runtime/extensibility/activate.js +47 -47
- package/libx/_runtime/extensibility/add.js +19 -13
- package/libx/_runtime/extensibility/addExtension.js +17 -13
- package/libx/_runtime/extensibility/defaults.js +25 -30
- 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 +78 -21
- package/libx/_runtime/extensibility/service.js +29 -12
- package/libx/_runtime/extensibility/token.js +56 -0
- package/libx/_runtime/extensibility/validation.js +6 -9
- package/libx/_runtime/fiori/generic/activate.js +0 -4
- package/libx/_runtime/fiori/generic/edit.js +1 -9
- package/libx/_runtime/fiori/generic/new.js +3 -28
- package/libx/_runtime/fiori/generic/patch.js +6 -7
- package/libx/_runtime/fiori/generic/prepare.js +11 -18
- package/libx/_runtime/fiori/generic/read.js +11 -1
- package/libx/_runtime/fiori/utils/handler.js +0 -17
- 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/AMQPWebhookMessaging.js +18 -19
- package/libx/_runtime/messaging/file-based.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/client.js +6 -2
- package/libx/_runtime/remote/utils/data.js +5 -0
- package/libx/_runtime/sqlite/Service.js +0 -1
- package/libx/odata/afterburner.js +79 -2
- package/libx/odata/cqn2odata.js +9 -7
- package/libx/odata/grammar.pegjs +161 -77
- 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 +1 -2
- 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 || 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
|
|
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
|
+
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.
|
|
51
|
-
|
|
52
|
-
|
|
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 =
|
|
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
|
+
}
|
|
@@ -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
|
}
|
|
@@ -11,7 +11,6 @@ const {
|
|
|
11
11
|
const { readAndDeleteKeywords, isActiveEntityRequested, getKeyData } = require('../utils/where')
|
|
12
12
|
const { isDraftRootEntity } = require('../../fiori/utils/csn')
|
|
13
13
|
const { getColumns } = require('../../cds-services/services/utils/columns')
|
|
14
|
-
const { setStatusCodeAndHeader, getKeyProperty } = require('../utils/handler')
|
|
15
14
|
|
|
16
15
|
const { DRAFT_COLUMNS_MAP } = require('../../common/constants/draft')
|
|
17
16
|
|
|
@@ -175,9 +174,6 @@ const _handler = async function (req) {
|
|
|
175
174
|
})
|
|
176
175
|
])
|
|
177
176
|
|
|
178
|
-
const k = getKeyProperty(req.target.keys)
|
|
179
|
-
setStatusCodeAndHeader(req._.odataRes, { [k]: result[k] }, req.target.name.replace(`${this.name}.`, ''), true)
|
|
180
|
-
|
|
181
177
|
return result
|
|
182
178
|
}
|
|
183
179
|
|
|
@@ -4,14 +4,7 @@ const { INSERT, SELECT, DELETE } = cds.ql
|
|
|
4
4
|
const { getCompositionTree } = require('../../common/composition')
|
|
5
5
|
const { getColumns } = require('../../cds-services/services/utils/columns')
|
|
6
6
|
const { getTransition } = require('../../common/utils/resolveView')
|
|
7
|
-
const {
|
|
8
|
-
draftIsLocked,
|
|
9
|
-
ensureDraftsSuffix,
|
|
10
|
-
ensureNoDraftsSuffix,
|
|
11
|
-
getSubCQNs,
|
|
12
|
-
setStatusCodeAndHeader,
|
|
13
|
-
filterKeys
|
|
14
|
-
} = require('../utils/handler')
|
|
7
|
+
const { draftIsLocked, ensureDraftsSuffix, ensureNoDraftsSuffix, getSubCQNs, filterKeys } = require('../utils/handler')
|
|
15
8
|
const { isActiveEntityRequested, getKeyData } = require('../utils/where')
|
|
16
9
|
|
|
17
10
|
const _getDraftColumns = draftUUID => ({
|
|
@@ -163,7 +156,6 @@ const _handler = async function (req) {
|
|
|
163
156
|
}
|
|
164
157
|
|
|
165
158
|
await Promise.all(insertCQNs.map(CQN => dbtx.run(CQN)))
|
|
166
|
-
setStatusCodeAndHeader(req._.odataRes, rootWhere, req.target.name.replace(`${this.name}.`, ''), false)
|
|
167
159
|
return results[0][0]
|
|
168
160
|
}
|
|
169
161
|
|
|
@@ -4,7 +4,7 @@ const { INSERT, SELECT, UPDATE } = cds.ql
|
|
|
4
4
|
const onDraftActivate = require('./activate')._handler
|
|
5
5
|
const { isNavigationToMany } = require('../utils/req')
|
|
6
6
|
const { getKeysCondition } = require('../utils/where')
|
|
7
|
-
const {
|
|
7
|
+
const { ensureDraftsSuffix } = require('../utils/handler')
|
|
8
8
|
const { DRAFT_COLUMNS_MAP } = require('../../common/constants/draft')
|
|
9
9
|
|
|
10
10
|
const _getUpdateDraftAdminCQN = ({ user, timestamp }, draftUUID) => {
|
|
@@ -56,17 +56,6 @@ const _handler = async function (req, next) {
|
|
|
56
56
|
return onDraftActivate(req, next)
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
// fill default values
|
|
60
|
-
const elements = req.target.elements
|
|
61
|
-
for (const column in elements) {
|
|
62
|
-
const col = elements[column]
|
|
63
|
-
if (col.default !== undefined && !(column in DRAFT_COLUMNS_MAP)) {
|
|
64
|
-
if ('val' in col.default) req.data[col.name] = col.default.val
|
|
65
|
-
else if ('ref' in col.default) req.data[col.name] = col.default.ref[0]
|
|
66
|
-
else req.data[col.name] = col.default
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
59
|
const navigationToMany = isNavigationToMany(req)
|
|
71
60
|
|
|
72
61
|
const adminDataCQN = navigationToMany
|
|
@@ -74,26 +63,12 @@ const _handler = async function (req, next) {
|
|
|
74
63
|
: _getInsertDraftAdminCQN(req, req.data.DraftAdministrativeData_DraftUUID)
|
|
75
64
|
const insertDataCQN = _getInsertDataCQN(req, req.data.DraftAdministrativeData_DraftUUID)
|
|
76
65
|
|
|
77
|
-
// read data as on db and return
|
|
78
|
-
const columns = Object.keys(req.target.elements)
|
|
79
|
-
.map(e => req.target.elements[e])
|
|
80
|
-
.filter(e => !e.isAssociation)
|
|
81
|
-
.map(e => e.name)
|
|
82
|
-
const readInsertDataCQN = SELECT.from(insertDataCQN.INSERT.into).columns(columns)
|
|
83
|
-
readInsertDataCQN.where(getKeysCondition(req.target, req.data))
|
|
84
|
-
|
|
85
66
|
const dbtx = cds.tx(req)
|
|
86
67
|
|
|
87
68
|
await Promise.all([dbtx.run(adminDataCQN), dbtx.run(insertDataCQN)])
|
|
88
69
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
req.reject(404)
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
removeDraftUUIDIfNecessary(req)(result[0])
|
|
95
|
-
|
|
96
|
-
return result[0]
|
|
70
|
+
req._.readAfterWrite = true
|
|
71
|
+
return { ...req.data, IsActiveEntity: false }
|
|
97
72
|
}
|
|
98
73
|
|
|
99
74
|
module.exports = cds.service.impl(function (srv, entity) {
|
|
@@ -29,7 +29,7 @@ const _getSelectCQN = (model, { target: { name } }, keysCondition, checkUser = t
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
// REVISIT: support navigation to one
|
|
32
|
-
return SELECT.
|
|
32
|
+
return SELECT.one(draftName)
|
|
33
33
|
.columns(columns)
|
|
34
34
|
.join('DRAFT.DraftAdministrativeData')
|
|
35
35
|
.on([
|
|
@@ -71,19 +71,18 @@ const _handler = async function (req) {
|
|
|
71
71
|
let result = await dbtx.run(_getSelectCQN(this.model, req, keysCondition))
|
|
72
72
|
|
|
73
73
|
// Potential timeout scenario supported
|
|
74
|
-
if (result
|
|
74
|
+
if (result.draftAdmin_inProcessByUser && result.draftAdmin_inProcessByUser !== req.user.id) {
|
|
75
75
|
// REVISIT: security log?
|
|
76
76
|
req.reject(403)
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
const updateDraftCQN = _getUpdateDraftCQN(req, keysCondition)
|
|
80
|
-
const updateDraftAdminCQN = getUpdateDraftAdminCQN(req, result
|
|
80
|
+
const updateDraftAdminCQN = getUpdateDraftAdminCQN(req, result.DraftAdministrativeData_DraftUUID)
|
|
81
81
|
|
|
82
82
|
await Promise.all([dbtx.run(updateDraftCQN), dbtx.run(updateDraftAdminCQN)])
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
return result[0]
|
|
83
|
+
req._.readAfterWrite = true
|
|
84
|
+
|
|
85
|
+
return { ...req.data, IsActiveEntity: false }
|
|
87
86
|
}
|
|
88
87
|
|
|
89
88
|
module.exports = cds.service.impl(function (srv, entity) {
|
|
@@ -5,8 +5,6 @@ const { isActiveEntityRequested } = require('../utils/where')
|
|
|
5
5
|
const { ensureDraftsSuffix, ensureNoDraftsSuffix } = require('../utils/handler')
|
|
6
6
|
const { getColumns } = require('../../cds-services/services/utils/columns')
|
|
7
7
|
|
|
8
|
-
const { DRAFT_COLUMNS_CQN } = require('../../common/constants/draft')
|
|
9
|
-
|
|
10
8
|
/**
|
|
11
9
|
* Generic Handler for PreparationAction requests.
|
|
12
10
|
* In case of success it returns the prepared draft entry.
|
|
@@ -19,15 +17,14 @@ const _handler = async function (req) {
|
|
|
19
17
|
}
|
|
20
18
|
|
|
21
19
|
const target = ensureDraftsSuffix(req.target.name)
|
|
22
|
-
const columns = [
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
]
|
|
20
|
+
const columns = getColumns(this.model.definitions[ensureNoDraftsSuffix(req.target.name)], {
|
|
21
|
+
keysOnly: true,
|
|
22
|
+
removeIgnore: true,
|
|
23
|
+
filterVirtual: true,
|
|
24
|
+
onlyNames: true
|
|
25
|
+
})
|
|
29
26
|
columns.push({ ref: ['DRAFT.DraftAdministrativeData', 'inProcessByUser'], as: 'draftAdmin_inProcessByUser' })
|
|
30
|
-
const select = SELECT.
|
|
27
|
+
const select = SELECT.one(target)
|
|
31
28
|
.columns(columns)
|
|
32
29
|
.join('DRAFT.DraftAdministrativeData')
|
|
33
30
|
.on([
|
|
@@ -36,18 +33,14 @@ const _handler = async function (req) {
|
|
|
36
33
|
{ ref: ['DRAFT.DraftAdministrativeData', 'DraftUUID'] }
|
|
37
34
|
])
|
|
38
35
|
.where(req.query.SELECT.from.ref[0].where)
|
|
39
|
-
|
|
40
36
|
const result = await cds.tx(req).run(select)
|
|
41
|
-
|
|
42
|
-
if (result.
|
|
43
|
-
|
|
44
|
-
if (result[0].draftAdmin_inProcessByUser !== req.user.id) {
|
|
37
|
+
if (!result) req.reject(404)
|
|
38
|
+
if (result.draftAdmin_inProcessByUser !== req.user.id) {
|
|
45
39
|
// REVISIT: security log?
|
|
46
40
|
req.reject(403, 'DRAFT_LOCKED_BY_ANOTHER_USER')
|
|
47
41
|
}
|
|
48
|
-
delete result
|
|
49
|
-
|
|
50
|
-
return result[0]
|
|
42
|
+
delete result.draftAdmin_inProcessByUser
|
|
43
|
+
return result
|
|
51
44
|
}
|
|
52
45
|
|
|
53
46
|
module.exports = cds.service.impl(function (srv, entity) {
|
|
@@ -120,6 +120,11 @@ const DRAFT_COLUMNS_CASTED = [
|
|
|
120
120
|
}
|
|
121
121
|
]
|
|
122
122
|
|
|
123
|
+
const DRAFT_COLUMNS_CASTED_WITH_DRAFTADMIN_UUID = [
|
|
124
|
+
...DRAFT_COLUMNS_CASTED,
|
|
125
|
+
{ ref: ['DraftAdministrativeData_DraftUUID'] }
|
|
126
|
+
]
|
|
127
|
+
|
|
123
128
|
// default draft values for active entities
|
|
124
129
|
const _getDefaultDraftProperties = ({ hasDraft, isActive = true, withDraftUUID = true }) => {
|
|
125
130
|
const columns = [
|
|
@@ -442,7 +447,10 @@ const _activeWithoutDraft = (req, draftWhere, columns) => {
|
|
|
442
447
|
|
|
443
448
|
const _draftOfWhichIAmOwner = (req, draftWhere, columns) => {
|
|
444
449
|
const { table, name } = _getTableName(req, true)
|
|
445
|
-
const outerMostColumns = _getOuterMostColumns(
|
|
450
|
+
const outerMostColumns = _getOuterMostColumns(
|
|
451
|
+
addColumnAlias(columns, name),
|
|
452
|
+
DRAFT_COLUMNS_CASTED_WITH_DRAFTADMIN_UUID
|
|
453
|
+
)
|
|
446
454
|
|
|
447
455
|
const cqn = SELECT.from(table)
|
|
448
456
|
.columns(...outerMostColumns)
|
|
@@ -1277,6 +1285,8 @@ const _handler = async function (req) {
|
|
|
1277
1285
|
const query4sql = cqn2cqn4sql(req.query, this.model, { _4fiori: true })
|
|
1278
1286
|
|
|
1279
1287
|
// Clone the request. Do not clone with Object.assign as that would skip all non-enumerable properties.
|
|
1288
|
+
// REVISIT: query4sql.clone() doesn't really clone the original query, hence _generateCQN will heavily modify
|
|
1289
|
+
// it, e.g. IsActiveEntity is stripped. This is a problem for subsequent handlers which rely on this information.
|
|
1280
1290
|
const reqClone = { __proto__: req, query: query4sql.clone() }
|
|
1281
1291
|
// Clone draft restrictions to the cloned query.
|
|
1282
1292
|
reqClone.query._draftRestrictions = query._draftRestrictions
|
|
@@ -183,15 +183,6 @@ const _aliased = (arr, columns, alias) =>
|
|
|
183
183
|
})
|
|
184
184
|
|
|
185
185
|
// Only works for root entity, otherwise the relative position needs to be adapted
|
|
186
|
-
const setStatusCodeAndHeader = (response, keys, entityName, isActiveEntity) => {
|
|
187
|
-
response.setStatusCode(201)
|
|
188
|
-
|
|
189
|
-
const keysString = Object.keys(keys)
|
|
190
|
-
.map(key => `${key}=${keys[key]}`)
|
|
191
|
-
.join(',')
|
|
192
|
-
response.setHeader('location', `../${entityName}(${keysString},IsActiveEntity=${isActiveEntity})`)
|
|
193
|
-
}
|
|
194
|
-
|
|
195
186
|
const removeDraftUUIDIfNecessary = req =>
|
|
196
187
|
req._.req && req._.req.headers && req._.req.headers['x-cds-odata-version'] === 'v2'
|
|
197
188
|
? () => {}
|
|
@@ -255,12 +246,6 @@ const draftIsLocked = lastChangedAt => {
|
|
|
255
246
|
return DRAFT_CANCEL_TIMEOUT_IN_MS > Date.now() - Date.parse(lastChangedAt)
|
|
256
247
|
}
|
|
257
248
|
|
|
258
|
-
const getKeyProperty = keys => {
|
|
259
|
-
return Object.keys(keys).find(k => {
|
|
260
|
-
return k !== 'IsActiveEntity' && !keys[k]._isAssociationStrict
|
|
261
|
-
})
|
|
262
|
-
}
|
|
263
|
-
|
|
264
249
|
const filterKeys = keys => {
|
|
265
250
|
return Object.keys(keys).filter(key => {
|
|
266
251
|
return key !== 'IsActiveEntity' && !keys[key]._isAssociationStrict
|
|
@@ -273,7 +258,6 @@ module.exports = {
|
|
|
273
258
|
getUpdateDraftAdminCQN,
|
|
274
259
|
getEnrichedCQN,
|
|
275
260
|
removeDraftUUIDIfNecessary,
|
|
276
|
-
setStatusCodeAndHeader,
|
|
277
261
|
isDraftActivateAction,
|
|
278
262
|
ensureDraftsSuffix,
|
|
279
263
|
ensureNoDraftsSuffix,
|
|
@@ -282,7 +266,6 @@ module.exports = {
|
|
|
282
266
|
proxifyToNoDraftsName,
|
|
283
267
|
addColumnAlias,
|
|
284
268
|
replaceRefWithDraft,
|
|
285
|
-
getKeyProperty,
|
|
286
269
|
filterKeys,
|
|
287
270
|
getDeleteDraftAdminCqn,
|
|
288
271
|
getCompositionTargets
|
|
@@ -91,7 +91,6 @@ class HanaDatabase extends DatabaseService {
|
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
_registerBeforeHandlers() {
|
|
94
|
-
this._ensureModel && this.before('*', this._ensureModel)
|
|
95
94
|
this.before(['CREATE', 'UPDATE'], '*', this._input) // > has to run before rewrite
|
|
96
95
|
this.before('READ', '*', search) // > has to run before rewrite
|
|
97
96
|
this.before(['CREATE', 'READ', 'UPDATE', 'DELETE'], '*', this._rewrite)
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
const cds = require('../cds')
|
|
2
2
|
|
|
3
|
+
const driver = require('./driver')
|
|
4
|
+
const isHdb = driver?.name === 'hdb'
|
|
5
|
+
|
|
3
6
|
const convertToBoolean = boolean => {
|
|
4
7
|
if (boolean === null) {
|
|
5
8
|
return null
|
|
@@ -35,7 +38,15 @@ const convertToISONoMillis = element => {
|
|
|
35
38
|
|
|
36
39
|
const convertToString = element => {
|
|
37
40
|
if (element) {
|
|
38
|
-
|
|
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)
|