@sap/cds 5.6.1 → 5.7.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.
Files changed (184) hide show
  1. package/CHANGELOG.md +136 -0
  2. package/_i18n/i18n_fr.properties +4 -4
  3. package/apis/cds.d.ts +7 -10
  4. package/apis/connect.d.ts +3 -3
  5. package/apis/core.d.ts +2 -4
  6. package/apis/models.d.ts +2 -3
  7. package/apis/ql.d.ts +0 -1
  8. package/apis/services.d.ts +7 -3
  9. package/bin/build/buildTaskFactory.js +16 -10
  10. package/bin/build/buildTaskProviderFactory.js +3 -3
  11. package/bin/build/constants.js +2 -1
  12. package/bin/build/provider/buildTaskProviderInternal.js +14 -14
  13. package/bin/build/provider/hana/2migration.js +2 -3
  14. package/bin/build/provider/hana/index.js +34 -0
  15. package/bin/build/provider/hana/migrationtable.js +90 -22
  16. package/bin/build/provider/hana/template/undeploy.json +5 -0
  17. package/bin/build/provider/node-cf/index.js +9 -2
  18. package/bin/serve.js +16 -18
  19. package/lib/compile/cdsc.js +15 -5
  20. package/lib/compile/etc/_localized.js +4 -4
  21. package/lib/compile/extend.js +8 -0
  22. package/lib/compile/index.js +3 -1
  23. package/lib/compile/minify.js +61 -0
  24. package/lib/compile/resolve.js +4 -1
  25. package/lib/compile/to/gql.js +9 -0
  26. package/lib/compile/to/sql.js +26 -30
  27. package/lib/connect/index.js +1 -1
  28. package/lib/core/entities.js +0 -3
  29. package/lib/core/infer.js +1 -0
  30. package/lib/core/reflect.js +1 -34
  31. package/lib/deploy.js +25 -17
  32. package/lib/env/defaults.js +3 -1
  33. package/lib/env/index.js +13 -4
  34. package/lib/env/presets.js +38 -0
  35. package/lib/env/requires.js +16 -11
  36. package/lib/index.js +13 -11
  37. package/lib/log/format/kibana.js +4 -2
  38. package/lib/log/index.js +2 -2
  39. package/lib/ql/Whereable.js +1 -0
  40. package/lib/req/cds-context.js +79 -0
  41. package/lib/req/context.js +5 -77
  42. package/lib/req/request.js +1 -1
  43. package/lib/serve/Service-api.js +8 -4
  44. package/lib/serve/Service-dispatch.js +0 -7
  45. package/lib/serve/Service-methods.js +6 -8
  46. package/lib/serve/Transaction.js +35 -30
  47. package/lib/serve/adapters.js +1 -4
  48. package/lib/utils/axios.js +1 -1
  49. package/libx/_runtime/audit/Service.js +44 -20
  50. package/libx/_runtime/audit/generic/personal/access.js +16 -11
  51. package/libx/_runtime/audit/generic/personal/modification.js +5 -5
  52. package/libx/_runtime/audit/generic/personal/utils.js +46 -37
  53. package/libx/_runtime/{common/auth → auth}/index.js +21 -7
  54. package/libx/_runtime/{common/auth → auth}/strategies/JWT.js +2 -2
  55. package/libx/_runtime/{common/auth → auth}/strategies/basic.js +2 -2
  56. package/libx/_runtime/{common/auth → auth}/strategies/dummy.js +1 -1
  57. package/libx/_runtime/{common/auth → auth}/strategies/mock.js +2 -2
  58. package/libx/_runtime/{common/auth → auth}/strategies/utils/uaa.js +1 -1
  59. package/libx/_runtime/{common/auth → auth}/strategies/utils/xssec.js +0 -0
  60. package/libx/_runtime/{common/auth → auth}/strategies/xsuaa.js +2 -2
  61. package/libx/_runtime/cds-services/adapter/odata-v4/Dispatcher.js +7 -2
  62. package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +0 -7
  63. package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +0 -8
  64. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +3 -4
  65. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +6 -7
  66. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/delete.js +3 -4
  67. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +2 -11
  68. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +16 -6
  69. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +26 -65
  70. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +0 -7
  71. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +3 -66
  72. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/ExpressionToCQN.js +26 -0
  73. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +5 -5
  74. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/utils.js +2 -2
  75. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriParser.js +13 -10
  76. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/invocation/ConditionalRequestControlCommand.js +0 -7
  77. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/invocation/SetResponseHeadersCommand.js +1 -0
  78. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/validator/ConditionalRequestValidator.js +0 -8
  79. package/libx/_runtime/cds-services/adapter/odata-v4/utils/result.js +18 -15
  80. package/libx/_runtime/cds-services/adapter/odata-v4/utils/stream.js +54 -76
  81. package/libx/_runtime/cds-services/adapter/rest/RestRequest.js +0 -7
  82. package/libx/_runtime/cds-services/adapter/rest/handlers/create.js +3 -6
  83. package/libx/_runtime/cds-services/adapter/rest/handlers/delete.js +3 -6
  84. package/libx/_runtime/cds-services/adapter/rest/handlers/operation.js +3 -6
  85. package/libx/_runtime/cds-services/adapter/rest/handlers/read.js +3 -6
  86. package/libx/_runtime/cds-services/adapter/rest/handlers/update.js +3 -6
  87. package/libx/_runtime/cds-services/adapter/rest/rest-to-cqn/index.js +2 -2
  88. package/libx/_runtime/cds-services/adapter/rest/utils/parse-url.js +8 -4
  89. package/libx/_runtime/cds-services/services/Service.js +0 -6
  90. package/libx/_runtime/cds-services/services/utils/columns.js +10 -3
  91. package/libx/_runtime/cds-services/services/utils/compareJson.js +4 -7
  92. package/libx/_runtime/cds-services/services/utils/differ.js +4 -1
  93. package/libx/_runtime/cds-services/services/utils/handlerUtils.js +1 -41
  94. package/libx/_runtime/cds-services/util/assert.js +1 -262
  95. package/libx/_runtime/cds.js +6 -9
  96. package/libx/_runtime/common/aspects/entity.js +1 -1
  97. package/libx/_runtime/common/composition/delete.js +4 -2
  98. package/libx/_runtime/common/composition/update.js +27 -35
  99. package/libx/_runtime/common/composition/utils.js +3 -7
  100. package/libx/_runtime/common/error/standardError.js +11 -0
  101. package/libx/_runtime/common/generic/auth.js +61 -30
  102. package/libx/_runtime/common/generic/crud.js +11 -23
  103. package/libx/_runtime/common/generic/input.js +20 -0
  104. package/libx/_runtime/common/generic/paging.js +2 -2
  105. package/libx/_runtime/common/generic/put.js +4 -10
  106. package/libx/_runtime/common/generic/sorting.js +12 -30
  107. package/libx/_runtime/common/perf/index.js +24 -0
  108. package/libx/_runtime/common/utils/cqn.js +58 -1
  109. package/libx/_runtime/common/utils/cqn2cqn4sql.js +289 -114
  110. package/libx/_runtime/common/utils/csn.js +38 -56
  111. package/libx/_runtime/common/utils/entityFromCqn.js +6 -6
  112. package/libx/_runtime/common/utils/resolveView.js +4 -5
  113. package/libx/_runtime/common/utils/rewriteAsterisks.js +46 -5
  114. package/libx/_runtime/common/utils/search2cqn4sql.js +21 -9
  115. package/libx/_runtime/common/utils/structured.js +35 -25
  116. package/libx/_runtime/db/Service.js +0 -6
  117. package/libx/_runtime/db/expand/expand-v2.js +130 -0
  118. package/libx/_runtime/db/expand/expandCQNToJoin.js +82 -61
  119. package/libx/_runtime/db/expand/index.js +3 -1
  120. package/libx/_runtime/db/generic/arrayed.js +14 -27
  121. package/libx/_runtime/db/generic/input.js +52 -10
  122. package/libx/_runtime/db/generic/integrity.js +367 -26
  123. package/libx/_runtime/db/generic/virtual.js +51 -13
  124. package/libx/_runtime/db/query/update.js +9 -3
  125. package/libx/_runtime/db/sql-builder/ExpressionBuilder.js +8 -9
  126. package/libx/_runtime/{common → db}/utils/propagateForeignKeys.js +11 -14
  127. package/libx/_runtime/fiori/generic/activate.js +1 -0
  128. package/libx/_runtime/fiori/generic/before.js +2 -1
  129. package/libx/_runtime/fiori/generic/edit.js +2 -1
  130. package/libx/_runtime/fiori/generic/patch.js +1 -1
  131. package/libx/_runtime/fiori/generic/read.js +151 -57
  132. package/libx/_runtime/fiori/uiflex/handler/transformRESULT.js +0 -4
  133. package/libx/_runtime/fiori/uiflex/index.js +1 -1
  134. package/libx/_runtime/fiori/uiflex/{extensibility/index.js → service.js} +6 -4
  135. package/libx/_runtime/fiori/utils/delete.js +7 -1
  136. package/libx/_runtime/hana/Service.js +1 -8
  137. package/libx/_runtime/hana/customBuilder/CustomSelectBuilder.js +5 -14
  138. package/libx/_runtime/hana/execute.js +10 -4
  139. package/libx/_runtime/hana/pool.js +55 -45
  140. package/libx/_runtime/hana/search.js +7 -6
  141. package/libx/_runtime/hana/search2cqn4sql.js +8 -5
  142. package/libx/_runtime/hana/searchToContains.js +3 -1
  143. package/libx/_runtime/index.js +5 -5
  144. package/libx/_runtime/messaging/AMQPWebhookMessaging.js +3 -3
  145. package/libx/_runtime/messaging/Outbox.js +53 -0
  146. package/libx/_runtime/messaging/common-utils/AMQPClient.js +17 -10
  147. package/libx/_runtime/messaging/common-utils/connections.js +14 -9
  148. package/libx/_runtime/messaging/common-utils/waitingTime.js +2 -0
  149. package/libx/_runtime/messaging/enterprise-messaging-shared.js +2 -3
  150. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +2 -2
  151. package/libx/_runtime/messaging/enterprise-messaging.js +21 -15
  152. package/libx/_runtime/messaging/file-based.js +5 -5
  153. package/libx/_runtime/messaging/message-queuing.js +2 -3
  154. package/libx/_runtime/messaging/outbox/OutboxRunner.js +75 -0
  155. package/libx/_runtime/messaging/outbox/utils.js +192 -0
  156. package/libx/_runtime/messaging/service.js +16 -30
  157. package/libx/_runtime/remote/Service.js +21 -2
  158. package/libx/_runtime/remote/utils/client.js +15 -3
  159. package/libx/_runtime/remote/utils/{dataConversion.js → data.js} +12 -2
  160. package/libx/_runtime/sqlite/Service.js +7 -10
  161. package/libx/_runtime/sqlite/customBuilder/CustomExpressionBuilder.js +19 -0
  162. package/libx/_runtime/sqlite/execute.js +18 -12
  163. package/libx/_runtime/types/api.js +2 -1
  164. package/libx/odata/{odata2cqn/afterburner.js → afterburner.js} +28 -16
  165. package/libx/odata/{cqn2odata/index.js → cqn2odata.js} +1 -1
  166. package/libx/odata/{odata2cqn/grammar.pegjs → grammar.pegjs} +182 -118
  167. package/libx/odata/index.js +18 -15
  168. package/libx/odata/parser.js +1 -0
  169. package/libx/odata/utils.js +57 -0
  170. package/libx/rest/RestAdapter.js +2 -6
  171. package/libx/rest/utils/data.js +1 -6
  172. package/package.json +4 -3
  173. package/server.js +13 -10
  174. package/srv/audit-log.cds +87 -0
  175. package/{libx/_runtime/fiori/uiflex/extensibility/index.cds → srv/flex.cds} +0 -0
  176. package/srv/flex.js +1 -0
  177. package/srv/outbox.cds +11 -0
  178. package/srv/outbox.js +0 -0
  179. package/libx/_runtime/cds-services/adapter/perf/performance.js +0 -104
  180. package/libx/_runtime/cds-services/adapter/perf/performanceMeasurement.js +0 -33
  181. package/libx/odata/odata2cqn/index.js +0 -3
  182. package/libx/odata/odata2cqn/parser.js +0 -1
  183. package/libx/odata/readme.md +0 -1
  184. package/libx/odata/utils/index.js +0 -64
@@ -1,7 +1,7 @@
1
- const cds = require('../../_runtime/cds')
1
+ const cds = require('../_runtime/cds')
2
2
 
3
- const { where2obj } = require('../../_runtime/common/utils/cqn')
4
- const { findCsnTargetFor } = require('../../_runtime/common/utils/csn')
3
+ const { where2obj } = require('../_runtime/common/utils/cqn')
4
+ const { findCsnTargetFor } = require('../_runtime/common/utils/csn')
5
5
 
6
6
  function _keysOf(entity) {
7
7
  return entity && entity.keys
@@ -11,11 +11,11 @@ function _keysOf(entity) {
11
11
  : []
12
12
  }
13
13
 
14
- function _getDefinition(definition, name) {
14
+ function _getDefinition(definition, name, namespace) {
15
15
  return (
16
16
  (definition.definitions && definition.definitions[name]) ||
17
17
  (definition.elements && definition.elements[name]) ||
18
- (definition.actions && definition.actions[name]) ||
18
+ (definition.actions && (definition.actions[name] || definition.actions[name.replace(namespace + '.', '')])) ||
19
19
  definition[name]
20
20
  )
21
21
  }
@@ -29,7 +29,7 @@ function _resolveAliasInWhere(where, entity) {
29
29
  }
30
30
 
31
31
  // case: single key without name, e.g., Foo(1)
32
- function _addRefToWhereIfNecessary(where, entity) {
32
+ function addRefToWhereIfNecessary(where, entity) {
33
33
  if (!where || where.length !== 1) return 0
34
34
  const keys = _keysOf(entity)
35
35
  if (keys.length !== 1) return 0
@@ -37,8 +37,9 @@ function _addRefToWhereIfNecessary(where, entity) {
37
37
  return 1
38
38
  }
39
39
 
40
- function _processSegments(cqn, model) {
41
- const { ref } = cqn.SELECT.from
40
+ function _processSegments(cqn, model, namespace) {
41
+ const from = _resolveFrom(cqn.SELECT.from)
42
+ const ref = from.ref
42
43
 
43
44
  let current = model
44
45
  let path
@@ -59,7 +60,15 @@ function _processSegments(cqn, model) {
59
60
  let base = ref[i - keyCount]
60
61
  if (!base.id) base = { id: base, where: [] }
61
62
  if (base.where.length) base.where.push('and')
62
- base.where.push({ ref: [key] }, '=', { val: element.type === 'cds.Integer' ? Number(seg) : seg })
63
+ if (ref[i].id) {
64
+ // > fix case key value parsed to collection with filter
65
+ const val = `${ref[i].id}(${Object.keys(params)
66
+ .map(k => `${k}='${params[k]}'`)
67
+ .join(',')})`
68
+ base.where.push({ ref: [key] }, '=', { val })
69
+ } else {
70
+ base.where.push({ ref: [key] }, '=', { val: element.type === 'cds.Integer' ? Number(seg) : seg })
71
+ }
63
72
  ref[i] = null
64
73
  ref[i - keyCount] = base
65
74
  incompleteKeys = keyCount < keys.length
@@ -71,7 +80,7 @@ function _processSegments(cqn, model) {
71
80
 
72
81
  path = path ? path + `${path.match(/:/) ? '.' : ':'}${seg}` : seg
73
82
  // REVISIT: replace use case: <namespace>.<entity>_history is at <namespace>.<entity>.history
74
- current = _getDefinition(current, seg) || _getDefinition(current, seg.replace(/_/g, '.'))
83
+ current = _getDefinition(current, seg, namespace) || _getDefinition(current, seg.replace(/_/g, '.'), namespace)
75
84
  // REVISIT: 404 or 400?
76
85
  if (!current) cds.error(`Invalid resource path "${path}"`, { code: 404 })
77
86
 
@@ -80,7 +89,7 @@ function _processSegments(cqn, model) {
80
89
  one = !!(ref[i].where || current._isSingleton)
81
90
  incompleteKeys = ref[i].where ? false : i === ref.length - 1 || one ? false : true
82
91
  if (ref[i].where) {
83
- keyCount += _addRefToWhereIfNecessary(ref[i].where, current)
92
+ keyCount += addRefToWhereIfNecessary(ref[i].where, current)
84
93
  _resolveAliasInWhere(ref[i].where, current)
85
94
  }
86
95
  } else if ({ action: 1, function: 1 }[current.kind]) {
@@ -98,7 +107,7 @@ function _processSegments(cqn, model) {
98
107
  incompleteKeys = one || i === ref.length - 1 ? false : true
99
108
  current = model.definitions[current.target]
100
109
  if (ref[i].where) {
101
- keyCount += _addRefToWhereIfNecessary(ref[i].where, current)
110
+ keyCount += addRefToWhereIfNecessary(ref[i].where, current)
102
111
  _resolveAliasInWhere(ref[i].where, current)
103
112
  }
104
113
  } else if (current._isStructured) {
@@ -125,7 +134,7 @@ function _processSegments(cqn, model) {
125
134
  }
126
135
 
127
136
  // remove all nulled refs
128
- cqn.SELECT.from.ref = ref.filter(r => r)
137
+ from.ref = ref.filter(r => r)
129
138
 
130
139
  // one?
131
140
  if (one) cqn.SELECT.one = true
@@ -135,11 +144,13 @@ function _processSegments(cqn, model) {
135
144
  cqn.__target = current
136
145
  }
137
146
 
147
+ const _resolveFrom = from => (from.SELECT ? _resolveFrom(from.SELECT.from) : from)
148
+
138
149
  function _4service(service) {
139
150
  const { namespace, model } = service
140
151
 
141
152
  return cqn => {
142
- const { ref } = cqn.SELECT.from
153
+ const { ref } = _resolveFrom(cqn.SELECT.from)
143
154
 
144
155
  // REVISIT: shouldn't be necessary
145
156
  /*
@@ -154,7 +165,7 @@ function _4service(service) {
154
165
  /*
155
166
  * key vs. path segments (/Books/1/author/books/2/...) and more
156
167
  */
157
- _processSegments(cqn, model)
168
+ _processSegments(cqn, model, namespace)
158
169
 
159
170
  return cqn
160
171
  }
@@ -166,5 +177,6 @@ module.exports = {
166
177
  for: service => {
167
178
  if (!cache.has(service)) cache.set(service, _4service(service))
168
179
  return cache.get(service)
169
- }
180
+ },
181
+ addRefToWhereIfNecessary
170
182
  }
@@ -1,4 +1,4 @@
1
- const { formatVal } = require('../utils')
1
+ const { formatVal } = require('./utils')
2
2
 
3
3
  const OPERATORS = {
4
4
  '=': 'eq',
@@ -27,9 +27,6 @@
27
27
  //
28
28
  // ---------- JavaScript Helpers -------------
29
29
  {
30
- const exception = (message, code = 400) => {
31
- error(JSON.stringify({ code, message }))
32
- }
33
30
  const $ = Object.assign
34
31
  const { strict, minimal } = options
35
32
  const stack = []
@@ -42,79 +39,86 @@
42
39
  return Number.isSafeInteger(n) ? n : str
43
40
  }
44
41
 
45
- // NOTe: mutation of the object property, it's NOT a pure function
46
- const correctAggAwayWhere = (where, colNames) => {
47
- const changedWhere = [...where];
48
-
49
- for (const item of changedWhere) {
50
- if (item.xpr) {
51
- item.xpr = correctAggAwayWhere(item.xpr, colNames)
52
- }
53
-
54
- if (item.args) {
55
- item.args = correctAggAwayWhere(item.args, colNames)
56
- }
57
-
58
- // $filter ohne $apply -> input set = entity -> kein null setzen
59
- // $apply mit filter transformation -> wie oben
60
- // $apply mit filter transformation + $filter -> filter in where, $filter in having
61
- // $apply mit filter transformation + groupby/aggregate/select + $filter -> filter in where, $filter in having
62
-
63
- // TODO fix this for $apply
64
- if(item.ref && !colNames.includes(item.ref.join(''))) {
65
- // item.ref = null;
42
+ const _compareRefs = col => exp => col === exp ||
43
+ (col.as && exp.as && col.as === exp.as) ||
44
+ (exp.as && col.ref && exp.as === col.ref[col.ref.length - 1]) ||
45
+ (col.ref && exp.ref && col.ref.join('') === exp.ref.join(''))
46
+ const _remapFunc = columns => c => {
47
+ if (Array.isArray(c)) return c.map(_remapFunc(columns))
48
+ const fnObj = c.ref && columns.find(col => col.as && col.func && col.as === c.ref[0])
49
+ if (fnObj) return fnObj
50
+ return c
51
+ }
52
+ const _replaceNullRef = groupBy => c => {
53
+ if (Array.isArray(c)) return c.map(_replaceNullRef(groupBy))
54
+ if (c.ref && !groupBy.find(_compareRefs(c))) return { val: null }
55
+ return c
56
+ }
57
+ const _expand = (columns, col) => {
58
+ if (col.ref.length === 1) {
59
+ if (!columns.find(_compareRefs(col))) columns.push(col)
60
+ } else {
61
+ const assoc = col.ref.shift()
62
+ const exp = columns.find(c => c.ref && c.ref[0] === assoc)
63
+ if (exp) {
64
+ _expand(exp.expand, col)
65
+ } else {
66
+ const exp = {
67
+ ref: [assoc],
68
+ expand: []
69
+ }
70
+ _expand(exp.expand, col)
71
+ columns.push(exp)
66
72
  }
67
- // REVISIT: { val:null } for should be also implemented
68
73
  }
69
-
70
- return changedWhere;
71
74
  }
72
-
73
- const correctAggAwayColumns = (SELECT) => {
74
- const groupBy = SELECT.groupBy;
75
- const where = SELECT.where;
76
- const columns = SELECT.columns || [];
77
- const aggregates = columns.filter((cur) => cur.as);
78
-
79
- let fromAggregate = [];
80
- let fromGroupBy = [];
81
-
82
- // handle $apply=aggregate(... as someProp)&$select=someProp,?...
83
- if (aggregates.length !== 0) {
84
- fromAggregate = columns.filter((cur) =>
85
- cur.ref ? aggregates.includes(cur.ref.join('')) : true
86
- );
75
+ const _handleApply = (cqn, apply, onlyColumnsFromExpand = false) => {
76
+ if (!apply) return
77
+ if (cqn.apply) delete cqn.apply
78
+ if (apply.apply || (apply.where && cqn.where) || (apply.search && cqn.search)) {
79
+ cqn.from = { SELECT: { from: cqn.from } }
87
80
  }
88
-
89
- // handle $apply=groupby((someProp,?...))&$select=?...
90
- if (groupBy) {
91
- const allowedNames = groupBy.map(({ ref }) => ref && ref.join(''));
92
- const allowedColumns = columns.filter((cur) =>
93
- cur.ref && allowedNames.includes(cur.ref.join(''))
94
- );
95
- fromGroupBy = allowedColumns.length === 0 ? [...groupBy] : allowedColumns;
81
+ if (apply.where) {
82
+ if (cqn.where) cqn.from.SELECT.where = apply.where
83
+ else cqn.where = apply.where
96
84
  }
97
-
98
- const newColumns = fromAggregate.length !== 0 || fromGroupBy.length !== 0
99
- ? [...fromGroupBy, ...fromAggregate]
100
- : SELECT.columns;
101
-
102
- let result = { ...SELECT }
103
- if (newColumns) result.columns = newColumns
104
- let newWhere = [];
105
-
106
- if (where && (groupBy || aggregates.length !== 0)) {
107
- // changing { ref: null } for aggregated-away props
108
- const colNames = columns.map((cur) => cur.ref && cur.ref.join('') || cur.as);
109
- result = { ...result, where: correctAggAwayWhere(where, colNames) }
85
+ if (apply.search) {
86
+ if (cqn.search) cqn.from.SELECT.search = apply.search
87
+ else cqn.search = apply.search
110
88
  }
111
-
112
- return result;
89
+ if (apply.groupBy) {
90
+ cqn.groupBy = []
91
+ for (const col of apply.groupBy) {
92
+ if (!cqn.groupBy.find(_compareRefs(col))) cqn.groupBy.push(col)
93
+ }
94
+ }
95
+ const aggregatedColumns = [...(cqn.groupBy || []), ...(apply.aggregate || [])]
96
+ if (aggregatedColumns.length) {
97
+ cqn.columns = cqn.columns && !onlyColumnsFromExpand
98
+ // using .reduce instead of .filter since columns order might be important
99
+ ? cqn.columns.reduce((columns, col) => {
100
+ const aggregatedColumn = aggregatedColumns.find(_compareRefs(col))
101
+ if (aggregatedColumn) columns.push(aggregatedColumn)
102
+ return columns
103
+ }, [])
104
+ : aggregatedColumns
105
+ if (cqn.where) {
106
+ cqn.where = cqn.where.map(_remapFunc(cqn.columns))
107
+ if (cqn.groupBy) {
108
+ cqn.having = cqn.where.map(_replaceNullRef(cqn.groupBy))
109
+ delete cqn.where
110
+ }
111
+ }
112
+ // expand navigation refs in aggregated columns
113
+ cqn.columns = cqn.columns.reduce((columns, col) => {
114
+ if (col.ref && col.ref.length > 1 && aggregatedColumns.find(_compareRefs(col))) {
115
+ _expand(columns, { ref: [...col.ref] })
116
+ } else columns.push(col)
117
+ return columns
118
+ }, [])
119
+ }
120
+ if (apply.apply) _handleApply(cqn.from.SELECT, apply.apply)
113
121
  }
114
-
115
- const _compareRefs = col => exp => col === exp ||
116
- (col.as && exp.as && col.as === exp.as) ||
117
- (col.ref && exp.ref && col.ref.join('') === exp.ref.join(''))
118
122
  }
119
123
 
120
124
  // ---------- Entity Paths ---------------
@@ -129,11 +133,19 @@
129
133
  delete SELECT.expand
130
134
  delete SELECT.limit
131
135
  delete SELECT.orderBy
136
+ if (SELECT.apply) {
137
+ SELECT.apply = { apply: SELECT.apply }
138
+ _handleApply(SELECT, SELECT.apply)
139
+ }
132
140
  return { SELECT }
133
141
  }
142
+ let onlyColumnsFromExpand
134
143
  if (SELECT.expand) {
135
144
  // Books?$expand=author w/o $select=author
136
- if (!SELECT.columns) SELECT.columns = ['*']
145
+ if (!SELECT.columns) {
146
+ SELECT.columns = ['*']
147
+ onlyColumnsFromExpand = true
148
+ }
137
149
  for (const exp of SELECT.expand) {
138
150
  const idx = SELECT.columns.findIndex(_compareRefs(exp))
139
151
  if (idx > -1) SELECT.columns.splice(idx, 1)
@@ -141,22 +153,25 @@
141
153
  }
142
154
  delete SELECT.expand
143
155
  }
144
- SELECT = correctAggAwayColumns(SELECT)
145
-
156
+ _handleApply(SELECT, SELECT.apply, onlyColumnsFromExpand)
146
157
  return { SELECT }
147
158
  }
148
159
 
149
160
  path
150
161
  = "$count" {count = true}
151
162
  / rv:$("$ref"/"$value") {return !TECHNICAL_OPTS.includes(rv) && {from: {ref: [rv]}}}
152
- / head:(identifier/val) filter:(OPEN CLOSE/OPEN args CLOSE)? tail:( '/' p:path {return p} )? {
163
+ / head:(
164
+ (identifier filter:(OPEN CLOSE/OPEN args CLOSE)? !segment) / val:(s:(!string val){return s[1]} / segment){return [val]}
165
+ ) tail:( '/' p:path {return p} )? {
166
+ const [id, filter] = head
153
167
  // minimal: val also as path segment
154
168
  const ref = [
155
169
  filter
156
170
  ? filter.length > 2
157
- ? { id: head, where: filter[1].map(f => f.val && f.val.match && f.val.match(/^"(.*)"$/) ? { val: f.val.match(/^"(.*)"$/)[1] } : f) }
158
- : { id: head, where: [] }
159
- : ( minimal ? `${Object.prototype.hasOwnProperty.call(head, 'val') ? head.val : head}` : head )
171
+ ? { id, where: filter[1].map(f => f.val && f.val.match && f.val.match(/^"(.*)"$/) ? { val: f.val.match(/^"(.*)"$/)[1] } : f) }
172
+ : { id, where: [] }
173
+ // hasOwnProperty in case of '{val:0}' and so on
174
+ : ( minimal ? `${typeof id === 'object' && Object.prototype.hasOwnProperty.call(id, 'val') ? id.val : id}` : id )
160
175
  ]
161
176
  if (tail && tail.from) {
162
177
  const more = tail.from.ref
@@ -170,7 +185,7 @@
170
185
 
171
186
  args
172
187
  = val:val {return [val]}
173
- / ref:ref o"="o val:(val/w:word{return {val: w}}) more:( COMMA args )? {
188
+ / ref:ref o"="o val:val more:( COMMA args )? {
174
189
  const args = [ ref, '=', val ]
175
190
  if (more) args.push ('and', ...more[1])
176
191
  return args
@@ -183,13 +198,16 @@
183
198
  ExpandOption =
184
199
  "$select=" o select ( COMMA select )* /
185
200
  "$expand=" o expand ( COMMA expand )* /
186
- "$filter=" o filter /
201
+ "$filter=" o f:filter{SELECT.where = f} /
187
202
  "$orderby=" o orderby ( COMMA orderby )* /
188
203
  "$top=" o top /
189
204
  "$skip=" o skip /
190
- "$search=" o search /
205
+ "$skiptoken=" o skip /
206
+ "$search=" o s:search {if (s) SELECT.search = s} /
191
207
  "$count=" o count /
192
- "$apply=" o apply /
208
+ "$apply=" (o{
209
+ SELECT.apply = {}
210
+ }) apply /
193
211
  custom
194
212
 
195
213
 
@@ -241,11 +259,15 @@
241
259
 
242
260
  skip
243
261
  = val:integer {
262
+ if (SELECT.limit && SELECT.limit.offset && SELECT.limit.offset.val) {
263
+ val += SELECT.limit.offset.val
264
+ }
244
265
  (SELECT.limit || (SELECT.limit={})).offset = {val}
245
266
  }
246
267
 
247
268
  search
248
- = p:search_clause {SELECT.search = p}
269
+ = p:search_clause {return p}
270
+ / o // Do not add search property for space only
249
271
 
250
272
  search_clause
251
273
  = p:( n:NOT? {return n?[n]:[]} )(
@@ -259,7 +281,7 @@
259
281
  {return p}
260
282
 
261
283
  filter
262
- = p:where_clause {SELECT.where = p}
284
+ = p:where_clause { return p }
263
285
 
264
286
  where_clause = p:( n:NOT? {return n?[n]:[]} )(
265
287
  OPEN xpr:where_clause CLOSE {p.push({xpr})}
@@ -352,7 +374,17 @@
352
374
  = operand (_ ("add" / "sub" / "mul" / "div" / "mod") _ operand)*
353
375
 
354
376
  operand "an operand"
355
- = function / ref / val / jsonObject / jsonArray / list
377
+ = navigationCount / function / ref / val / jsonObject / jsonArray / list
378
+
379
+ navigationCount "navigation with $count"
380
+ = navigationPath:(head:identifier key:(OPEN keyArgs:args CLOSE {return keyArgs;})? '/' {
381
+ if (key) {
382
+ // we have key in xpr
383
+ return {id: head, where: key}
384
+ }
385
+ return head;
386
+ })+ count: '$count'
387
+ { return {func: 'count', as: '$count', args: [{ref: navigationPath}]} }
356
388
 
357
389
  ref "a reference"
358
390
  = head:identifier tail:( '/' n:identifier {return n})*
@@ -366,7 +398,7 @@
366
398
 
367
399
  val
368
400
  = val:(bool / date) {return {val}}
369
- / guid
401
+ / val:guid {return {val}}
370
402
  / val:number {return typeof val === 'number' ? {val} : { val, literal:'number' }}
371
403
  / val:string {return {val}}
372
404
 
@@ -380,7 +412,9 @@
380
412
 
381
413
  function "a function call"
382
414
  = func:$[a-z]+ OPEN a:operand more:( COMMA o:operand {return o} )* CLOSE {
383
- if (strict && !(func in strict.functions)) exception("'"+ func +"' is an unknown function in OData URL spec (strict mode)")
415
+ if (strict && !(func in strict.functions)) {
416
+ throw Object.assign(new Error(`"${func}" is an unknown function in OData URL spec (strict mode)`), { statusCode: 400 })
417
+ }
384
418
  return { func, args:[a,...more] }
385
419
  }
386
420
 
@@ -402,7 +436,6 @@
402
436
  "aggregate" aggregateTrafo /
403
437
  "groupby" groupbyTrafo /
404
438
  "filter" filterTrafo /
405
- countTrafo /
406
439
 
407
440
  // REVISIT: All transformations below need improvment
408
441
  // and should supported by CAP
@@ -410,31 +443,45 @@
410
443
  "search" searchTrafo /
411
444
  "concat" concatTrafo /
412
445
  "compute" computeTrafo /
413
- "bottompercent" commonFuncTrafo /
414
- "bottomsum" commonFuncTrafo /
415
- "toppercent" commonFuncTrafo /
416
- "topsum" commonFuncTrafo /
446
+ func:("topcount"/"bottomcount"/"topsum"/"bottomsum"/"toppercent"/"bottompercent") args:commonFuncTrafo {
447
+ const SUPPORTED_APPLY_TRANSFORMATIONS = {
448
+ "topcount": true,
449
+ "bottomcount": true,
450
+ "topsum": false,
451
+ "bottomsum": false,
452
+ "toppercent": false,
453
+ "bottompercent": false
454
+ }
455
+ if (!SUPPORTED_APPLY_TRANSFORMATIONS[func]) {
456
+ throw Object.assign(new Error(`Transformation "${func}" in $apply is not supported yet.`), { statusCode: 501 })
457
+ }
458
+ (SELECT.apply.aggregate || (SELECT.apply.aggregate = [])).push({ func, args })
459
+ } /
417
460
  identityTrafo
418
461
  // customFunction
419
462
  )
420
463
 
421
464
  aggregateTrafo
422
- = OPEN o aggregateItem (o COMMA o aggregateItem)* o CLOSE
465
+ = OPEN o head:aggregateItem tail:(o COMMA o p:aggregateItem {return p})* o CLOSE {
466
+ let _apply = SELECT.apply
467
+ if (_apply.aggregate) {
468
+ SELECT.apply = { apply: _apply }
469
+ if (_apply.groupBy) SELECT.apply.groupBy = _apply.groupBy
470
+ _apply = SELECT.apply
471
+ }
472
+ _apply.aggregate = [head, ...tail]
473
+ }
423
474
  aggregateItem
424
- = res:("$count" alias:asAlias { return { func: 'count', args: ['*'], as: alias } }
475
+ = res:("$count" as:asAlias { return { func: 'count', args: [{ val: 1 }], as } }
425
476
  / aggregateExpr
426
- ) {
427
- SELECT.columns = Array.isArray(SELECT.columns) ? SELECT.columns : []
428
- if (!SELECT.columns.find(_compareRefs(res))) SELECT.columns.push(res)
429
- return res
430
- }
477
+ ) { return res }
431
478
  aggregateExpr
432
479
  = path:(
433
480
  ref
434
481
  // / mathCalc - needs CAP support
435
482
  )
436
- func:aggregateWith aggregateFrom? alias:asAlias
437
- { return { func, args: [ path ], as: alias } }
483
+ func:aggregateWith aggregateFrom? as:asAlias
484
+ { return { func, args: [ path ], as } }
438
485
  / identifier OPEN aggregateExpr CLOSE // needs CAP support
439
486
  // / customAggregate // needs CAP support
440
487
  aggregateWith
@@ -445,22 +492,27 @@
445
492
  = _ "as" _ alias:identifier { return alias; }
446
493
 
447
494
  groupbyTrafo
448
- = OPEN o (OPEN groupByElem (COMMA o groupByElem)* CLOSE) (COMMA o apply)? o CLOSE
495
+ = OPEN OPEN head:groupByElem tail:(COMMA p:groupByElem {return p})* (CLOSE {
496
+ let _apply = SELECT.apply
497
+ if (_apply.groupBy || _apply.where || _apply.search) {
498
+ SELECT.apply = { apply: _apply }
499
+ _apply = SELECT.apply
500
+ }
501
+ _apply.groupBy = [head, ...tail]
502
+ }) (COMMA apply)? CLOSE
449
503
  groupByElem
450
- = val:(rollupSpec / ref)
451
- { (SELECT.groupBy || (SELECT.groupBy = [])).push(val) }
504
+ = c:(rollupSpec / ref) { return c }
452
505
  rollupSpec // TODO fix this + add CAP support
453
506
  = rollup:("rollup" OPEN o ('$all' / ref) (o COMMA ref)+ o CLOSE) {const err = new Error("Rollup in groupby is not supported yet.");err.statusCode=501;throw err;}
454
507
 
455
- filterTrafo = OPEN o filter o CLOSE
456
-
457
- countTrafo
458
- = trafo:("topcount" / "bottomcount") OPEN o val:number o COMMA o ref:ref o CLOSE
459
- {
460
- const oredrObj = { ...ref, sort: trafo === 'topcount' ? 'desc' : 'asc' };
461
- SELECT.orderBy = SELECT.orderBy ? [...SELECT.orderBy, oredrObj] : [oredrObj];
462
- (SELECT.limit || (SELECT.limit={})).rows = {val};
508
+ filterTrafo = OPEN o (where:filter{
509
+ let _apply = SELECT.apply
510
+ if (_apply.where) {
511
+ SELECT.apply = { apply: _apply }
512
+ _apply = SELECT.apply
463
513
  }
514
+ _apply.where = where
515
+ }) o CLOSE
464
516
 
465
517
 
466
518
  // All transformations below need improvment
@@ -471,14 +523,22 @@
471
523
  / filterTrafo (o COMMA expandTrafo)*
472
524
  ) o CLOSE
473
525
 
474
- searchTrafo = OPEN o search o CLOSE
526
+ searchTrafo = OPEN o (search:search{
527
+ if (!search) return
528
+ let _apply = SELECT.apply
529
+ if (_apply.search) {
530
+ SELECT.apply = { apply: _apply }
531
+ _apply = SELECT.apply
532
+ }
533
+ _apply.search = search
534
+ }) o CLOSE
475
535
 
476
536
  concatTrafo = OPEN o apply (o COMMA o apply)+ o CLOSE
477
537
 
478
538
  computeTrafo = OPEN o computeExpr (o COMMA o computeExpr)* o CLOSE
479
539
  computeExpr = where_clause asAlias
480
540
 
481
- commonFuncTrafo = OPEN o operand o COMMA o operand o CLOSE
541
+ commonFuncTrafo = OPEN o first:operand o COMMA o second:operand o CLOSE { return [first, second] }
482
542
 
483
543
  identityTrafo = "identity"
484
544
 
@@ -506,18 +566,22 @@
506
566
  )?)
507
567
 
508
568
  number
509
- = s:$( [+-]? [0-9]+ ("."[0-9]+)? ("e"[0-9]+)? )
510
- {return safeNumber(s)}
569
+ = s:$( [+-]? [0-9]+ ("."[0-9]+)? ("e"[0-9]+)? ) {return safeNumber(s)}
511
570
 
512
571
  integer
513
- = s:$( [+-]? [0-9]+ )
514
- {return parseInt(s)}
572
+ = s:$( [+-]? [0-9]+ ) {return parseInt(s)}
515
573
 
516
574
  identifier
517
575
  = !bool !guid s:$([_a-zA-Z][_a-zA-Z0-9"."]*) {return s}
518
576
 
519
- guid = val:$([0-9a-zA-Z]+ "-" ([0-9a-zA-Z]+ "-"?)+)
520
- {return {val}}
577
+ guid
578
+ = $(hex16 hex16 "-"? hex16 "-"? hex16 "-"? hex16 "-"? hex16 hex16 hex16)
579
+
580
+ hex16
581
+ = $([0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F])
582
+
583
+ segment
584
+ = val:$([a-zA-Z0-9-"."_~!$&'()*+,;=:@]+){return {val}}
521
585
 
522
586
  //
523
587
  // ---------- Punctuation ----------
@@ -1,10 +1,10 @@
1
1
  const cds = require('../_runtime/cds')
2
2
  const { SELECT } = cds.ql
3
3
 
4
- const odata2cqn = require('./odata2cqn')
4
+ const odata2cqn = require('./parser').parse
5
5
  const cqn2odata = require('./cqn2odata')
6
6
 
7
- const afterburner = require('./odata2cqn/afterburner')
7
+ const afterburner = require('./afterburner')
8
8
  const { getSafeNumber: safeNumber } = require('./utils')
9
9
 
10
10
  const strict = {
@@ -30,6 +30,18 @@ const strict = {
30
30
  }
31
31
  }
32
32
 
33
+ const _2query = cqn => {
34
+ if (cqn.SELECT.from.SELECT) cqn.SELECT.from = _2query(cqn.SELECT.from)
35
+ const query = cqn.SELECT.one ? SELECT.one(cqn.SELECT.from) : SELECT.from(cqn.SELECT.from)
36
+ for (const prop in cqn.SELECT) {
37
+ if (prop === 'from') continue
38
+ // explicitly use cds.ql to get '_includes_or__' magic for cqn.SELECT.where containing 'or'
39
+ if (prop === 'where' || prop === 'having') query[prop](cqn.SELECT[prop])
40
+ else query.SELECT[prop] = cqn.SELECT[prop]
41
+ }
42
+ return query
43
+ }
44
+
33
45
  /*
34
46
  * cds.odata API
35
47
  */
@@ -39,6 +51,7 @@ module.exports = {
39
51
  if (url.url) url = url.url
40
52
  // REVISIT: for okra, remove when no longer needed
41
53
  else if (url.getIncomingRequest) url = url.getIncomingRequest().url
54
+
42
55
  url = decodeURIComponent(url)
43
56
 
44
57
  options = options === 'strict' ? { strict } : options.strict ? { ...options, strict } : options
@@ -48,25 +61,15 @@ module.exports = {
48
61
  let cqn
49
62
  try {
50
63
  cqn = odata2cqn(url, options)
51
- } catch (e) {
52
- // REVISIT: additional try in catch isn't nice -> find better way
53
- // known gaps -> e.message is a stringified error -> use that
54
- // unknown errors -> e is the error to keep
55
- let err = e
56
- try {
57
- err = JSON.parse(e.message)
58
- } catch {
59
- /* nothing to do */
60
- }
64
+ } catch (err) {
61
65
  err.message = 'Parsing URL failed with error: ' + err.message
62
66
  err.statusCode = err.statusCode || 400
63
67
  throw err
64
68
  }
65
69
 
66
- if (typeof options.afterburner === 'function') cqn = options.afterburner(cqn)
70
+ if (options.afterburner) cqn = options.afterburner(cqn)
67
71
 
68
- const query = cqn.SELECT.one ? SELECT.one(cqn.SELECT.from) : SELECT.from(cqn.SELECT.from)
69
- Object.assign(query.SELECT, cqn.SELECT)
72
+ const query = _2query(cqn)
70
73
 
71
74
  // REVISIT: _target vs __target, i.e., pseudo csn vs actual csn
72
75
  // DO NOT USE __target outside of libx/rest!!!