@sap/cds 9.0.4 → 9.2.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 (77) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/bin/deploy.js +29 -0
  3. package/bin/serve.js +1 -5
  4. package/lib/compile/etc/csv.js +11 -6
  5. package/lib/compile/for/lean_drafts.js +29 -7
  6. package/lib/compile/load.js +8 -5
  7. package/lib/compile/to/hdbtabledata.js +1 -1
  8. package/lib/dbs/cds-deploy.js +5 -34
  9. package/lib/env/cds-env.js +2 -1
  10. package/lib/env/cds-requires.js +4 -1
  11. package/lib/env/defaults.js +0 -11
  12. package/lib/env/schemas/cds-rc.js +218 -6
  13. package/lib/index.js +38 -38
  14. package/lib/log/cds-error.js +12 -11
  15. package/lib/log/format/json.js +1 -1
  16. package/lib/ql/SELECT.js +31 -0
  17. package/lib/ql/resolve.js +1 -1
  18. package/lib/req/context.js +1 -1
  19. package/lib/req/request.js +1 -1
  20. package/lib/req/validate.js +17 -19
  21. package/lib/srv/cds.Service.js +18 -28
  22. package/lib/srv/middlewares/auth/ias-auth.js +29 -2
  23. package/lib/srv/middlewares/auth/jwt-auth.js +11 -1
  24. package/lib/srv/middlewares/auth/xssec.js +1 -1
  25. package/lib/srv/srv-models.js +1 -1
  26. package/lib/srv/srv-tx.js +2 -2
  27. package/lib/utils/cds-utils.js +35 -2
  28. package/lib/utils/csv-reader.js +1 -1
  29. package/lib/utils/inflect.js +2 -2
  30. package/lib/utils/tar.js +60 -23
  31. package/lib/utils/version.js +18 -0
  32. package/libx/_runtime/cds.js +1 -1
  33. package/libx/_runtime/common/aspects/any.js +1 -23
  34. package/libx/_runtime/common/generic/crud.js +1 -3
  35. package/libx/_runtime/common/generic/input.js +113 -52
  36. package/libx/_runtime/common/generic/sorting.js +1 -1
  37. package/libx/_runtime/common/generic/temporal.js +0 -6
  38. package/libx/_runtime/common/utils/draft.js +1 -1
  39. package/libx/_runtime/common/utils/entityFromCqn.js +1 -1
  40. package/libx/_runtime/common/utils/propagateForeignKeys.js +1 -1
  41. package/libx/_runtime/common/utils/resolveView.js +2 -2
  42. package/libx/_runtime/common/utils/structured.js +2 -2
  43. package/libx/_runtime/common/utils/templateProcessor.js +0 -5
  44. package/libx/_runtime/common/utils/vcap.js +1 -1
  45. package/libx/_runtime/fiori/lean-draft.js +529 -143
  46. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +3 -2
  47. package/libx/_runtime/messaging/service.js +1 -1
  48. package/libx/_runtime/remote/utils/client.js +2 -1
  49. package/libx/common/assert/utils.js +2 -12
  50. package/libx/common/utils/streaming.js +4 -9
  51. package/libx/http/location.js +1 -0
  52. package/libx/odata/ODataAdapter.js +47 -43
  53. package/libx/odata/index.js +1 -1
  54. package/libx/odata/middleware/batch.js +6 -2
  55. package/libx/odata/middleware/create.js +1 -1
  56. package/libx/odata/middleware/error.js +27 -17
  57. package/libx/odata/middleware/operation.js +15 -21
  58. package/libx/odata/middleware/stream.js +1 -1
  59. package/libx/odata/parse/afterburner.js +22 -8
  60. package/libx/odata/parse/cqn2odata.js +16 -10
  61. package/libx/odata/parse/grammar.peggy +185 -134
  62. package/libx/odata/parse/parser.js +1 -1
  63. package/libx/odata/utils/index.js +1 -36
  64. package/libx/odata/utils/metadata.js +34 -1
  65. package/libx/odata/utils/odataBind.js +2 -1
  66. package/libx/odata/utils/result.js +22 -20
  67. package/libx/queue/index.js +7 -4
  68. package/libx/rest/RestAdapter.js +1 -2
  69. package/libx/rest/middleware/create.js +5 -2
  70. package/package.json +2 -2
  71. package/server.js +1 -1
  72. package/bin/deploy/to-hana.js +0 -1
  73. package/lib/utils/check-version.js +0 -9
  74. package/lib/utils/unit.js +0 -19
  75. package/libx/_runtime/cds-services/util/assert.js +0 -181
  76. package/libx/_runtime/types/api.js +0 -129
  77. package/libx/common/assert/validation.js +0 -109
@@ -177,7 +177,9 @@
177
177
 
178
178
  const _handleApply = (cqn, apply) => {
179
179
  let newCqn = _convertApply({ from: cqn.from }, apply)
180
-
180
+ //Delete apply as it was successfully converted & to not spill into final query
181
+ delete cqn.apply
182
+ //Normal query options have to be applied after convert, as _convertApply calls itself recursively and else normal options might be redundant in nested SELECTs
181
183
  if (Array.isArray(newCqn)) {
182
184
  for (let i = 0; i < newCqn.length; i++) {
183
185
  newCqn[i] = _addNormalQueryOptions(newCqn[i], cqn)
@@ -185,99 +187,104 @@
185
187
  } else {
186
188
  newCqn = _addNormalQueryOptions(newCqn, cqn)
187
189
  }
188
-
190
+ if (newCqn.SELECT?.recurse && cqn.where) {
191
+ const where = cqn.where
192
+ delete cqn.where
193
+ const columns = newCqn.SELECT.columns
194
+ delete newCqn.SELECT.columns
195
+ newCqn = { SELECT: { from: newCqn, where, columns } }
196
+ }
189
197
  return newCqn
190
198
  }
191
199
 
192
- const _addNormalQueryOptions = (cqn, topCqn) => {
193
- if (!topCqn) return cqn
194
- if (topCqn.apply) delete topCqn.apply
200
+ function isQueryWithAggregation(cqn) {
201
+ return cqn.SELECT.groupBy ||
202
+ cqn.SELECT.columns?.some(col => col && typeof col === 'object' && 'func' in col)
203
+ }
195
204
 
196
- let newCqn = {}
205
+ // topCqn is not allowed to be mutated as that would spill in case of concat transformations
206
+ const _addNormalQueryOptions = (cqn, topCqn) => {
207
+ const QUERY_WITH_AGGREGATION = isQueryWithAggregation(cqn);
197
208
  if (
198
209
  (topCqn.columns && topCqn.columns[0].as === '$count') ||
199
- (cqn.SELECT.where && topCqn.where) ||
210
+ //In QUERY_WITH_AGGREGATION topCqn.where is a having and thus no further nesting needed
211
+ (!QUERY_WITH_AGGREGATION && topCqn.where && (cqn.SELECT.where || cqn.SELECT.limit)) ||
200
212
  (cqn.SELECT.limit && topCqn.limit) ||
201
213
  (cqn.SELECT.orderBy && topCqn.orderBy) ||
202
214
  (cqn.SELECT.search && topCqn.search)
203
215
  ) {
204
- newCqn.from = cqn
205
- } else newCqn = cqn.SELECT
206
-
207
-
208
- // Collect & Select Aggregated Columns
209
- const aggregatedColumns = newCqn.columns && newCqn.columns.length
210
- ? newCqn.columns.filter(col => col && typeof col === 'object' && 'func' in col)
211
- : []
212
- newCqn.columns = aggregatedColumns.length ? aggregatedColumns : []
213
-
214
- if (newCqn.groupBy && newCqn.groupBy.length) {
215
- // Expand & Select Columns from GroupBy
216
-
217
- const groupByColumns = newCqn.groupBy.reduce((expandedGroupByColumns, column) => {
218
- if (column.ref && column.ref.length > 1) {
219
- const columnToExpand = { ref: [...column.ref] }
220
- if (column.expand && column.expand.length) columnToExpand.expand = [...column.expand]
221
- if (column.as) columnToExpand.as = column.as
222
- _expand(expandedGroupByColumns, columnToExpand, true)
223
- } else expandedGroupByColumns.push({ ...column })
224
- return expandedGroupByColumns
225
- }, [])
226
-
227
- newCqn.columns ??= []
228
- newCqn.columns.push(...groupByColumns)
229
- }
230
-
231
- for (const queryOption in topCqn) {
232
- if (queryOption === 'columns') {
233
- const mergedColumns = []
234
-
235
- // Add aggregation columns
236
- if (newCqn.columns.length) mergedColumns.push(...newCqn.columns)
237
-
238
- // Use columns from parsed CQN if there's no grouping
239
- if (!newCqn.groupBy || !newCqn.groupBy.length) {
240
- for (const column of topCqn.columns)
241
- if (!mergedColumns.some(_compareRefs(column))) mergedColumns.push(column)
216
+ //Have topCqn as new cqn but assign existing cqn as SELECT.from.SELECT
217
+ const newCqn = Object.assign({}, topCqn)
218
+ newCqn.from = cqn;
219
+ cqn = {SELECT: newCqn}
220
+ } else {
221
+ let from = cqn.SELECT.from;
222
+ let columns = cqn.SELECT.columns;
223
+ //Needed in case topCqn.where is a having and not a where
224
+ let originalWhere = cqn.SELECT.where;
225
+ // cqn.SELECT must be the base as else concat queries would be overridden
226
+ cqn.SELECT = Object.assign(cqn.SELECT, topCqn)
227
+ //Reapplied to ensure SELECT.from.SELECT is not lost
228
+ cqn.SELECT.from = from;
229
+ //Reapplied as topCqn.columns have special handling further down below to be merged into existing columns
230
+ cqn.SELECT.columns = columns;
231
+
232
+ // When a group by already exists or a column is aggregated and a where follows it is a having
233
+ // In the scenario where a topCqn.where exists and the query is not aggregating, topCqn.where is used as the cqn.SELECT.where.
234
+ // Those cannot conflict as the top if statement ensures that in this case a SELECT.from.SELECT is created
235
+ if (QUERY_WITH_AGGREGATION && topCqn.where) {
236
+ const newHaving = topCqn.where
237
+ .map(_remapFunc(cqn.SELECT.columns))
238
+ //In a query with only aggregation groupBy would be undefined causing a crash
239
+ .map(_replaceNullRef(cqn.SELECT.groupBy ?? []));
240
+ if (!cqn.SELECT.having) {
241
+ cqn.SELECT.having = newHaving
242
242
  } else {
243
- _mergeColumnForests(mergedColumns, topCqn.columns)
243
+ //The existing cqn.SELECT.having was already mapped in _convertApply
244
+ cqn.SELECT.having = [
245
+ {xpr: cqn.SELECT.having},
246
+ 'and',
247
+ {xpr: newHaving}
248
+ ];
244
249
  }
250
+ //Correct back to original where
251
+ cqn.SELECT.where = originalWhere;
252
+ }
253
+ }
245
254
 
246
- newCqn.columns = mergedColumns
255
+ if (topCqn.columns) {
256
+ const mergedColumns = [];
257
+ if (!cqn.SELECT.columns) {
258
+ cqn.SELECT.columns = [];
259
+ }
260
+ // Add aggregation columns
261
+ if (cqn.SELECT.columns.length) {
262
+ mergedColumns.push(...cqn.SELECT.columns)
263
+ }
247
264
 
248
- } else if (queryOption !== 'from') {
249
- newCqn[queryOption] = topCqn[queryOption]
265
+ // Use columns from parsed CQN if there's no grouping
266
+ if (!cqn.SELECT.groupBy || !cqn.SELECT.groupBy.length) {
267
+ for (const column of topCqn.columns) {
268
+ if (!mergedColumns.some(_compareRefs(column))) {
269
+ mergedColumns.push(column)
270
+ }
271
+ }
272
+ } else {
273
+ _mergeColumnForests(mergedColumns, topCqn.columns)
250
274
  }
275
+ // topCqn is later applied to cqn.SELECT and thus the merged columns have to be assigned to them
276
+ cqn.SELECT.columns = mergedColumns
251
277
  }
252
278
 
253
- if (newCqn.groupBy && newCqn.where) {
254
- // Replace Where by Having, to be evaluated after GroupBy
255
-
256
- newCqn.having = newCqn.where
257
- .map(_remapFunc(newCqn.columns))
258
- .map(_replaceNullRef(newCqn.groupBy))
259
- delete newCqn.where
260
- }
261
-
262
- if (!newCqn.columns.length) delete newCqn.columns
263
- return { SELECT: newCqn }
279
+ if (!cqn.SELECT.columns?.length) delete cqn.SELECT.columns
280
+ return cqn
264
281
  }
265
282
 
266
283
  const _convertApply = (cqn, apply) => {
267
- if (!apply) return
268
-
269
- if (cqn.apply) delete cqn.apply
284
+ delete cqn.apply
270
285
  if (apply.identity && cqn.from.SELECT) cqn = cqn.from.SELECT
271
- if (
272
- apply.apply ||
273
- (apply.where && cqn.where) ||
274
- (apply.search && cqn.search) ||
275
- (apply.limit && cqn.limit) ||
276
- (apply.orderBy && cqn.orderBy)
277
- ) {
278
- cqn.from = { SELECT: { ...cqn } }
279
- }
280
-
286
+ if (apply.apply) cqn.from = { SELECT: { ...cqn } }
287
+
281
288
  const _toplevels = cqn => {
282
289
  cqn.recurse = { ref: ['parent'] }
283
290
 
@@ -370,36 +377,60 @@
370
377
  if (apply.search) cqn.search = apply.search
371
378
  if (apply.limit) cqn.limit = apply.limit
372
379
  if (apply.orderBy) cqn.orderBy = apply.orderBy
380
+
373
381
  if (apply.groupBy)
374
382
  cqn.groupBy = apply.groupBy.reduce((groupBy, column) => {
375
383
  if (!groupBy.some(_compareRefs(column))) groupBy.push(column)
376
384
  return groupBy
377
385
  }, [])
378
386
 
379
- if (apply.aggregate && apply.aggregate.length) cqn.columns = [...(cqn.columns || []), ...apply.aggregate]
380
- if (cqn.groupBy && cqn.groupBy.length) cqn.columns = [...(cqn.columns || []), ...cqn.groupBy]
381
-
382
- if (apply.apply && cqn.from) _convertApply(cqn.from.SELECT, apply.apply)
387
+ if (apply.aggregate && apply.aggregate.length) {
388
+ cqn.columns = (cqn.columns || []).concat(apply.aggregate);
389
+ }
390
+ if (cqn.groupBy && cqn.groupBy.length) {
391
+ const groupByColumns = cqn.groupBy.reduce((expandedGroupByColumns, column) => {
392
+ if (column.ref && column.ref.length > 1) {
393
+ const columnToExpand = { ref: [...column.ref] }
394
+ if (column.expand && column.expand.length) columnToExpand.expand = [...column.expand]
395
+ if (column.as) columnToExpand.as = column.as
396
+ _expand(expandedGroupByColumns, columnToExpand, true)
397
+ } else expandedGroupByColumns.push({ ...column })
398
+ return expandedGroupByColumns
399
+ }, [])
400
+ cqn.columns = (cqn.columns || []).concat(groupByColumns);
401
+ }
402
+ if (apply.having) {
403
+ cqn.having = apply.having
404
+ .map(_remapFunc(cqn.columns))
405
+ .map(_replaceNullRef(cqn.groupBy ?? []))
406
+ }
383
407
 
384
- if (apply.concat && Array.isArray(apply.concat)) {
385
- const additionalQueries = []
386
-
408
+ if (apply.apply) {
409
+ _convertApply(cqn.from.SELECT, apply.apply)
410
+ }
411
+
412
+ if (Array.isArray(apply.concat)) {
413
+ const concatenatedQueries = []
414
+ // Refrain from nesting if no property other than 'from' was transferred from apply to cqn
415
+ // With concat the current level SELECT is the base for multiple queries, and is used as the FROM clause in those,
416
+ // thus it has to be wrapped in a SELECT for a proper CQN structure, like SELECT.from.SELECT.<cqn-props>
417
+ // However if concat is the first transformation the SELECT just has the from and thus SELECT.from.SELECT nesting can be avoided
418
+ const newFrom = Object.keys(cqn).length === 1
419
+ ? cqn.from
420
+ : { SELECT: cqn }
387
421
  for (let select of apply.concat) {
388
- select.from = Object.keys(cqn).length === 1
389
- ? cqn.from
390
- : { SELECT: cqn }
391
-
392
- const nextCqn = _convertApply(select, select.apply)
393
-
394
-
395
- if (Array.isArray(nextCqn)) additionalQueries.push(...nextCqn)
396
- else additionalQueries.push(nextCqn)
422
+ select.from = newFrom
423
+ if (select.apply) {
424
+ const nextCqn = _convertApply(select, select.apply)
425
+ if (Array.isArray(nextCqn)) {
426
+ concatenatedQueries.push(...nextCqn)
427
+ } else {
428
+ concatenatedQueries.push(nextCqn)
429
+ }
430
+ }
397
431
  }
398
-
399
- cqn = additionalQueries
432
+ return concatenatedQueries
400
433
  }
401
-
402
- if (Array.isArray(cqn)) return cqn
403
434
  return { SELECT: cqn }
404
435
  }
405
436
 
@@ -527,7 +558,7 @@
527
558
  = "$count" {count = true}
528
559
  / rv:$("$ref"/"$value") {return !TECHNICAL_OPTS.includes(rv) && {from: {ref: [rv]}}}
529
560
  / head:(
530
- (identifier filter:(OPEN CLOSE/OPEN args CLOSE)? !segment) / val:segment{return [val]}
561
+ (identifier filter:(OPEN CLOSE/OPEN guardedargs CLOSE)? !segment) / val:segment{return [val]}
531
562
  )? tail:((s:"/" {return s;}) path?)? {
532
563
  tail = tail && tail[1]
533
564
  if (!head && !tail) {
@@ -575,6 +606,14 @@
575
606
  return args
576
607
  }
577
608
 
609
+ guardedargs
610
+ = ref:ref o"="o val:guardedval more:( COMMA guardedargs )? {
611
+ const args = [ ref, '=', val ]
612
+ if (more) args.push ('and', ...more[1])
613
+ return args
614
+ }
615
+ / val:guardedval {return [val]}
616
+
578
617
  // `path` cannot be used in hierarchy functions, hence use a simpler definition
579
618
  simplePath
580
619
  = i:identifier filter:(OPEN CLOSE/OPEN a:args CLOSE{ return a })? tail:("/" s:simplePath{ return s })*{
@@ -798,38 +837,6 @@
798
837
  count
799
838
  = val:bool { if(val) SELECT.count = true }
800
839
 
801
- transformations
802
- = mainTransformation:trafo additionalTransformation:("/" t2:trafo {
803
- return t2
804
- })* {
805
- if(mainTransformation === undefined) return
806
- additionalTransformation = (Array.isArray(additionalTransformation)) ? additionalTransformation : [additionalTransformation]
807
- // Loop through additionalTransformation
808
- // Loop through each element, add it to current level, if element is already part of result, increase level
809
- for(let trafos of additionalTransformation) {
810
- for(const trafo in trafos) {
811
- if (trafo === 'limit' && trafos.limit && mainTransformation.limit && mainTransformation.limit.offset && trafos.limit.rows)
812
- mainTransformation.limit.rows = trafos.limit.rows
813
- else if(
814
- mainTransformation[trafo] ||
815
- (trafo === 'groupBy' && (mainTransformation.where || mainTransformation.search)) ||
816
- (trafo === 'aggregate' && 'groupBy' in mainTransformation && !('groupBy' in trafos))
817
- ) {
818
- let _apply = mainTransformation
819
- mainTransformation = { apply: _apply }
820
- if (trafo === 'limit' && trafos[trafo].offset && _apply.limit && _apply.limit.offset && _apply.limit.offset.val)
821
- trafos[trafo].offset += _apply.limit.offset.val
822
-
823
- _apply = mainTransformation
824
- _apply[trafo] = trafos[trafo]
825
- } else {
826
- mainTransformation[trafo] = trafos[trafo]
827
- }
828
- }
829
- }
830
- return {apply: mainTransformation}
831
- }
832
-
833
840
  deltaToken = "$deltatoken=" o token:([^&]*) { return token }
834
841
 
835
842
  valList "value with double-quoted string"
@@ -840,8 +847,8 @@
840
847
 
841
848
  aliasedParamVal = val / jsonObject / jsonArray / "[" list:innerListParam "]" { return { list } }
842
849
 
843
- custom = k:$([[a-zA-Z0-9-_.~!\[\]]+) "="? v:$([^&]*)? {
844
- _custom(k, v)
850
+ custom = k:$([a-zA-Z0-9_.~!\[\]\-]+) "="? v:$([^&]*)? {
851
+ return _custom(k, v)
845
852
  }
846
853
 
847
854
  aliasedParam "an aliased parameter (@param)" = "@" i:identifier { return "@" + i }
@@ -904,7 +911,7 @@
904
911
  }
905
912
  return { ref:[ head, ...tail ] }
906
913
  }
907
-
914
+
908
915
  val
909
916
  = val:bool {return {val}}
910
917
  / val:date {return {val}}
@@ -916,6 +923,10 @@
916
923
  / val:aliasedParam {return {val}}
917
924
  / null
918
925
 
926
+ guardedval
927
+ = val:val {return val}
928
+ / val:simpleword {throw Object.assign(new Error(`Invalid value: ${val}`), { statusCode: 400 })};
929
+
919
930
  null "null" = "null" {return {val: null }}
920
931
 
921
932
  // REVISIT why not JSON.parse() and return JS object?
@@ -965,8 +976,45 @@
965
976
  //
966
977
  // ---------- Transformations ------------
967
978
  // Odata spec: http://docs.oasis-open.org/odata/odata-data-aggregation-ext/v4.0/odata-data-aggregation-ext-v4.0.html
968
-
969
- trafo
979
+
980
+ transformations
981
+ = mainTransformation:transformation additionalTransformation:("/" t2:transformation {
982
+ return t2
983
+ })* {
984
+ if(mainTransformation === undefined) return
985
+ additionalTransformation = Array.isArray(additionalTransformation) ? additionalTransformation : [additionalTransformation]
986
+ // Loop through each element, add it to current level, if element is already part of result, increase level
987
+ for(let trafos of additionalTransformation) {
988
+ for(const transformation in trafos) {
989
+ if (transformation === 'where' && (mainTransformation.groupBy || mainTransformation.aggregate) && !mainTransformation.having) {
990
+ //When a group by or aggregate preceed a where, the where is a having
991
+ mainTransformation.having = trafos[transformation]
992
+ }
993
+ else if (transformation === 'limit' && mainTransformation.limit?.offset && trafos.limit.rows) {
994
+ //To avoid nesting in case first trafo is skip second is top, than they should be merged
995
+ //Inverse case first top than skip should not be nested as second skip will redcue the amount of returned items
996
+ mainTransformation.limit.rows = trafos.limit.rows
997
+ }
998
+ else if (transformation === 'limit' && mainTransformation.limit?.offset?.val && trafos.limit.offset?.val) {
999
+ //To avoid nesting in case of two subsequent skips as they can be merged
1000
+ mainTransformation.limit.offset.val += trafos.limit.offset.val
1001
+ }
1002
+ else if(
1003
+ mainTransformation[transformation] ||
1004
+ //Case like aggregation follows with a / after groupBy - if they should be on the same level the aggregation is part of groupBy
1005
+ (transformation === 'aggregate' && mainTransformation.groupBy && !trafos.groupBy)
1006
+ ) {
1007
+ mainTransformation = { apply: mainTransformation }
1008
+ mainTransformation[transformation] = trafos[transformation]
1009
+ } else {
1010
+ mainTransformation[transformation] = trafos[transformation]
1011
+ }
1012
+ }
1013
+ }
1014
+ return {apply: mainTransformation}
1015
+ }
1016
+
1017
+ transformation
970
1018
  = (
971
1019
  "aggregate" agg:aggregateTrafo{return agg} /
972
1020
  "groupby" group:groupbyTrafo{return group} /
@@ -1170,10 +1218,13 @@
1170
1218
 
1171
1219
  doubleQuotedString "a doubled quoted string"
1172
1220
  = '"' s:$('\\"' / '\\\\' / [^"])* '"'
1173
- {return s.replace(/\\\\/g,"\\").replace(/\\"/g,'"')}
1221
+ {return s.replace(/\\(["\\])/g, '$1')}
1174
1222
 
1175
1223
  word "a string"
1176
1224
  = $([^ \t\n()"&;]+)
1225
+
1226
+ simpleword "a string and/or number"
1227
+ = $([a-zA-Z0-9]+)
1177
1228
 
1178
1229
  time "a time"
1179
1230
  = $([0-9][0-9]":"[0-9][0-9]":"[0-9][0-9])
@@ -1199,7 +1250,7 @@
1199
1250
  = s:$( [+-]? [0-9]+ ) { return parseInt(s) }
1200
1251
 
1201
1252
  identifier "an identifier"
1202
- = !bool !guid s:$([_a-zA-Z][_a-zA-Z0-9"."\u0041-\u005A\u0061-\u007A\u00AA\u00B5\u00BA\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u0527\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA\u05F0-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u08A0\u08A2-\u08AC\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0977\u0979-\u097F\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C33\u0C35-\u0C39\u0C3D\u0C58\u0C59\u0C60\u0C61\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D60\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F4\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1877\u1880-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191C\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19C1-\u19C7\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1CE9-\u1CEC\u1CEE-\u1CF1\u1CF5\u1CF6\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2183\u2184\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005\u3006\u3031-\u3035\u303B\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FCC\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA697\uA6A0-\uA6E5\uA717-\uA71F\uA722-\uA788\uA78B-\uA78E\uA790-\uA793\uA7A0-\uA7AA\uA7F8-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA80-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uABC0-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]*) { return s }
1253
+ = !bool !guid s:$([_a-zA-Z][_a-zA-Z0-9"."\u00AA\u00B5\u00BA\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u0527\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA\u05F0-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u08A0\u08A2-\u08AC\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0977\u0979-\u097F\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C33\u0C35-\u0C39\u0C3D\u0C58\u0C59\u0C60\u0C61\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D60\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F4\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1877\u1880-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191C\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19C1-\u19C7\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1CE9-\u1CEC\u1CEE-\u1CF1\u1CF5\u1CF6\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2183\u2184\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005\u3006\u3031-\u3035\u303B\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FCC\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA697\uA6A0-\uA6E5\uA717-\uA71F\uA722-\uA788\uA78B-\uA78E\uA790-\uA793\uA7A0-\uA7AA\uA7F8-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA80-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uABC0-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]*) { return s }
1203
1254
 
1204
1255
  guid "a guid"
1205
1256
  = $( hex16 hex16 "-"? hex16 "-"? hex16 "-"? hex16 "-"? hex16 hex16 hex16 )
@@ -1214,7 +1265,7 @@
1214
1265
  = $( [a-zA-Z0-9-"."_~!$'()*+,;=:@"/""?"]+ )
1215
1266
 
1216
1267
  binary "a binary" // > url-safe base64
1217
- = "binary'" s:$([a-zA-Z0-9-_]+ ("=="/"=")?) "'" { return cds.env.features.base64_binaries ? standardBase64(s) : Buffer.from(s, 'base64') }
1268
+ = "binary'" s:$([a-zA-Z0-9_\-]+ ("=="/"=")?) "'" { return cds.env.features.base64_binaries ? standardBase64(s) : Buffer.from(s, 'base64') }
1218
1269
 
1219
1270
  //
1220
1271
  // ---------- Punctuation ----------