@sap/cds 5.6.3 → 5.7.3

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 (187) hide show
  1. package/CHANGELOG.md +135 -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 +3 -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 +0 -34
  31. package/lib/deploy.js +25 -17
  32. package/lib/env/defaults.js +3 -1
  33. package/lib/env/index.js +8 -3
  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 -69
  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 +29 -1
  73. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/expandToCQN.js +7 -1
  74. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +9 -5
  75. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/utils.js +2 -2
  76. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriParser.js +13 -10
  77. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/invocation/ConditionalRequestControlCommand.js +0 -7
  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/stream.js +54 -76
  80. package/libx/_runtime/cds-services/adapter/rest/RestRequest.js +0 -7
  81. package/libx/_runtime/cds-services/adapter/rest/handlers/create.js +3 -6
  82. package/libx/_runtime/cds-services/adapter/rest/handlers/delete.js +3 -6
  83. package/libx/_runtime/cds-services/adapter/rest/handlers/operation.js +3 -6
  84. package/libx/_runtime/cds-services/adapter/rest/handlers/read.js +3 -6
  85. package/libx/_runtime/cds-services/adapter/rest/handlers/update.js +3 -6
  86. package/libx/_runtime/cds-services/adapter/rest/rest-to-cqn/index.js +2 -2
  87. package/libx/_runtime/cds-services/adapter/rest/utils/parse-url.js +8 -4
  88. package/libx/_runtime/cds-services/services/Service.js +0 -6
  89. package/libx/_runtime/cds-services/services/utils/columns.js +10 -3
  90. package/libx/_runtime/cds-services/services/utils/compareJson.js +4 -7
  91. package/libx/_runtime/cds-services/services/utils/differ.js +4 -1
  92. package/libx/_runtime/cds-services/services/utils/handlerUtils.js +1 -41
  93. package/libx/_runtime/cds-services/util/assert.js +1 -262
  94. package/libx/_runtime/cds.js +6 -9
  95. package/libx/_runtime/common/aspects/entity.js +1 -1
  96. package/libx/_runtime/common/composition/delete.js +4 -2
  97. package/libx/_runtime/common/composition/update.js +22 -38
  98. package/libx/_runtime/common/composition/utils.js +3 -7
  99. package/libx/_runtime/common/error/standardError.js +11 -0
  100. package/libx/_runtime/common/generic/auth.js +63 -33
  101. package/libx/_runtime/common/generic/crud.js +11 -23
  102. package/libx/_runtime/common/generic/input.js +20 -0
  103. package/libx/_runtime/common/generic/paging.js +2 -2
  104. package/libx/_runtime/common/generic/put.js +4 -10
  105. package/libx/_runtime/common/generic/sorting.js +12 -30
  106. package/libx/_runtime/common/i18n/messages.properties +2 -0
  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 +298 -121
  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 +38 -52
  119. package/libx/_runtime/db/expand/index.js +3 -1
  120. package/libx/_runtime/db/generic/arrayed.js +3 -1
  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/read.js +12 -8
  125. package/libx/_runtime/db/query/update.js +9 -3
  126. package/libx/_runtime/db/sql-builder/ExpressionBuilder.js +8 -9
  127. package/libx/_runtime/{common → db}/utils/propagateForeignKeys.js +11 -14
  128. package/libx/_runtime/fiori/generic/activate.js +1 -0
  129. package/libx/_runtime/fiori/generic/before.js +2 -1
  130. package/libx/_runtime/fiori/generic/edit.js +1 -0
  131. package/libx/_runtime/fiori/generic/patch.js +1 -1
  132. package/libx/_runtime/fiori/generic/read.js +128 -57
  133. package/libx/_runtime/fiori/uiflex/handler/transformRESULT.js +0 -4
  134. package/libx/_runtime/fiori/uiflex/index.js +1 -1
  135. package/libx/_runtime/fiori/uiflex/{extensibility/index.js → service.js} +6 -4
  136. package/libx/_runtime/fiori/utils/delete.js +7 -1
  137. package/libx/_runtime/hana/Service.js +1 -8
  138. package/libx/_runtime/hana/customBuilder/CustomSelectBuilder.js +5 -14
  139. package/libx/_runtime/hana/execute.js +10 -4
  140. package/libx/_runtime/hana/pool.js +55 -45
  141. package/libx/_runtime/hana/search.js +7 -6
  142. package/libx/_runtime/hana/search2cqn4sql.js +8 -5
  143. package/libx/_runtime/hana/searchToContains.js +3 -1
  144. package/libx/_runtime/index.js +5 -5
  145. package/libx/_runtime/messaging/AMQPWebhookMessaging.js +3 -3
  146. package/libx/_runtime/messaging/Outbox.js +53 -0
  147. package/libx/_runtime/messaging/common-utils/AMQPClient.js +17 -10
  148. package/libx/_runtime/messaging/common-utils/connections.js +14 -9
  149. package/libx/_runtime/messaging/common-utils/waitingTime.js +2 -0
  150. package/libx/_runtime/messaging/enterprise-messaging-shared.js +2 -3
  151. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +2 -2
  152. package/libx/_runtime/messaging/enterprise-messaging.js +21 -15
  153. package/libx/_runtime/messaging/file-based.js +5 -5
  154. package/libx/_runtime/messaging/message-queuing-utils/options-messaging.js +1 -0
  155. package/libx/_runtime/messaging/message-queuing.js +2 -3
  156. package/libx/_runtime/messaging/outbox/OutboxRunner.js +75 -0
  157. package/libx/_runtime/messaging/outbox/utils.js +192 -0
  158. package/libx/_runtime/messaging/service.js +16 -30
  159. package/libx/_runtime/remote/Service.js +15 -0
  160. package/libx/_runtime/remote/utils/client.js +15 -3
  161. package/libx/_runtime/remote/utils/{dataConversion.js → data.js} +12 -2
  162. package/libx/_runtime/sqlite/Service.js +7 -10
  163. package/libx/_runtime/sqlite/customBuilder/CustomExpressionBuilder.js +19 -0
  164. package/libx/_runtime/sqlite/execute.js +18 -12
  165. package/libx/_runtime/types/api.js +2 -1
  166. package/libx/gql/resolvers/parse/ast2cqn/columns.js +1 -1
  167. package/libx/odata/{odata2cqn/afterburner.js → afterburner.js} +28 -16
  168. package/libx/odata/{cqn2odata/index.js → cqn2odata.js} +1 -1
  169. package/libx/odata/{odata2cqn/grammar.pegjs → grammar.pegjs} +222 -130
  170. package/libx/odata/index.js +23 -14
  171. package/libx/odata/parser.js +1 -0
  172. package/libx/odata/utils.js +57 -0
  173. package/libx/rest/RestAdapter.js +2 -6
  174. package/libx/rest/utils/data.js +1 -6
  175. package/package.json +4 -3
  176. package/server.js +4 -5
  177. package/srv/audit-log.cds +87 -0
  178. package/{libx/_runtime/fiori/uiflex/extensibility/index.cds → srv/flex.cds} +0 -0
  179. package/srv/flex.js +1 -0
  180. package/srv/outbox.cds +11 -0
  181. package/srv/outbox.js +0 -0
  182. package/libx/_runtime/cds-services/adapter/perf/performance.js +0 -104
  183. package/libx/_runtime/cds-services/adapter/perf/performanceMeasurement.js +0 -33
  184. package/libx/odata/odata2cqn/index.js +0 -3
  185. package/libx/odata/odata2cqn/parser.js +0 -1
  186. package/libx/odata/readme.md +0 -1
  187. package/libx/odata/utils/index.js +0 -64
@@ -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,102 @@
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)
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)
52
72
  }
53
-
54
- if (item.args) {
55
- item.args = correctAggAwayWhere(item.args, colNames)
73
+ }
74
+ }
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 } }
80
+ }
81
+ if (apply.where) {
82
+ if (cqn.where) cqn.from.SELECT.where = apply.where
83
+ else cqn.where = apply.where
84
+ }
85
+ if (apply.search) {
86
+ if (cqn.search) cqn.from.SELECT.search = apply.search
87
+ else cqn.search = apply.search
88
+ }
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)
56
93
  }
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;
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
+ }
66
111
  }
67
- // REVISIT: { val:null } for should be also implemented
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
+ }, [])
68
119
  }
69
-
70
- return changedWhere;
120
+ if (apply.apply) _handleApply(cqn.from.SELECT, apply.apply)
71
121
  }
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
- );
87
- }
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;
122
+ const _setLimitOffset = val => {
123
+ if (SELECT.limit && SELECT.limit.offset && SELECT.limit.offset.val) {
124
+ val += SELECT.limit.offset.val
96
125
  }
126
+ (SELECT.limit || (SELECT.limit={})).offset = {val}
127
+ }
97
128
 
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) }
129
+ const _removeLambdaPrefix = (prefix, elements) => {
130
+ for (const e of elements) {
131
+ // remove the prefix identifier
132
+ if (e.ref && e.ref[0] === prefix) e.ref.shift()
133
+ if (e.func) _removeLambdaPrefix(prefix, e.args)
110
134
  }
111
135
 
112
- return result;
136
+ return elements
113
137
  }
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
138
  }
119
139
 
120
140
  // ---------- Entity Paths ---------------
@@ -129,11 +149,19 @@
129
149
  delete SELECT.expand
130
150
  delete SELECT.limit
131
151
  delete SELECT.orderBy
152
+ if (SELECT.apply) {
153
+ SELECT.apply = { apply: SELECT.apply }
154
+ _handleApply(SELECT, SELECT.apply)
155
+ }
132
156
  return { SELECT }
133
157
  }
158
+ let onlyColumnsFromExpand
134
159
  if (SELECT.expand) {
135
160
  // Books?$expand=author w/o $select=author
136
- if (!SELECT.columns) SELECT.columns = ['*']
161
+ if (!SELECT.columns) {
162
+ SELECT.columns = ['*']
163
+ onlyColumnsFromExpand = true
164
+ }
137
165
  for (const exp of SELECT.expand) {
138
166
  const idx = SELECT.columns.findIndex(_compareRefs(exp))
139
167
  if (idx > -1) SELECT.columns.splice(idx, 1)
@@ -141,22 +169,26 @@
141
169
  }
142
170
  delete SELECT.expand
143
171
  }
144
- SELECT = correctAggAwayColumns(SELECT)
145
-
172
+ if (SELECT.count && SELECT.apply) SELECT.__countAggregated = true
173
+ _handleApply(SELECT, SELECT.apply, onlyColumnsFromExpand)
146
174
  return { SELECT }
147
175
  }
148
176
 
149
177
  path
150
178
  = "$count" {count = true}
151
179
  / 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} )? {
180
+ / head:(
181
+ (identifier filter:(OPEN CLOSE/OPEN args CLOSE)? !segment) / val:(s:(!string val){return s[1]} / segment){return [val]}
182
+ ) tail:( '/' p:path {return p} )? {
183
+ const [id, filter] = head
153
184
  // minimal: val also as path segment
154
185
  const ref = [
155
186
  filter
156
187
  ? 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 )
188
+ ? { id, where: filter[1].map(f => f.val && f.val.match && f.val.match(/^"(.*)"$/) ? { val: f.val.match(/^"(.*)"$/)[1] } : f) }
189
+ : { id, where: [] }
190
+ // hasOwnProperty in case of '{val:0}' and so on
191
+ : ( minimal ? `${typeof id === 'object' && Object.prototype.hasOwnProperty.call(id, 'val') ? id.val : id}` : id )
160
192
  ]
161
193
  if (tail && tail.from) {
162
194
  const more = tail.from.ref
@@ -170,7 +202,7 @@
170
202
 
171
203
  args
172
204
  = val:val {return [val]}
173
- / ref:ref o"="o val:(val/w:word{return {val: w}}) more:( COMMA args )? {
205
+ / ref:ref o"="o val:val more:( COMMA args )? {
174
206
  const args = [ ref, '=', val ]
175
207
  if (more) args.push ('and', ...more[1])
176
208
  return args
@@ -179,18 +211,23 @@
179
211
  //
180
212
  // ---------- Query Options ------------
181
213
 
182
- QueryOption = ExpandOption
214
+ QueryOption = ExpandOption /
215
+ "$skiptoken=" o skiptoken /
216
+ "$apply=" (o{
217
+ SELECT.apply = {}
218
+ }) apply /
219
+ custom
220
+ // @OData spec for $expand:
221
+ // "Allowed system query options are $filter, $select, $orderby, $skip, $top, $count, $search, and $expand."
183
222
  ExpandOption =
184
223
  "$select=" o select ( COMMA select )* /
185
224
  "$expand=" o expand ( COMMA expand )* /
186
- "$filter=" o filter /
225
+ "$filter=" o f:filter{SELECT.where = f} /
187
226
  "$orderby=" o orderby ( COMMA orderby )* /
188
227
  "$top=" o top /
189
228
  "$skip=" o skip /
190
- "$search=" o search /
191
- "$count=" o count /
192
- "$apply=" o apply /
193
- custom
229
+ "$search=" o s:search {if (s) SELECT.search = s} /
230
+ "$count=" o count
194
231
 
195
232
 
196
233
  select
@@ -233,19 +270,32 @@
233
270
  })
234
271
  )? // --- end of nested query options
235
272
  ( COMMA expand )?
273
+ ("/$count" {
274
+ const err = new Error("EXPAND_COUNT_UNSUPPORTED");
275
+ err.statusCode=501;
276
+ throw err;
277
+ })?
236
278
 
237
279
  top
238
280
  = val:integer {
239
281
  (SELECT.limit || (SELECT.limit={})).rows = {val}
240
282
  }
241
283
 
284
+ skiptoken
285
+ = val:integer? skiptoken:skiptokenChars? {
286
+ // REVISIT ignore non-numeric $skiptoken as not supported by CQN
287
+ if (skiptoken) return
288
+ _setLimitOffset(val)
289
+ }
290
+
242
291
  skip
243
292
  = val:integer {
244
- (SELECT.limit || (SELECT.limit={})).offset = {val}
293
+ _setLimitOffset(val)
245
294
  }
246
295
 
247
296
  search
248
- = p:search_clause {SELECT.search = p}
297
+ = p:search_clause {return p}
298
+ / o // Do not add search property for space only
249
299
 
250
300
  search_clause
251
301
  = p:( n:NOT? {return n?[n]:[]} )(
@@ -259,7 +309,7 @@
259
309
  {return p}
260
310
 
261
311
  filter
262
- = p:where_clause {SELECT.where = p}
312
+ = p:where_clause { return p }
263
313
 
264
314
  where_clause = p:( n:NOT? {return n?[n]:[]} )(
265
315
  OPEN xpr:where_clause CLOSE {p.push({xpr})}
@@ -304,18 +354,14 @@
304
354
  p:( n:NOT? { return n ? [n] : [] } )(
305
355
  OPEN xpr:inner_lambda CLOSE { p.push('(', ...xpr, ')') }
306
356
  / comp:comparison { p.push(...comp) }
357
+ / func:function { p.push(func) }
307
358
  / lambda:lambda { p.push(...lambda)}
308
359
  )
309
360
  ( ao:(AND/OR) more:inner_lambda { p.push(ao, ...more) } )*
310
361
  { return p }
311
362
 
312
363
  lambda_clause = prefix:identifier ":" inner:inner_lambda {
313
- for (const e of inner) {
314
- // remove the prefix identifier
315
- if (e.ref && e.ref[0] === prefix) e.ref.shift()
316
- }
317
-
318
- return inner
364
+ return _removeLambdaPrefix(prefix, inner)
319
365
  }
320
366
 
321
367
  any = "any" OPEN p:lambda_clause? CLOSE { return p }
@@ -352,7 +398,17 @@
352
398
  = operand (_ ("add" / "sub" / "mul" / "div" / "mod") _ operand)*
353
399
 
354
400
  operand "an operand"
355
- = function / ref / val / jsonObject / jsonArray / list
401
+ = navigationCount / function / ref / val / jsonObject / jsonArray / list
402
+
403
+ navigationCount "navigation with $count"
404
+ = navigationPath:(head:identifier key:(OPEN keyArgs:args CLOSE {return keyArgs;})? '/' {
405
+ if (key) {
406
+ // we have key in xpr
407
+ return {id: head, where: key}
408
+ }
409
+ return head;
410
+ })+ count: '$count'
411
+ { return {func: 'count', as: '$count', args: [{ref: navigationPath}]} }
356
412
 
357
413
  ref "a reference"
358
414
  = head:identifier tail:( '/' n:identifier {return n})*
@@ -366,7 +422,7 @@
366
422
 
367
423
  val
368
424
  = val:(bool / date) {return {val}}
369
- / guid
425
+ / val:guid {return {val}}
370
426
  / val:number {return typeof val === 'number' ? {val} : { val, literal:'number' }}
371
427
  / val:string {return {val}}
372
428
 
@@ -379,14 +435,16 @@
379
435
  { return { list: any.replace(/"/g,'').split(',').map(ele => ({ val: ele })) } }
380
436
 
381
437
  function "a function call"
382
- = 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)")
384
- return { func, args:[a,...more] }
438
+ = func:$[a-zA-Z]+ OPEN a:operand more:( COMMA o:operand {return o} )* CLOSE {
439
+ if (strict && !(func.toLowerCase() in strict.functions)) {
440
+ throw Object.assign(new Error(`"${func}" is an unknown function in OData URL spec (strict mode)`), { statusCode: 400 })
441
+ }
442
+ return { func: func.toLowerCase(), args:[a,...more] }
385
443
  }
386
444
 
387
445
  boolish "a boolean function"
388
- = func:("contains"/"endswith"/"startswith") OPEN a:operand COMMA b:operand CLOSE
389
- { return { func, args:[a,b] }}
446
+ = func:("contains"i/"endswith"i/"startswith"i) OPEN a:operand COMMA b:operand CLOSE
447
+ { return { func: func.toLowerCase(), args:[a,b] }}
390
448
 
391
449
  NOT = o "NOT"i _ {return 'not'}
392
450
  AND = _ "AND"i _ {return 'and'}
@@ -402,7 +460,6 @@
402
460
  "aggregate" aggregateTrafo /
403
461
  "groupby" groupbyTrafo /
404
462
  "filter" filterTrafo /
405
- countTrafo /
406
463
 
407
464
  // REVISIT: All transformations below need improvment
408
465
  // and should supported by CAP
@@ -410,57 +467,77 @@
410
467
  "search" searchTrafo /
411
468
  "concat" concatTrafo /
412
469
  "compute" computeTrafo /
413
- "bottompercent" commonFuncTrafo /
414
- "bottomsum" commonFuncTrafo /
415
- "toppercent" commonFuncTrafo /
416
- "topsum" commonFuncTrafo /
470
+ func:("topcount"i/"bottomcount"i/"topsum"i/"bottomsum"i/"toppercent"i/"bottompercent"i) args:commonFuncTrafo {
471
+ const SUPPORTED_APPLY_TRANSFORMATIONS = {
472
+ "topcount": true,
473
+ "bottomcount": true,
474
+ "topsum": false,
475
+ "bottomsum": false,
476
+ "toppercent": false,
477
+ "bottompercent": false
478
+ }
479
+ func = func.toLowerCase()
480
+ if (!SUPPORTED_APPLY_TRANSFORMATIONS[func]) {
481
+ throw Object.assign(new Error(`Transformation "${func}" in $apply is not supported yet.`), { statusCode: 501 })
482
+ }
483
+ (SELECT.apply.aggregate || (SELECT.apply.aggregate = [])).push({ func, args })
484
+ } /
417
485
  identityTrafo
418
486
  // customFunction
419
487
  )
420
488
 
421
489
  aggregateTrafo
422
- = OPEN o aggregateItem (o COMMA o aggregateItem)* o CLOSE
490
+ = OPEN o head:aggregateItem tail:(o COMMA o p:aggregateItem {return p})* o CLOSE {
491
+ let _apply = SELECT.apply
492
+ if (_apply.aggregate) {
493
+ SELECT.apply = { apply: _apply }
494
+ if (_apply.groupBy) SELECT.apply.groupBy = _apply.groupBy
495
+ _apply = SELECT.apply
496
+ }
497
+ _apply.aggregate = [head, ...tail]
498
+ }
423
499
  aggregateItem
424
- = res:("$count" alias:asAlias { return { func: 'count', args: ['*'], as: alias } }
500
+ = res:("$count" as:asAlias { return { func: 'count', args: [{ val: 1 }], as } }
425
501
  / 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
- }
502
+ ) { return res }
431
503
  aggregateExpr
432
504
  = path:(
433
505
  ref
434
506
  // / mathCalc - needs CAP support
435
507
  )
436
- func:aggregateWith aggregateFrom? alias:asAlias
437
- { return { func, args: [ path ], as: alias } }
508
+ func:aggregateWith aggregateFrom? as:asAlias
509
+ { return { func, args: [ path ], as } }
438
510
  / identifier OPEN aggregateExpr CLOSE // needs CAP support
439
511
  // / customAggregate // needs CAP support
440
512
  aggregateWith
441
- = _ "with" _ func:$[a-z]+ { return func; }
513
+ = _ "with" _ func:$[a-zA-Z]+ { return func.toLowerCase(); }
442
514
  aggregateFrom
443
515
  = _ "from" _ ref aggregateWith aggregateFrom? // needs CAP support
444
516
  asAlias
445
517
  = _ "as" _ alias:identifier { return alias; }
446
518
 
447
519
  groupbyTrafo
448
- = OPEN o (OPEN groupByElem (COMMA o groupByElem)* CLOSE) (COMMA o apply)? o CLOSE
520
+ = OPEN OPEN head:groupByElem tail:(COMMA p:groupByElem {return p})* (CLOSE {
521
+ let _apply = SELECT.apply
522
+ if (_apply.groupBy || _apply.where || _apply.search) {
523
+ SELECT.apply = { apply: _apply }
524
+ _apply = SELECT.apply
525
+ }
526
+ _apply.groupBy = [head, ...tail]
527
+ }) (COMMA apply)? CLOSE
449
528
  groupByElem
450
- = val:(rollupSpec / ref)
451
- { (SELECT.groupBy || (SELECT.groupBy = [])).push(val) }
529
+ = c:(rollupSpec / ref) { return c }
452
530
  rollupSpec // TODO fix this + add CAP support
453
531
  = 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
532
 
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};
533
+ filterTrafo = OPEN o (where:filter{
534
+ let _apply = SELECT.apply
535
+ if (_apply.where) {
536
+ SELECT.apply = { apply: _apply }
537
+ _apply = SELECT.apply
463
538
  }
539
+ _apply.where = where
540
+ }) o CLOSE
464
541
 
465
542
 
466
543
  // All transformations below need improvment
@@ -471,14 +548,22 @@
471
548
  / filterTrafo (o COMMA expandTrafo)*
472
549
  ) o CLOSE
473
550
 
474
- searchTrafo = OPEN o search o CLOSE
551
+ searchTrafo = OPEN o (search:search{
552
+ if (!search) return
553
+ let _apply = SELECT.apply
554
+ if (_apply.search) {
555
+ SELECT.apply = { apply: _apply }
556
+ _apply = SELECT.apply
557
+ }
558
+ _apply.search = search
559
+ }) o CLOSE
475
560
 
476
561
  concatTrafo = OPEN o apply (o COMMA o apply)+ o CLOSE
477
562
 
478
563
  computeTrafo = OPEN o computeExpr (o COMMA o computeExpr)* o CLOSE
479
564
  computeExpr = where_clause asAlias
480
565
 
481
- commonFuncTrafo = OPEN o operand o COMMA o operand o CLOSE
566
+ commonFuncTrafo = OPEN o first:operand o COMMA o second:operand o CLOSE { return [first, second] }
482
567
 
483
568
  identityTrafo = "identity"
484
569
 
@@ -506,18 +591,25 @@
506
591
  )?)
507
592
 
508
593
  number
509
- = s:$( [+-]? [0-9]+ ("."[0-9]+)? ("e"[0-9]+)? )
510
- {return safeNumber(s)}
594
+ = s:$( [+-]? [0-9]+ ("."[0-9]+)? ("e"[0-9]+)? ) {return safeNumber(s)}
511
595
 
512
596
  integer
513
- = s:$( [+-]? [0-9]+ )
514
- {return parseInt(s)}
597
+ = s:$( [+-]? [0-9]+ ) {return parseInt(s)}
515
598
 
516
599
  identifier
517
600
  = !bool !guid s:$([_a-zA-Z][_a-zA-Z0-9"."]*) {return s}
518
601
 
519
- guid = val:$([0-9a-zA-Z]+ "-" ([0-9a-zA-Z]+ "-"?)+)
520
- {return {val}}
602
+ guid
603
+ = $(hex16 hex16 "-"? hex16 "-"? hex16 "-"? hex16 "-"? hex16 hex16 hex16)
604
+
605
+ hex16
606
+ = $([0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F])
607
+
608
+ segment
609
+ = val:$([a-zA-Z0-9-"."_~!$&'()*+,;=:@]+){return {val}}
610
+
611
+ skiptokenChars
612
+ = $([a-zA-Z0-9-"."_~!$'()*+,;=:@"/""?"]+)
521
613
 
522
614
  //
523
615
  // ---------- Punctuation ----------
@@ -1,11 +1,12 @@
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
+ const getError = require('../_runtime/common/error')
9
10
 
10
11
  const strict = {
11
12
  functions: {
@@ -30,6 +31,18 @@ const strict = {
30
31
  }
31
32
  }
32
33
 
34
+ const _2query = cqn => {
35
+ if (cqn.SELECT.from.SELECT) cqn.SELECT.from = _2query(cqn.SELECT.from)
36
+ const query = cqn.SELECT.one ? SELECT.one(cqn.SELECT.from) : SELECT.from(cqn.SELECT.from)
37
+ for (const prop in cqn.SELECT) {
38
+ if (prop === 'from') continue
39
+ // explicitly use cds.ql to get '_includes_or__' magic for cqn.SELECT.where containing 'or'
40
+ if (prop === 'where' || prop === 'having') query[prop](cqn.SELECT[prop])
41
+ else query.SELECT[prop] = cqn.SELECT[prop]
42
+ }
43
+ return query
44
+ }
45
+
33
46
  /*
34
47
  * cds.odata API
35
48
  */
@@ -39,6 +52,7 @@ module.exports = {
39
52
  if (url.url) url = url.url
40
53
  // REVISIT: for okra, remove when no longer needed
41
54
  else if (url.getIncomingRequest) url = url.getIncomingRequest().url
55
+
42
56
  url = decodeURIComponent(url)
43
57
 
44
58
  options = options === 'strict' ? { strict } : options.strict ? { ...options, strict } : options
@@ -48,25 +62,20 @@ module.exports = {
48
62
  let cqn
49
63
  try {
50
64
  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 */
65
+ } catch (err) {
66
+ if (err.message === 'EXPAND_COUNT_UNSUPPORTED') {
67
+ throw getError(err.statusCode || 400, err.message)
60
68
  }
69
+
70
+ // TODO adjust this to behave like above
61
71
  err.message = 'Parsing URL failed with error: ' + err.message
62
72
  err.statusCode = err.statusCode || 400
63
73
  throw err
64
74
  }
65
75
 
66
- if (typeof options.afterburner === 'function') cqn = options.afterburner(cqn)
76
+ if (options.afterburner) cqn = options.afterburner(cqn)
67
77
 
68
- const query = cqn.SELECT.one ? SELECT.one(cqn.SELECT.from) : SELECT.from(cqn.SELECT.from)
69
- Object.assign(query.SELECT, cqn.SELECT)
78
+ const query = _2query(cqn)
70
79
 
71
80
  // REVISIT: _target vs __target, i.e., pseudo csn vs actual csn
72
81
  // DO NOT USE __target outside of libx/rest!!!