@sap/cds 6.0.4 → 6.1.0

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 (120) hide show
  1. package/CHANGELOG.md +128 -18
  2. package/apis/cds.d.ts +11 -7
  3. package/apis/log.d.ts +48 -0
  4. package/apis/ql.d.ts +72 -15
  5. package/bin/build/buildTaskHandler.js +5 -2
  6. package/bin/build/constants.js +4 -1
  7. package/bin/build/provider/buildTaskHandlerEdmx.js +11 -39
  8. package/bin/build/provider/buildTaskHandlerFeatureToggles.js +13 -32
  9. package/bin/build/provider/buildTaskHandlerInternal.js +56 -4
  10. package/bin/build/provider/buildTaskProviderInternal.js +22 -14
  11. package/bin/build/provider/hana/index.js +8 -7
  12. package/bin/build/provider/java/index.js +18 -8
  13. package/bin/build/provider/mtx/index.js +7 -4
  14. package/bin/build/provider/mtx/resourcesTarBuilder.js +64 -35
  15. package/bin/build/provider/mtx-extension/index.js +57 -0
  16. package/bin/build/provider/mtx-sidecar/index.js +46 -18
  17. package/bin/build/provider/nodejs/index.js +34 -13
  18. package/bin/deploy/to-hana/cfUtil.js +7 -2
  19. package/bin/serve.js +7 -4
  20. package/lib/compile/{index.js → cds-compile.js} +0 -0
  21. package/lib/compile/extend.js +15 -5
  22. package/lib/compile/minify.js +1 -15
  23. package/lib/compile/parse.js +1 -1
  24. package/lib/compile/resolve.js +2 -2
  25. package/lib/compile/to/srvinfo.js +6 -4
  26. package/lib/{deploy.js → dbs/cds-deploy.js} +7 -6
  27. package/lib/env/{index.js → cds-env.js} +1 -17
  28. package/lib/env/{requires.js → cds-requires.js} +24 -3
  29. package/lib/env/defaults.js +7 -1
  30. package/lib/env/schemas/cds-package.json +11 -0
  31. package/lib/env/schemas/cds-rc.json +605 -0
  32. package/lib/index.js +19 -16
  33. package/lib/log/{errors.js → cds-error.js} +1 -1
  34. package/lib/log/{index.js → cds-log.js} +0 -0
  35. package/lib/ql/SELECT.js +1 -1
  36. package/lib/ql/{index.js → cds-ql.js} +0 -0
  37. package/lib/req/context.js +35 -7
  38. package/lib/req/locale.js +5 -1
  39. package/lib/{serve → srv}/adapters.js +23 -19
  40. package/lib/{connect → srv}/bindings.js +0 -0
  41. package/lib/{connect/index.js → srv/cds-connect.js} +1 -1
  42. package/lib/{serve/index.js → srv/cds-serve.js} +0 -0
  43. package/lib/{serve → srv}/factory.js +1 -1
  44. package/lib/{serve/Service-api.js → srv/srv-api.js} +14 -6
  45. package/lib/{serve/Service-dispatch.js → srv/srv-dispatch.js} +3 -2
  46. package/lib/{serve/Service-handlers.js → srv/srv-handlers.js} +10 -0
  47. package/lib/{serve/Service-methods.js → srv/srv-methods.js} +10 -8
  48. package/lib/srv/srv-models.js +206 -0
  49. package/lib/{serve/Transaction.js → srv/srv-tx.js} +6 -1
  50. package/lib/utils/{tests.js → cds-test.js} +2 -2
  51. package/lib/utils/cds-utils.js +146 -0
  52. package/lib/utils/index.js +2 -145
  53. package/lib/utils/jest.js +43 -0
  54. package/lib/utils/resources/index.js +14 -24
  55. package/lib/utils/resources/tar.js +18 -41
  56. package/libx/_runtime/auth/index.js +13 -10
  57. package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +7 -19
  58. package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +1 -4
  59. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +1 -4
  60. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +1 -4
  61. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/delete.js +1 -4
  62. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +2 -2
  63. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +6 -19
  64. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +1 -4
  65. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +1 -1
  66. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +1 -4
  67. package/libx/_runtime/cds-services/adapter/odata-v4/to.js +38 -4
  68. package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +1 -3
  69. package/libx/_runtime/cds-services/services/utils/differ.js +4 -0
  70. package/libx/_runtime/cds-services/util/errors.js +1 -29
  71. package/libx/_runtime/common/i18n/messages.properties +2 -1
  72. package/libx/_runtime/common/perf/index.js +10 -15
  73. package/libx/_runtime/common/utils/cqn2cqn4sql.js +0 -1
  74. package/libx/_runtime/common/utils/entityFromCqn.js +8 -5
  75. package/libx/_runtime/common/utils/template.js +1 -1
  76. package/libx/_runtime/db/Service.js +2 -14
  77. package/libx/_runtime/db/expand/expandCQNToJoin.js +28 -25
  78. package/libx/_runtime/db/generic/input.js +4 -0
  79. package/libx/_runtime/db/sql-builder/SelectBuilder.js +37 -18
  80. package/libx/_runtime/extensibility/activate.js +47 -47
  81. package/libx/_runtime/extensibility/add.js +19 -13
  82. package/libx/_runtime/extensibility/addExtension.js +17 -13
  83. package/libx/_runtime/extensibility/defaults.js +25 -30
  84. package/libx/_runtime/extensibility/linter/allowlist_checker.js +373 -0
  85. package/libx/_runtime/extensibility/linter/annotations_checker.js +113 -0
  86. package/libx/_runtime/extensibility/linter/checker_base.js +20 -0
  87. package/libx/_runtime/extensibility/linter/namespace_checker.js +180 -0
  88. package/libx/_runtime/extensibility/linter.js +32 -0
  89. package/libx/_runtime/extensibility/push.js +78 -21
  90. package/libx/_runtime/extensibility/service.js +29 -12
  91. package/libx/_runtime/extensibility/token.js +56 -0
  92. package/libx/_runtime/extensibility/validation.js +6 -9
  93. package/libx/_runtime/fiori/generic/new.js +0 -11
  94. package/libx/_runtime/hana/Service.js +0 -1
  95. package/libx/_runtime/hana/conversion.js +12 -1
  96. package/libx/_runtime/hana/customBuilder/CustomFunctionBuilder.js +4 -3
  97. package/libx/_runtime/hana/customBuilder/CustomSelectBuilder.js +5 -0
  98. package/libx/_runtime/hana/pool.js +6 -10
  99. package/libx/_runtime/hana/search2Contains.js +0 -5
  100. package/libx/_runtime/hana/search2cqn4sql.js +1 -0
  101. package/libx/_runtime/messaging/outbox/utils.js +1 -1
  102. package/libx/_runtime/messaging/service.js +11 -6
  103. package/libx/_runtime/remote/utils/data.js +5 -0
  104. package/libx/_runtime/sqlite/Service.js +0 -1
  105. package/libx/odata/afterburner.js +79 -2
  106. package/libx/odata/cqn2odata.js +9 -7
  107. package/libx/odata/grammar.pegjs +157 -76
  108. package/libx/odata/index.js +9 -3
  109. package/libx/odata/parser.js +1 -1
  110. package/libx/odata/utils.js +39 -5
  111. package/libx/rest/RestAdapter.js +1 -2
  112. package/libx/rest/middleware/delete.js +4 -5
  113. package/libx/rest/middleware/parse.js +3 -2
  114. package/package.json +3 -3
  115. package/server.js +1 -1
  116. package/srv/extensibility-service.cds +6 -3
  117. package/srv/model-provider.cds +3 -1
  118. package/srv/model-provider.js +84 -104
  119. package/srv/mtx.js +7 -1
  120. package/libx/_runtime/cds-services/adapter/odata-v4/Dispatcher.js +0 -240
@@ -1,6 +1,7 @@
1
1
  const cds = require('../cds')
2
2
  const queued = require('./common-utils/queued')
3
3
  const OutboxService = require('./Outbox')
4
+ const ExtendedModels = require('../../../lib/srv/srv-models')
4
5
 
5
6
  const appId = require('./common-utils/appId')
6
7
 
@@ -76,9 +77,17 @@ class MessagingService extends OutboxService {
76
77
  return super.init()
77
78
  }
78
79
 
79
- emit(event, data, headers) {
80
+ async emit(event, data, headers) {
80
81
  const _msg = typeof event === 'object' ? event : { event, data, headers }
81
- const msg = _msg instanceof cds.Event ? _msg : new cds.Event(this.message4(_msg))
82
+ if (_msg instanceof cds.Event) return super.emit(_msg)
83
+ if (_msg.inbound && !cds.context) {
84
+ cds.context = { tenant: _msg.tenant, user: cds.User.privileged }
85
+ if (cds.model) {
86
+ const ctx = cds.context
87
+ ctx.model = await ExtendedModels.model4(ctx.tenant, ctx.features)
88
+ }
89
+ }
90
+ const msg = new cds.Event(this.message4(_msg))
82
91
  return super.emit(msg)
83
92
  }
84
93
 
@@ -112,10 +121,6 @@ class MessagingService extends OutboxService {
112
121
 
113
122
  message4(msg) {
114
123
  const _msg = { ...msg }
115
- if (msg.inbound && !cds.context) {
116
- // REVISIT: why are all inbound messages executed with privileged user?
117
- cds.context = { tenant: msg.tenant, user: cds.User.privileged }
118
- }
119
124
  _msg.event = _warnAndStripTopicPrefix(_msg.event, this.LOG)
120
125
  if (!_msg.headers) _msg.headers = {}
121
126
  if (!_msg.inbound) {
@@ -114,6 +114,7 @@ const _convertValue = (ieee754Compatible, exponentialDecimals) => (value, elemen
114
114
 
115
115
  return value
116
116
  }
117
+ const _PT = ([hh, mm, ss]) => `PT${hh}H${mm}M${ss}S`
117
118
 
118
119
  const _convertPayloadValue = (value, element) => {
119
120
  const type = _elementType(element)
@@ -121,6 +122,10 @@ const _convertPayloadValue = (value, element) => {
121
122
  // see https://www.odata.org/documentation/odata-version-2-0/json-format/
122
123
  if (value == null) return value
123
124
  switch (type) {
125
+ case 'cds.Time':
126
+ return value.match(/^(PT)([H,M,S,0-9])*$/) ? value : _PT(value.split(':'))
127
+ case 'cds.Decimal':
128
+ return typeof value === 'string' ? value : new String(value)
124
129
  case 'cds.Date':
125
130
  case 'cds.DateTime':
126
131
  return `/Date(${new Date(value).getTime()})/`
@@ -70,7 +70,6 @@ module.exports = class SQLiteDatabase extends DatabaseService {
70
70
  }
71
71
 
72
72
  _registerBeforeHandlers() {
73
- this._ensureModel && this.before('*', this._ensureModel)
74
73
  this.before(['CREATE', 'UPDATE'], '*', this._input) // > has to run before rewrite
75
74
  this.before(['CREATE', 'READ', 'UPDATE', 'DELETE'], '*', this._rewrite)
76
75
 
@@ -111,6 +111,77 @@ function addRefToWhereIfNecessary(where, entity) {
111
111
  return 1
112
112
  }
113
113
 
114
+ function getResolvedElement(entity, { ref }) {
115
+ const element = entity.elements[ref[0]]
116
+ if (element && element.isAssociation && ref.length > 1) {
117
+ return getResolvedElement(element._target, { ref: ref.slice(1) })
118
+ } else if (element && element._isStructured) {
119
+ return getResolvedElement(element, { ref: ref.slice(1) })
120
+ }
121
+
122
+ return element
123
+ }
124
+
125
+ function _processWhere(where, entity) {
126
+ for (let i = 0; i < where.length; i++) {
127
+ const ref = where[i]
128
+ const val = where[i + 2]
129
+
130
+ if (ref === '(' || ref === ')' || ref === 'and' || ref === 'or' || ref === 'not' || val === 'not' || ref.func) {
131
+ continue
132
+ }
133
+ if (ref.xpr) {
134
+ _processWhere(ref.xpr, entity)
135
+ continue
136
+ }
137
+
138
+ let valIndex = -1
139
+ let refIndex = -1
140
+ if (typeof val === 'object') {
141
+ if (val.val !== undefined) valIndex = i + 2
142
+ if (val.ref != undefined) refIndex = i + 2
143
+ }
144
+ if (typeof ref === 'object') {
145
+ if (ref.val !== undefined) valIndex = i
146
+ if (ref.ref != undefined) refIndex = i
147
+ }
148
+
149
+ // no need to check ref = ref or val = val, if no ref or no val exists we can't do anything
150
+ if (valIndex === refIndex || valIndex === -1 || refIndex == -1) continue
151
+
152
+ const realRef = where[refIndex]
153
+ const element = getResolvedElement(entity, realRef)
154
+
155
+ if (element) {
156
+ i += 2
157
+ where[valIndex].val = _convertVal(element, where[valIndex].val)
158
+ }
159
+ }
160
+ }
161
+
162
+ function _convertVal(element, value) {
163
+ if (value === null) return value
164
+ switch (element._type) {
165
+ case 'cds.Integer':
166
+ // eslint-disable-next-line no-case-declarations
167
+ const n = Number(value)
168
+ if (Number.isSafeInteger(n)) return n
169
+ throw new Error('Not a valid integer') // TODO
170
+ case 'cds.String':
171
+ case 'cds.LargeString':
172
+ case 'cds.Decimal':
173
+ case 'cds.DecimalFloat':
174
+ case 'cds.Double':
175
+ case 'cds.Integer64':
176
+ if (typeof value === 'string') return value
177
+ return String(value)
178
+ case 'cds.Boolean':
179
+ return typeof value === 'string' ? value === 'true' : value
180
+ default:
181
+ return value
182
+ }
183
+ }
184
+
114
185
  function _processSegments(from, model, namespace) {
115
186
  const { ref } = from
116
187
 
@@ -142,8 +213,7 @@ function _processSegments(from, model, namespace) {
142
213
  .join(',')})`
143
214
  base.where.push({ ref: [key] }, '=', { val })
144
215
  } else {
145
- const val =
146
- element._type === 'cds.Integer' ? Number(seg) : element._type === 'cds.Boolean' ? seg === 'true' : seg
216
+ const val = _convertVal(element, seg)
147
217
  base.where.push({ ref: [key] }, '=', { val })
148
218
  }
149
219
  ref[i] = null
@@ -168,6 +238,7 @@ function _processSegments(from, model, namespace) {
168
238
  keyCount += addRefToWhereIfNecessary(ref[i].where, current)
169
239
  _resolveAliasesInXpr(ref[i].where, current)
170
240
  _resolveAliasInParams(params, current)
241
+ _processWhere(ref[i].where, current)
171
242
  }
172
243
 
173
244
  _addDefaultParams(ref[i], current)
@@ -201,6 +272,7 @@ function _processSegments(from, model, namespace) {
201
272
  _resolveAliasInParams(params, current)
202
273
  // in case of Foo(1), params will be {} (before addRefToWhereIfNecessary was called)
203
274
  if (!Object.keys(params).length) params = where2obj(ref[i].where)
275
+ _processWhere(ref[i].where, current)
204
276
  _checkAllKeysProvided(params, current)
205
277
  }
206
278
  } else if ({ action: 1, function: 1 }[current.kind]) {
@@ -221,6 +293,7 @@ function _processSegments(from, model, namespace) {
221
293
  if (ref[i].where) {
222
294
  keyCount += addRefToWhereIfNecessary(ref[i].where, current)
223
295
  _resolveAliasesInXpr(ref[i].where, current)
296
+ _processWhere(ref[i].where, current)
224
297
  }
225
298
  } else if (current._isStructured) {
226
299
  // > nested property
@@ -349,6 +422,10 @@ function _4service(service) {
349
422
  */
350
423
  const { one, current, target } = _processSegments(from, model, namespace)
351
424
 
425
+ if (cqn.SELECT.where) {
426
+ _processWhere(cqn.SELECT.where, root)
427
+ }
428
+
352
429
  // one?
353
430
  if (one) cqn.SELECT.one = true
354
431
 
@@ -114,11 +114,11 @@ const _odataV2Func = (func, args) => {
114
114
  }
115
115
  }
116
116
 
117
- const _format = (cur, element, target, kind, isLambda) => {
118
- if (typeof cur !== 'object') return encodeURIComponent(formatVal(cur, element, target, kind))
117
+ const _format = (cur, elementName, target, kind, isLambda) => {
118
+ if (typeof cur !== 'object') return encodeURIComponent(formatVal(cur, elementName, target, kind))
119
119
  if (hasValidProps(cur, 'ref'))
120
120
  return encodeURIComponent(isLambda ? [LAMBDA_VARIABLE, ...cur.ref].join('/') : cur.ref[0].id || cur.ref.join('/'))
121
- if (hasValidProps(cur, 'val')) return encodeURIComponent(formatVal(cur.val, element, target, kind))
121
+ if (hasValidProps(cur, 'val')) return encodeURIComponent(formatVal(cur.val, elementName, target, kind))
122
122
  if (hasValidProps(cur, 'xpr')) return `(${_xpr(cur.xpr, target, kind, isLambda)})`
123
123
  // REVISIT: How to detect the types for all functions?
124
124
  if (hasValidProps(cur, 'func', 'args')) {
@@ -180,7 +180,8 @@ function _xpr(expr, target, kind, isLambda) {
180
180
  res.push(OPERATORS[cur] || cur.toLowerCase())
181
181
  }
182
182
  } else {
183
- const formatted = _format(cur, res[res.length - 2], target, kind, isLambda)
183
+ const ref = expr[i - 2]
184
+ const formatted = _format(cur, ref?.ref && (ref.ref.length ? ref.ref : ref.ref[0]), target, kind, isLambda)
184
185
  if (formatted !== undefined) res.push(formatted)
185
186
  }
186
187
  }
@@ -406,7 +407,7 @@ function $orderBy(orderBy) {
406
407
  return '$orderby=' + res.join(',')
407
408
  }
408
409
 
409
- function parseSearch(search) {
410
+ function parseSearch(search, kind) {
410
411
  const res = []
411
412
 
412
413
  for (const cur of search) {
@@ -417,7 +418,8 @@ function parseSearch(search) {
417
418
 
418
419
  if (hasValidProps(cur, 'val')) {
419
420
  // search term must not be formatted
420
- res.push(`"${encodeURIComponent(cur.val)}"`)
421
+ if (kind === 'odata-v2') res.push(`${encodeURIComponent(cur.val)}`)
422
+ else res.push(`"${encodeURIComponent(cur.val)}"`)
421
423
  }
422
424
 
423
425
  if (typeof cur === 'string') {
@@ -433,7 +435,7 @@ function parseSearch(search) {
433
435
  }
434
436
 
435
437
  function $search(search, kind) {
436
- const expr = parseSearch(search).join('%20').replace('(%20', '(').replace('%20)', ')')
438
+ const expr = parseSearch(search, kind).join('%20').replace('(%20', '(').replace('%20)', ')')
437
439
 
438
440
  if (expr) {
439
441
  // odata-v2 may support custom query option "search"
@@ -27,6 +27,7 @@
27
27
  //
28
28
  // ---------- JavaScript Helpers -------------
29
29
  {
30
+
30
31
  const $ = Object.assign
31
32
  const { strict, minimal } = options
32
33
  const stack = []
@@ -243,7 +244,7 @@
243
244
 
244
245
  ODataRelativeURI // Note: case-sensitive!
245
246
  = '/'? (p:path { SELECT = p })
246
- ( o"?"o QueryOption ( o'&'o QueryOption )* )? o "?"? o {
247
+ ( o"?"o (QueryOption ( o'&'o QueryOption )*)? )? o {
247
248
  if (count) {
248
249
  // columns set because of $count: ignore $select, $expand, $top, $skip, $orderby
249
250
  // REVISIT: don't ignore query options but throw bad request (as okra did)?
@@ -334,46 +335,39 @@
334
335
  QueryOption = option:ExpandOption { if(option && option.apply) SELECT.apply = option.apply} /
335
336
  "$skiptoken=" o skiptoken /
336
337
  format /
337
- custom
338
+ custom /
339
+ aliasedParamEqualsVal
338
340
  // @OData spec for $expand:
339
341
  // "Allowed system query options are $filter, $select, $orderby, $skip, $top, $count, $search, $expand and $apply (http://docs.oasis-open.org/odata/odata-data-aggregation-ext/v4.0/cs02/odata-data-aggregation-ext-v4.0-cs02.html#_The_expand_Transformation)."
340
342
  ExpandOption =
341
343
  "$select=" o select ( COMMA select )* /
342
- "$expand=" o expand ( COMMA expand )* /
343
- "$filter=" o f:filter{SELECT.where = f} /
344
+ "$expand=" o expand ( COMMA expand )* expandCount? /
345
+ "$filter=" o f:filter { SELECT.where = f } /
344
346
  "$orderby=" o o:orderby ( COMMA o2:orderby{_setOrderBy(o2)} )* {_setOrderBy(o,true)} /
345
- "$top=" o val:top{(SELECT.limit || (SELECT.limit={})).rows = {val}} /
346
- "$skip=" o val:skip{_setLimitOffset(val)} /
347
- "$search=" o s:search {if (s) SELECT.search = s} /
347
+ "$top=" o val:top { (SELECT.limit || (SELECT.limit={})).rows = {val} } /
348
+ "$skip=" o val:skip { _setLimitOffset(val) } /
349
+ "$search=" o s:search { if (s) SELECT.search = s } /
348
350
  "$count=" o count /
349
- "$apply=" o trafos:transformations {return trafos}
351
+ "$apply=" o trafos:transformations { return trafos }
350
352
 
351
353
 
352
354
  select
353
- = col:('*'/ref) {
355
+ = col:('*' / ref) {
354
356
  SELECT.columns = Array.isArray(SELECT.columns) ? SELECT.columns : []
355
357
  if (!SELECT.columns.find(_compareRefs(col))) SELECT.columns.push(col)
356
358
  return col
357
359
  }
358
360
 
359
- //REVISIT: per OData spec $apply should be also supported inside of $expand
360
- expand =
361
- (
362
- c:('*'/ref) {
363
- const col = c === '*' ? {} : c
364
- col.expand = '*'
365
- if (!Array.isArray(SELECT.expand)) SELECT.expand = []
366
- if (!SELECT.expand.find(_compareRefs(col))) SELECT.expand.push(col)
367
- return col
368
- }
369
- )
370
- ( // --- nested query options, if any
371
- (OPEN {
372
- stack.push (SELECT)
373
- SELECT = SELECT.expand[SELECT.expand.length-1]
374
- SELECT.expand = '*' // by default expand everything
375
- })(
376
- expandOptions:( o ";"? o option:ExpandOption{return option})*
361
+ expandCount
362
+ = "/$count" {
363
+ const err = new Error("EXPAND_COUNT_UNSUPPORTED");
364
+ err.statusCode=501;
365
+ throw err;
366
+ }
367
+
368
+ expandQueryOptions
369
+ = (
370
+ expandOptions:(option:ExpandOption o ";"? { return option })*
377
371
  {
378
372
  if (expandOptions.find(option => option && option.apply !== undefined)) {
379
373
  const err = new Error("EXPAND_APPLY_UNSUPPORTED");
@@ -390,29 +384,50 @@
390
384
  if (Array.isArray(SELECT.expand) && SELECT.expand.indexOf('*') === -1) SELECT.expand.unshift('*')
391
385
  }
392
386
  }
393
- )(CLOSE {
387
+ )
388
+
389
+ expandQueryOption
390
+ = (OPEN {
391
+ stack.push (SELECT)
392
+ SELECT = SELECT.expand[SELECT.expand.length-1]
393
+ SELECT.expand = '*' // by default expand everything
394
+ })
395
+ expandQueryOptions?
396
+ (CLOSE {
394
397
  SELECT = stack.pop()
395
398
  })
396
- )? // --- end of nested query options
397
- ( COMMA expand )?
398
- ("/$count" {
399
- const err = new Error("EXPAND_COUNT_UNSUPPORTED");
400
- err.statusCode=501;
401
- throw err;
402
- })?
399
+
400
+
401
+ //REVISIT: per OData spec $apply should be also supported inside of $expand
402
+ expand
403
+ = (
404
+ c:('*' / ref) {
405
+ const col = c === '*' ? {} : c
406
+ col.expand = '*'
407
+ if (!Array.isArray(SELECT.expand)) SELECT.expand = []
408
+ if (!SELECT.expand.find(_compareRefs(col))) SELECT.expand.push(col)
409
+ return col
410
+ }
411
+ )
412
+ expandQueryOption?
413
+
403
414
  top
404
- = val:integer {return val}
415
+ = val:integer { return val }
416
+
405
417
  skiptoken
406
418
  = val:integer? skiptoken:skiptokenChars? {
407
419
  // REVISIT ignore non-numeric $skiptoken as not supported by CQN
408
420
  if (skiptoken) return
409
421
  _setLimitOffset(val)
410
422
  }
423
+
411
424
  skip
412
- = val:integer {return val}
425
+ = val:integer { return val }
426
+
413
427
  search
414
428
  = p:search_clause {return p}
415
429
  / o // Do not add search property for space only
430
+
416
431
  search_clause
417
432
  = p:( n:NOT? {return n?[n]:[]} )(
418
433
  OPEN xpr:search_clause CLOSE {p.push({xpr})}
@@ -422,9 +437,11 @@
422
437
  val:word {p.push({val})}
423
438
  )
424
439
  )( ao:(AND/OR/AND_SPACE) more:search_clause {p.push(ao,...more)} )*
425
- {return p}
440
+ { return p }
441
+
426
442
  filter
427
443
  = p:where_clause { return p }
444
+
428
445
  where_clause = p:( n:NOT? {return n?[n]:[]} )(
429
446
  OPEN xpr:where_clause CLOSE {p.push({xpr})}
430
447
  / comp:comparison {p.push(...comp)}
@@ -437,8 +454,10 @@
437
454
  }
438
455
  / func:boolish {p.push(func)}
439
456
  / val:bool {p.push({val})}
457
+ / list:listFilter {p.push(...list)}
440
458
  )( ao:(AND/OR) more:where_clause {p.push(ao,...more)} )*
441
- {return p}
459
+ { return p }
460
+
442
461
  lambda =
443
462
  nav:( n:identifier {return[n]} ) '/' ( n:identifier '/' {nav.push(n)} )*
444
463
  xpr:(
@@ -467,28 +486,48 @@
467
486
  }
468
487
  )
469
488
  { return xpr }
489
+
470
490
  inner_lambda =
471
491
  p:( n:NOT? { return n ? [n] : [] } )(
472
492
  OPEN xpr:inner_lambda CLOSE { p.push('(', ...xpr, ')') }
473
493
  / comp:comparison { p.push(...comp) }
474
494
  / func:function { p.push(func) }
475
495
  / lambda:lambda { p.push(...lambda)}
496
+ / list:listFilter {p.push(...list)}
476
497
  )
477
498
  ( ao:(AND/OR) more:inner_lambda { p.push(ao, ...more) } )*
478
499
  { return p }
479
- lambda_clause = prefix:identifier ":" inner:inner_lambda {
480
- return _removeLambdaPrefix(prefix, inner)
481
- }
482
- any = "any" OPEN p:lambda_clause? CLOSE { return p }
483
- all = "all" OPEN p:lambda_clause CLOSE { return p }
500
+
501
+ lambda_clause =
502
+ prefix:identifier ":" inner:inner_lambda {
503
+ return _removeLambdaPrefix(prefix, inner)
504
+ }
505
+
506
+ any =
507
+ "any" OPEN p:lambda_clause? CLOSE { return p }
508
+
509
+ all =
510
+ "all" OPEN p:lambda_clause CLOSE { return p }
511
+
484
512
  orderby
485
- = ref:(lambda{const err = new Error("ORDERBY_LAMBDA_UNSUPPORTED");err.statusCode=501;throw err;}/function/ref) sort:( _ s:$("asc"/"desc") {return s})? {
513
+ = ref:(
514
+ lambda {
515
+ const err = new Error("ORDERBY_LAMBDA_UNSUPPORTED");
516
+ err.statusCode=501;
517
+ throw err;
518
+ } /
519
+ function /
520
+ ref
521
+ )
522
+ sort:( _ s:$("asc" / "desc") { return s })? {
486
523
  // TODO: Lambda support
487
524
  const appendObj = $(ref, sort && {sort});
488
525
  return appendObj;
489
526
  }
527
+
490
528
  count
491
529
  = val:bool { if(val) SELECT.count = true }
530
+
492
531
  transformations
493
532
  = mainTransformation:trafo additionalTransformation:("/" t2:trafo {
494
533
  return t2
@@ -523,6 +562,8 @@
523
562
 
524
563
  custom
525
564
  = [a-zA-Z0-9-_.~]+ "=" [^&]*
565
+ aliasedParam "an aliased parameter (@param)" = "@" i:identifier { return "@" + i }
566
+ aliasedParamEqualsVal "@alias=value" = a:aliasedParam "=" v:([^&]*) {return a + "=" + v}
526
567
 
527
568
  format = "$format=" f:$([^&]*) {
528
569
  if (f.toLowerCase() !== "json") {
@@ -531,17 +572,25 @@
531
572
  throw err;
532
573
  }
533
574
  }
575
+
534
576
  //
535
577
  // ---------- Expressions ------------
536
- comparison "a comparison"
537
- = a:operand _ o:$("eq"/"ne"/"lt"/"gt"/"le"/"ge") _ b:operand {
538
- const op = { eq:'=', ne:'!=', lt:'<', gt:'>', le:'<=', ge:'>=' }[o]||o
578
+ comparison
579
+ = a:operand _ o:$("eq" / "ne" / "lt" / "gt" / "le" / "ge") _ b:operand {
580
+ const op = { eq:'=', ne:'!=', lt:'<', gt:'>', le:'<=', ge:'>=' }[o] || o
539
581
  return [ a, op, b ]
540
582
  }
583
+
584
+ listFilter
585
+ = a:operand _ "in" _ b:listRoundBrackets {
586
+ return [ a, "in", b ]
587
+ }
541
588
  mathCalc
542
589
  = operand (_ ("add" / "sub" / "mul" / "div" / "mod") _ operand)*
543
- operand "an operand"
590
+
591
+ operand
544
592
  = navigationCount / function / val / ref / jsonObject / jsonArray / list
593
+
545
594
  navigationCount "navigation with $count"
546
595
  = navigationPath:(head:identifier key:(OPEN keyArgs:args CLOSE {return keyArgs;})? '/' {
547
596
  if (key) {
@@ -551,6 +600,7 @@
551
600
  return head;
552
601
  })+ count: '$count'
553
602
  { return {func: 'count', as: '$count', args: [{ref: navigationPath}]} }
603
+
554
604
  ref "a reference"
555
605
  = head:identifier tail:( '/' n:identifier {return n})*
556
606
  {
@@ -559,34 +609,56 @@
559
609
  }
560
610
  return { ref:[ head, ...tail ] }
561
611
  }
612
+
562
613
  val
563
614
  = val:(bool / date) {return {val}}
564
615
  / val:guid {return {val}}
565
616
  / val:number {return typeof val === 'number' ? {val} : { val, literal:'number' }}
566
617
  / val:string {return {val}}
567
618
  / val:binary {return {val}}
568
- jsonObject = val:$("{" (jsonObject / [^}])* "}") {return {val}}
569
- jsonArray = val:$("[" o "]" / "[" o "{" (jsonArray / [^\]])* "]") {return {val}}
570
-
571
- list
619
+ / val:aliasedParam {return {val}}
620
+ / null
621
+
622
+ null "null" = "null" {return {val: null }}
623
+
624
+ jsonObject "a json object"
625
+ = val:$("{" (jsonObject / [^}])* "}") {return {val}}
626
+
627
+ jsonArray "a json array"
628
+ = val:$("[" o "]" / "[" o "{" (jsonArray / [^\]])* "]") {return {val}}
629
+
630
+ list "a list"
572
631
  = "[" any:$([^\]])* "]" // > needs improvment
573
632
  { return { list: any.replace(/"/g,'').split(',').map(ele => ({ val: ele })) } }
574
633
 
575
- function "a function call"
576
- = func:$[a-zA-Z]+ OPEN a:operand more:( COMMA o:operand {return o} )* CLOSE {
634
+ listRoundBrackets "a list"
635
+ = OPEN list:(val1:val val2:("," v:val { return v })* { return [val1, ...val2] }) CLOSE // > needs improvment
636
+ {
637
+ return { list }
638
+ }
639
+
640
+
641
+ functionName "a function name"
642
+ = $[a-zA-Z]+
643
+
644
+ function
645
+ = func:functionName OPEN fnArgs:functionArgs CLOSE {
577
646
  if (strict && !(func.toLowerCase() in strict.functions)) {
578
647
  throw Object.assign(new Error(`"${func}" is an unknown function in OData URL spec (strict mode)`), { statusCode: 400 })
579
648
  }
580
- return { func: func.toLowerCase(), args:[a,...more] }
649
+ return { func: func.toLowerCase(), args:[fnArgs.a,...fnArgs.more] }
581
650
  }
582
651
 
583
- boolish "a boolean function"
652
+ functionArgs
653
+ = a:operand more:( COMMA o:operand {return o} )* {return { a, more }}
654
+
655
+ boolish
584
656
  = func:("contains"i/"endswith"i/"startswith"i) OPEN a:operand COMMA b:operand CLOSE
585
657
  { return { func: func.toLowerCase(), args:[a,b] }}
586
658
 
587
659
  NOT = o "NOT"i _ {return 'not'}
588
660
  AND = _ "AND"i _ {return 'and'}
589
- AND_SPACE = _ {return 'and'}
661
+ AND_SPACE = _ {return 'and'}
590
662
  OR = _ "OR"i _ {return 'or'}
591
663
 
592
664
 
@@ -622,7 +694,7 @@
622
694
  }
623
695
  return {aggregate: [{ func, args }]}
624
696
  } /
625
- identity: identityTrafo{return identity}
697
+ identity: identityTrafo {return identity}
626
698
  // customFunction
627
699
  )
628
700
 
@@ -663,7 +735,11 @@
663
735
  groupByElem
664
736
  = c:(rollupSpec / ref) { return c }
665
737
  rollupSpec // TODO fix this + add CAP support
666
- = 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;}
738
+ = rollup:("rollup" OPEN o ('$all' / ref) (o COMMA ref)+ o CLOSE) {
739
+ const err = new Error("Rollup in groupby is not supported yet.");
740
+ err.statusCode=501;
741
+ throw err;
742
+ }
667
743
 
668
744
  filterTrafo = OPEN o where:(where:filter{
669
745
  return where
@@ -707,20 +783,21 @@
707
783
  //
708
784
  // ---------- Literals -----------
709
785
 
710
- bool = b:("true" / "false") { return b === 'true'}
786
+ bool "a boolean"
787
+ = b:("true" / "false") { return b === 'true'}
711
788
 
712
- string "Edm.String"
713
- = "'" s:$("''"/[^'])* "'"
789
+ string "a single quoted string" // "Edm.String"
790
+ = "'" s:$("''" / [^'])* "'" // 'A user''s story'
714
791
  {return s.replace(/''/g,"'")}
715
792
 
716
- doubleQuotedString
793
+ doubleQuotedString "a doubled quoted string"
717
794
  = '"' s:$('\\"'/[^"])* '"'
718
795
  {return s.replace(/\\\\/g,"\\").replace(/\\"/g,'"')}
719
796
 
720
- word
797
+ word "a string"
721
798
  = $([^ \t\n()"&;]+)
722
799
 
723
- date
800
+ date "a date"
724
801
  = s:$( [0-9]+"-"[0-9][0-9]"-"[0-9][0-9] // date
725
802
  ( "T"[0-9][0-9]":"[0-9][0-9](":"[0-9][0-9]("."[0-9]+)?)? // time
726
803
  ( "Z" / (("+" / "-")[0-9][0-9]":"[0-9][0-9]) )? // timezone (Z or +-hh:mm)
@@ -730,19 +807,23 @@
730
807
  return s
731
808
  }
732
809
 
733
- number
734
- = s:$( [+-]? [0-9]+ ("."[0-9]+)? ("e"[0-9]+)? ) { return safeNumber(s) }
810
+ // to avoid 123-123-123 being matched as number and showing error for "-123-123"
811
+ endsWithMinus
812
+ = [0-9]+ "-"
813
+
814
+ number "a number"
815
+ = !endsWithMinus s:$( [+-]? [0-9]+ ("."[0-9]+)? ("e"[0-9]+)? ) { return safeNumber(s) }
735
816
 
736
- integer
817
+ integer "an integer"
737
818
  = s:$( [+-]? [0-9]+ ) { return parseInt(s) }
738
819
 
739
- identifier
820
+ identifier "an identifier"
740
821
  = !bool !guid s:$([_a-zA-Z][_a-zA-Z0-9"."]*) { return s }
741
822
 
742
- guid
823
+ guid "a guid"
743
824
  = $( hex16 hex16 "-"? hex16 "-"? hex16 "-"? hex16 "-"? hex16 hex16 hex16 )
744
825
 
745
- hex16
826
+ hex16 "a hex value"
746
827
  = $( [0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F] )
747
828
 
748
829
  segment // > everything except / and ?
@@ -751,7 +832,7 @@
751
832
  skiptokenChars
752
833
  = $( [a-zA-Z0-9-"."_~!$'()*+,;=:@"/""?"]+ )
753
834
 
754
- binary // > url-safe base64
835
+ binary "a binary" // > url-safe base64
755
836
  = "binary'" s:$([a-zA-Z0-9-_]+ ("=="/"=")?) "'" { return standardBase64(s) }
756
837
 
757
838
  //
@@ -766,8 +847,8 @@
766
847
  //
767
848
  // ---------- Whitespaces -----------
768
849
 
769
- o "optional whitespaces" = $[ \t\n]*
770
- _ "mandatory whitespaces" = $[ \t\n]+
850
+ o "an optional whitespace" = $[ \t\n]*
851
+ _ "a whitespace" = $[ \t\n]+
771
852
 
772
853
  //
773
- // ------------------------------------
854
+ // ------------------------------------