@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.
- package/CHANGELOG.md +128 -18
- 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/deploy/to-hana/cfUtil.js +7 -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} +7 -6
- 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 +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/SELECT.js +1 -1
- package/lib/ql/{index.js → cds-ql.js} +0 -0
- 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} +0 -0
- package/lib/{serve → srv}/factory.js +1 -1
- 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 -145
- 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 +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/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/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/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 +0 -1
- 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 +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
|
@@ -114,13 +114,13 @@ const getErrorHandler = (crashOnError = true, srv) => {
|
|
|
114
114
|
|
|
115
115
|
// invoke srv.on('error', function (err, req) { ... }) here in special situations
|
|
116
116
|
// REVISIT: if for compat reasons, remove once cds^5.1
|
|
117
|
-
if (srv._handlers._error) {
|
|
117
|
+
if (srv._handlers._error.length) {
|
|
118
118
|
let ctx = cds.context
|
|
119
119
|
if (!ctx) {
|
|
120
120
|
// > error before req was dispatched
|
|
121
121
|
ctx = new cds.Request({ req, res: req.res, user: req.user || new cds.User.Anonymous() })
|
|
122
122
|
for (const each of srv._handlers._error) each.handler.call(srv, err, ctx)
|
|
123
|
-
} else
|
|
123
|
+
} else {
|
|
124
124
|
// > error after req was dispatched, e.g., serialization error in okra
|
|
125
125
|
for (const each of srv._handlers._error) each.handler.call(srv, err, ctx)
|
|
126
126
|
}
|
|
@@ -5,10 +5,6 @@ const { toODataResult } = require('../utils/result')
|
|
|
5
5
|
const { normalizeError } = require('../../../../common/error/frontend')
|
|
6
6
|
const getError = require('../../../../common/error')
|
|
7
7
|
|
|
8
|
-
const _getMetadata4Tenant = async (tenant, locale, service) => {
|
|
9
|
-
return await cds.mtx.getEdmx(tenant, service.name, locale)
|
|
10
|
-
}
|
|
11
|
-
|
|
12
8
|
/**
|
|
13
9
|
* Provide localized metadata handler.
|
|
14
10
|
*
|
|
@@ -23,21 +19,12 @@ const metadata = service => {
|
|
|
23
19
|
const locale = odataRes.getContract().getLocale()
|
|
24
20
|
|
|
25
21
|
try {
|
|
26
|
-
let edmx
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if (!edmx) {
|
|
33
|
-
edmx = cds.localize(
|
|
34
|
-
service.model,
|
|
35
|
-
locale,
|
|
36
|
-
// REVISIT: we could cache this in a weak map
|
|
37
|
-
cds.compile.to.edmx(service.model, { service: service.definition.name })
|
|
38
|
-
)
|
|
39
|
-
}
|
|
40
|
-
|
|
22
|
+
let edmx = cds.localize(
|
|
23
|
+
service.model,
|
|
24
|
+
locale,
|
|
25
|
+
// REVISIT: we could cache this in model._cached
|
|
26
|
+
cds.compile.to.edmx(service.model, { service: service.definition.name })
|
|
27
|
+
)
|
|
41
28
|
return next(null, toODataResult(edmx))
|
|
42
29
|
} catch (e) {
|
|
43
30
|
if (LOG._error) {
|
|
@@ -484,10 +484,7 @@ const read = service => {
|
|
|
484
484
|
} catch (e) {
|
|
485
485
|
err = e
|
|
486
486
|
|
|
487
|
-
if (changeset) {
|
|
488
|
-
// for passing into rollback
|
|
489
|
-
odataReq.getBatchApplicationData().errors[changeset].push({ error: e, req })
|
|
490
|
-
} else {
|
|
487
|
+
if (!changeset) {
|
|
491
488
|
// REVISIT: rollback needed if an error occurred before commit attempted -> how to distinguish?
|
|
492
489
|
await tx.rollback(e).catch(() => {})
|
|
493
490
|
}
|
|
@@ -6,7 +6,7 @@ module.exports = srv => {
|
|
|
6
6
|
const requires = getRequiresAsArray(srv.definition)
|
|
7
7
|
const restricted = isRestricted(srv)
|
|
8
8
|
|
|
9
|
-
return (odataReq, odataRes, next)
|
|
9
|
+
return function ODataRequestHandler(odataReq, odataRes, next) {
|
|
10
10
|
const req = odataReq.getBatchApplicationData()
|
|
11
11
|
? odataReq.getBatchApplicationData().req
|
|
12
12
|
: odataReq.getIncomingRequest()
|
|
@@ -174,10 +174,7 @@ const update = service => {
|
|
|
174
174
|
} catch (e) {
|
|
175
175
|
err = e
|
|
176
176
|
|
|
177
|
-
if (changeset) {
|
|
178
|
-
// for passing into rollback
|
|
179
|
-
odataReq.getBatchApplicationData().errors[changeset].push({ error: e, req })
|
|
180
|
-
} else {
|
|
177
|
+
if (!changeset) {
|
|
181
178
|
// REVISIT: rollback needed if an error occurred before commit attempted -> how to distinguish?
|
|
182
179
|
await tx.rollback(e).catch(() => {})
|
|
183
180
|
}
|
|
@@ -1,7 +1,41 @@
|
|
|
1
|
-
const
|
|
1
|
+
const { alias2ref } = require('../../../common/utils/csn') // REVISIT: eliminate that
|
|
2
|
+
const cds = require('../../../cds')
|
|
3
|
+
const OData = require('./OData')
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
+
/**
|
|
6
|
+
* This is the express handler for a specific OData endpoint.
|
|
7
|
+
* Note: the same service can be served at different endpoints.
|
|
8
|
+
*/
|
|
9
|
+
module.exports = srv => {
|
|
10
|
+
const okra = new OkraAdapter(srv)
|
|
11
|
+
return okra.process.bind(okra)
|
|
5
12
|
}
|
|
6
13
|
|
|
7
|
-
|
|
14
|
+
function OkraAdapter(srv, model = srv.model) {
|
|
15
|
+
const edm = cds.compile.to.edm(model, { service: srv.definition?.name || srv.name })
|
|
16
|
+
alias2ref(srv, edm) // REVISIT: eliminate that -> done again and again -> search for _alias2ref
|
|
17
|
+
return new OData(edm, model, srv.options).addCDSServiceToChannel(srv)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
//////////////////////////////////////////////////////////////////////////////
|
|
21
|
+
//
|
|
22
|
+
// REVISIT: Move to ExtensibilityService
|
|
23
|
+
//
|
|
24
|
+
if (cds.mtx || cds.requires.extensibility || cds.requires.toggles)
|
|
25
|
+
module.exports = srv => {
|
|
26
|
+
const id = `${++unique} - ${srv.path}` // REVISIT: this is to allow running multiple express apps serving same endpoints, as done by some questionable tests
|
|
27
|
+
return function ODataAdapter(req, res) {
|
|
28
|
+
const model = cds.context?.model || srv.model
|
|
29
|
+
if (!model._cached) Object.defineProperty(model, '_cached', { value: {} })
|
|
30
|
+
|
|
31
|
+
// Note: cache is attached to model cache so they get disposed when models are evicted from cache
|
|
32
|
+
let adapters = model._cached._odata_adapters || (model._cached._odata_adapters = {})
|
|
33
|
+
let okra = adapters[id]
|
|
34
|
+
if (!okra) {
|
|
35
|
+
const _srv = { __proto__: srv, _real_srv: srv, model } // REVISIT: we need to do that better in new adapters
|
|
36
|
+
okra = adapters[id] = new OkraAdapter(_srv, model)
|
|
37
|
+
}
|
|
38
|
+
return okra.process(req, res)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
let unique = 0
|
|
@@ -22,10 +22,8 @@ const _isNoAccessError = e => Number(e.code) === 403 || Number(e.code) === 401
|
|
|
22
22
|
const _isNotFoundError = e => Number(e.code) === 404
|
|
23
23
|
const _isEntityNotReadableError = e => Number(e.code) === 405
|
|
24
24
|
|
|
25
|
-
const _handleReadError =
|
|
25
|
+
const _handleReadError = err => {
|
|
26
26
|
if (!(_isNoAccessError(err) || _isEntityNotReadableError(err) || _isNotFoundError(err))) throw err
|
|
27
|
-
const log = Object.assign(err, { level: 'ERROR', message: normalizeError(err, req).error.message })
|
|
28
|
-
process.env.NODE_ENV !== 'production' && LOG._warn && LOG.warn(log)
|
|
29
27
|
}
|
|
30
28
|
|
|
31
29
|
const _getOperationQueryColumns = urlQueryOptions => {
|
|
@@ -18,6 +18,10 @@ module.exports = class Differ {
|
|
|
18
18
|
_createSelectColumnsForDelete(entity) {
|
|
19
19
|
const columns = []
|
|
20
20
|
for (const element of Object.values(entity.elements)) {
|
|
21
|
+
// Don't take into account virtual or computed properties to make the diff result
|
|
22
|
+
// consistent with the ones for UPDATE/CREATE (where we don't have access to that
|
|
23
|
+
// information).
|
|
24
|
+
if (!element.key && (element.virtual || element['@Core.Computed'])) continue
|
|
21
25
|
if (element.isComposition) {
|
|
22
26
|
if (element._target._hasPersistenceSkip) continue
|
|
23
27
|
columns.push({
|
|
@@ -9,35 +9,7 @@ const getFeatureNotSupportedError = message => {
|
|
|
9
9
|
return getError(501, `Feature is not supported: ${message}`)
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
const getAuditLogNotWrittenError = (rootCauseError, phase, event) => {
|
|
13
|
-
const errorMessage =
|
|
14
|
-
!phase || event === 'READ' ? 'Audit log could not be written' : `Audit log could not be written ${phase}`
|
|
15
|
-
const error = new Error(errorMessage)
|
|
16
|
-
error.rootCause = rootCauseError
|
|
17
|
-
return error
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const hasBeenCalledError = (method, query) => {
|
|
21
|
-
return new Error(`Method ${method} has been called before. Invalid CQN: ${JSON.stringify(query)}`)
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const unexpectedFunctionCallError = (functionName, expectedFunction) => {
|
|
25
|
-
return new Error(`Cannot build CQN object. Invalid call of "${functionName}" before "${expectedFunction}"`)
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const invalidFunctionArgumentError = (statement, arg) => {
|
|
29
|
-
const details = JSON.stringify(arg, (key, value) => (value === undefined ? '__undefined__' : value)).replace(
|
|
30
|
-
/"__undefined__"/g,
|
|
31
|
-
'undefined'
|
|
32
|
-
)
|
|
33
|
-
return new Error(`Cannot build ${statement} statement. Invalid data provided: ${details}`)
|
|
34
|
-
}
|
|
35
|
-
|
|
36
12
|
module.exports = {
|
|
37
13
|
getModelNotDefinedError,
|
|
38
|
-
getFeatureNotSupportedError
|
|
39
|
-
getAuditLogNotWrittenError,
|
|
40
|
-
hasBeenCalledError,
|
|
41
|
-
unexpectedFunctionCallError,
|
|
42
|
-
invalidFunctionArgumentError
|
|
14
|
+
getFeatureNotSupportedError
|
|
43
15
|
}
|
|
@@ -74,8 +74,9 @@ ENTITY_IS_NOT_CRUD=Entity "{0}" is not {1}
|
|
|
74
74
|
ENTITY_IS_NOT_CRUD_VIA_NAVIGATION=Entity "{0}" is not {1} via navigation "{2}"
|
|
75
75
|
ENTITY_IS_AUTOEXPOSED=Entity "{0}" is not explicitly exposed as part of the service
|
|
76
76
|
EXPAND_IS_RESTRICTED=Navigation property "{0}" is not allowed for expand operation
|
|
77
|
-
EXPAND_COUNT_UNSUPPORTED="
|
|
77
|
+
EXPAND_COUNT_UNSUPPORTED="/$count" is not supported for expand operation
|
|
78
78
|
ORDERBY_LAMBDA_UNSUPPORTED="$orderby" does not support lambda
|
|
79
|
+
EXPAND_APPLY_UNSUPPORTED="$apply" is not supported for expand operation
|
|
79
80
|
|
|
80
81
|
# rest protocol adapter
|
|
81
82
|
INVALID_RESOURCE="{0}" is not a valid resource
|
|
@@ -4,21 +4,16 @@ const _statisticsRequested = req =>
|
|
|
4
4
|
(req.query && req.query['sap-statistics'] === 'true') ||
|
|
5
5
|
(req.headers && req.headers['sap-statistics'] === 'true' && (!req.query || !req.query['sap-statistics']))
|
|
6
6
|
|
|
7
|
-
module.exports =
|
|
8
|
-
if (
|
|
9
|
-
else app._perf_measured = true
|
|
7
|
+
module.exports = function sap_statistics(req, res, next) {
|
|
8
|
+
if (!_statisticsRequested(req)) return next()
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
const t0 = performance.now()
|
|
11
|
+
const { writeHead } = res
|
|
12
|
+
res.writeHead = function (...args) {
|
|
13
|
+
const total = Number((performance.now() - t0) / 1000).toFixed(2)
|
|
14
|
+
if (res.statusCode < 400) res.setHeader('sap-statistics', `total=${total}`)
|
|
15
|
+
writeHead.call(this, ...args)
|
|
16
|
+
}
|
|
13
17
|
|
|
14
|
-
|
|
15
|
-
const { writeHead } = res
|
|
16
|
-
res.writeHead = function (...args) {
|
|
17
|
-
const total = Number((performance.now() - t0) / 1000).toFixed(2)
|
|
18
|
-
if (res.statusCode < 400) res.setHeader('sap-statistics', `total=${total}`)
|
|
19
|
-
writeHead.call(this, ...args)
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
next()
|
|
23
|
-
})
|
|
18
|
+
next()
|
|
24
19
|
}
|
|
@@ -1,25 +1,28 @@
|
|
|
1
1
|
const { ensureNoDraftsSuffix } = require('../../common/utils/draft')
|
|
2
2
|
|
|
3
|
-
const traverseFroms = (cqn, cb) => {
|
|
3
|
+
const traverseFroms = (cqn, cb, aliasForSet) => {
|
|
4
4
|
while (cqn.SELECT) cqn = cqn.SELECT.from
|
|
5
5
|
|
|
6
6
|
// Do the most likely first -> {ref}
|
|
7
7
|
if (cqn.ref) {
|
|
8
|
-
return cb(cqn)
|
|
8
|
+
return cb(cqn, aliasForSet)
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
if (cqn.SET) {
|
|
12
|
-
|
|
12
|
+
// if a union has an alias, we should use it for the columns we get out of the union
|
|
13
|
+
return cqn.SET.args.map(a => traverseFroms(a, cb, cqn.as))
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
if (cqn.join) {
|
|
16
|
-
return cqn.args.map(a => traverseFroms(a, cb))
|
|
17
|
+
return cqn.args.map(a => traverseFroms(a, cb, aliasForSet))
|
|
17
18
|
}
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
const getEntityNameFromCQN = cqn => {
|
|
21
22
|
const res = []
|
|
22
|
-
traverseFroms(cqn,
|
|
23
|
+
traverseFroms(cqn, (from, aliasForSet) =>
|
|
24
|
+
res.push({ entityName: from.ref[0].id || from.ref[0], alias: aliasForSet || from.as })
|
|
25
|
+
)
|
|
23
26
|
return res.length === 1 ? res[0] : res.find(n => n.entityName !== 'DRAFT.DraftAdministrativeData') || {}
|
|
24
27
|
}
|
|
25
28
|
|
|
@@ -134,7 +134,7 @@ const getCache = (anything, cache, newCacheFn) => {
|
|
|
134
134
|
}
|
|
135
135
|
|
|
136
136
|
module.exports = (usecase, tx, target, ...args) => {
|
|
137
|
-
// get model first as it may be added to tx (cf. "_ensureModel")
|
|
137
|
+
// get model first as it may be added to tx (cf. "_ensureModel") // REVISIT: _ensureModel is gone
|
|
138
138
|
const model = tx.model
|
|
139
139
|
if (!model) return
|
|
140
140
|
|
|
@@ -24,28 +24,16 @@ class DatabaseService extends cds.Service {
|
|
|
24
24
|
this[`_${each}`] = generic[each]
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
// REVISIT: ensures tenant-aware this.model if this is a transaction -> this should be fixed in mtx integration, not here
|
|
28
|
-
this._ensureModel = function (req) {
|
|
29
|
-
if (this.context) {
|
|
30
|
-
// if the tx was initiated in messaging, then this.context._model is not unfolded
|
|
31
|
-
// -> use this.context._model._4odata if present
|
|
32
|
-
const { _model } = this.context
|
|
33
|
-
if (_model) this.model = _model._4odata || _model
|
|
34
|
-
else this.model = req._model
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
this._ensureModel._initial = true
|
|
38
|
-
|
|
39
27
|
// REVISIT: how to generic handler registration?
|
|
40
28
|
}
|
|
41
29
|
|
|
42
30
|
/** Database services don't support custom-defined operations */
|
|
43
|
-
operations() {
|
|
31
|
+
get operations() {
|
|
44
32
|
return []
|
|
45
33
|
}
|
|
46
34
|
|
|
47
35
|
/** Database services don't support custom-defined events */
|
|
48
|
-
events() {
|
|
36
|
+
get events() {
|
|
49
37
|
return []
|
|
50
38
|
}
|
|
51
39
|
|
|
@@ -12,6 +12,8 @@ const { DRAFT_COLUMNS_MAP } = require('../../common/constants/draft')
|
|
|
12
12
|
|
|
13
13
|
const { filterKeys } = require('../../fiori/utils/handler')
|
|
14
14
|
|
|
15
|
+
const getError = require('../../common/error')
|
|
16
|
+
|
|
15
17
|
// Symbols are used to add extra information in response structure
|
|
16
18
|
const GET_KEY_VALUE = Symbol.for('sap.cds.getKeyValue')
|
|
17
19
|
const TO_MANY = Symbol.for('sap.cds.toMany')
|
|
@@ -94,6 +96,16 @@ class JoinCQNFromExpanded {
|
|
|
94
96
|
return this._isDraft
|
|
95
97
|
}
|
|
96
98
|
|
|
99
|
+
// There can be a limit/offset for the target entity.
|
|
100
|
+
// The current expand implementation applys a `DISTINCT` on
|
|
101
|
+
// `filterExpand`, which changes the sorting in absence of `ORDER BY`.
|
|
102
|
+
// Therefore, add an implicit `ORDER BY` in those cases.
|
|
103
|
+
_addImplicitOrderBy(cqn, entity, alias) {
|
|
104
|
+
if (cqn.orderBy || !cqn.limit || !entity) return // not needed
|
|
105
|
+
const orderByColumns = cqn.groupBy || getAllKeys(entity).map(key => ({ ref: [alias, key] }))
|
|
106
|
+
cqn.orderBy = orderByColumns
|
|
107
|
+
}
|
|
108
|
+
|
|
97
109
|
/**
|
|
98
110
|
* Build first level of expanding regarding to many and all to one if not part of a nested to many expand.
|
|
99
111
|
*
|
|
@@ -123,6 +135,7 @@ class JoinCQNFromExpanded {
|
|
|
123
135
|
})
|
|
124
136
|
// expand to one
|
|
125
137
|
const entity = this._csn.definitions[joinArgs[0].SELECT.from.SET.args[1].SELECT.from.ref[0]]
|
|
138
|
+
this._addImplicitOrderBy(readToOneCQN, entity, tableAlias)
|
|
126
139
|
const givenColumns = readToOneCQN.columns
|
|
127
140
|
readToOneCQN.columns = []
|
|
128
141
|
this._expandedToFlat({ entity, givenColumns, readToOneCQN, tableAlias, toManyTree, defaultLanguage })
|
|
@@ -130,7 +143,7 @@ class JoinCQNFromExpanded {
|
|
|
130
143
|
const table = unionTable || this._getRef(SELECT).table
|
|
131
144
|
const isDraftTree = this._isDraftTree(table)
|
|
132
145
|
const entity = this._getEntityForTable(table)
|
|
133
|
-
|
|
146
|
+
this._addImplicitOrderBy(readToOneCQN, entity, tableAlias)
|
|
134
147
|
if (unionTable) readToOneCQN[IS_UNION_DRAFT] = true
|
|
135
148
|
|
|
136
149
|
readToOneCQN[IS_ACTIVE] = isDraftTree ? this._isDraftTargetActive(table) : true
|
|
@@ -455,14 +468,10 @@ class JoinCQNFromExpanded {
|
|
|
455
468
|
|
|
456
469
|
if (element.ref) {
|
|
457
470
|
element.ref[1] = Object.assign({}, element.ref[1])
|
|
458
|
-
element.ref[1].args = element.ref[1].args.map(arg =>
|
|
459
|
-
return this._mapArg(arg, cqn, tableAlias)
|
|
460
|
-
})
|
|
471
|
+
element.ref[1].args = element.ref[1].args.map(arg => this._mapArg(arg, cqn, tableAlias))
|
|
461
472
|
} else {
|
|
462
473
|
element.args = element.args.slice(0)
|
|
463
|
-
element.args = element.args.map(arg =>
|
|
464
|
-
return this._mapArg(arg, cqn, tableAlias)
|
|
465
|
-
})
|
|
474
|
+
element.args = element.args.map(arg => this._mapArg(arg, cqn, tableAlias))
|
|
466
475
|
}
|
|
467
476
|
}
|
|
468
477
|
|
|
@@ -684,6 +693,17 @@ class JoinCQNFromExpanded {
|
|
|
684
693
|
c => !expandedEntity.keys[c].isAssociation && !(c in DRAFT_COLUMNS_MAP)
|
|
685
694
|
)
|
|
686
695
|
const user = (cds.context && cds.context.user && cds.context.user.id) || 'anonymous'
|
|
696
|
+
|
|
697
|
+
const assoc = entity.associations[column.ref[0]]
|
|
698
|
+
if (assoc.is2one && assoc.on) {
|
|
699
|
+
const onCond = expandedEntity._relations[assoc.name].join('target', 'source')
|
|
700
|
+
const xpr = onCond[0].xpr
|
|
701
|
+
const fks = (xpr && xpr.filter(e => e.ref && e.ref[0] === 'target').map(e => e.ref[1])) || []
|
|
702
|
+
for (const k of fks) {
|
|
703
|
+
if (!cols.includes(k)) cols.push(k)
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
687
707
|
const unionFrom = getCQNUnionFrom(cols, expandedEntity.name, expandedEntity.name + '.drafts', ks, user)
|
|
688
708
|
readToOneCQN.from.args[1] = {
|
|
689
709
|
SELECT: {
|
|
@@ -1162,7 +1182,7 @@ class JoinCQNFromExpanded {
|
|
|
1162
1182
|
cqn.orderBy = this._copyOrderBy(column.orderBy, tableAlias, expandedEntity)
|
|
1163
1183
|
}
|
|
1164
1184
|
|
|
1165
|
-
if (column.limit)
|
|
1185
|
+
if (column.limit) throw getError(501, 'Pagination is not supported in expand')
|
|
1166
1186
|
|
|
1167
1187
|
cqn = this._adaptWhereOrderBy(cqn, tableAlias)
|
|
1168
1188
|
|
|
@@ -1227,23 +1247,6 @@ class JoinCQNFromExpanded {
|
|
|
1227
1247
|
return assoc.target + '_drafts'
|
|
1228
1248
|
}
|
|
1229
1249
|
|
|
1230
|
-
_getLimitInSelect(cqn, columns, limit, orderBy, expandedEntity) {
|
|
1231
|
-
const select = {
|
|
1232
|
-
SELECT: {
|
|
1233
|
-
columns: this._copyColumns(columns, 'limitFilter'),
|
|
1234
|
-
from: { ref: [cqn.from.args[0].ref[0]], as: 'limitFilter' },
|
|
1235
|
-
where: this._convertOnToWhere(cqn.from.on, cqn.from.args[0].as, 'limitFilter'),
|
|
1236
|
-
limit: limit
|
|
1237
|
-
}
|
|
1238
|
-
}
|
|
1239
|
-
|
|
1240
|
-
if (orderBy) {
|
|
1241
|
-
select.SELECT.orderBy = this._copyOrderBy(orderBy, 'limitFilter', expandedEntity)
|
|
1242
|
-
}
|
|
1243
|
-
|
|
1244
|
-
return select
|
|
1245
|
-
}
|
|
1246
|
-
|
|
1247
1250
|
_isPathExpressionToOne(ref, entity) {
|
|
1248
1251
|
const ref0 = ref[0]
|
|
1249
1252
|
const el = entity.elements[ref0]
|
|
@@ -178,6 +178,10 @@ const _pickDraft = element => {
|
|
|
178
178
|
|
|
179
179
|
if (element.virtual) categories.push('virtual')
|
|
180
180
|
|
|
181
|
+
if (element.default && !DRAFT_COLUMNS_MAP[element.name]) {
|
|
182
|
+
categories.push({ category: 'default', args: element })
|
|
183
|
+
}
|
|
184
|
+
|
|
181
185
|
// REVISIT: element._foreignKeys.length seems to be a very broad check
|
|
182
186
|
if (element.isAssociation && element._foreignKeys.length) {
|
|
183
187
|
categories.push({ category: 'propagateForeignKeys' })
|
|
@@ -98,9 +98,7 @@ class SelectBuilder extends BaseBuilder {
|
|
|
98
98
|
this._orderBy(noQuoting)
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
|
|
102
|
-
this._limit()
|
|
103
|
-
}
|
|
101
|
+
this._limit()
|
|
104
102
|
|
|
105
103
|
if (this._obj.SELECT.forUpdate) {
|
|
106
104
|
this._forUpdate()
|
|
@@ -322,11 +320,8 @@ class SelectBuilder extends BaseBuilder {
|
|
|
322
320
|
|
|
323
321
|
_where() {
|
|
324
322
|
const entityName = this._obj.SELECT.from.ref && this._obj.SELECT.from.ref[0]
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
entityName ? { ...this._options, entityName } : this._options,
|
|
328
|
-
this._csn
|
|
329
|
-
).build()
|
|
323
|
+
const options = entityName ? { ...this._options, entityName } : this._options
|
|
324
|
+
const where = new this.ExpressionBuilder(this._obj.SELECT.where, options, this._csn).build()
|
|
330
325
|
this._outputObj.sql.push('WHERE', where.sql)
|
|
331
326
|
this._outputObj.values.push(...where.values)
|
|
332
327
|
}
|
|
@@ -415,6 +410,34 @@ class SelectBuilder extends BaseBuilder {
|
|
|
415
410
|
this._outputObj.sql.push(sqls.join(', '))
|
|
416
411
|
}
|
|
417
412
|
|
|
413
|
+
_addRows() {
|
|
414
|
+
if (this._obj.SELECT.limit) {
|
|
415
|
+
if (this._obj.SELECT.limit.rows !== undefined) {
|
|
416
|
+
// limit (no placeholder for statement caching)
|
|
417
|
+
this._outputObj.sql.push('LIMIT', this._obj.SELECT.limit.rows.val)
|
|
418
|
+
} else {
|
|
419
|
+
// rows parameter is mandatory for SQL
|
|
420
|
+
throw new Error('Rows parameter is missing in SELECT.limit(rows, offset)')
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
_addOne() {
|
|
426
|
+
this._outputObj.sql.push('LIMIT', 1)
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
_addOffset() {
|
|
430
|
+
// offset
|
|
431
|
+
if (this._obj.SELECT.limit && this._obj.SELECT.limit.offset !== undefined) {
|
|
432
|
+
if (typeof this._obj.SELECT.limit.offset.val === 'number' && !this._parameterizedNumbers) {
|
|
433
|
+
this._outputObj.sql.push('OFFSET', this._obj.SELECT.limit.offset.val)
|
|
434
|
+
} else {
|
|
435
|
+
this._outputObj.sql.push('OFFSET', '?')
|
|
436
|
+
this._outputObj.values.push(this._obj.SELECT.limit.offset.val)
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
418
441
|
/**
|
|
419
442
|
* sql limit clause will be generated without placeholders.
|
|
420
443
|
* reason is optimizing paging queries. number of rows does not change.
|
|
@@ -423,17 +446,13 @@ class SelectBuilder extends BaseBuilder {
|
|
|
423
446
|
* offset will still use placeholders, as it'll change during the paging queries.
|
|
424
447
|
*/
|
|
425
448
|
_limit() {
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
if (typeof this._obj.SELECT.limit.offset.val === 'number' && !this._parameterizedNumbers) {
|
|
431
|
-
this._outputObj.sql.push('OFFSET', this._obj.SELECT.limit.offset.val)
|
|
432
|
-
} else {
|
|
433
|
-
this._outputObj.sql.push('OFFSET', '?')
|
|
434
|
-
this._outputObj.values.push(this._obj.SELECT.limit.offset.val)
|
|
435
|
-
}
|
|
449
|
+
if (this._obj.SELECT.one) {
|
|
450
|
+
this._addOne()
|
|
451
|
+
} else {
|
|
452
|
+
this._addRows()
|
|
436
453
|
}
|
|
454
|
+
|
|
455
|
+
this._addOffset()
|
|
437
456
|
}
|
|
438
457
|
|
|
439
458
|
_parameters() {
|
|
@@ -1,69 +1,69 @@
|
|
|
1
1
|
const cds = require('../cds')
|
|
2
2
|
|
|
3
3
|
const handleDefaults = require('./defaults')
|
|
4
|
+
const Extensions = 'cds.xt.Extensions'
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
// REVISIT: Reuse ratio = 0
|
|
7
|
+
const _calculateExtensions = async function (ID, tag) {
|
|
6
8
|
let active, inactive
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
inactiveCqn.where('ID !=', ID)
|
|
12
|
-
} else {
|
|
13
|
-
inactiveCqn.where('tag !=', tag)
|
|
14
|
-
}
|
|
15
|
-
inactive = await tx.run(inactiveCqn)
|
|
16
|
-
const activeCqn = SELECT.from('cds.xt.Extensions').where({ activated: 'database' })
|
|
17
|
-
if (ID) {
|
|
18
|
-
activeCqn.or({ ID })
|
|
19
|
-
} else {
|
|
20
|
-
if (tag) activeCqn.or({ tag })
|
|
21
|
-
}
|
|
22
|
-
active = await tx.run(activeCqn)
|
|
23
|
-
if (inactive.length) {
|
|
24
|
-
const deleteCqn = DELETE.from('cds.xt.Extensions').where(inactiveCqn.SELECT.where)
|
|
25
|
-
await tx.run(deleteCqn)
|
|
26
|
-
}
|
|
9
|
+
if (tag || ID) {
|
|
10
|
+
const inactiveCqn = SELECT.from(Extensions).where({ activated: 'propertyBag' })
|
|
11
|
+
if (ID) {
|
|
12
|
+
inactiveCqn.where('ID !=', ID)
|
|
27
13
|
} else {
|
|
28
|
-
|
|
29
|
-
inactive = []
|
|
30
|
-
active = await tx.run(SELECT.from('cds.xt.Extensions'))
|
|
14
|
+
inactiveCqn.where('(tag !=', tag, 'or tag =', null, ')')
|
|
31
15
|
}
|
|
32
|
-
|
|
16
|
+
inactive = await cds.db.run(inactiveCqn)
|
|
17
|
+
const activeCqn = SELECT.from(Extensions).where({ activated: 'database' })
|
|
18
|
+
if (ID) {
|
|
19
|
+
activeCqn.or({ ID })
|
|
20
|
+
} else if (tag) {
|
|
21
|
+
activeCqn.or({ tag })
|
|
22
|
+
}
|
|
23
|
+
active = await cds.db.run(activeCqn)
|
|
24
|
+
if (inactive.length) {
|
|
25
|
+
const deleteCqn = DELETE.from(Extensions).where(inactiveCqn.SELECT.where)
|
|
26
|
+
await cds.db.run(deleteCqn)
|
|
27
|
+
}
|
|
28
|
+
} else {
|
|
29
|
+
// activate all
|
|
30
|
+
inactive = []
|
|
31
|
+
active = await cds.db.run(SELECT.from(Extensions))
|
|
32
|
+
}
|
|
33
33
|
|
|
34
34
|
return { active, inactive }
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
37
|
+
// REVISIT: Reuse ratio = 0
|
|
38
|
+
const _restoreExtensions = async function (active, inactive, appCsn) {
|
|
39
|
+
// delete all extensions
|
|
40
|
+
await cds.db.run(DELETE.from(Extensions))
|
|
41
|
+
// active
|
|
42
|
+
active.forEach(row => {
|
|
43
|
+
row.csn = row.csn.replace(/,"@cds.extension":true/g, '')
|
|
44
|
+
row.activated = 'database'
|
|
45
|
+
row.timestamp = '$now'
|
|
46
|
+
})
|
|
47
|
+
await cds.db.run(INSERT.into(Extensions).entries(active))
|
|
48
|
+
// inactive
|
|
49
|
+
if (inactive.length) {
|
|
50
|
+
for (const na of inactive) {
|
|
51
|
+
for (const extension of JSON.parse(na.csn).extensions) {
|
|
52
|
+
await handleDefaults(extension, appCsn, cds.db)
|
|
54
53
|
}
|
|
55
|
-
await tx.run(INSERT.into('cds.xt.Extensions').entries(inactive))
|
|
56
54
|
}
|
|
57
|
-
|
|
55
|
+
await cds.db.run(INSERT.into(Extensions).entries(inactive))
|
|
56
|
+
}
|
|
58
57
|
}
|
|
59
58
|
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
// REVISIT: Review with Vitaly: (1) Delete Inactives > (2) DS.extend(t) > (3) Delete All > (4) Restore All ???
|
|
60
|
+
const activate = async function (ID, tag, tenant, appCsn) {
|
|
61
|
+
const { active, inactive } = await _calculateExtensions(ID, tag)
|
|
62
62
|
|
|
63
63
|
const { 'cds.xt.DeploymentService': ds } = cds.services
|
|
64
64
|
await ds.extend(tenant)
|
|
65
65
|
|
|
66
|
-
await _restoreExtensions(
|
|
66
|
+
await _restoreExtensions(active, inactive, appCsn)
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
module.exports = activate
|