@sap/cds 5.9.0 → 5.9.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,18 @@
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 5.9.1 - 2022-03-31
8
+
9
+ ### Fixed
10
+
11
+ - Function arguments might be escaped too often
12
+ - URL encoding for remote services for CQN queries
13
+ - `cds serve` during development again redirects URLs with for UI apps in a folder with the same name as a service, so `/foo/webapp` would redirect to `/foo`. This got broken in 5.8.3.
14
+ - Endless loop in localization handling
15
+ - Ensure service impl while extending entity from the service
16
+ - Post-processing of custom draft queries
17
+ - No minifying of CSN artifacts for Java build
18
+
7
19
  ## Version 5.9.0 - 2022-03-25
8
20
 
9
21
  ### Added
@@ -1,4 +1,6 @@
1
1
  const cds = require('../../lib')
2
+ const DEBUG = cds.debug('fiori/routes')
3
+ const {dirname, relative, join} = require('path')
2
4
 
3
5
  // Only for local cds runs w/o approuter:
4
6
  // If there is a relative URL in UI5's manifest.json for the datasource,
@@ -11,16 +13,20 @@ cds.on ('bootstrap', app => {
11
13
  const v2Prefix = (env.odata.v2proxy && env.odata.v2proxy.urlpath) || '/v2'
12
14
  const serviceForUri = {}
13
15
 
14
- dataSourceURIs (env.folders.app).forEach(uri => {
15
- app.use('*/'+uri, ({originalUrl}, res, next)=> { // */browse/webapp[/prefix]/browse/
16
+ dataSourceURIs (env.folders.app).forEach(({appPath, dataSourceUri}) => {
17
+ const uiRoutes = [
18
+ join('/', appPath, dataSourceUri, '*'), // /uiApp/webapp/browse/*
19
+ join('/', appPath, '*', dataSourceUri, '*') // /uiApp/webapp/*/browse/*
20
+ ].map(r => r.replace(/\\/g, '/')) // handle Windows \
21
+ DEBUG && DEBUG ('Register routes', uiRoutes)
22
+
23
+ app.use(uiRoutes, ({originalUrl}, res, next)=> {
16
24
  // any of our special URLs ($fiori-, $api-docs) ? -> next
17
25
  if (originalUrl.startsWith('/$')) return next()
18
- // is there a service starting with the URL? -> next
19
- if (cds.service.providers.find (srv => originalUrl.startsWith(srv.path))) return next()
20
26
 
21
27
  // is there a service for '[prefix]/browse' ?
22
- const srv = serviceForUri[uri] || (serviceForUri[uri] =
23
- cds.service.providers.find (srv => ('/'+uri).lastIndexOf(srv.path) >=0))
28
+ const srv = serviceForUri[dataSourceUri] || (serviceForUri[dataSourceUri] =
29
+ cds.service.providers.find (srv => ('/'+dataSourceUri).lastIndexOf(srv.path) >=0))
24
30
  if (srv) {
25
31
  let redirectUrl
26
32
  // odata-proxy may be in the line with its /v2 prefix. Make sure we retain it.
@@ -30,7 +36,7 @@ cds.on ('bootstrap', app => {
30
36
  else // --> /browse/webapp[/prefix]/browse/ -> /browse
31
37
  redirectUrl = originalUrl.substring(originalUrl.lastIndexOf(srv.path+'/'))
32
38
  if (originalUrl !== redirectUrl) {// safeguard to prevent running in loops
33
- // console.log ('>>', req.originalUrl, '->', redirectUrl)
39
+ DEBUG && DEBUG ('Redirecting', {src: originalUrl}, '~>', {target: redirectUrl})
34
40
  return res.redirect (308, redirectUrl)
35
41
  }
36
42
  }
@@ -41,10 +47,11 @@ cds.on ('bootstrap', app => {
41
47
  function dataSourceURIs (dir) {
42
48
  const uris = new Set()
43
49
  find (dir, ['*/manifest.json', '*/*/manifest.json']).forEach(file => {
50
+ const appPath = relative(join(cds.root, dir), dirname(file))
44
51
  const {dataSources: ds} = JSON.parse(fs.readFileSync(file))['sap.app'] || {}
45
52
  Object.keys (ds||[])
46
53
  .filter (k => ds[k].uri && !ds[k].uri.startsWith('/')) // only consider relative URLs)
47
- .forEach(k => uris.add(ds[k].uri))
54
+ .forEach(k => uris.add({ appPath, dataSourceUri: ds[k].uri }))
48
55
  })
49
56
  return uris
50
57
  }
@@ -105,25 +105,7 @@ const _options = {for: Object.assign (_options4, {
105
105
  */
106
106
  module.exports = exports = {__proto__:compile, _options,
107
107
  for: {__proto__: compile.for,
108
- odata: (csn,o) => {
109
- if (features.ucsn) {
110
- const { cloneCsn } = require('@sap/cds-compiler/lib/model/csnUtils') // REVISIT: This should be done by the compiler
111
- if (compile.version() >= "2.12.1") {
112
- const generateDrafts = require('@sap/cds-compiler/lib/transform/draft/odata')
113
- const compiled = generateDrafts(cloneCsn(csn, {}), { messages: [] })
114
- compiled.meta._4odata = true
115
- return compiled
116
- } else {
117
- // not yet in compiler branch, can't add drafts
118
- const cloned = cloneCsn(csn, {})
119
- cloned.meta._4odata = true
120
- return cloned
121
- }
122
- }
123
- const compiled = compile.for.odata (csn, _options.for.odata(o))
124
- compiled.meta._4odata = true
125
- return compiled
126
- },
108
+ odata: (csn,o) => compile.for.odata (csn, _options.for.odata(o)),
127
109
  },
128
110
  to: {__proto__: compile.to,
129
111
  edmx: Object.assign ((csn,o) => compile.to.edmx (csn, _options.for.edm(o)), {
@@ -96,13 +96,13 @@ function unfold_csn (m) { // NOSONAR
96
96
  }
97
97
 
98
98
 
99
- const $localized = '$$localized', _is_localized = (d,_path) => {
99
+ const $localized = '$$localized', _is_localized = (d,_path={}) => {
100
100
  if (d.own($localized)) return true
101
101
  if (!d.elements || d.name.endsWith('.texts')) return false
102
102
  // if (d.elements.texts && d.elements.texts.target === `${d.name}.texts`) return d.set($localized,true)
103
103
  for (let each in d.elements) {
104
104
  const e = d.elements [each]
105
- if (e.localized || e._target && !(_path && e._target.name in _path) && _is_localized(e._target,{..._path, [d.name]:1 })) {
105
+ if (e.localized || e._target && !(_path && e._target.name in _path) && _is_localized(e._target,Object.assign(_path, { [d.name]:1 }))) {
106
106
  return d.set($localized,true)
107
107
  }
108
108
  }
@@ -4,7 +4,7 @@ const _cached = Symbol('for Java')
4
4
  module.exports = function cds_compile_for_java (csn,o) {
5
5
  if (!csn) return
6
6
  const cached = csn[_cached]; if (cached) return cached
7
- csn = cds.minify (csn)
7
+ // csn = cds.minify (csn)
8
8
  csn = cds.compile.for.drafts (csn,o)
9
9
  // Add a parsed _where clause for @restrict.{grant,where} annotations
10
10
  if (csn.definitions) for (let {'@restrict':rr} of Object.values(csn.definitions)) if (rr) {
@@ -4,7 +4,7 @@ const _cached = Symbol('for Node.js')
4
4
  module.exports = function cds_compile_for_nodejs (csn,o) {
5
5
  if (!csn) return
6
6
  const cached = csn[_cached]; if (cached) return cached
7
- csn = cds.minify (csn)
7
+ // csn = cds.minify (csn)
8
8
  csn = cds.compile.for.drafts (csn,o) //> creates a partial copy -> avoid any cds.linked() before
9
9
  csn = cds.compile._localized.unfold_csn (csn)
10
10
  csn = cds.linked (csn)
@@ -13,4 +13,4 @@ module.exports = function cds_compile_for_odata (csn,_o) {
13
13
  return _4odata
14
14
  }
15
15
 
16
- const _is_odata = csn => csn.meta && csn.meta._4odata
16
+ const _is_odata = csn => csn.meta && csn.meta.transformation === 'odata'
@@ -17,12 +17,13 @@ function createOdataService(service) {
17
17
  return odataService
18
18
  }
19
19
 
20
+ const { Service } = require('../../../../../../lib/serve/factory')
20
21
  async function createNewService(name, csn, defaultOptions) {
21
- const options = Object.assign({}, defaultOptions, { reflectedModel: csn })
22
-
23
- const service = new cds.ApplicationService(name, csn, options)
24
- await service.init()
25
- if (options.impl) service.impl(options.impl)
22
+ const options = Object.assign({}, defaultOptions)
23
+ const service = new Service(name, csn, options)
24
+ if (!service.path) service.path = cds.service.path4(service)
25
+ if (service.init) await service.prepend(service.init)
26
+ if (options.impl) await service.prepend(options.impl)
26
27
 
27
28
  return createOdataService(service)
28
29
  }
@@ -783,8 +783,8 @@ const _convertSelect = (query, model, _options) => {
783
783
  if (target && target._unresolved && typeof target.name === 'string') {
784
784
  target = model.definitions[ensureNoDraftsSuffix(target.name)] || target
785
785
  }
786
- if (target && !target._unresolved) {
787
- const cols = getColumns(target, { onlyNames: true })
786
+ if (target && !Object.prototype.hasOwnProperty.call(target, '_unresolved')) {
787
+ const cols = getColumns(target, { onlyNames: true, filterVirtual: true })
788
788
  query.columns(cols)
789
789
  if (target._isDraftEnabled && query._target._unresolved) {
790
790
  query.SELECT.columns.push(...getDraftColumnsCQNForDraft(target))
@@ -192,6 +192,11 @@ const _getMapperForListedElements = (conversionMap, csn, cqn) => {
192
192
  * @private
193
193
  */
194
194
  const getPostProcessMapper = (conversionMap, csn = {}, cqn = {}) => {
195
+ // No mapper defined or irrelevant as no READ request
196
+ if (!Object.prototype.hasOwnProperty.call(cqn, 'SELECT')) {
197
+ return new Map()
198
+ }
199
+
195
200
  return cqn.SELECT.columns ? _getMapperForListedElements(conversionMap, csn, cqn) : new Map()
196
201
  }
197
202
 
@@ -56,12 +56,13 @@ class FunctionBuilder extends BaseBuilder {
56
56
  }
57
57
 
58
58
  _escapeLikeParameters(parameters) {
59
- for (const parameter of parameters) {
60
- if (parameter.val) parameter.val = parameter.val.replace(/(\^|_|%)/g, '^$1')
61
- else if (parameter.func) parameter.args = this._escapeLikeParameters(parameter.args)
62
- }
63
-
64
- return parameters
59
+ return parameters.map(parameter => {
60
+ if (parameter.val) return { ...parameter, val: parameter.val.replace(/(\^|_|%)/g, '^$1') }
61
+ if (parameter.func) {
62
+ return { ...parameter, args: this._escapeLikeParameters(parameter.args) }
63
+ }
64
+ return parameter
65
+ })
65
66
  }
66
67
 
67
68
  _handleFunction() {
@@ -117,8 +118,7 @@ class FunctionBuilder extends BaseBuilder {
117
118
  const functionName = this._functionName()
118
119
  const not = functionName.startsWith('not') ? 'NOT ' : ''
119
120
  const columns = this._columns(args)
120
- const params = args.slice(1)
121
- this._escapeLikeParameters(params)
121
+ const params = this._escapeLikeParameters(args.slice(1))
122
122
 
123
123
  const _pattern = (() => {
124
124
  if (functionName.includes('contains')) return _ => ["'%'", _, "'%'"]
@@ -339,12 +339,10 @@ const _cqnToReqOptions = (query, kind, model, target) => {
339
339
  const queryObject = cds.odata.urlify(query, { kind, model })
340
340
  const reqOptions = {
341
341
  method: queryObject.method,
342
- url: encodeURI(
343
- queryObject.path
344
- // ugly workaround for Okra not allowing spaces in ( x eq 1 )
345
- .replace(/\( /g, '(')
346
- .replace(/ \)/g, ')')
347
- )
342
+ url: queryObject.path
343
+ // ugly workaround for Okra not allowing spaces in ( x eq 1 )
344
+ .replace(/\( /g, '(')
345
+ .replace(/ \)/g, ')')
348
346
  }
349
347
  if (queryObject.method !== 'GET' && queryObject.method !== 'HEAD') {
350
348
  reqOptions.data = kind === 'odata-v2' ? convertV2PayloadData(queryObject.body, target) : queryObject.body
@@ -83,14 +83,10 @@ function _args(args) {
83
83
 
84
84
  if (hasValidProps(cur, 'func', 'args')) {
85
85
  res.push(`${cur.func}(${_args(cur.args)})`)
86
- }
87
-
88
- if (hasValidProps(cur, 'ref')) {
89
- res.push(cur.ref.join('/'))
90
- }
91
-
92
- if (hasValidProps(cur, 'val')) {
93
- res.push(formatVal(cur.val))
86
+ } else if (hasValidProps(cur, 'ref')) {
87
+ res.push(_format(cur))
88
+ } else if (hasValidProps(cur, 'val')) {
89
+ res.push(_format(cur))
94
90
  }
95
91
  }
96
92
 
@@ -98,13 +94,13 @@ function _args(args) {
98
94
  }
99
95
 
100
96
  const _in = (column, /* in */ collection, target, kind, isLambda) => {
101
- const ref = isLambda ? [LAMBDA_VARIABLE, ...column.ref].join('/') : column.ref.join('/')
97
+ const ref = _format(column, null, target, kind, isLambda)
102
98
  // { val: [ 1, 2, 3 ] } or { list: [ { val: 1}, { val: 2}, { val: 3} ] }
103
99
  const values = collection.val || collection.list
104
100
  if (values && values.length) {
105
101
  // REVISIT: what about OData `in` operator?
106
- const expressions = values.map(value => `${ref} eq ${_format(value, ref, target, kind, isLambda)}`)
107
- return expressions.join(' or ')
102
+ const expressions = values.map(value => `${ref}%20eq%20${_format(value, ref, target, kind, isLambda)}`)
103
+ return expressions.join('%20or%20')
108
104
  }
109
105
  }
110
106
 
@@ -119,9 +115,10 @@ const _odataV2Func = (func, args) => {
119
115
  }
120
116
 
121
117
  const _format = (cur, element, target, kind, isLambda) => {
122
- if (typeof cur !== 'object') return formatVal(cur, element, target, kind)
123
- if (hasValidProps(cur, 'ref')) return isLambda ? [LAMBDA_VARIABLE, ...cur.ref].join('/') : cur.ref.join('/')
124
- if (hasValidProps(cur, 'val')) return formatVal(cur.val, element, target, kind)
118
+ if (typeof cur !== 'object') return encodeURIComponent(formatVal(cur, element, target, kind))
119
+ if (hasValidProps(cur, 'ref'))
120
+ return encodeURIComponent(isLambda ? [LAMBDA_VARIABLE, ...cur.ref].join('/') : cur.ref.join('/'))
121
+ if (hasValidProps(cur, 'val')) return encodeURIComponent(formatVal(cur.val, element, target, kind))
125
122
  if (hasValidProps(cur, 'xpr')) return `(${_xpr(cur.xpr, target, kind, isLambda)})`
126
123
  // REVISIT: How to detect the types for all functions?
127
124
  if (hasValidProps(cur, 'func', 'args')) {
@@ -156,7 +153,7 @@ function _xpr(expr, target, kind, isLambda) {
156
153
  } else if (isOrIsNotValue) {
157
154
  // REVISIT: "is" only used for null values?
158
155
  const operator = isOrIsNotValue[1] /* 'is not' */ ? 'ne' : 'eq'
159
- res.push(...[operator, formatVal(isOrIsNotValue[2])])
156
+ res.push(...[operator, _format({ val: isOrIsNotValue[2] })])
160
157
  } else if (cur === 'between') {
161
158
  // ref gt low.val and ref lt high.val
162
159
  const between = [expr[i - 1], 'gt', expr[i + 1], 'and', expr[i - 1], 'lt', expr[i + 3]]
@@ -188,7 +185,7 @@ function _xpr(expr, target, kind, isLambda) {
188
185
  }
189
186
  }
190
187
 
191
- return res.join(' ')
188
+ return res.join('%20')
192
189
  }
193
190
 
194
191
  const _keysOfWhere = (where, kind, target) => {
@@ -202,11 +199,11 @@ const _keysOfWhere = (where, kind, target) => {
202
199
  const res = []
203
200
  for (const cur of where) {
204
201
  if (hasValidProps(cur, 'ref')) {
205
- res.push(cur.ref.join('/'))
202
+ res.push(_format(cur))
206
203
  } else if (hasValidProps(cur, 'val')) {
207
204
  // find previous ref
208
205
  const element = res[res.length - 2]
209
- res.push(formatVal(cur.val, element, target, kind))
206
+ res.push(_format(cur, element, target, kind))
210
207
  } else if (cur === 'and') {
211
208
  res.push(',')
212
209
  } else {
@@ -267,15 +264,15 @@ const _parseColumnsV2 = (columns, prefix = []) => {
267
264
 
268
265
  if (hasValidProps(column, 'expand')) {
269
266
  const parsed = _parseColumnsV2(column.expand, [refName])
270
- expand.push(refName, ...parsed.expand)
267
+ expand.push(encodeURIComponent(refName), ...parsed.expand)
271
268
  select.push(...parsed.select)
272
269
  } else {
273
- select.push(refName)
270
+ select.push(encodeURIComponent(refName))
274
271
  }
275
272
  }
276
273
 
277
274
  if (column === '*') {
278
- select.push(`${prefix.join('/')}/*`)
275
+ select.push(encodeURIComponent(`${prefix.join('/')}/*`))
279
276
  }
280
277
  }
281
278
 
@@ -288,7 +285,7 @@ const _parseColumns = columns => {
288
285
 
289
286
  for (const column of columns) {
290
287
  if (hasValidProps(column, 'ref')) {
291
- let refName = column.ref.join('/')
288
+ let refName = _format(column)
292
289
  if (hasValidProps(column, 'expand')) {
293
290
  // REVISIT: incomplete, see test Foo?$expand=invoices($count=true;$expand=item($search="some"))
294
291
  if (!columns.some(c => !c.expand)) select.push(refName)
@@ -350,16 +347,16 @@ function $orderBy(orderBy) {
350
347
 
351
348
  for (const cur of orderBy) {
352
349
  if (hasValidProps(cur, 'ref', 'sort')) {
353
- res.push(cur.ref.join('/') + ' ' + cur.sort)
350
+ res.push(_format(cur) + '%20' + cur.sort)
354
351
  continue
355
352
  }
356
353
 
357
354
  if (hasValidProps(cur, 'ref')) {
358
- res.push(cur.ref.join('/'))
355
+ res.push(_format(cur))
359
356
  }
360
357
 
361
358
  if (hasValidProps(cur, 'func', 'sort')) {
362
- res.push(`${cur.func}(${_args(cur.args)})` + ' ' + cur.sort)
359
+ res.push(`${cur.func}(${_args(cur.args)})` + '%20' + cur.sort)
363
360
  continue
364
361
  }
365
362
 
@@ -382,7 +379,7 @@ function parseSearch(search) {
382
379
 
383
380
  if (hasValidProps(cur, 'val')) {
384
381
  // search term must not be formatted
385
- res.push(`"${cur.val}"`)
382
+ res.push(`"${encodeURIComponent(cur.val)}"`)
386
383
  }
387
384
 
388
385
  if (typeof cur === 'string') {
@@ -398,7 +395,7 @@ function parseSearch(search) {
398
395
  }
399
396
 
400
397
  function $search(search, kind) {
401
- const expr = parseSearch(search).join(' ').replace('( ', '(').replace(' )', ')')
398
+ const expr = parseSearch(search).join('%20').replace('(%20', '(').replace('%20)', ')')
402
399
 
403
400
  if (expr) {
404
401
  // odata-v2 may support custom query option "search"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sap/cds",
3
- "version": "5.9.0",
3
+ "version": "5.9.1",
4
4
  "description": "SAP Cloud Application Programming Model - CDS for Node.js",
5
5
  "homepage": "https://cap.cloud.sap/",
6
6
  "keywords": [
@@ -32,7 +32,7 @@
32
32
  "dependencies": {
33
33
  "@sap-cloud-sdk/core": "^1.41",
34
34
  "@sap-cloud-sdk/util": "^1.41",
35
- "@sap/cds-compiler": "^2.4.4",
35
+ "@sap/cds-compiler": "^2.13.0",
36
36
  "@sap/cds-foss": "^3"
37
37
  },
38
38
  "husky": {