@sap/cds 6.1.1 → 6.1.2

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,11 +4,32 @@
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.1.2 - 2022-09-05
8
+
9
+ ### Fixed
10
+
11
+ - Missing key insertion from where clauses in references for deep update statements
12
+ - Prevent duplicate entries for some `INSERT` statements
13
+ - Log details were not properly displayed in Kibana
14
+ - getCsn in model-provider if `cds.requires.toggles` is false
15
+ - HTTP calls in messaging have the correct content length
16
+ - Performance issue for OData <entity>/$count requests
17
+ - Typescript definition for SQL-native variant of `srv.run`, like `srv.run('SELECT * from Authors where name like ?',['%Poe%'])`
18
+ - Typescript definitions for `srv.run( [query] )` and `srv.send( {query, headers} )`
19
+ - Typescript definitions for `cds.log` are improved, level indicators like `cds.log()._debug` added
20
+ - Typescript definitions for `tx` now carry additional service methods
21
+ - `cds login` now returns errors with a better root cause messages
22
+ - `$expand` requests for to-one associations that do not select the foreign key
23
+ - `UPDATE` statement accepts empty objects: `UPDATE('Foo').with({ bar: {} })`
24
+ - URI encoding of parameters in remote service calls
25
+
26
+ ### Removed
27
+
7
28
  ## Version 6.1.1 - 2022-08-24
8
29
 
9
30
  ### Added
10
- - The configuration schema now includes `cds.extends` and `new-fields` (in `cds.xt.ExtensibilityService`)
11
31
 
32
+ - The configuration schema now includes `cds.extends` and `new-fields` (in `cds.xt.ExtensibilityService`)
12
33
  - `srv.run(fn)` now accepts a function as first argument, which will be run in an active outer transaction, if any, or in a newly created one. This is in contrast to `srv.tx(fn)` which always creates a new tx.
13
34
  ```js
14
35
  cds.run (tx => { // nested operations are guaranteed to run in a tx
package/apis/log.d.ts CHANGED
@@ -1,44 +1,122 @@
1
- /**
2
- * Returns a trace logger for the given module if trace is switched on for it,
3
- * otherwise returns null. All cds runtime packages use this method for their
4
- * trace and debug output.
5
- *
6
- * @see [capire](https://cap.cloud.sap/docs/node.js/cds-log)
7
- *
8
- * By default this logger would prefix all output with '[sql] - '.
9
- * You can change this by specifying another prefix in the options:
10
- *
11
- * const LOG = cds.log('sql|db',{ prefix:'cds.ql' })
12
- *
13
- * Call cds.log() for a given module again to dynamically change the log level
14
- * of all formerly created loggers, for example:
15
- *
16
- * const LOG = cds.log('sql')
17
- * LOG.info ('this will show, as default level is info')
18
- * cds.log('sql','warn')
19
- * LOG.info ('this will be suppressed now')
20
- *
21
- */
22
1
  export = cds
23
2
  declare class cds {
24
- log(name: string, options?: string | number | { level: number, prefix: string }): Logger
3
+ /**
4
+ * Create a new logger, or install a custom log formatter
5
+ */
6
+ log: LogFactory
7
+
8
+ /**
9
+ * Shortcut to `cds.log(...).debug`, returning `undefined` if `cds.log(...)._debug` is `false`.
10
+ * Use like this:
11
+ * ```
12
+ * const dbg = cds.debug('foo')
13
+ * ...
14
+ * dbg && dbg('message')
15
+ * ```
16
+ *
17
+ * @param name logger name
18
+ */
19
+ debug(name: string): undefined | Log
25
20
  }
26
21
 
27
- declare class Logger {
22
+ declare type LogFactory = {
23
+ /**
24
+ * Returns a trace logger for the given module if trace is switched on for it,
25
+ * otherwise returns null. All cds runtime packages use this method for their
26
+ * trace and debug output.
27
+ *
28
+ * By default this logger would prefix all output with `[sql] - `
29
+ * You can change this by specifying another prefix in the options:
30
+ * ```
31
+ * const LOG = cds.log('sql|db',{ prefix:'cds.ql' })
32
+ * ```
33
+ * Call `cds.log()` for a given module again to dynamically change the log level
34
+ * of all formerly created loggers, for example:
35
+ * ```
36
+ * const LOG = cds.log('sql')
37
+ * LOG.info ('this will show, as default level is info')
38
+ * cds.log('sql','warn')
39
+ * LOG.info ('this will be suppressed now')
40
+ * ```
41
+ * @param name logger name
42
+ * @param options level and prefix
43
+ * @returns the logger
44
+ *
45
+ * @see [capire](https://cap.cloud.sap/docs/node.js/cds-log)
46
+ */
47
+ (name: string, options?: string | number | { level?: number, prefix?: string }): Logger
48
+
28
49
  /**
29
- * Formats a log outputs by returning an array of arguments which are passed to
30
- * console.log() et al.
31
- * You can assign custom formatters like that:
50
+ * Set a custom formatter function like that:
51
+ * ```
52
+ * cds.log.format = (module, level, ...args) => [ '[', module, ']', ...args ]
53
+ * ```
54
+ *
55
+ * The formatter shall return an array of arguments, which are passed to the logger (for example, `console.log()`)
32
56
  *
33
- * cds.log.format = (module, level, ...args) => [ '[', module, ']', ...args ]
57
+ * @param module logger name
58
+ * @param level log level
59
+ * @param args additional arguments
34
60
  */
35
61
  format(module: string, level: number, args: any[]): any[]
36
- trace(message?: any, ...optionalParams: any[]): void
37
- debug(message?: any, ...optionalParams: any[]): void
38
- log(message?: any, ...optionalParams: any[]): void
39
- info(message?: any, ...optionalParams: any[]): void
40
- warn(message?: any, ...optionalParams: any[]): void
41
- error(message?: any, ...optionalParams: any[]): void
62
+ }
63
+
64
+ declare class Logger {
65
+ /**
66
+ * Logs with 'trace' level
67
+ */
68
+ trace: Log
69
+ /**
70
+ * Logs with 'debug' level
71
+ */
72
+ debug: Log
73
+ /**
74
+ * Logs with 'info' level
75
+ */
76
+ info: Log
77
+ /**
78
+ * Logs with 'warn' level
79
+ */
80
+ warn: Log
81
+ /**
82
+ * Logs with 'error' level
83
+ */
84
+ error: Log
85
+ /**
86
+ * Logs with default level
87
+ */
88
+ log: Log
89
+
90
+ /**
91
+ * @returns whether 'trace' level is active
92
+ */
93
+ _trace: boolean
94
+ /**
95
+ * @returns whether 'debug' level is active
96
+ */
97
+ _debug: boolean
98
+ /**
99
+ * @returns whether 'info' level is active
100
+ */
101
+ _info: boolean
102
+ /**
103
+ * @returns whether 'warn' level is active
104
+ */
105
+ _warn: boolean
106
+ /**
107
+ * @returns whether 'error' level is active
108
+ */
109
+ _error: boolean
110
+ }
111
+
112
+ declare type Log = {
113
+ /**
114
+ * Logs a message
115
+ *
116
+ * @param message text to log
117
+ * @param optionalParams additional parameters, same as in `console.log(text, param1, ...)`
118
+ */
119
+ (message?: any, ...optionalParams: any[]): void
42
120
  }
43
121
 
44
122
  declare enum levels {
@@ -34,7 +34,7 @@ export class QueryAPI {
34
34
  /**
35
35
  * @see [docs](https://cap.cloud.sap/docs/node.js/services#srv-run)
36
36
  */
37
- run (query : ConstructedQuery) : Promise<ResultSet | any>
37
+ run (query : ConstructedQuery|ConstructedQuery[]) : Promise<ResultSet | any>
38
38
 
39
39
  /**
40
40
  * @see [docs](https://cap.cloud.sap/docs/node.js/services#srv-run)
@@ -42,6 +42,11 @@ export class QueryAPI {
42
42
  run (query : Query) : Promise<ResultSet | any>
43
43
 
44
44
  /**
45
+ * @see [docs](https://cap.cloud.sap/docs/node.js/services#srv-run-sql)
46
+ */
47
+ run (query : string, args? : any[]|object) : Promise<ResultSet | any>
48
+
49
+ /**
45
50
  * @see [docs](https://cap.cloud.sap/docs/node.js/services#srv-run)
46
51
  */
47
52
  foreach (query : Query, callback: (row:object) => void) : this
@@ -141,6 +146,12 @@ export class Service extends QueryAPI {
141
146
  send (details: { event: Events, data?: object, headers?: object }) : Promise<this>
142
147
 
143
148
  /**
149
+ * Constructs and sends a synchronous request.
150
+ * @see [capire docs](https://cap.cloud.sap/docs/node.js/services#srv-send)
151
+ */
152
+ send (details: { query: ConstructedQuery, headers?: object }) : Promise<this>
153
+
154
+ /**
144
155
  * Constructs and sends a GET request.
145
156
  * @see [capire docs](https://cap.cloud.sap/docs/node.js/services#srv-send)
146
157
  */
@@ -190,7 +201,7 @@ export class Service extends QueryAPI {
190
201
 
191
202
  }
192
203
 
193
- export interface Transaction extends QueryAPI {
204
+ export interface Transaction extends Service {
194
205
  commit() : Promise<void>
195
206
  rollback() : Promise<void>
196
207
  }
package/lib/index.js CHANGED
@@ -73,7 +73,7 @@ const cds = module.exports = extend (new facade) .with ({
73
73
  Request: require ('./req/request'),
74
74
  Event: require ('./req/event'),
75
75
  User: require ('./req/user'),
76
- ql: lazy => require ('./ql/cds-ql'),
76
+ ql: require ('./ql/cds-ql'),
77
77
  tx: (..._) => (cds.db || cds.Service.prototype) .tx (..._),
78
78
  /** @type Service */ db: undefined,
79
79
 
@@ -56,6 +56,24 @@ module.exports = (module, level, ...args) => {
56
56
  if (cf.length) toLog['#cf'] = { string: cf }
57
57
  }
58
58
 
59
+ const getCircularReplacer = () => {
60
+ const seen = new WeakSet()
61
+ return (key, value) => {
62
+ if (typeof value === "object" && value !== null) {
63
+ if (seen.has(value)) {
64
+ return 'cyclic'
65
+ }
66
+ seen.add(value)
67
+ }
68
+ return value
69
+ }
70
+ }
71
+
59
72
  // return array with the stringified toLog (to avoid multiple log lines) as the sole element
60
- return [util.inspect(toLog, { compact: true, breakLength: Infinity, depth: null })]
73
+ try {
74
+ return [JSON.stringify(toLog)]
75
+ } catch (e) {
76
+ // try again with removed circular references
77
+ return [JSON.stringify(toLog, getCircularReplacer())]
78
+ }
61
79
  }
package/lib/ql/UPDATE.js CHANGED
@@ -54,8 +54,8 @@ module.exports = class UPDATE extends Whereable {
54
54
  const o = args[0]
55
55
  for (let col in o) {
56
56
  let op = '=', v = o[col]
57
- if (typeof v === 'object' && !(v === null || v.map || v.pipe || v instanceof Buffer || v instanceof Date)) {
58
- let o = Object.keys(v)[0] || this._expected `${{v}} to be an object with an operator as single key`
57
+ if (typeof v === 'object' && !(v === null || v.map || v.pipe || v instanceof Buffer || v instanceof Date || v instanceof RegExp)) {
58
+ let o = Object.keys(v)[0] //|| this._expected `${{v}} to be an object with an operator as single key`
59
59
  if (o in operators) v = v[op=o]
60
60
  }
61
61
  _add (this, col, op, v && (v.val !== undefined || v.ref || v.xpr || v.func || v.SELECT) ? v : {val:v})
package/lib/ql/cds-ql.js CHANGED
@@ -1,4 +1,5 @@
1
1
  const cds = require('../index')
2
+ const Query = require('./Query')
2
3
  require = path => { // eslint-disable-line no-global-assign
3
4
  const clazz = module.require (path); if (!clazz._api) return clazz
4
5
  Object.defineProperty (clazz.prototype, 'cmd', { value: path.match(/\w+$/)[0] })
@@ -6,12 +7,14 @@ require = path => { // eslint-disable-line no-global-assign
6
7
  }
7
8
 
8
9
  module.exports = Object.assign (_deprecated_srv_ql, { cdr: true,
10
+ Query,
9
11
  SELECT: require('./SELECT'),
10
12
  INSERT: require('./INSERT'),
11
13
  UPDATE: require('./UPDATE'),
12
14
  DELETE: require('./DELETE'),
13
15
  CREATE: require('./CREATE'),
14
16
  DROP: require('./DROP'),
17
+ clone(q) { return Query.prototype.clone.call(q) }
15
18
  })
16
19
 
17
20
  function _deprecated_srv_ql() { // eslint-disable-next-line no-console
@@ -24,7 +27,7 @@ function _deprecated_srv_ql() { // eslint-disable-next-line no-console
24
27
 
25
28
  module.exports._reset = ()=>{ // for strange tests only
26
29
  const _name = cds.env.sql.names === 'quoted' ? n =>`"${n}"` : n => n.replace(/[.:]/g,'_')
27
- Object.defineProperty (require('./Query').prototype,'valueOf',{ configurable:1, value: function(cmd=this.cmd) {
30
+ Object.defineProperty (Query.prototype,'valueOf',{ configurable:1, value: function(cmd=this.cmd) {
28
31
  return `${cmd} ${_name(this._target.name)} `
29
32
  }})
30
33
  return this
@@ -81,7 +81,8 @@ const _getCount = async (tx, readReq) => {
81
81
  // REVISIT: this process appears to be rather clumsy
82
82
  // Copy CQN including from, where and search + changing columns
83
83
  const select = SELECT.from(readReq.query.SELECT.from)
84
- select.SELECT.columns = [{ func: 'count', args: [{ val: '1' }], as: '$count' }]
84
+ // { val: 1 } is used on purpose, as "numbers" are not used as param in prepared stmt
85
+ select.SELECT.columns = [{ func: 'count', args: [{ val: 1 }], as: '$count' }]
85
86
 
86
87
  if (readReq.query.SELECT.where) select.SELECT.where = readReq.query.SELECT.where
87
88
  if (readReq.query.SELECT.search) select.SELECT.search = readReq.query.SELECT.search
@@ -27,13 +27,18 @@ function _getOnCondElements(onCond, onCondElements = []) {
27
27
  return onCondElements
28
28
  }
29
29
 
30
- function _modifyWhereWithNavigations(where, newWhere, entityKey, targetKey) {
31
- if (where) {
30
+ function _mergeWhere(base, additional) {
31
+ if (additional?.length) {
32
32
  // copy where else query will be modified
33
- const whereCopy = deepCopyArray(where)
34
- if (newWhere.length > 0) newWhere.push('and')
35
- newWhere.push(...whereCopy)
33
+ const whereCopy = deepCopyArray(additional)
34
+ if (base.length > 0) base.push('and')
35
+ base.push(...whereCopy)
36
36
  }
37
+ return base
38
+ }
39
+
40
+ function _modifyWhereWithNavigations(where, newWhere, entityKey, targetKey) {
41
+ _mergeWhere(newWhere, where)
37
42
 
38
43
  newWhere.forEach(element => {
39
44
  if (element.ref && element.ref[0] === targetKey) {
@@ -85,7 +90,10 @@ function _getWhereFromUpdate(query, target, model) {
85
90
  return where
86
91
  }
87
92
 
88
- return query.UPDATE.where
93
+ const where = query.UPDATE.where || []
94
+ if (query.UPDATE.entity.ref?.length === 1 && query.UPDATE.entity.ref[0].where)
95
+ return _mergeWhere(query.UPDATE.entity.ref[0].where, where)
96
+ return where
89
97
  }
90
98
 
91
99
  // params: data, req, service/tx
@@ -696,7 +696,7 @@ class JoinCQNFromExpanded {
696
696
 
697
697
  const assoc = entity.associations[column.ref[0]]
698
698
  if (assoc.is2one && assoc.on) {
699
- const onCond = expandedEntity._relations[assoc.name].join('target', 'source')
699
+ const onCond = entity._relations[assoc.name].join('target', 'source')
700
700
  const xpr = onCond[0].xpr
701
701
  const fks = (xpr && xpr.filter(e => e.ref && e.ref[0] === 'target').map(e => e.ref[1])) || []
702
702
  for (const k of fks) {
@@ -306,7 +306,7 @@ class InsertBuilder extends BaseBuilder {
306
306
  const purelyManagedColumnValues = this._getAnnotatedInsertColumnValues(annotatedColumns, purelyManagedColumns)
307
307
 
308
308
  this._addUuidToColumns(columns, flattenColumnMap)
309
- columns.push(...flattenColumnMap.keys())
309
+ columns.push(...Array.from(flattenColumnMap.keys()).filter(k => !columns.includes(k)))
310
310
 
311
311
  this._addEntries(valuesArray, { columns, flattenColumnMap, purelyManagedColumnValues, insertAnnotatedColumns })
312
312
 
@@ -43,7 +43,8 @@ module.exports = {
43
43
  )
44
44
  response.send(data)
45
45
  } catch (error) {
46
- error.message = `Authentication failed with root cause '${error.message}'. Passcode URL: https://${parsedUrl.hostname}/passcode`
46
+ const rootCause = error.response?.data ? JSON.stringify(error.response?.data) : error.message
47
+ error.message = `Authentication failed with root cause '${rootCause}'. Passcode URL: https://${parsedUrl.hostname}/passcode`
47
48
  const {
48
49
  constructor: { name },
49
50
  message
@@ -160,7 +160,7 @@ const isActiveEntityRequested = where => {
160
160
 
161
161
  while (where[i]) {
162
162
  if (where[i].xpr) {
163
- const isRequested = isActiveEntityRequested(where.xpr)
163
+ const isRequested = isActiveEntityRequested(where[i].xpr)
164
164
  if (isRequested) return true
165
165
  }
166
166
  if (
@@ -19,7 +19,7 @@ const authorizedRequest = ({ method, uri, path, oa2, tenant, dataObj, headers, t
19
19
  if (dataObj) {
20
20
  data = JSON.stringify(dataObj)
21
21
  httpOptions.headers['Content-Type'] = 'application/json'
22
- httpOptions.headers['Content-Length'] = data.length
22
+ httpOptions.headers['Content-Length'] = Buffer.byteLength(data)
23
23
  }
24
24
 
25
25
  if (headers) {
@@ -34,7 +34,6 @@ class EMManagement {
34
34
  this.namespace = namespace
35
35
  this.LOG = LOG
36
36
  }
37
-
38
37
  async getQueue(queueName = this.queueName) {
39
38
  this.LOG._info &&
40
39
  this.LOG.info(
@@ -303,7 +302,7 @@ class EMManagement {
303
302
  grantType: 'client_credentials',
304
303
  clientId: this.optionsMessagingREST.oa2.client,
305
304
  clientSecret: this.optionsMessagingREST.oa2.secret,
306
- tokenUrl: this.optionsMessagingREST.oa2.endpoint
305
+ tokenUrl: this.optionsMessagingREST.oa2.endpoint // this is the changed tokenUrl
307
306
  }
308
307
  }
309
308
 
@@ -200,8 +200,12 @@ const _keysOfWhere = (where, kind, target) => {
200
200
  if (where.length === 3) {
201
201
  const [left, op, right] = where
202
202
  if (op === '=' && (('val' in left && right.ref) || (left.ref && 'val' in right))) {
203
- if ('val' in left) return `(${formatVal(left.val, right.ref.join('/'), target, kind)})`
204
- return `(${formatVal(right.val, left.ref.join('/'), target, kind)})`
203
+ const formattedValue =
204
+ 'val' in left
205
+ ? formatVal(left.val, right.ref.join('/'), target, kind)
206
+ : formatVal(right.val, left.ref.join('/'), target, kind)
207
+
208
+ return `(${encodeURIComponent(formattedValue)})`
205
209
  }
206
210
  }
207
211
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sap/cds",
3
- "version": "6.1.1",
3
+ "version": "6.1.2",
4
4
  "description": "SAP Cloud Application Programming Model - CDS for Node.js",
5
5
  "homepage": "https://cap.cloud.sap/",
6
6
  "keywords": [
@@ -109,7 +109,7 @@ module.exports = class ModelProviderService extends cds.ApplicationService {
109
109
  const extensions = !base && await _getExtensions4 (req.data.tenant)
110
110
  if (!extensions && checkExt) req.reject(404, 'Missing extensions')
111
111
 
112
- const features = !toggles ? [] : toggles === '*' || toggles.includes('*') ? [fts] : toggles.map (f => fts.replace('*',f))
112
+ const features = (!toggles || !main.requires.toggles) ? [] : toggles === '*' || toggles.includes('*') ? [fts] : toggles.map (f => fts.replace('*',f))
113
113
  const models = cds.resolve (['*',...features], main); if (!models) return
114
114
 
115
115
  DEBUG && DEBUG ('loading models for', { tenant, toggles } ,'from', models.map (cds.utils.local))