@sap/cds 5.6.4 → 5.7.4

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 (183) hide show
  1. package/CHANGELOG.md +134 -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 +3 -1
  38. package/lib/log/index.js +2 -2
  39. package/lib/req/cds-context.js +79 -0
  40. package/lib/req/context.js +5 -77
  41. package/lib/req/request.js +1 -1
  42. package/lib/serve/Service-api.js +8 -4
  43. package/lib/serve/Service-dispatch.js +0 -7
  44. package/lib/serve/Service-methods.js +6 -8
  45. package/lib/serve/Transaction.js +35 -30
  46. package/lib/serve/adapters.js +1 -4
  47. package/lib/utils/axios.js +1 -1
  48. package/libx/_runtime/audit/Service.js +44 -20
  49. package/libx/_runtime/audit/generic/personal/access.js +16 -11
  50. package/libx/_runtime/audit/generic/personal/modification.js +5 -5
  51. package/libx/_runtime/audit/generic/personal/utils.js +46 -37
  52. package/libx/_runtime/{common/auth → auth}/index.js +21 -7
  53. package/libx/_runtime/{common/auth → auth}/strategies/JWT.js +2 -2
  54. package/libx/_runtime/{common/auth → auth}/strategies/basic.js +2 -2
  55. package/libx/_runtime/{common/auth → auth}/strategies/dummy.js +1 -1
  56. package/libx/_runtime/{common/auth → auth}/strategies/mock.js +2 -2
  57. package/libx/_runtime/{common/auth → auth}/strategies/utils/uaa.js +1 -1
  58. package/libx/_runtime/{common/auth → auth}/strategies/utils/xssec.js +0 -0
  59. package/libx/_runtime/{common/auth → auth}/strategies/xsuaa.js +2 -2
  60. package/libx/_runtime/cds-services/adapter/odata-v4/Dispatcher.js +7 -2
  61. package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +0 -7
  62. package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +0 -8
  63. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +3 -4
  64. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +6 -7
  65. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/delete.js +3 -4
  66. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +2 -11
  67. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +16 -6
  68. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +26 -69
  69. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +0 -7
  70. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +3 -66
  71. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/ExpressionToCQN.js +29 -1
  72. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/expandToCQN.js +7 -1
  73. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +4 -0
  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/validator/ConditionalRequestValidator.js +0 -8
  78. package/libx/_runtime/cds-services/adapter/odata-v4/utils/stream.js +54 -76
  79. package/libx/_runtime/cds-services/adapter/rest/RestRequest.js +0 -7
  80. package/libx/_runtime/cds-services/adapter/rest/handlers/create.js +3 -6
  81. package/libx/_runtime/cds-services/adapter/rest/handlers/delete.js +3 -6
  82. package/libx/_runtime/cds-services/adapter/rest/handlers/operation.js +3 -6
  83. package/libx/_runtime/cds-services/adapter/rest/handlers/read.js +3 -6
  84. package/libx/_runtime/cds-services/adapter/rest/handlers/update.js +3 -6
  85. package/libx/_runtime/cds-services/adapter/rest/utils/parse-url.js +8 -4
  86. package/libx/_runtime/cds-services/services/Service.js +1 -7
  87. package/libx/_runtime/cds-services/services/utils/columns.js +10 -3
  88. package/libx/_runtime/cds-services/services/utils/compareJson.js +3 -6
  89. package/libx/_runtime/cds-services/services/utils/differ.js +4 -1
  90. package/libx/_runtime/cds-services/services/utils/handlerUtils.js +1 -41
  91. package/libx/_runtime/cds-services/util/assert.js +1 -262
  92. package/libx/_runtime/cds.js +6 -9
  93. package/libx/_runtime/common/aspects/entity.js +1 -1
  94. package/libx/_runtime/common/composition/delete.js +4 -2
  95. package/libx/_runtime/common/composition/update.js +22 -38
  96. package/libx/_runtime/common/composition/utils.js +3 -7
  97. package/libx/_runtime/common/error/standardError.js +11 -0
  98. package/libx/_runtime/common/generic/auth.js +63 -33
  99. package/libx/_runtime/common/generic/crud.js +11 -23
  100. package/libx/_runtime/common/generic/input.js +20 -0
  101. package/libx/_runtime/common/generic/put.js +4 -10
  102. package/libx/_runtime/common/generic/sorting.js +12 -30
  103. package/libx/_runtime/common/i18n/messages.properties +2 -0
  104. package/libx/_runtime/common/perf/index.js +24 -0
  105. package/libx/_runtime/common/utils/cqn.js +58 -1
  106. package/libx/_runtime/common/utils/cqn2cqn4sql.js +298 -121
  107. package/libx/_runtime/common/utils/csn.js +38 -56
  108. package/libx/_runtime/common/utils/entityFromCqn.js +6 -6
  109. package/libx/_runtime/common/utils/resolveView.js +4 -5
  110. package/libx/_runtime/common/utils/rewriteAsterisks.js +46 -5
  111. package/libx/_runtime/common/utils/search2cqn4sql.js +21 -9
  112. package/libx/_runtime/common/utils/structured.js +35 -25
  113. package/libx/_runtime/db/Service.js +0 -6
  114. package/libx/_runtime/db/data-conversion/post-processing.js +22 -22
  115. package/libx/_runtime/db/expand/expand-v2.js +130 -0
  116. package/libx/_runtime/db/expand/expandCQNToJoin.js +57 -75
  117. package/libx/_runtime/db/expand/index.js +3 -1
  118. package/libx/_runtime/db/generic/input.js +52 -10
  119. package/libx/_runtime/db/generic/integrity.js +367 -26
  120. package/libx/_runtime/db/generic/virtual.js +51 -13
  121. package/libx/_runtime/db/query/read.js +12 -8
  122. package/libx/_runtime/db/query/update.js +9 -3
  123. package/libx/_runtime/db/sql-builder/ExpressionBuilder.js +8 -9
  124. package/libx/_runtime/{common → db}/utils/propagateForeignKeys.js +11 -14
  125. package/libx/_runtime/fiori/generic/activate.js +1 -0
  126. package/libx/_runtime/fiori/generic/before.js +2 -1
  127. package/libx/_runtime/fiori/generic/edit.js +1 -0
  128. package/libx/_runtime/fiori/generic/patch.js +1 -1
  129. package/libx/_runtime/fiori/generic/read.js +128 -57
  130. package/libx/_runtime/fiori/uiflex/index.js +1 -1
  131. package/libx/_runtime/fiori/uiflex/{extensibility/index.js → service.js} +3 -3
  132. package/libx/_runtime/fiori/utils/delete.js +7 -1
  133. package/libx/_runtime/hana/Service.js +1 -8
  134. package/libx/_runtime/hana/customBuilder/CustomSelectBuilder.js +5 -14
  135. package/libx/_runtime/hana/execute.js +10 -4
  136. package/libx/_runtime/hana/pool.js +55 -45
  137. package/libx/_runtime/hana/search.js +7 -6
  138. package/libx/_runtime/hana/search2cqn4sql.js +8 -5
  139. package/libx/_runtime/hana/searchToContains.js +3 -1
  140. package/libx/_runtime/index.js +5 -5
  141. package/libx/_runtime/messaging/AMQPWebhookMessaging.js +3 -3
  142. package/libx/_runtime/messaging/Outbox.js +53 -0
  143. package/libx/_runtime/messaging/common-utils/AMQPClient.js +17 -10
  144. package/libx/_runtime/messaging/common-utils/connections.js +14 -9
  145. package/libx/_runtime/messaging/common-utils/waitingTime.js +2 -0
  146. package/libx/_runtime/messaging/enterprise-messaging-shared.js +2 -3
  147. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +2 -2
  148. package/libx/_runtime/messaging/enterprise-messaging.js +21 -15
  149. package/libx/_runtime/messaging/file-based.js +5 -5
  150. package/libx/_runtime/messaging/message-queuing-utils/options-messaging.js +1 -0
  151. package/libx/_runtime/messaging/message-queuing.js +2 -3
  152. package/libx/_runtime/messaging/outbox/OutboxRunner.js +75 -0
  153. package/libx/_runtime/messaging/outbox/utils.js +192 -0
  154. package/libx/_runtime/messaging/service.js +16 -30
  155. package/libx/_runtime/remote/Service.js +15 -0
  156. package/libx/_runtime/remote/utils/client.js +15 -3
  157. package/libx/_runtime/remote/utils/{dataConversion.js → data.js} +12 -2
  158. package/libx/_runtime/sqlite/Service.js +7 -10
  159. package/libx/_runtime/sqlite/customBuilder/CustomExpressionBuilder.js +19 -0
  160. package/libx/_runtime/sqlite/execute.js +18 -12
  161. package/libx/_runtime/types/api.js +2 -1
  162. package/libx/gql/resolvers/parse/ast2cqn/columns.js +1 -1
  163. package/libx/odata/{odata2cqn/afterburner.js → afterburner.js} +19 -15
  164. package/libx/odata/{cqn2odata/index.js → cqn2odata.js} +1 -1
  165. package/libx/odata/{odata2cqn/grammar.pegjs → grammar.pegjs} +217 -148
  166. package/libx/odata/index.js +21 -13
  167. package/libx/odata/parser.js +1 -0
  168. package/libx/odata/utils.js +57 -0
  169. package/libx/rest/RestAdapter.js +2 -6
  170. package/libx/rest/utils/data.js +1 -6
  171. package/package.json +4 -3
  172. package/server.js +4 -5
  173. package/srv/audit-log.cds +87 -0
  174. package/{libx/_runtime/fiori/uiflex/extensibility/index.cds → srv/flex.cds} +0 -0
  175. package/srv/flex.js +1 -0
  176. package/srv/outbox.cds +11 -0
  177. package/srv/outbox.js +0 -0
  178. package/libx/_runtime/cds-services/adapter/perf/performance.js +0 -104
  179. package/libx/_runtime/cds-services/adapter/perf/performanceMeasurement.js +0 -33
  180. package/libx/odata/odata2cqn/index.js +0 -3
  181. package/libx/odata/odata2cqn/parser.js +0 -1
  182. package/libx/odata/readme.md +0 -1
  183. package/libx/odata/utils/index.js +0 -64
@@ -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,94 +39,101 @@
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)
121
+ }
122
+ const _setLimitOffset = val => {
123
+ if (SELECT.limit && SELECT.limit.offset && SELECT.limit.offset.val) {
124
+ val += SELECT.limit.offset.val
125
+ }
126
+ (SELECT.limit || (SELECT.limit={})).offset = {val}
113
127
  }
114
128
 
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
-
119
- const _getMergedPath = (head, filter, segment) => {
120
- if (!head) head = ''
121
- if (filter) {
122
- head += filter.reduce((acc, cur) => {
123
- for (const c of cur) {
124
- if (typeof c === 'string') acc += c
125
- else acc += c.val ? typeof c.val === 'string' ? `'${c.val}'` : c.val : c.ref[0]
126
- }
127
- return acc
128
- }, '')
129
- filter = undefined
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)
130
134
  }
131
- head += segment
132
- return [head, filter, segment]
135
+
136
+ return elements
133
137
  }
134
138
  }
135
139
 
@@ -145,11 +149,19 @@
145
149
  delete SELECT.expand
146
150
  delete SELECT.limit
147
151
  delete SELECT.orderBy
152
+ if (SELECT.apply) {
153
+ SELECT.apply = { apply: SELECT.apply }
154
+ _handleApply(SELECT, SELECT.apply)
155
+ }
148
156
  return { SELECT }
149
157
  }
158
+ let onlyColumnsFromExpand
150
159
  if (SELECT.expand) {
151
160
  // Books?$expand=author w/o $select=author
152
- if (!SELECT.columns) SELECT.columns = ['*']
161
+ if (!SELECT.columns) {
162
+ SELECT.columns = ['*']
163
+ onlyColumnsFromExpand = true
164
+ }
153
165
  for (const exp of SELECT.expand) {
154
166
  const idx = SELECT.columns.findIndex(_compareRefs(exp))
155
167
  if (idx > -1) SELECT.columns.splice(idx, 1)
@@ -157,24 +169,26 @@
157
169
  }
158
170
  delete SELECT.expand
159
171
  }
160
- SELECT = correctAggAwayColumns(SELECT)
161
-
172
+ if (SELECT.count && SELECT.apply) SELECT.__countAggregated = true
173
+ _handleApply(SELECT, SELECT.apply, onlyColumnsFromExpand)
162
174
  return { SELECT }
163
175
  }
164
176
 
165
177
  path
166
178
  = "$count" {count = true}
167
179
  / rv:$("$ref"/"$value") {return !TECHNICAL_OPTS.includes(rv) && {from: {ref: [rv]}}}
168
- / head:(identifier/val)? filter:(OPEN CLOSE/OPEN args CLOSE)? segment:(segment)? tail:( '/' p:path {return p} )? {
169
- // OData vs. REST style
170
- if (segment) [head, filter, segment] = _getMergedPath(head, filter, segment)
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
171
184
  // minimal: val also as path segment
172
185
  const ref = [
173
186
  filter
174
187
  ? filter.length > 2
175
- ? { id: head, where: filter[1].map(f => f.val && f.val.match && f.val.match(/^"(.*)"$/) ? { val: f.val.match(/^"(.*)"$/)[1] } : f) }
176
- : { id: head, where: [] }
177
- : ( 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 )
178
192
  ]
179
193
  if (tail && tail.from) {
180
194
  const more = tail.from.ref
@@ -188,7 +202,7 @@
188
202
 
189
203
  args
190
204
  = val:val {return [val]}
191
- / ref:ref o"="o val:(val/w:word{return {val: w}}) more:( COMMA args )? {
205
+ / ref:ref o"="o val:val more:( COMMA args )? {
192
206
  const args = [ ref, '=', val ]
193
207
  if (more) args.push ('and', ...more[1])
194
208
  return args
@@ -197,19 +211,23 @@
197
211
  //
198
212
  // ---------- Query Options ------------
199
213
 
200
- 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."
201
222
  ExpandOption =
202
223
  "$select=" o select ( COMMA select )* /
203
224
  "$expand=" o expand ( COMMA expand )* /
204
- "$filter=" o filter /
225
+ "$filter=" o f:filter{SELECT.where = f} /
205
226
  "$orderby=" o orderby ( COMMA orderby )* /
206
227
  "$top=" o top /
207
228
  "$skip=" o skip /
208
- "$skiptoken=" o skip /
209
- "$search=" o search /
210
- "$count=" o count /
211
- "$apply=" o apply /
212
- custom
229
+ "$search=" o s:search {if (s) SELECT.search = s} /
230
+ "$count=" o count
213
231
 
214
232
 
215
233
  select
@@ -252,22 +270,32 @@
252
270
  })
253
271
  )? // --- end of nested query options
254
272
  ( COMMA expand )?
273
+ ("/$count" {
274
+ const err = new Error("EXPAND_COUNT_UNSUPPORTED");
275
+ err.statusCode=501;
276
+ throw err;
277
+ })?
255
278
 
256
279
  top
257
280
  = val:integer {
258
281
  (SELECT.limit || (SELECT.limit={})).rows = {val}
259
282
  }
260
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
+
261
291
  skip
262
292
  = val:integer {
263
- if (SELECT.limit && SELECT.limit.offset && SELECT.limit.offset.val) {
264
- val += SELECT.limit.offset.val
265
- }
266
- (SELECT.limit || (SELECT.limit={})).offset = {val}
293
+ _setLimitOffset(val)
267
294
  }
268
295
 
269
296
  search
270
- = p:search_clause {SELECT.search = p}
297
+ = p:search_clause {return p}
298
+ / o // Do not add search property for space only
271
299
 
272
300
  search_clause
273
301
  = p:( n:NOT? {return n?[n]:[]} )(
@@ -281,7 +309,7 @@
281
309
  {return p}
282
310
 
283
311
  filter
284
- = p:where_clause {SELECT.where = p}
312
+ = p:where_clause { return p }
285
313
 
286
314
  where_clause = p:( n:NOT? {return n?[n]:[]} )(
287
315
  OPEN xpr:where_clause CLOSE {p.push({xpr})}
@@ -326,18 +354,14 @@
326
354
  p:( n:NOT? { return n ? [n] : [] } )(
327
355
  OPEN xpr:inner_lambda CLOSE { p.push('(', ...xpr, ')') }
328
356
  / comp:comparison { p.push(...comp) }
357
+ / func:function { p.push(func) }
329
358
  / lambda:lambda { p.push(...lambda)}
330
359
  )
331
360
  ( ao:(AND/OR) more:inner_lambda { p.push(ao, ...more) } )*
332
361
  { return p }
333
362
 
334
363
  lambda_clause = prefix:identifier ":" inner:inner_lambda {
335
- for (const e of inner) {
336
- // remove the prefix identifier
337
- if (e.ref && e.ref[0] === prefix) e.ref.shift()
338
- }
339
-
340
- return inner
364
+ return _removeLambdaPrefix(prefix, inner)
341
365
  }
342
366
 
343
367
  any = "any" OPEN p:lambda_clause? CLOSE { return p }
@@ -374,7 +398,17 @@
374
398
  = operand (_ ("add" / "sub" / "mul" / "div" / "mod") _ operand)*
375
399
 
376
400
  operand "an operand"
377
- = 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}]} }
378
412
 
379
413
  ref "a reference"
380
414
  = head:identifier tail:( '/' n:identifier {return n})*
@@ -388,7 +422,7 @@
388
422
 
389
423
  val
390
424
  = val:(bool / date) {return {val}}
391
- / guid
425
+ / val:guid {return {val}}
392
426
  / val:number {return typeof val === 'number' ? {val} : { val, literal:'number' }}
393
427
  / val:string {return {val}}
394
428
 
@@ -401,14 +435,16 @@
401
435
  { return { list: any.replace(/"/g,'').split(',').map(ele => ({ val: ele })) } }
402
436
 
403
437
  function "a function call"
404
- = func:$[a-z]+ OPEN a:operand more:( COMMA o:operand {return o} )* CLOSE {
405
- if (strict && !(func in strict.functions)) exception("'"+ func +"' is an unknown function in OData URL spec (strict mode)")
406
- 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] }
407
443
  }
408
444
 
409
445
  boolish "a boolean function"
410
- = func:("contains"/"endswith"/"startswith") OPEN a:operand COMMA b:operand CLOSE
411
- { 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] }}
412
448
 
413
449
  NOT = o "NOT"i _ {return 'not'}
414
450
  AND = _ "AND"i _ {return 'and'}
@@ -424,7 +460,6 @@
424
460
  "aggregate" aggregateTrafo /
425
461
  "groupby" groupbyTrafo /
426
462
  "filter" filterTrafo /
427
- countTrafo /
428
463
 
429
464
  // REVISIT: All transformations below need improvment
430
465
  // and should supported by CAP
@@ -432,57 +467,77 @@
432
467
  "search" searchTrafo /
433
468
  "concat" concatTrafo /
434
469
  "compute" computeTrafo /
435
- "bottompercent" commonFuncTrafo /
436
- "bottomsum" commonFuncTrafo /
437
- "toppercent" commonFuncTrafo /
438
- "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
+ } /
439
485
  identityTrafo
440
486
  // customFunction
441
487
  )
442
488
 
443
489
  aggregateTrafo
444
- = 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
+ }
445
499
  aggregateItem
446
- = res:("$count" alias:asAlias { return { func: 'count', args: ['*'], as: alias } }
500
+ = res:("$count" as:asAlias { return { func: 'count', args: [{ val: 1 }], as } }
447
501
  / aggregateExpr
448
- ) {
449
- SELECT.columns = Array.isArray(SELECT.columns) ? SELECT.columns : []
450
- if (!SELECT.columns.find(_compareRefs(res))) SELECT.columns.push(res)
451
- return res
452
- }
502
+ ) { return res }
453
503
  aggregateExpr
454
504
  = path:(
455
505
  ref
456
506
  // / mathCalc - needs CAP support
457
507
  )
458
- func:aggregateWith aggregateFrom? alias:asAlias
459
- { return { func, args: [ path ], as: alias } }
508
+ func:aggregateWith aggregateFrom? as:asAlias
509
+ { return { func, args: [ path ], as } }
460
510
  / identifier OPEN aggregateExpr CLOSE // needs CAP support
461
511
  // / customAggregate // needs CAP support
462
512
  aggregateWith
463
- = _ "with" _ func:$[a-z]+ { return func; }
513
+ = _ "with" _ func:$[a-zA-Z]+ { return func.toLowerCase(); }
464
514
  aggregateFrom
465
515
  = _ "from" _ ref aggregateWith aggregateFrom? // needs CAP support
466
516
  asAlias
467
517
  = _ "as" _ alias:identifier { return alias; }
468
518
 
469
519
  groupbyTrafo
470
- = 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
471
528
  groupByElem
472
- = val:(rollupSpec / ref)
473
- { (SELECT.groupBy || (SELECT.groupBy = [])).push(val) }
529
+ = c:(rollupSpec / ref) { return c }
474
530
  rollupSpec // TODO fix this + add CAP support
475
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;}
476
532
 
477
- filterTrafo = OPEN o filter o CLOSE
478
-
479
- countTrafo
480
- = trafo:("topcount" / "bottomcount") OPEN o val:number o COMMA o ref:ref o CLOSE
481
- {
482
- const oredrObj = { ...ref, sort: trafo === 'topcount' ? 'desc' : 'asc' };
483
- SELECT.orderBy = SELECT.orderBy ? [...SELECT.orderBy, oredrObj] : [oredrObj];
484
- (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
485
538
  }
539
+ _apply.where = where
540
+ }) o CLOSE
486
541
 
487
542
 
488
543
  // All transformations below need improvment
@@ -493,14 +548,22 @@
493
548
  / filterTrafo (o COMMA expandTrafo)*
494
549
  ) o CLOSE
495
550
 
496
- 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
497
560
 
498
561
  concatTrafo = OPEN o apply (o COMMA o apply)+ o CLOSE
499
562
 
500
563
  computeTrafo = OPEN o computeExpr (o COMMA o computeExpr)* o CLOSE
501
564
  computeExpr = where_clause asAlias
502
565
 
503
- 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] }
504
567
 
505
568
  identityTrafo = "identity"
506
569
 
@@ -537,10 +600,16 @@
537
600
  = !bool !guid s:$([_a-zA-Z][_a-zA-Z0-9"."]*) {return s}
538
601
 
539
602
  guid
540
- = val:$([0-9a-zA-Z]+ "-" ([0-9a-zA-Z]+ "-"?)+) {return {val}}
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])
541
607
 
542
608
  segment
543
- = s:$([a-zA-Z0-9-"."_~!$&'()*+,;=:@]+)
609
+ = val:$([a-zA-Z0-9-"."_~!$&'()*+,;=:@]+){return {val}}
610
+
611
+ skiptokenChars
612
+ = $([a-zA-Z0-9-"."_~!$'()*+,;=:@"/""?"]+)
544
613
 
545
614
  //
546
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
  */
@@ -49,16 +62,12 @@ module.exports = {
49
62
  let cqn
50
63
  try {
51
64
  cqn = odata2cqn(url, options)
52
- } catch (e) {
53
- // REVISIT: additional try in catch isn't nice -> find better way
54
- // known gaps -> e.message is a stringified error -> use that
55
- // unknown errors -> e is the error to keep
56
- let err = e
57
- try {
58
- err = JSON.parse(e.message)
59
- } catch {
60
- /* nothing to do */
65
+ } catch (err) {
66
+ if (err.message === 'EXPAND_COUNT_UNSUPPORTED') {
67
+ throw getError(err.statusCode || 400, err.message)
61
68
  }
69
+
70
+ // TODO adjust this to behave like above
62
71
  err.message = 'Parsing URL failed with error: ' + err.message
63
72
  err.statusCode = err.statusCode || 400
64
73
  throw err
@@ -66,8 +75,7 @@ module.exports = {
66
75
 
67
76
  if (options.afterburner) cqn = options.afterburner(cqn)
68
77
 
69
- const query = cqn.SELECT.one ? SELECT.one(cqn.SELECT.from) : SELECT.from(cqn.SELECT.from)
70
- Object.assign(query.SELECT, cqn.SELECT)
78
+ const query = _2query(cqn)
71
79
 
72
80
  // REVISIT: _target vs __target, i.e., pseudo csn vs actual csn
73
81
  // DO NOT USE __target outside of libx/rest!!!