@sap/cds 6.3.2 → 6.4.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 +95 -0
- package/apis/cds.d.ts +3 -1
- package/apis/core.d.ts +118 -90
- package/apis/cqn.d.ts +11 -2
- package/apis/internal/inference.d.ts +7 -2
- package/apis/ql.d.ts +49 -11
- package/apis/serve.d.ts +8 -1
- package/apis/services.d.ts +311 -305
- package/bin/build/buildTaskEngine.js +28 -36
- package/bin/build/buildTaskFactory.js +32 -81
- package/bin/build/buildTaskHandler.js +3 -2
- package/bin/build/buildTaskProvider.js +2 -2
- package/bin/build/buildTaskProviderFactory.js +5 -14
- package/bin/build/constants.js +0 -1
- package/bin/build/provider/buildTaskHandlerEdmx.js +7 -6
- package/bin/build/provider/buildTaskHandlerFeatureToggles.js +6 -5
- package/bin/build/provider/buildTaskHandlerInternal.js +9 -30
- package/bin/build/provider/buildTaskProviderInternal.js +70 -58
- package/bin/build/provider/fiori/index.js +6 -5
- package/bin/build/provider/hana/2migration.js +20 -3
- package/bin/build/provider/hana/2tabledata.js +1 -0
- package/bin/build/provider/hana/index.js +40 -17
- package/bin/build/provider/java/index.js +10 -10
- package/bin/build/provider/mtx/index.js +25 -16
- package/bin/build/provider/mtx/resourcesTarBuilder.js +22 -27
- package/bin/build/provider/mtx-extension/index.js +3 -2
- package/bin/build/provider/mtx-sidecar/index.js +16 -15
- package/bin/build/provider/nodejs/index.js +14 -56
- package/bin/build/util.js +56 -16
- package/bin/deploy/to-hana/cfUtil.js +2 -0
- package/bin/deploy/to-hana/gitUtil.js +1 -1
- package/bin/deploy/to-hana/hana.js +45 -38
- package/bin/deploy/to-hana/hdiDeployUtil.js +17 -12
- package/bin/deploy/to-hana/mtaUtil.js +13 -14
- package/bin/mtx/in-cds.js +3 -1
- package/bin/serve.js +1 -1
- package/bin/version.js +2 -1
- package/lib/auth/index.js +17 -15
- package/lib/compile/cds-compile.js +1 -0
- package/lib/compile/cdsc.js +1 -0
- package/lib/compile/etc/_localized.js +2 -2
- package/lib/compile/for/lean_drafts.js +83 -0
- package/lib/compile/for/nodejs.js +1 -0
- package/lib/compile/minify.js +2 -1
- package/lib/compile/to/gql.js +1 -1
- package/lib/compile/to/sql.js +11 -1
- package/lib/core/entities.js +1 -1
- package/lib/core/index.js +9 -9
- package/lib/core/infer.js +1 -0
- package/lib/dbs/cds-deploy.js +97 -41
- package/lib/env/cds-env.js +9 -10
- package/lib/env/cds-requires.js +8 -2
- package/lib/env/defaults.js +0 -4
- package/lib/env/schemas/cds-rc.json +38 -0
- package/lib/ql/SELECT.js +10 -4
- package/lib/srv/bindings.js +1 -1
- package/lib/srv/factory.js +1 -1
- package/lib/srv/middlewares/cds-context.js +0 -2
- package/lib/srv/middlewares/ctx-auth.js +11 -0
- package/lib/srv/middlewares/ctx-model.js +22 -20
- package/lib/srv/middlewares/index.js +7 -9
- package/lib/srv/protocols/_legacy.js +4 -0
- package/lib/srv/protocols/graphql.js +2 -2
- package/lib/srv/protocols/index.js +7 -3
- package/lib/srv/srv-api.js +1 -0
- package/lib/srv/srv-methods.js +1 -1
- package/lib/utils/cds-utils.js +11 -0
- package/lib/utils/data.js +2 -2
- package/lib/utils/inflect.js +13 -12
- package/lib/utils/tar.js +43 -13
- package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +2 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/delete.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +1 -15
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/errors/UriSyntaxError.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriParser.js +6 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/utils/BufferedWriter.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/validator/ConditionalRequestValidator.js +0 -12
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/oDataConfiguration.js +1 -7
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/result.js +4 -0
- package/libx/_runtime/cds-services/services/Service.js +23 -1
- package/libx/_runtime/cds-services/util/assert.js +0 -41
- package/libx/_runtime/common/composition/data.js +5 -1
- package/libx/_runtime/common/generic/auth/utils.js +3 -3
- package/libx/_runtime/common/generic/crud.js +1 -1
- package/libx/_runtime/common/generic/input.js +4 -24
- package/libx/_runtime/common/generic/paging.js +10 -9
- package/libx/_runtime/common/utils/cqn2cqn4sql.js +31 -0
- package/libx/_runtime/common/utils/csn.js +21 -15
- package/libx/_runtime/common/utils/draft.js +2 -1
- package/libx/_runtime/common/utils/resolveView.js +27 -4
- package/libx/_runtime/common/utils/rewriteAsterisks.js +3 -1
- package/libx/_runtime/common/utils/rowUUIDGenerator.js +21 -0
- package/libx/_runtime/common/utils/templateProcessor.js +12 -15
- package/libx/_runtime/common/utils/templateProcessorPathSerializer.js +23 -0
- package/libx/_runtime/db/expand/expandCQNToJoin.js +29 -12
- package/libx/_runtime/db/generic/input.js +7 -13
- package/libx/_runtime/db/sql-builder/InsertBuilder.js +5 -1
- package/libx/_runtime/db/sql-builder/UpsertBuilder.js +24 -0
- package/libx/_runtime/db/sql-builder/annotations.js +6 -3
- package/libx/_runtime/db/sql-builder/index.js +2 -0
- package/libx/_runtime/db/sql-builder/sqlFactory.js +9 -0
- package/libx/_runtime/db/utils/columns.js +4 -2
- package/libx/_runtime/fiori/generic/read.js +1 -12
- package/libx/_runtime/fiori/lean-draft.js +657 -0
- package/libx/_runtime/fiori/utils/handler.js +1 -1
- package/libx/_runtime/hana/Service.js +1 -1
- package/libx/_runtime/hana/execute.js +5 -5
- package/libx/_runtime/hana/pool.js +16 -1
- package/libx/_runtime/messaging/enterprise-messaging-utils/getTenantInfo.js +2 -1
- package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +1 -1
- package/libx/_runtime/messaging/enterprise-messaging.js +2 -3
- package/libx/_runtime/messaging/outbox/utils.js +109 -70
- package/libx/_runtime/messaging/service.js +16 -7
- package/libx/_runtime/remote/Service.js +15 -2
- package/libx/_runtime/remote/utils/client.js +41 -11
- package/libx/_runtime/sqlite/Service.js +4 -1
- package/libx/_runtime/sqlite/convertDraftAdminPathExpression.js +56 -0
- package/libx/_runtime/sqlite/customBuilder/CustomUpsertBuilder.js +41 -0
- package/libx/_runtime/sqlite/customBuilder/index.js +5 -0
- package/libx/_runtime/sqlite/execute.js +1 -1
- package/libx/_runtime/types/api.js +2 -2
- package/libx/rest/RestAdapter.js +15 -13
- package/package.json +1 -1
- package/server.js +2 -19
package/lib/utils/inflect.js
CHANGED
|
@@ -1,24 +1,25 @@
|
|
|
1
|
-
const last = /\w+$/
|
|
2
1
|
|
|
3
|
-
|
|
2
|
+
this.singular4 = (dn,stripped) => {
|
|
4
3
|
let n = dn.name || dn; if (stripped) n = n.match(last)[0]
|
|
5
4
|
return dn['@singular'] || (
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
5
|
+
/species|news$/i.test(n) ? n :
|
|
6
|
+
/ess$/.test(n) ? n : // Address
|
|
7
|
+
/ees$/.test(n) ? n.slice(0, -1) : // Employees --> Employee
|
|
8
|
+
/[sz]es$/.test(n) ? n.slice(0, -2) :
|
|
9
|
+
/[^aeiou]ies$/.test(n) ? n.slice(0, -3) + 'y' : // Deliveries --> Delivery
|
|
10
|
+
/s$/.test(n) ? n.slice(0, -1) :
|
|
12
11
|
n
|
|
13
12
|
)
|
|
14
13
|
}
|
|
15
14
|
|
|
16
|
-
|
|
15
|
+
this.plural4 = (dn,stripped) => {
|
|
17
16
|
let n = dn.name || dn; if (stripped) n = n.match(last)[0]
|
|
18
17
|
return dn['@plural'] || (
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
/analysis|status|species|sheep|news$/i.test(n) ? n :
|
|
19
|
+
/[^aeiou]y$/.test(n) ? n.slice(0,-1) + 'ies' :
|
|
20
|
+
/(s|x|z|ch|sh)$/.test(n) ? n + 'es' :
|
|
22
21
|
n + 's'
|
|
23
22
|
)
|
|
24
23
|
}
|
|
24
|
+
|
|
25
|
+
const last = /\w+$/
|
package/lib/utils/tar.js
CHANGED
|
@@ -15,16 +15,37 @@ const win = path => {
|
|
|
15
15
|
if (Array.isArray(path)) return path.map(el => win(el))
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
async function copyDir(src, dest) {
|
|
19
|
+
if ((await fs.promises.stat(src)).isDirectory()) {
|
|
20
|
+
const entries = await fs.promises.readdir(src)
|
|
21
|
+
return Promise.all(entries.map(async each => copyDir(path.join(src, each), path.join(dest, each))))
|
|
22
|
+
} else {
|
|
23
|
+
await fs.promises.mkdir(path.dirname(dest), { recursive: true })
|
|
24
|
+
return fs.promises.copyFile(src, dest)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Copy resources containing files and folders to temp dir on Windows and pack temp dir.
|
|
19
29
|
// cli tar has a size limit on Windows.
|
|
20
|
-
const createTemp = async (root,
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
await
|
|
30
|
+
const createTemp = async (root, resources) => {
|
|
31
|
+
// Asynchronously copies the entire content from src to dest.
|
|
32
|
+
const temp = await fs.promises.mkdtemp(`${fs.realpathSync(require('os').tmpdir())}${path.sep}tar-`)
|
|
33
|
+
for (let resource of resources) {
|
|
34
|
+
const destination = path.join(temp, path.relative(root, resource))
|
|
35
|
+
if ((await fs.promises.stat(resource)).isFile()) {
|
|
36
|
+
const dirName = path.dirname(destination)
|
|
37
|
+
if (!await exists(dirName)) {
|
|
38
|
+
await fs.promises.mkdir(dirName, { recursive: true })
|
|
39
|
+
}
|
|
40
|
+
await fs.promises.copyFile(resource, destination)
|
|
41
|
+
} else {
|
|
42
|
+
if (fs.promises.cp) {
|
|
43
|
+
await fs.promises.cp(resource, destination, { recursive: true })
|
|
44
|
+
} else {
|
|
45
|
+
// node < 16
|
|
46
|
+
await copyDir(resource, destination)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
28
49
|
}
|
|
29
50
|
|
|
30
51
|
return temp
|
|
@@ -58,8 +79,9 @@ exports.create = async (dir='.', ...args) => {
|
|
|
58
79
|
|
|
59
80
|
if (typeof dir === 'string') dir = _resolve(dir)
|
|
60
81
|
if (Array.isArray(dir)) [ dir, ...args ] = [ cds.root, dir, ...args ]
|
|
61
|
-
|
|
82
|
+
|
|
62
83
|
let c, temp
|
|
84
|
+
args = args.filter(el => el)
|
|
63
85
|
if (process.platform === 'win32') {
|
|
64
86
|
const spawnDir = (dir, args) => {
|
|
65
87
|
if (args.some(arg => arg === '-f')) return spawn ('tar', ['c', '-C', win(dir), ...win(args)])
|
|
@@ -76,7 +98,7 @@ exports.create = async (dir='.', ...args) => {
|
|
|
76
98
|
} else {
|
|
77
99
|
if (Array.isArray(args[0])) args.push (...args.shift().map (f => path.isAbsolute(f) ? path.relative(dir,f) : f))
|
|
78
100
|
else args.push('.')
|
|
79
|
-
|
|
101
|
+
|
|
80
102
|
c = spawn ('tar', ['c', '-C', dir, ...args])
|
|
81
103
|
}
|
|
82
104
|
|
|
@@ -88,8 +110,16 @@ exports.create = async (dir='.', ...args) => {
|
|
|
88
110
|
* @example const buffer = await tar.c('src/dir')
|
|
89
111
|
*/
|
|
90
112
|
then (r,e) {
|
|
91
|
-
const bb=[]
|
|
92
|
-
|
|
113
|
+
const bb=[]
|
|
114
|
+
const eb=[]
|
|
115
|
+
c.stdout.on('data', b => bb.push(b))
|
|
116
|
+
c.stderr.on('data', b => eb.push(b))
|
|
117
|
+
c.on('close', (code) => {
|
|
118
|
+
if (code === 0) {
|
|
119
|
+
return r(Buffer.concat(bb))
|
|
120
|
+
}
|
|
121
|
+
e(new Error('tar: ' + Buffer.concat(eb)))
|
|
122
|
+
})
|
|
93
123
|
c.on('error', e)
|
|
94
124
|
if (process.platform === 'win32') {
|
|
95
125
|
c.on('close', async () => temp && exists(temp) && await rimraf(temp))
|
|
@@ -162,10 +162,10 @@ class ODataRequest extends cds.Request {
|
|
|
162
162
|
* super
|
|
163
163
|
*/
|
|
164
164
|
const { user } = req
|
|
165
|
-
|
|
165
|
+
const tenant = req.tenant || user?.tenant
|
|
166
166
|
// REVISIT: public API for query options (express style req.query already in use)?
|
|
167
167
|
const _queryOptions = odataReq.getQueryOptions()
|
|
168
|
-
super({ event, target, data, query, user, method, headers, req, res, _queryOptions })
|
|
168
|
+
super({ event, target, data, query, user, method, headers, req, res, _queryOptions, tenant })
|
|
169
169
|
}
|
|
170
170
|
|
|
171
171
|
/*
|
|
@@ -82,7 +82,7 @@ const action = service => {
|
|
|
82
82
|
await tx.rollback(e).catch(() => {})
|
|
83
83
|
}
|
|
84
84
|
} finally {
|
|
85
|
-
req.messages && odataRes.setHeader('sap-messages', getSapMessages(req.messages, req.
|
|
85
|
+
req.messages && odataRes.setHeader('sap-messages', getSapMessages(req.messages, req.http.req))
|
|
86
86
|
|
|
87
87
|
if (err) next(err)
|
|
88
88
|
else next(null, toODataResult(result, req))
|
|
@@ -68,7 +68,7 @@ const create = service => {
|
|
|
68
68
|
await tx.rollback(e).catch(() => {})
|
|
69
69
|
}
|
|
70
70
|
} finally {
|
|
71
|
-
req.messages && odataRes.setHeader('sap-messages', getSapMessages(req.messages, req.
|
|
71
|
+
req.messages && odataRes.setHeader('sap-messages', getSapMessages(req.messages, req.http.req))
|
|
72
72
|
|
|
73
73
|
if (err) next(err)
|
|
74
74
|
else next(null, toODataResult(result, req))
|
|
@@ -49,7 +49,7 @@ const del = service => {
|
|
|
49
49
|
await tx.rollback(e).catch(() => {})
|
|
50
50
|
}
|
|
51
51
|
} finally {
|
|
52
|
-
req.messages && odataRes.setHeader('sap-messages', getSapMessages(req.messages, req.
|
|
52
|
+
req.messages && odataRes.setHeader('sap-messages', getSapMessages(req.messages, req.http.req))
|
|
53
53
|
|
|
54
54
|
if (err) next(err)
|
|
55
55
|
else next(null, null)
|
|
@@ -20,20 +20,6 @@ const metadata = service => {
|
|
|
20
20
|
|
|
21
21
|
try {
|
|
22
22
|
const { 'cds.xt.ModelProviderService': mps } = cds.services
|
|
23
|
-
// REVISIT: The following block should replaced with the one commented bellow after the next release of cds-mtxs.
|
|
24
|
-
// Currently lkg tests fail w/o the try-catch.
|
|
25
|
-
let edmx
|
|
26
|
-
try {
|
|
27
|
-
if (mps) edmx = await mps.getEdmx({ tenant, model: service.model, service: service.definition.name, locale })
|
|
28
|
-
// eslint-disable-next-line no-empty
|
|
29
|
-
} catch (_) {}
|
|
30
|
-
if (!edmx)
|
|
31
|
-
edmx = cds.localize(
|
|
32
|
-
service.model,
|
|
33
|
-
locale,
|
|
34
|
-
cds.compile.to.edmx(service.model, { service: service.definition.name })
|
|
35
|
-
)
|
|
36
|
-
/*
|
|
37
23
|
let edmx = mps
|
|
38
24
|
? await mps.getEdmx({ tenant, model: service.model, service: service.definition.name, locale })
|
|
39
25
|
: cds.localize(
|
|
@@ -41,7 +27,7 @@ const metadata = service => {
|
|
|
41
27
|
locale,
|
|
42
28
|
// REVISIT: we could cache this in model._cached
|
|
43
29
|
cds.compile.to.edmx(service.model, { service: service.definition.name })
|
|
44
|
-
)
|
|
30
|
+
)
|
|
45
31
|
return next(null, toODataResult(edmx))
|
|
46
32
|
} catch (e) {
|
|
47
33
|
if (LOG._error) {
|
|
@@ -491,7 +491,7 @@ const read = service => {
|
|
|
491
491
|
await tx.rollback(e).catch(() => {})
|
|
492
492
|
}
|
|
493
493
|
} finally {
|
|
494
|
-
req.messages && odataRes.setHeader('sap-messages', getSapMessages(req.messages, req.
|
|
494
|
+
req.messages && odataRes.setHeader('sap-messages', getSapMessages(req.messages, req.http.req))
|
|
495
495
|
|
|
496
496
|
if (err) next(err)
|
|
497
497
|
else next(null, result, additional)
|
|
@@ -179,7 +179,7 @@ const update = service => {
|
|
|
179
179
|
await tx.rollback(e).catch(() => {})
|
|
180
180
|
}
|
|
181
181
|
} finally {
|
|
182
|
-
req.messages && odataRes.setHeader('sap-messages', getSapMessages(req.messages, req.
|
|
182
|
+
req.messages && odataRes.setHeader('sap-messages', getSapMessages(req.messages, req.http.req))
|
|
183
183
|
|
|
184
184
|
if (err) next(err)
|
|
185
185
|
else if (primitive && result) {
|
package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/errors/UriSyntaxError.js
CHANGED
|
@@ -37,7 +37,7 @@ UriSyntaxError.Message = {
|
|
|
37
37
|
KEY_VALUE_NOT_FOUND: "No '%s' value found for key '%s'",
|
|
38
38
|
PREVIOUS_TYPE_HAS_NO_MEDIA: "Previous segment type '%s' does not have a media resource",
|
|
39
39
|
MUST_BE_COUNT_OR_BOUND_OPERATION: "Expected current segment '%s' to be '$count' or a bound operation",
|
|
40
|
-
MUST_BE_COUNT_OR_REF_OR_BOUND_OPERATION: "Expected current segment '%s' to be '$count', '$ref', or a
|
|
40
|
+
MUST_BE_COUNT_OR_REF_OR_BOUND_OPERATION: "Expected current segment '%s' to be '$count', '$ref', a bound operation or a key value with a proper type",
|
|
41
41
|
|
|
42
42
|
ALIAS_NOT_FOUND: "Parameter alias '%s' not found",
|
|
43
43
|
WRONG_ALIAS_VALUE: "Wrong value for parameter alias '%s'",
|
|
@@ -143,7 +143,12 @@ class UriParser {
|
|
|
143
143
|
* @returns {UriInfo} the result of parsing
|
|
144
144
|
*/
|
|
145
145
|
parseRelativeUri (uri, queryOptions) {
|
|
146
|
-
let uriPathSegments
|
|
146
|
+
let uriPathSegments
|
|
147
|
+
try {
|
|
148
|
+
uriPathSegments = uri.split('/').map(decodeURIComponent)
|
|
149
|
+
} catch (error) {
|
|
150
|
+
throw new UriSyntaxError('wrong percent encoding in uri: ' + uri)
|
|
151
|
+
}
|
|
147
152
|
|
|
148
153
|
let uriInfo = new UriInfo()
|
|
149
154
|
|
|
@@ -24,18 +24,6 @@ class ConditionalRequestValidator {
|
|
|
24
24
|
if (method !== HttpMethods.GET && !ifMatch && !ifNoneMatch) throw new PreconditionRequiredError()
|
|
25
25
|
return
|
|
26
26
|
}
|
|
27
|
-
|
|
28
|
-
if (ifMatch || ifNoneMatch) {
|
|
29
|
-
// Careless clients send this also for DELETE and POST, other careless clients send the star in double-quotes.
|
|
30
|
-
if ([HttpMethods.POST].includes(method)) {
|
|
31
|
-
if (
|
|
32
|
-
(ifMatch && ifMatch.trim() !== '*' && ifMatch.trim() !== '"*"') ||
|
|
33
|
-
(ifNoneMatch && ifNoneMatch.trim() !== '*' && ifNoneMatch.trim() !== '"*"')
|
|
34
|
-
) {
|
|
35
|
-
throw new PreconditionFailedError()
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
27
|
}
|
|
40
28
|
|
|
41
29
|
/**
|
|
@@ -18,12 +18,6 @@ const _getEntitySets = (edm, namespace) => {
|
|
|
18
18
|
return entities
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
const _getConcurrent = (namespace, element, csn) => {
|
|
22
|
-
// autoexposed entities now used . in csn and _ in edm
|
|
23
|
-
const e = findCsnTargetFor(element, csn, namespace)
|
|
24
|
-
return !!e._etag
|
|
25
|
-
}
|
|
26
|
-
|
|
27
21
|
const oDataConfiguration = (edm, csn) => {
|
|
28
22
|
let namespace
|
|
29
23
|
for (const prop in edm) {
|
|
@@ -44,7 +38,7 @@ const oDataConfiguration = (edm, csn) => {
|
|
|
44
38
|
|
|
45
39
|
configuration[entitySet] = {
|
|
46
40
|
maxPageSize: getMaxPageSize(e),
|
|
47
|
-
isConcurrent:
|
|
41
|
+
isConcurrent: !!e._etag
|
|
48
42
|
}
|
|
49
43
|
|
|
50
44
|
// custom aggregates
|
|
@@ -208,6 +208,9 @@ const _processCategory = (req, category, elementInfo, options, previousResult) =
|
|
|
208
208
|
localizeAfterDraftActivate(row, key, req.locale)
|
|
209
209
|
break
|
|
210
210
|
|
|
211
|
+
case '@cds.Boolean':
|
|
212
|
+
if (row[key] != null) row[key] = !!row[key]
|
|
213
|
+
|
|
211
214
|
// no default
|
|
212
215
|
}
|
|
213
216
|
}
|
|
@@ -270,6 +273,7 @@ const _pick = options => (element, target) => {
|
|
|
270
273
|
|
|
271
274
|
if (element['@odata.etag']) categories.push('@odata.etag')
|
|
272
275
|
if (element._type === 'cds.Decimal') categories.push('@cds.Decimal')
|
|
276
|
+
if (cds.db?.kind === 'better-sqlite' && element._type === 'cds.Boolean') categories.push('@cds.Boolean')
|
|
273
277
|
|
|
274
278
|
categories.push(..._assocs(element, target))
|
|
275
279
|
|
|
@@ -50,7 +50,29 @@ class ApplicationService extends cds.Service {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
registerFioriHandlers() {
|
|
53
|
-
|
|
53
|
+
if (cds.env.features.lean_draft) {
|
|
54
|
+
const {
|
|
55
|
+
onNewDraft,
|
|
56
|
+
onDraftPrepare,
|
|
57
|
+
onDraftActivate,
|
|
58
|
+
onPatch,
|
|
59
|
+
onDraftEdit,
|
|
60
|
+
onDelete
|
|
61
|
+
} = require('../../fiori/lean-draft')
|
|
62
|
+
const LOG = cds.log('fiori|drafts')
|
|
63
|
+
|
|
64
|
+
for (let each of this.entities)
|
|
65
|
+
if (each.drafts) {
|
|
66
|
+
LOG.debug('serving drafts for', { entity: each.name })
|
|
67
|
+
this.on('NEW', each, onNewDraft)
|
|
68
|
+
this.on('PATCH', each, onPatch)
|
|
69
|
+
this.on('EDIT', each, onDraftEdit)
|
|
70
|
+
this.on('draftPrepare', each, onDraftPrepare)
|
|
71
|
+
this.on('draftActivate', each, onDraftActivate)
|
|
72
|
+
this.on('draftActivate', each, onDraftActivate)
|
|
73
|
+
this.on(['CANCEL', 'DELETE'], each, onDelete)
|
|
74
|
+
}
|
|
75
|
+
} else return require('../../fiori/generic').impl.call(this)
|
|
54
76
|
}
|
|
55
77
|
|
|
56
78
|
registerCrudHandlers() {
|
|
@@ -259,46 +259,6 @@ const _checkFormatElement = (element, value, errors, key, pathSegments) => {
|
|
|
259
259
|
}
|
|
260
260
|
}
|
|
261
261
|
|
|
262
|
-
// check for forbidden deep operations for association
|
|
263
|
-
const checkIfAssocDeep = (element, value, req) => {
|
|
264
|
-
if (!value) return
|
|
265
|
-
|
|
266
|
-
if (element.on) {
|
|
267
|
-
req.error(
|
|
268
|
-
assertError(
|
|
269
|
-
element.is2one
|
|
270
|
-
? { code: ASSERT_DEEP_ASSOCIATION, args: ['unmanaged to-one', element.name] }
|
|
271
|
-
: { code: ASSERT_DEEP_ASSOCIATION, args: ['to-many', element.name] },
|
|
272
|
-
element,
|
|
273
|
-
value
|
|
274
|
-
)
|
|
275
|
-
)
|
|
276
|
-
|
|
277
|
-
return
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
if (element.is2one) {
|
|
281
|
-
// managed to one
|
|
282
|
-
Object.keys(value).forEach(prop => {
|
|
283
|
-
if (typeof value[prop] !== 'object') {
|
|
284
|
-
const foreignKey = element._foreignKeys.find(fk => fk.childElement.name === prop)
|
|
285
|
-
if (foreignKey) return
|
|
286
|
-
|
|
287
|
-
const key = element.keys.find(element => element.ref[0] === prop)
|
|
288
|
-
if (key) return
|
|
289
|
-
|
|
290
|
-
const err = assertError(
|
|
291
|
-
{ code: ASSERT_DEEP_ASSOCIATION, args: ['managed to-one', element.name] },
|
|
292
|
-
element,
|
|
293
|
-
value
|
|
294
|
-
)
|
|
295
|
-
err.target += `.${prop}`
|
|
296
|
-
req.error(err)
|
|
297
|
-
}
|
|
298
|
-
})
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
|
|
302
262
|
/**
|
|
303
263
|
* @param {import('../../types/api').InputConstraints} constraints
|
|
304
264
|
*/
|
|
@@ -407,7 +367,6 @@ module.exports = {
|
|
|
407
367
|
checkInputConstraints,
|
|
408
368
|
checkKeys,
|
|
409
369
|
assertError,
|
|
410
|
-
checkIfAssocDeep,
|
|
411
370
|
checkStaticElementByKey,
|
|
412
371
|
assertNotNullError,
|
|
413
372
|
assertTargets
|
|
@@ -283,7 +283,11 @@ const _selectDeepUpdateData = async args => {
|
|
|
283
283
|
|
|
284
284
|
// if a view has an orderBy with renamed field, we need to resolve it
|
|
285
285
|
const _resolveOrderBy = (orderBy, transitions) => {
|
|
286
|
-
|
|
286
|
+
// no resolved entity found
|
|
287
|
+
if (!transitions?.length) return
|
|
288
|
+
// if there are no renamed fields, no need to resolve
|
|
289
|
+
if (!transitions[0].mapping.size) return
|
|
290
|
+
if (orderBy) orderBy.map(el => (el.ref[0] = transitions[0].mapping.get(el.ref[0]).ref[0]))
|
|
287
291
|
}
|
|
288
292
|
|
|
289
293
|
/*
|
|
@@ -9,9 +9,9 @@ const reject = (req, reason = null) => {
|
|
|
9
9
|
// unauthorized or forbidden?
|
|
10
10
|
if (req.user._is_anonymous) {
|
|
11
11
|
// REVISIT: challenges handling should be done in protocol adapter (i.e., express error middleware)
|
|
12
|
-
// REVISIT: improve `req.
|
|
13
|
-
if (req.
|
|
14
|
-
req.
|
|
12
|
+
// REVISIT: improve `req.http.req` check if this is an HTTP request
|
|
13
|
+
if (req.http?.req && req.user._challenges && req.user._challenges.length > 0) {
|
|
14
|
+
req.http.res.set('WWW-Authenticate', req.user._challenges.join(';'))
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
// REVISIT: security log in else case?
|
|
@@ -26,7 +26,7 @@ const _targetEntityDoesNotExist = async req => {
|
|
|
26
26
|
|
|
27
27
|
exports.impl = cds.service.impl(function () {
|
|
28
28
|
// eslint-disable-next-line complexity
|
|
29
|
-
this.on(['CREATE', 'READ', 'UPDATE', 'DELETE'], '*', async function (req) {
|
|
29
|
+
this.on(['CREATE', 'READ', 'UPDATE', 'DELETE', 'UPSERT'], '*', async function (req) {
|
|
30
30
|
if (typeof req.query !== 'string' && req.target && req.target._hasPersistenceSkip) {
|
|
31
31
|
throw getError({
|
|
32
32
|
code: 501,
|
|
@@ -16,6 +16,7 @@ const { checkInputConstraints, assertTargets } = require('../../cds-services/uti
|
|
|
16
16
|
const getTemplate = require('../utils/template')
|
|
17
17
|
const templateProcessor = require('../utils/templateProcessor')
|
|
18
18
|
const { getDataFromCQN, setDataFromCQN } = require('../utils/data')
|
|
19
|
+
const getRowUUIDGeneratorFn = require('../utils/rowUUIDGenerator')
|
|
19
20
|
|
|
20
21
|
const _shouldSuppressErrorPropagation = (event, value) => {
|
|
21
22
|
return (
|
|
@@ -34,24 +35,6 @@ const _getSimpleCategory = category => {
|
|
|
34
35
|
return category
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
const _rowKeysGenerator = eventName => {
|
|
38
|
-
if (eventName === 'UPDATE') return
|
|
39
|
-
return (keyNames, row, template) => {
|
|
40
|
-
for (const keyName of keyNames) {
|
|
41
|
-
if (Object.prototype.hasOwnProperty.call(row, keyName)) {
|
|
42
|
-
continue
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const elementInfo = template.elements.get(keyName)
|
|
46
|
-
const plain = elementInfo && elementInfo.picked && elementInfo.picked.plain
|
|
47
|
-
if (!plain || !plain.categories) continue
|
|
48
|
-
if (plain.categories.includes('uuid')) {
|
|
49
|
-
row[keyName] = cds.utils.uuid()
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
38
|
const _isDraftCoreComputed = (req, element, event) =>
|
|
56
39
|
cds.env.features.preserve_computed !== false &&
|
|
57
40
|
req._ &&
|
|
@@ -65,10 +48,7 @@ const _isStreamingProperty = (elements, row, property) =>
|
|
|
65
48
|
)
|
|
66
49
|
|
|
67
50
|
const _getMediaTypeValue = req =>
|
|
68
|
-
req.
|
|
69
|
-
req._.req.headers['content-type'] &&
|
|
70
|
-
!req._.req.headers['content-type'].match(/json|multipart/i) &&
|
|
71
|
-
req._.req.headers['content-type']
|
|
51
|
+
!req.http?.req?.headers?.['content-type'].match(/json|multipart/i) && req.http?.req?.headers?.['content-type']
|
|
72
52
|
|
|
73
53
|
const _preProcessAssertTarget = (assocInfo, assertMap) => {
|
|
74
54
|
const { element: assoc, row } = assocInfo
|
|
@@ -265,7 +245,7 @@ async function commonGenericInput(req) {
|
|
|
265
245
|
}
|
|
266
246
|
|
|
267
247
|
const pathOptions = {
|
|
268
|
-
|
|
248
|
+
rowUUIDGenerator: getRowUUIDGeneratorFn(req.event),
|
|
269
249
|
includeKeyValues: true,
|
|
270
250
|
pathSegments: []
|
|
271
251
|
}
|
|
@@ -276,7 +256,7 @@ async function commonGenericInput(req) {
|
|
|
276
256
|
if (pathSegment) pathOptions.pathSegments.push(pathSegment)
|
|
277
257
|
|
|
278
258
|
if (keys && 'IsActiveEntity' in keys) {
|
|
279
|
-
pathOptions.
|
|
259
|
+
pathOptions.draftKeys = { IsActiveEntity: keys.IsActiveEntity }
|
|
280
260
|
}
|
|
281
261
|
}
|
|
282
262
|
|
|
@@ -1,23 +1,24 @@
|
|
|
1
1
|
const cds = require('../../cds')
|
|
2
|
-
const {
|
|
2
|
+
const { getPageSize } = require('../utils/page')
|
|
3
3
|
|
|
4
4
|
const commonGenericPaging = function (req) {
|
|
5
5
|
// only if http request
|
|
6
|
-
if (!
|
|
6
|
+
if (!req.http?.req) return
|
|
7
7
|
|
|
8
8
|
// target === null if view with parameters
|
|
9
|
-
if (!req.target || !req.query
|
|
9
|
+
if (!req.target || !req.query?.SELECT || req.query.SELECT.one) return
|
|
10
10
|
|
|
11
11
|
_addPaging(req.query, req.target)
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
const _addPaging = function (
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
const _addPaging = function ({ SELECT }, target) {
|
|
15
|
+
const { rows } = SELECT.limit || (SELECT.limit = {})
|
|
16
|
+
const conf = getPageSize(target)
|
|
17
|
+
SELECT.limit.rows = {
|
|
18
|
+
val: !rows ? conf.default : Math.min(rows.val ?? rows, conf.max)
|
|
19
|
+
}
|
|
19
20
|
//Handle nested limits
|
|
20
|
-
if (
|
|
21
|
+
if (SELECT.from.SELECT?.limit) _addPaging(SELECT.from, target)
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
/**
|
|
@@ -804,6 +804,33 @@ const _convertSelect = (query, model, _options) => {
|
|
|
804
804
|
return query
|
|
805
805
|
}
|
|
806
806
|
|
|
807
|
+
const _convertUpsert = (query, model) => {
|
|
808
|
+
// resolve path expression
|
|
809
|
+
const resolvedIntoClause = _convertPathExpressionForInsert(query.UPSERT.into, model)
|
|
810
|
+
|
|
811
|
+
const target = model.definitions[resolvedIntoClause]
|
|
812
|
+
if (!target) {
|
|
813
|
+
// if there is no target, just return original query, as a copy is not deep anyways and all the sub items of query.UPSERT are referenced only anyways
|
|
814
|
+
return query
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// overwrite only .into, foreign keys are already set
|
|
818
|
+
// 'a' added as placeholder since its overwritten by Object.assign below
|
|
819
|
+
const upsert = UPSERT.into('a')
|
|
820
|
+
|
|
821
|
+
// REVISIT flatten structured types, currently its done in SQL builder
|
|
822
|
+
|
|
823
|
+
// We add all previous properties ot the newly created query.
|
|
824
|
+
// Reason is to not lose the query API functionality
|
|
825
|
+
Object.assign(upsert.UPSERT, query.UPSERT, { into: { ref: [resolvedIntoClause], as: query.UPSERT.into.as } })
|
|
826
|
+
|
|
827
|
+
const resolved = resolveView(upsert, model, cds.db)
|
|
828
|
+
// required for deplyoing of extensions, not used anywhere else except UpsertBuilder
|
|
829
|
+
resolved._target = resolved.UPSERT?._transitions?.[0].target || query._target
|
|
830
|
+
// resolved._target = query._target
|
|
831
|
+
return resolved
|
|
832
|
+
}
|
|
833
|
+
|
|
807
834
|
const _convertInsert = (query, model) => {
|
|
808
835
|
// resolve path expression
|
|
809
836
|
const resolvedIntoClause = _convertPathExpressionForInsert(query.INSERT.into, model)
|
|
@@ -950,6 +977,10 @@ const cqn2cqn4sql = (query, model, options = { suppressSearch: false }) => {
|
|
|
950
977
|
return _convertInsert(query, model)
|
|
951
978
|
}
|
|
952
979
|
|
|
980
|
+
if (query.UPSERT) {
|
|
981
|
+
return _convertUpsert(query, model)
|
|
982
|
+
}
|
|
983
|
+
|
|
953
984
|
if (query.DELETE) {
|
|
954
985
|
return _convertDelete(query, model, options)
|
|
955
986
|
}
|
|
@@ -129,21 +129,22 @@ const _findCsnTarget = (edmName, model, namespace) => {
|
|
|
129
129
|
return target
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
+
const _initializeCache = (model, namespace) => {
|
|
133
|
+
const cache = {}
|
|
134
|
+
for (const name in model.definitions) {
|
|
135
|
+
// do no cache entities within different namespace
|
|
136
|
+
if (!name.startsWith(`${namespace}.`)) continue
|
|
137
|
+
// cut off namespace and underscoreify entity name (OData does not allow dots)
|
|
138
|
+
cache[name.replace(new RegExp(`^${namespace}\\.`), '').replace(/\./g, '_')] = model.definitions[name]
|
|
139
|
+
}
|
|
140
|
+
return cache
|
|
141
|
+
}
|
|
142
|
+
|
|
132
143
|
const findCsnTargetFor = (edmName, model, namespace) => {
|
|
133
|
-
const cache =
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
Object.defineProperty(cache, namespace, {
|
|
138
|
-
get() {
|
|
139
|
-
const _ = {}
|
|
140
|
-
for (const name in model.definitions) {
|
|
141
|
-
if (!name.startsWith(`${namespace}.`)) continue
|
|
142
|
-
_[name.replace(new RegExp(`^${namespace}\\.`), '').replace(/\./g, '_')] = model.definitions[name]
|
|
143
|
-
}
|
|
144
|
-
return _
|
|
145
|
-
}
|
|
146
|
-
})[namespace]
|
|
144
|
+
const cache = model._edmToCSNNameMap || (model._edmToCSNNameMap = {})
|
|
145
|
+
const edm2csnMap = cache[namespace] || (cache[namespace] = _initializeCache(model, namespace))
|
|
146
|
+
|
|
147
|
+
if (edm2csnMap[edmName]) return edm2csnMap[edmName]
|
|
147
148
|
|
|
148
149
|
const target = _findCsnTarget(edmName, model, namespace)
|
|
149
150
|
|
|
@@ -226,7 +227,12 @@ function getDraftTreeRoot(entity, model) {
|
|
|
226
227
|
for (const k in model.definitions) {
|
|
227
228
|
const e = model.definitions[k]
|
|
228
229
|
if (e.kind !== 'entity' || !e.compositions) continue
|
|
229
|
-
for (const c in e.compositions)
|
|
230
|
+
for (const c in e.compositions)
|
|
231
|
+
if (
|
|
232
|
+
e.compositions[c].target === current.name ||
|
|
233
|
+
e.compositions[c].target === current.name.replace(/\.drafts/, '')
|
|
234
|
+
)
|
|
235
|
+
parents.push(e)
|
|
230
236
|
}
|
|
231
237
|
if (parents.length > 1 && parents.some(p => p !== parents[0])) {
|
|
232
238
|
// > unable to determine single parent
|
|
@@ -19,7 +19,8 @@ const ensureUnlocalized = table => {
|
|
|
19
19
|
return _table
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
const ensureDraftsSuffix = name =>
|
|
22
|
+
const ensureDraftsSuffix = name =>
|
|
23
|
+
name.endsWith('_drafts') || name.endsWith('.drafts') ? name : `${ensureUnlocalized(name)}_drafts`
|
|
23
24
|
|
|
24
25
|
const ensureNoDraftsSuffix = name => name.replace(/_drafts$/g, '')
|
|
25
26
|
|