@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 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
@@ -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): Promise<this>
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 = path.join(this.handler.buildOptions.root, cds.env.folders.db)
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
- const npmRootCall = await execAsync('npm root -g');
101
- const globalNodeModules = npmRootCall.stdout.toString().trim();
102
- return [cwd, globalNodeModules, '@sap/hdi-deploy']
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 => console.error(error.toString())
116
+ stderrCB: error => LOG.error(error.toString())
111
117
  }
112
118
  if (LOG.level !== SILENT) {
113
- callbacks.stdoutCB = (data) => console.log(data.toString());
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
@@ -26,6 +26,7 @@ const roots = _roots ({
26
26
  Association: {type:'type'},
27
27
  Composition: {type:'Association'},
28
28
  service: {type:'context'},
29
+ $self: {}, //> to support polymorphic self links like in: action foo( self: [many] $self, ...)
29
30
  })
30
31
 
31
32
  /**
@@ -7,8 +7,6 @@ module.exports = ()=> {
7
7
  const ctx = {}
8
8
  ctx.http = { req, res }
9
9
  ctx.id = _id4(req)
10
- ctx.user = req.user
11
- ctx.tenant = req.tenant || ctx.user?.tenant
12
10
  cds._context.run (ctx, next)
13
11
  }
14
12
 
@@ -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
- if (cds.requires.extensibility || cds.requires.toggles || cds.mtx) {
4
- const { model4 } = require('../srv-models')
5
- return async function cds_context_model (req,res, next) {
6
- if (req.baseUrl.startsWith('/-/')) return next() //> our own tech services cannot be extended
7
- const ctx = cds.context
8
- if (ctx.tenant) try {
9
- // if (req.headers.features) ctx.user.features = req.headers.features //> currently done in basic-auth only
10
- ctx.model = req.__model = await model4 (ctx.tenant, ctx.features) // REVISIT: req.__model is because of Okra
11
- } catch (e) {
12
- console.error(e)
13
- return res.status(503) .json ({ // REVISIT: we should throw a simple error, nothing else! -> this is overly OData-specific!
14
- error: { code: '503', message:
15
- process.env.NODE_ENV === 'production' ? 'Service Unavailable' :
16
- 'Unable to get context-specific model due to: ' + e.message
17
- }
18
- })
19
- }
20
- next()
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
- else return []
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
- trace(), // provides detailed trace logs when DEBUG=trace
10
- auth(), // provides req.user & tenant
11
- context(), // provides cds.context
12
- ctx_model(), // fills in cds.context.model
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('@sap/cds-graphql/lib') // eslint-disable-line cds/no-missing-dependencies
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 (services, options))
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 = Object.assign (ProtocolAdapter.serveAll, { ProtocolAdapter, protocol4 })
88
- if (!cds.requires.middlewares) module.exports.ProtocolAdapter = require('./_legacy')
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
+ }
@@ -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 log = cds.log('deploy')
17
- if (log._info) log.info('Deleting all data in', this._deletes)
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
- // Copy files to temp dir on Windows and pack temp dir.
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, files) => {
21
- const temp = await fs.promises.mkdtemp(`${fs.realpathSync(require('os').tmpdir())}${path.sep}tar-`)
22
- for (const file of files) {
23
- const fname = path.relative(root, file)
24
- const destination = path.join(temp, fname)
25
- const dirname = path.dirname(destination)
26
- if (!await exists(dirname)) await fs.promises.mkdir(dirname, { recursive: true })
27
- await fs.promises.copyFile(file, destination)
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 { getDefaultPageSize, getMaxPageSize } = require('../utils/page')
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 (query, target) {
15
- let { rows, offset } = query.SELECT.limit || {}
16
- rows = rows && 'val' in rows ? rows.val : getDefaultPageSize(target)
17
- offset = offset && 'val' in offset ? offset.val : 0
18
- query.limit(...[Math.min(rows, getMaxPageSize(target)), offset])
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 (query.SELECT.from.SELECT?.limit) _addPaging(query.SELECT.from, target)
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 = getAnnotatedColumns(entityName, this._csn)
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._outputObj = {
12
- sql: ['UPSERT'],
13
- values: []
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 undefined
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: insertAnnotatedColumns,
43
- updateAnnotatedColumns: 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 < args.length; 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
- do {
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
- } while (resultSet.nextResult())
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
- // REVISIT: We need to copy over the implementation for annotation handling
6
- build() {
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
- const entityName = this._into()
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
- if (this._obj.INSERT.values || this._obj.INSERT.rows) {
29
- if (annotatedColumns && !this._obj.INSERT.columns) {
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
- const csnKeys = this._obj._target ? this._obj._target.keys : this._csn.definitions[this._obj.INSERT.into].keys
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
- if (updates.length) {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sap/cds",
3
- "version": "6.4.0",
3
+ "version": "6.4.1",
4
4
  "description": "SAP Cloud Application Programming Model - CDS for Node.js",
5
5
  "homepage": "https://cap.cloud.sap/",
6
6
  "keywords": [
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, correlate,
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
  }