@sap/cds 6.4.0 → 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 +19 -0
- package/apis/cds.d.ts +2 -0
- package/apis/ql.d.ts +4 -0
- package/apis/services.d.ts +9 -1
- package/bin/build/provider/mtx/resourcesTarBuilder.js +1 -1
- package/bin/deploy/to-hana/hdiDeployUtil.js +11 -5
- package/lib/auth/index.js +17 -15
- package/lib/core/index.js +1 -0
- 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/utils/data.js +2 -2
- package/lib/utils/tar.js +31 -9
- package/libx/_runtime/common/generic/crud.js +1 -1
- package/libx/_runtime/common/generic/paging.js +8 -7
- package/libx/_runtime/common/utils/cqn2cqn4sql.js +31 -0
- package/libx/_runtime/common/utils/resolveView.js +2 -0
- package/libx/_runtime/db/sql-builder/InsertBuilder.js +5 -1
- package/libx/_runtime/db/sql-builder/UpsertBuilder.js +9 -32
- package/libx/_runtime/db/sql-builder/annotations.js +6 -3
- package/libx/_runtime/hana/Service.js +1 -1
- package/libx/_runtime/hana/execute.js +5 -5
- package/libx/_runtime/sqlite/Service.js +1 -1
- package/libx/_runtime/sqlite/customBuilder/CustomUpsertBuilder.js +20 -38
- package/package.json +1 -1
- package/server.js +2 -20
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,25 @@
|
|
|
4
4
|
- The format is based on [Keep a Changelog](http://keepachangelog.com/).
|
|
5
5
|
- This project adheres to [Semantic Versioning](http://semver.org/).
|
|
6
6
|
|
|
7
|
+
## Version 6.4.1 - 2022-01-16
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- `cds build` correctly creates a `resources.tgz` file for MTXS projects on Windows
|
|
12
|
+
- `cds.deploy` for HANA now doesn't try to search for a globally installed `@sap/hdi-deploy` if there's no `npm` installed, e.g. on a Node.js server without `npm`.
|
|
13
|
+
- Signature for `cds.ql.UPSERT`
|
|
14
|
+
- Signature for `<srv>.delete().where()`
|
|
15
|
+
- Signature for `SELECT.alias`
|
|
16
|
+
- `UPSERT` requests for SQLite if only keys are provided
|
|
17
|
+
- `cds.test` doesn't log database resets with `autoReset` enabled any more.
|
|
18
|
+
- The `cds.deploy` output for HANA is now correctly formatted in Kibana.
|
|
19
|
+
- SAP HANA stored procedures containing implicit selects
|
|
20
|
+
- Shorthand configuration for `graphql` in `cds.env.protocols`
|
|
21
|
+
- If `cds.env.protocols` is set, `cds.requires.middlewares` is automatically turned on
|
|
22
|
+
- `cds.context` middleware is split to initial handling of request correlation and user propagation
|
|
23
|
+
- fix view resolving and managed data for UPSERT
|
|
24
|
+
- `cds.linked` supports polymorphic self links like in: `action foo( self: [many] $self, ...)`
|
|
25
|
+
|
|
7
26
|
## Version 6.4.0 - 2022-12-15
|
|
8
27
|
|
|
9
28
|
### Added
|
package/apis/cds.d.ts
CHANGED
|
@@ -16,6 +16,7 @@ declare global {
|
|
|
16
16
|
// these provide the functionality from SELECT, INSERT, etc in the global facade
|
|
17
17
|
const SELECT: typeof cds.ql.SELECT
|
|
18
18
|
const INSERT: typeof cds.ql.INSERT
|
|
19
|
+
const UPSERT: typeof cds.ql.UPSERT
|
|
19
20
|
const UPDATE: typeof cds.ql.UPDATE
|
|
20
21
|
const DELETE: typeof cds.ql.DELETE
|
|
21
22
|
const CREATE: typeof cds.ql.CREATE
|
|
@@ -24,6 +25,7 @@ declare global {
|
|
|
24
25
|
// and these allow us to use them as type too, i.e. `const q: SELECT<Book> = ...`
|
|
25
26
|
type SELECT<T> = ql.SELECT<T>
|
|
26
27
|
type INSERT<T> = ql.INSERT<T>
|
|
28
|
+
type UPSERT<T> = ql.UPSERT<T>
|
|
27
29
|
type UPDATE<T> = ql.UPDATE<T>
|
|
28
30
|
type DELETE<T> = ql.DELETE<T>
|
|
29
31
|
type CREATE<T> = ql.CREATE<T>
|
package/apis/ql.d.ts
CHANGED
|
@@ -95,6 +95,8 @@ declare class QL<T> {
|
|
|
95
95
|
SELECT : StaticSELECT<T>
|
|
96
96
|
INSERT : typeof INSERT
|
|
97
97
|
& ((...entries:object[]) => INSERT<any>) & ((entries:object[]) => INSERT<any>)
|
|
98
|
+
UPSERT: typeof UPSERT
|
|
99
|
+
& ((...entries:object[]) => UPSERT<any>) & ((entries:object[]) => UPSERT<any>)
|
|
98
100
|
UPDATE : typeof UPDATE
|
|
99
101
|
& typeof UPDATE.entity
|
|
100
102
|
DELETE : typeof DELETE
|
|
@@ -137,6 +139,8 @@ export class SELECT<T> extends ConstructedQuery {
|
|
|
137
139
|
& ((rows : number, offset? : number) => this)
|
|
138
140
|
forShareLock () : this
|
|
139
141
|
forUpdate ({wait}? : {wait?: number}) : this
|
|
142
|
+
alias (as: string) : this
|
|
143
|
+
|
|
140
144
|
|
|
141
145
|
// Not yet public
|
|
142
146
|
// fullJoin (other: string, as: string) : this
|
package/apis/services.d.ts
CHANGED
|
@@ -30,6 +30,14 @@ export class QueryAPI {
|
|
|
30
30
|
<T>(data: object | object[]): INSERT<T>
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* @see [docs](https://cap.cloud.sap/docs/node.js/services#srv-run)
|
|
35
|
+
*/
|
|
36
|
+
upsert: {
|
|
37
|
+
<T extends ArrayConstructable<any>>(data: T): UPSERT<T>
|
|
38
|
+
<T>(data: object | object[]): UPSERT<T>
|
|
39
|
+
}
|
|
40
|
+
|
|
33
41
|
/**
|
|
34
42
|
* @see [docs](https://cap.cloud.sap/docs/node.js/services#srv-run)
|
|
35
43
|
*/
|
|
@@ -204,7 +212,7 @@ export class Service extends QueryAPI {
|
|
|
204
212
|
* Constructs and sends a DELETE request.
|
|
205
213
|
* @see [capire docs](https://cap.cloud.sap/docs/node.js/services#srv-send)
|
|
206
214
|
*/
|
|
207
|
-
delete(entityOrPath: Target, data?: object):
|
|
215
|
+
delete(entityOrPath: Target, data?: object): DELETE<T>
|
|
208
216
|
/**
|
|
209
217
|
* @see [docs](https://cap.cloud.sap/docs/node.js/services#srv-run)
|
|
210
218
|
*/
|
|
@@ -37,7 +37,7 @@ class ResourcesTarBuilder {
|
|
|
37
37
|
if (root) {
|
|
38
38
|
resources = this._getHanaResources(root)
|
|
39
39
|
} else {
|
|
40
|
-
root =
|
|
40
|
+
root = this.handler.buildOptions.root
|
|
41
41
|
resources = await this._getSqliteResources(model)
|
|
42
42
|
}
|
|
43
43
|
return { root, resources }
|
|
@@ -97,9 +97,15 @@ Add it either as a devDependency using 'npm install -D ${this.deployerName}' or
|
|
|
97
97
|
|
|
98
98
|
|
|
99
99
|
async _npmSearchPaths(cwd) {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
100
|
+
// REVISIT: we shouldn't have to rely on `npm` on the server
|
|
101
|
+
try {
|
|
102
|
+
const npmRootCall = await execAsync('npm root -g');
|
|
103
|
+
const globalNodeModules = npmRootCall.stdout.toString().trim();
|
|
104
|
+
return [cwd, globalNodeModules, '@sap/hdi-deploy']
|
|
105
|
+
} catch (error) {
|
|
106
|
+
if (/Command failed: npm.*not found/s.test(error.message)) return [cwd, '@sap/hdi-deploy']
|
|
107
|
+
else throw error
|
|
108
|
+
}
|
|
103
109
|
}
|
|
104
110
|
|
|
105
111
|
|
|
@@ -107,10 +113,10 @@ Add it either as a devDependency using 'npm install -D ${this.deployerName}' or
|
|
|
107
113
|
const hdiDeployLib = await this._getHdiDeployLib(dbDir);
|
|
108
114
|
return new Promise((resolve, reject) => {
|
|
109
115
|
const callbacks = {
|
|
110
|
-
stderrCB: error =>
|
|
116
|
+
stderrCB: error => LOG.error(error.toString())
|
|
111
117
|
}
|
|
112
118
|
if (LOG.level !== SILENT) {
|
|
113
|
-
callbacks.stdoutCB = (data) =>
|
|
119
|
+
callbacks.stdoutCB = (data) => LOG.log(data.toString());
|
|
114
120
|
}
|
|
115
121
|
|
|
116
122
|
hdiDeployLib.deploy(dbDir, env, (error, response) => {
|
package/lib/auth/index.js
CHANGED
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
|
|
2
|
+
const cds = require ('../index'), { path, local } = cds.utils
|
|
3
|
+
|
|
4
|
+
const _require = require; require = cds.lazified (module) // eslint-disable-line no-global-assign
|
|
5
|
+
module.exports = Object.assign (auth_factory, {
|
|
6
|
+
mocked: require('./basic-auth'),
|
|
7
|
+
basic: require('./basic-auth'),
|
|
8
|
+
dummy: require('./dummy-auth'),
|
|
9
|
+
ias: require('./ias-auth'),
|
|
10
|
+
jwt: require('./jwt-auth'),
|
|
11
|
+
xsuaa: require('./jwt-auth'),
|
|
12
|
+
})
|
|
13
|
+
require = _require // eslint-disable-line no-global-assign
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Constructs one of the above middlewares as configured
|
|
18
|
+
*/
|
|
2
19
|
function auth_factory (options) {
|
|
3
|
-
const cds = require ('../index'), { path, local } = cds.utils
|
|
4
20
|
const o = { ...options, ...cds.requires.auth }
|
|
5
21
|
let kind = o.kind || o.strategy
|
|
6
22
|
let middleware = cds.auth[kind]
|
|
@@ -16,17 +32,3 @@ function auth_factory (options) {
|
|
|
16
32
|
}
|
|
17
33
|
return middleware(o)
|
|
18
34
|
}
|
|
19
|
-
|
|
20
|
-
const { lazified } = require('../lazy')
|
|
21
|
-
const _require = require; require = lazified (module) // eslint-disable-line no-global-assign
|
|
22
|
-
|
|
23
|
-
module.exports = lazified (Object.assign (auth_factory, {
|
|
24
|
-
mocked: require('./basic-auth'),
|
|
25
|
-
basic: require('./basic-auth'),
|
|
26
|
-
dummy: require('./dummy-auth'),
|
|
27
|
-
ias: require('./ias-auth'),
|
|
28
|
-
jwt: require('./jwt-auth'),
|
|
29
|
-
xsuaa: require('./jwt-auth'),
|
|
30
|
-
}))
|
|
31
|
-
|
|
32
|
-
require = _require // eslint-disable-line no-global-assign
|
package/lib/core/index.js
CHANGED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const cds = require ('../../index')
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Propagates auth results to cds.context
|
|
5
|
+
*/
|
|
6
|
+
module.exports = ()=> function cds_context_auth (req, res, next) {
|
|
7
|
+
const ctx = cds.context
|
|
8
|
+
ctx.user = req.user
|
|
9
|
+
ctx.tenant = req.tenant || ctx.user?.tenant
|
|
10
|
+
next()
|
|
11
|
+
}
|
|
@@ -1,24 +1,26 @@
|
|
|
1
|
-
module.exports = ()=>{
|
|
1
|
+
module.exports = ()=> {
|
|
2
|
+
|
|
2
3
|
const cds = require ('../../index')
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
4
|
+
const context_model_required = cds.requires.extensibility || cds.requires.toggles || cds.mtx
|
|
5
|
+
if (!context_model_required) return []
|
|
6
|
+
|
|
7
|
+
const { model4 } = require('../srv-models')
|
|
8
|
+
return async function cds_context_model (req,res, next) {
|
|
9
|
+
if (req.baseUrl.startsWith('/-/')) return next() //> our own tech services cannot be extended
|
|
10
|
+
const ctx = cds.context
|
|
11
|
+
if (ctx.tenant) try {
|
|
12
|
+
// if (req.headers.features) ctx.user.features = req.headers.features //> currently done in basic-auth only
|
|
13
|
+
ctx.model = req.__model = await model4 (ctx.tenant, ctx.features) // REVISIT: req.__model is because of Okra
|
|
14
|
+
} catch (e) {
|
|
15
|
+
console.error(e)
|
|
16
|
+
return res.status(503) .json ({ // REVISIT: we should throw a simple error, nothing else! -> this is overly OData-specific!
|
|
17
|
+
error: { code: '503', message:
|
|
18
|
+
process.env.NODE_ENV === 'production' ? 'Service Unavailable' :
|
|
19
|
+
'Unable to get context-specific model due to: ' + e.message
|
|
20
|
+
}
|
|
21
|
+
})
|
|
21
22
|
}
|
|
23
|
+
next()
|
|
22
24
|
}
|
|
23
|
-
|
|
25
|
+
|
|
24
26
|
}
|
|
@@ -1,22 +1,20 @@
|
|
|
1
1
|
const auth = exports.auth = require('../../auth')
|
|
2
2
|
const context = exports.context = require('./cds-context')
|
|
3
|
+
const ctx_auth = exports.ctx_auth = require('./ctx-auth')
|
|
3
4
|
const ctx_model = exports.ctx_model = require('./ctx-model')
|
|
4
5
|
const errors = exports.errors = require('./errors')
|
|
5
6
|
const trace = exports.trace = require('./trace')
|
|
6
7
|
|
|
7
8
|
// middlewares running before protocol adapters
|
|
8
9
|
exports.before = [
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
context(), // provides cds.context
|
|
11
|
+
trace(), // provides detailed trace logs when DEBUG=trace
|
|
12
|
+
auth(), // provides req.user & tenant
|
|
13
|
+
ctx_auth(), // propagates auth results to cds.context
|
|
14
|
+
ctx_model(), // fills in cds.context.model, in case of extensibility
|
|
13
15
|
]
|
|
14
16
|
|
|
17
|
+
// middlewares running after protocol adapters -> usually error middlewares
|
|
15
18
|
exports.after = [
|
|
16
|
-
// usually error middlewares
|
|
17
19
|
errors(),
|
|
18
20
|
]
|
|
19
|
-
|
|
20
|
-
exports.bootstrap = ()=>{
|
|
21
|
-
require('../protocols')()
|
|
22
|
-
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
const libx = require('../../../libx/_runtime')
|
|
2
2
|
const cds_context_model = require('../srv-models')
|
|
3
|
+
const cds_context = require('../middlewares/cds-context')()
|
|
4
|
+
const ctx_auth = require('../middlewares/ctx-auth')()
|
|
3
5
|
const { ProtocolAdapter } = require('.')
|
|
4
6
|
|
|
5
7
|
class LegacyProtocolAdapter extends ProtocolAdapter {
|
|
@@ -14,9 +16,11 @@ class LegacyProtocolAdapter extends ProtocolAdapter {
|
|
|
14
16
|
static serve (srv, /* in: */ app) {
|
|
15
17
|
return super.serve (srv, app, { before: [
|
|
16
18
|
// async (req, res, next) => { await 1; next() }, // REVISIT: AsyncResource.bind() -> enable to break cds/tests/_runtime/odata/__tests__/integration/crud-with-mtx.test.js with existing, non-middleware mode, *w/o* fix to BufferedWriter
|
|
19
|
+
cds_context,
|
|
17
20
|
cap_req_logger,
|
|
18
21
|
libx.perf,
|
|
19
22
|
libx.auth(srv),
|
|
23
|
+
ctx_auth,
|
|
20
24
|
cds_context_model.middleware4(srv)
|
|
21
25
|
], after:[] })
|
|
22
26
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const cds = require ('../../index'), { decodeURIComponent } = cds.utils
|
|
2
2
|
const LOG = cds.log('graphql')
|
|
3
3
|
|
|
4
|
-
const GraphQLAdapter = require('@
|
|
4
|
+
const GraphQLAdapter = require('@cap-js/graphql') // eslint-disable-line cds/no-missing-dependencies
|
|
5
5
|
const express = require ('express') // eslint-disable-line cds/no-missing-dependencies
|
|
6
6
|
|
|
7
7
|
function CDSGraphQLAdapter (options) {
|
|
@@ -33,7 +33,7 @@ function CDSGraphQLAdapter (options) {
|
|
|
33
33
|
})
|
|
34
34
|
|
|
35
35
|
/** The global /graphql route */
|
|
36
|
-
.use (new GraphQLAdapter (
|
|
36
|
+
.use (new GraphQLAdapter (options))
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
module.exports = CDSGraphQLAdapter
|
|
@@ -10,7 +10,6 @@ class ProtocolAdapter {
|
|
|
10
10
|
for (let [k,o] of Object.entries(protocols)) if (typeof o === 'string') protocols[k] = {path:o}
|
|
11
11
|
if (!protocols.odata) protocols.odata = { impl: join(__dirname,'odata-v4') }
|
|
12
12
|
if (!protocols.rest) protocols.rest = { impl: join(__dirname,'rest') }
|
|
13
|
-
|
|
14
13
|
// odata must always be first for fallback
|
|
15
14
|
return this.protocols = { odata: protocols.odata, ...protocols }
|
|
16
15
|
}
|
|
@@ -84,5 +83,10 @@ const protocols = Object.keys(ProtocolAdapter.init())
|
|
|
84
83
|
const protocol4 = (def, _default = protocols[0]) => def['@protocol'] || protocols.find(p => def['@'+p]) || _default
|
|
85
84
|
const is_global = adapter => adapter.length === 1 && !/^(function )?(\w+\s+)?\((srv|service)/.test(adapter)
|
|
86
85
|
|
|
87
|
-
module.exports =
|
|
88
|
-
if (
|
|
86
|
+
module.exports = { ProtocolAdapter, protocol4 }
|
|
87
|
+
if (cds.env.protocols) {
|
|
88
|
+
cds.middlewares = require('../middlewares')
|
|
89
|
+
ProtocolAdapter.serveAll()
|
|
90
|
+
} else if (!cds.requires.middlewares) {
|
|
91
|
+
module.exports.ProtocolAdapter = require('./_legacy')
|
|
92
|
+
}
|
package/lib/srv/srv-api.js
CHANGED
|
@@ -76,6 +76,7 @@ class Service extends require('./srv-handlers') {
|
|
|
76
76
|
insert (...args) { return INSERT(...args).bind(this) }
|
|
77
77
|
create (...args) { return INSERT.into(...args).bind(this) }
|
|
78
78
|
update (...args) { return UPDATE.entity(...args).bind(this) }
|
|
79
|
+
upsert (...args) { return UPSERT(...args).bind(this) }
|
|
79
80
|
exists (...args) { return SELECT.one([1]).from(...args).bind(this) }
|
|
80
81
|
|
|
81
82
|
/**
|
package/lib/utils/data.js
CHANGED
|
@@ -13,8 +13,8 @@ class DataUtil {
|
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
15
|
if (this._deletes.length > 0) {
|
|
16
|
-
const
|
|
17
|
-
if (
|
|
16
|
+
const LOG = cds.log('deploy')
|
|
17
|
+
if (!this._autoReset) LOG.info('Deleting all data for', db.model.each('entity'))
|
|
18
18
|
await db.run(this._deletes)
|
|
19
19
|
}
|
|
20
20
|
}
|
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
|
|
@@ -60,6 +81,7 @@ exports.create = async (dir='.', ...args) => {
|
|
|
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)])
|
|
@@ -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,
|
|
@@ -1,5 +1,5 @@
|
|
|
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
|
|
@@ -11,13 +11,14 @@ const commonGenericPaging = function (req) {
|
|
|
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
|
}
|
|
@@ -679,6 +679,8 @@ const findQueryTarget = q => {
|
|
|
679
679
|
? q.INSERT._transitions[q.INSERT._transitions.length - 1].target
|
|
680
680
|
: q.UPDATE
|
|
681
681
|
? q.UPDATE._transitions[q.UPDATE._transitions.length - 1].target
|
|
682
|
+
: q.UPSERT
|
|
683
|
+
? q.UPSERT._transitions[q.UPSERT._transitions.length - 1].target
|
|
682
684
|
: q.DELETE
|
|
683
685
|
? q.DELETE._transitions[q.DELETE._transitions.length - 1].target
|
|
684
686
|
: undefined
|
|
@@ -36,6 +36,10 @@ class InsertBuilder extends BaseBuilder {
|
|
|
36
36
|
this._csn = csn
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
annotatedColumns(entityName, csn) {
|
|
40
|
+
return getAnnotatedColumns(entityName, csn)
|
|
41
|
+
}
|
|
42
|
+
|
|
39
43
|
/**
|
|
40
44
|
* Builds an Object based on the properties of the CQN object.
|
|
41
45
|
*
|
|
@@ -77,7 +81,7 @@ class InsertBuilder extends BaseBuilder {
|
|
|
77
81
|
this._findUuidKeys(entityName)
|
|
78
82
|
|
|
79
83
|
this._columnIndexesToDelete = []
|
|
80
|
-
const annotatedColumns =
|
|
84
|
+
const annotatedColumns = this.annotatedColumns(entityName, this._csn)
|
|
81
85
|
|
|
82
86
|
if (this._obj.INSERT.columns) {
|
|
83
87
|
this._removeAlreadyExistingInsertAnnotatedColumnsFromMap(annotatedColumns)
|
|
@@ -6,40 +6,17 @@ class UpsertBuilder extends InsertBuilder {
|
|
|
6
6
|
super(obj, options, csn)
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
annotatedColumns(entityName, csn) {
|
|
10
|
+
const { updateAnnotatedColumns } = getAnnotatedColumns(entityName, csn)
|
|
11
|
+
return { insertAnnotatedColumns: updateAnnotatedColumns }
|
|
12
|
+
}
|
|
13
|
+
|
|
9
14
|
// REVISIT: We need to copy over the implementation for annotation handling
|
|
10
15
|
build() {
|
|
11
|
-
this.
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
this._obj = { INSERT: this._obj.UPSERT, _target: this._obj._target }
|
|
16
|
-
|
|
17
|
-
const entityName = this._into()
|
|
18
|
-
|
|
19
|
-
this._columnIndexesToDelete = []
|
|
20
|
-
const annotatedColumns = getAnnotatedColumns(entityName, this._csn)
|
|
21
|
-
// hack: treat update annotations as insert because of sql builder impl
|
|
22
|
-
if (annotatedColumns) {
|
|
23
|
-
annotatedColumns.insertAnnotatedColumns = annotatedColumns.updateAnnotatedColumns
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
if (this._obj.INSERT.columns) {
|
|
27
|
-
this._removeAlreadyExistingInsertAnnotatedColumnsFromMap(annotatedColumns)
|
|
28
|
-
this._columns(annotatedColumns)
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
if (this._obj.INSERT.values || this._obj.INSERT.rows) {
|
|
32
|
-
if (annotatedColumns && !this._obj.INSERT.columns) {
|
|
33
|
-
// if columns not provided get indexes from csn
|
|
34
|
-
this._getAnnotatedColumnIndexes(annotatedColumns)
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
this._values(annotatedColumns)
|
|
38
|
-
} else if (this._obj.INSERT.entries && this._obj.INSERT.entries.length !== 0) {
|
|
39
|
-
this._entries(annotatedColumns)
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
this._outputObj.sql = this._outputObj.sql.join(' ') + ' WITH PRIMARY KEY'
|
|
16
|
+
this._obj = { INSERT: this._obj.UPSERT }
|
|
17
|
+
super.build()
|
|
18
|
+
this._outputObj.sql = this._outputObj.sql.replace('INSERT INTO', 'UPSERT')
|
|
19
|
+
this._outputObj.sql += ' WITH PRIMARY KEY'
|
|
43
20
|
return this._outputObj
|
|
44
21
|
}
|
|
45
22
|
}
|
|
@@ -16,7 +16,10 @@ const _getAnnotationNames = column => {
|
|
|
16
16
|
const getAnnotatedColumns = (entityName, csn) => {
|
|
17
17
|
const entityNameWithoutSuffix = ensureNoDraftsSuffix(entityName)
|
|
18
18
|
if (!csn || !csn.definitions[entityNameWithoutSuffix]) {
|
|
19
|
-
return
|
|
19
|
+
return {
|
|
20
|
+
insertAnnotatedColumns: new Map(),
|
|
21
|
+
updateAnnotatedColumns: new Map()
|
|
22
|
+
}
|
|
20
23
|
}
|
|
21
24
|
const columns = getColumns(csn.definitions[entityNameWithoutSuffix])
|
|
22
25
|
const insertAnnotatedColumns = new Map()
|
|
@@ -39,8 +42,8 @@ const getAnnotatedColumns = (entityName, csn) => {
|
|
|
39
42
|
}
|
|
40
43
|
|
|
41
44
|
return {
|
|
42
|
-
insertAnnotatedColumns
|
|
43
|
-
updateAnnotatedColumns
|
|
45
|
+
insertAnnotatedColumns,
|
|
46
|
+
updateAnnotatedColumns
|
|
44
47
|
}
|
|
45
48
|
}
|
|
46
49
|
|
|
@@ -93,7 +93,7 @@ class HanaDatabase extends DatabaseService {
|
|
|
93
93
|
_registerBeforeHandlers() {
|
|
94
94
|
this.before(['CREATE', 'UPDATE'], '*', this._input) // > has to run before rewrite
|
|
95
95
|
this.before('READ', '*', search) // > has to run before rewrite
|
|
96
|
-
this.before(['CREATE', 'READ', 'UPDATE', 'DELETE'], '*', this._rewrite)
|
|
96
|
+
this.before(['CREATE', 'READ', 'UPDATE', 'DELETE', 'UPSERT'], '*', this._rewrite)
|
|
97
97
|
|
|
98
98
|
this.before('READ', '*', localized) // > has to run after rewrite
|
|
99
99
|
this.before('READ', '*', this._virtual)
|
|
@@ -61,7 +61,7 @@ function _hdbGetResultForProcedure(rows, args, outParameters) {
|
|
|
61
61
|
// merge table output params into scalar params
|
|
62
62
|
if (args && args.length && outParameters) {
|
|
63
63
|
const params = outParameters.filter(md => !(md.PARAMETER_NAME in rows))
|
|
64
|
-
for (let i = 0; i <
|
|
64
|
+
for (let i = 0; i < params.length; i++) {
|
|
65
65
|
result[params[i].PARAMETER_NAME] = args[i]
|
|
66
66
|
}
|
|
67
67
|
}
|
|
@@ -82,14 +82,14 @@ function _hcGetResultForProcedure(stmt, resultSet, outParameters) {
|
|
|
82
82
|
// merge table output params into scalar params
|
|
83
83
|
const params = Array.isArray(outParameters) && outParameters.filter(md => !(md.PARAMETER_NAME in result))
|
|
84
84
|
if (params && params.length) {
|
|
85
|
-
let i = 0
|
|
86
|
-
|
|
87
|
-
const parameterName = params[i++].PARAMETER_NAME
|
|
85
|
+
for (let i = 0; i < params.length; i++) {
|
|
86
|
+
const parameterName = params[i].PARAMETER_NAME
|
|
88
87
|
result[parameterName] = []
|
|
89
88
|
while (resultSet.next()) {
|
|
90
89
|
result[parameterName].push(resultSet.getValues())
|
|
91
90
|
}
|
|
92
|
-
|
|
91
|
+
resultSet.nextResult()
|
|
92
|
+
}
|
|
93
93
|
}
|
|
94
94
|
return result
|
|
95
95
|
}
|
|
@@ -73,7 +73,7 @@ module.exports = class SQLiteDatabase extends DatabaseService {
|
|
|
73
73
|
|
|
74
74
|
_registerBeforeHandlers() {
|
|
75
75
|
this.before(['CREATE', 'UPDATE'], '*', this._input) // > has to run before rewrite
|
|
76
|
-
this.before(['CREATE', 'READ', 'UPDATE', 'DELETE'], '*', this._rewrite)
|
|
76
|
+
this.before(['CREATE', 'READ', 'UPDATE', 'DELETE', 'UPSERT'], '*', this._rewrite)
|
|
77
77
|
|
|
78
78
|
if (cds.env.features.lean_draft && cds.db?.kind !== 'better-sqlite')
|
|
79
79
|
this.before('READ', '*', convertDraftAdminPathExpression)
|
|
@@ -2,56 +2,38 @@ const InsertBuilder = require('../../db/sql-builder').InsertBuilder
|
|
|
2
2
|
const getAnnotatedColumns = require('../../db/sql-builder/annotations')
|
|
3
3
|
|
|
4
4
|
class CustomUpsertBuilder extends InsertBuilder {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
this._outputObj = {
|
|
8
|
-
sql: ['INSERT', 'INTO'],
|
|
9
|
-
values: []
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
this._obj = { INSERT: this._obj.UPSERT, _target: this._obj._target }
|
|
5
|
+
annotatedColumns(entityName, csn) {
|
|
6
|
+
const { updateAnnotatedColumns } = getAnnotatedColumns(entityName, csn)
|
|
13
7
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
this._columnIndexesToDelete = []
|
|
17
|
-
const annotatedColumns = getAnnotatedColumns(entityName, this._csn)
|
|
18
|
-
// hack: treat update annotations as insert because of sql builder impl
|
|
19
|
-
if (annotatedColumns) {
|
|
20
|
-
annotatedColumns.insertAnnotatedColumns = annotatedColumns.updateAnnotatedColumns
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
if (this._obj.INSERT.columns) {
|
|
24
|
-
this._removeAlreadyExistingInsertAnnotatedColumnsFromMap(annotatedColumns)
|
|
25
|
-
this._columns(annotatedColumns)
|
|
8
|
+
if (updateAnnotatedColumns?.size) {
|
|
9
|
+
this.managedCols = Array.from(updateAnnotatedColumns.keys())
|
|
26
10
|
}
|
|
27
11
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
// if columns not provided get indexes from csn
|
|
31
|
-
this._getAnnotatedColumnIndexes(annotatedColumns)
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
this._values(annotatedColumns)
|
|
35
|
-
} else if (this._obj.INSERT.entries && this._obj.INSERT.entries.length !== 0) {
|
|
36
|
-
this._entries(annotatedColumns)
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const insertSql = this._outputObj.sql.join(' ')
|
|
12
|
+
return { insertAnnotatedColumns: updateAnnotatedColumns }
|
|
13
|
+
}
|
|
40
14
|
|
|
41
|
-
|
|
15
|
+
// REVISIT: We need to copy over the implementation for annotation handling
|
|
16
|
+
build() {
|
|
17
|
+
this._obj = { INSERT: this._obj.UPSERT, _target: this._obj._target }
|
|
18
|
+
super.build()
|
|
19
|
+
const csnKeys =
|
|
20
|
+
(this._obj._target ? this._obj._target.keys : this._csn.definitions[this._obj.INSERT.into].keys) || {}
|
|
42
21
|
const keys = Object.keys(csnKeys).filter(k => !csnKeys[k].isAssociation)
|
|
43
|
-
const conflict = ` ON CONFLICT(${keys}) DO UPDATE SET `
|
|
44
22
|
const updates = []
|
|
45
23
|
const columns = this._obj.INSERT.columns || Object.keys(this._obj.INSERT.entries[0])
|
|
24
|
+
if (this.managedCols) {
|
|
25
|
+
columns.push(...this.managedCols)
|
|
26
|
+
}
|
|
27
|
+
|
|
46
28
|
columns.forEach(col => {
|
|
47
29
|
const col_ = col.replace(/\./g, '_')
|
|
48
30
|
if (!keys.includes(col_)) updates.push(`${col_}=excluded.${col_}`)
|
|
49
31
|
})
|
|
32
|
+
const conflict = updates.length
|
|
33
|
+
? ` ON CONFLICT(${keys}) DO UPDATE SET ` + updates.join(', ')
|
|
34
|
+
: ` ON CONFLICT(${keys}) DO NOTHING`
|
|
50
35
|
|
|
51
|
-
|
|
52
|
-
this._outputObj.sql = insertSql + conflict + updates.join(', ')
|
|
53
|
-
}
|
|
54
|
-
|
|
36
|
+
this._outputObj.sql = this._outputObj.sql + conflict
|
|
55
37
|
return this._outputObj
|
|
56
38
|
}
|
|
57
39
|
}
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -47,7 +47,6 @@ module.exports = async function cds_server (options) {
|
|
|
47
47
|
if (cds.requires.messaging) await cds.connect.to ('messaging')
|
|
48
48
|
|
|
49
49
|
// serve all services declared in models
|
|
50
|
-
if (cds.requires.middlewares) cds.middlewares.bootstrap(); else if (o.correlate) app.use (o.correlate)
|
|
51
50
|
await cds.serve (o.service,o) .in (app)
|
|
52
51
|
await cds.emit ('served', cds.services) //> hook for listeners
|
|
53
52
|
|
|
@@ -71,7 +70,7 @@ module.exports = async function cds_server (options) {
|
|
|
71
70
|
//
|
|
72
71
|
const defaults = {
|
|
73
72
|
|
|
74
|
-
cors,
|
|
73
|
+
cors,
|
|
75
74
|
|
|
76
75
|
get static() { return cds.env.folders.app }, //> defaults to ./app
|
|
77
76
|
|
|
@@ -100,7 +99,7 @@ const _app_serve = function (endpoint) { return {
|
|
|
100
99
|
}}
|
|
101
100
|
|
|
102
101
|
|
|
103
|
-
function cors (req, res, next) {
|
|
102
|
+
function cors (req, res, next) { // REVISIT: should that move into middlewares?
|
|
104
103
|
const { origin } = req.headers
|
|
105
104
|
if (origin) res.set('access-control-allow-origin', origin)
|
|
106
105
|
if (origin && req.method === 'OPTIONS')
|
|
@@ -108,23 +107,6 @@ function cors (req, res, next) {
|
|
|
108
107
|
next()
|
|
109
108
|
}
|
|
110
109
|
|
|
111
|
-
function correlate (req, res, next) {
|
|
112
|
-
// derive correlation id from req
|
|
113
|
-
const id = req.headers['x-correlation-id'] || req.headers['x-correlationid']
|
|
114
|
-
|| req.headers['x-request-id'] || req.headers['x-vcap-request-id']
|
|
115
|
-
|| cds.utils.uuid()
|
|
116
|
-
// new intermediate cds.context, if necessary
|
|
117
|
-
if (!cds.context) cds.context = { id }
|
|
118
|
-
// guarantee x-correlation-id going forward and set on res
|
|
119
|
-
req.headers['x-correlation-id'] = id
|
|
120
|
-
res.set('X-Correlation-ID', id)
|
|
121
|
-
// guaranteed access to cds.context._.req -> REVISIT
|
|
122
|
-
if (!cds.context._) cds.context._ = {}
|
|
123
|
-
if (!cds.context._.req) cds.context._.req = req
|
|
124
|
-
if (!cds.context._.res) cds.context._.res = res
|
|
125
|
-
next()
|
|
126
|
-
}
|
|
127
|
-
|
|
128
110
|
function express_static (dir) {
|
|
129
111
|
return express.static (path.resolve (cds.root,dir))
|
|
130
112
|
}
|