@sap/cds 9.0.3 → 9.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.
@@ -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,102 @@
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
+ newCqn = { SELECT: { from: newCqn, where } }
194
+ }
189
195
  return newCqn
190
196
  }
191
197
 
192
- const _addNormalQueryOptions = (cqn, topCqn) => {
193
- if (!topCqn) return cqn
194
- if (topCqn.apply) delete topCqn.apply
198
+ function isQueryWithAggregation(cqn) {
199
+ return cqn.SELECT.groupBy ||
200
+ cqn.SELECT.columns?.some(col => col && typeof col === 'object' && 'func' in col)
201
+ }
195
202
 
196
- let newCqn = {}
203
+ // topCqn is not allowed to be mutated as that would spill in case of concat transformations
204
+ const _addNormalQueryOptions = (cqn, topCqn) => {
205
+ const QUERY_WITH_AGGREGATION = isQueryWithAggregation(cqn);
197
206
  if (
198
207
  (topCqn.columns && topCqn.columns[0].as === '$count') ||
199
- (cqn.SELECT.where && topCqn.where) ||
208
+ //In QUERY_WITH_AGGREGATION topCqn.where is a having and thus no further nesting needed
209
+ (!QUERY_WITH_AGGREGATION && topCqn.where && (cqn.SELECT.where || cqn.SELECT.limit)) ||
200
210
  (cqn.SELECT.limit && topCqn.limit) ||
201
211
  (cqn.SELECT.orderBy && topCqn.orderBy) ||
202
212
  (cqn.SELECT.search && topCqn.search)
203
213
  ) {
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)
214
+ //Have topCqn as new cqn but assign existing cqn as SELECT.from.SELECT
215
+ const newCqn = Object.assign({}, topCqn)
216
+ newCqn.from = cqn;
217
+ cqn = {SELECT: newCqn}
218
+ } else {
219
+ let from = cqn.SELECT.from;
220
+ let columns = cqn.SELECT.columns;
221
+ //Needed in case topCqn.where is a having and not a where
222
+ let originalWhere = cqn.SELECT.where;
223
+ // cqn.SELECT must be the base as else concat queries would be overridden
224
+ cqn.SELECT = Object.assign(cqn.SELECT, topCqn)
225
+ //Reapplied to ensure SELECT.from.SELECT is not lost
226
+ cqn.SELECT.from = from;
227
+ //Reapplied as topCqn.columns have special handling further down below to be merged into existing columns
228
+ cqn.SELECT.columns = columns;
229
+
230
+ // When a group by already exists or a column is aggregated and a where follows it is a having
231
+ // In the scenario where a topCqn.where exists and the query is not aggregating, topCqn.where is used as the cqn.SELECT.where.
232
+ // Those cannot conflict as the top if statement ensures that in this case a SELECT.from.SELECT is created
233
+ if (QUERY_WITH_AGGREGATION && topCqn.where) {
234
+ const newHaving = topCqn.where
235
+ .map(_remapFunc(cqn.SELECT.columns))
236
+ //In a query with only aggregation groupBy would be undefined causing a crash
237
+ .map(_replaceNullRef(cqn.SELECT.groupBy ?? []));
238
+ if (!cqn.SELECT.having) {
239
+ cqn.SELECT.having = newHaving
242
240
  } else {
243
- _mergeColumnForests(mergedColumns, topCqn.columns)
241
+ //The existing cqn.SELECT.having was already mapped in _convertApply
242
+ cqn.SELECT.having = [
243
+ {xpr: cqn.SELECT.having},
244
+ 'and',
245
+ {xpr: newHaving}
246
+ ];
244
247
  }
248
+ //Correct back to original where
249
+ cqn.SELECT.where = originalWhere;
250
+ }
251
+ }
245
252
 
246
- newCqn.columns = mergedColumns
253
+ if (topCqn.columns) {
254
+ const mergedColumns = [];
255
+ if (!cqn.SELECT.columns) {
256
+ cqn.SELECT.columns = [];
257
+ }
258
+ // Add aggregation columns
259
+ if (cqn.SELECT.columns.length) {
260
+ mergedColumns.push(...cqn.SELECT.columns)
261
+ }
247
262
 
248
- } else if (queryOption !== 'from') {
249
- newCqn[queryOption] = topCqn[queryOption]
263
+ // Use columns from parsed CQN if there's no grouping
264
+ if (!cqn.SELECT.groupBy || !cqn.SELECT.groupBy.length) {
265
+ for (const column of topCqn.columns) {
266
+ if (!mergedColumns.some(_compareRefs(column))) {
267
+ mergedColumns.push(column)
268
+ }
269
+ }
270
+ } else {
271
+ _mergeColumnForests(mergedColumns, topCqn.columns)
250
272
  }
273
+ // topCqn is later applied to cqn.SELECT and thus the merged columns have to be assigned to them
274
+ cqn.SELECT.columns = mergedColumns
251
275
  }
252
276
 
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 }
277
+ if (!cqn.SELECT.columns?.length) delete cqn.SELECT.columns
278
+ return cqn
264
279
  }
265
280
 
266
281
  const _convertApply = (cqn, apply) => {
267
- if (!apply) return
268
-
269
- if (cqn.apply) delete cqn.apply
282
+ delete cqn.apply
270
283
  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
-
284
+ if (apply.apply) cqn.from = { SELECT: { ...cqn } }
285
+
281
286
  const _toplevels = cqn => {
282
287
  cqn.recurse = { ref: ['parent'] }
283
288
 
@@ -370,36 +375,60 @@
370
375
  if (apply.search) cqn.search = apply.search
371
376
  if (apply.limit) cqn.limit = apply.limit
372
377
  if (apply.orderBy) cqn.orderBy = apply.orderBy
378
+
373
379
  if (apply.groupBy)
374
380
  cqn.groupBy = apply.groupBy.reduce((groupBy, column) => {
375
381
  if (!groupBy.some(_compareRefs(column))) groupBy.push(column)
376
382
  return groupBy
377
383
  }, [])
378
384
 
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)
385
+ if (apply.aggregate && apply.aggregate.length) {
386
+ cqn.columns = (cqn.columns || []).concat(apply.aggregate);
387
+ }
388
+ if (cqn.groupBy && cqn.groupBy.length) {
389
+ const groupByColumns = cqn.groupBy.reduce((expandedGroupByColumns, column) => {
390
+ if (column.ref && column.ref.length > 1) {
391
+ const columnToExpand = { ref: [...column.ref] }
392
+ if (column.expand && column.expand.length) columnToExpand.expand = [...column.expand]
393
+ if (column.as) columnToExpand.as = column.as
394
+ _expand(expandedGroupByColumns, columnToExpand, true)
395
+ } else expandedGroupByColumns.push({ ...column })
396
+ return expandedGroupByColumns
397
+ }, [])
398
+ cqn.columns = (cqn.columns || []).concat(groupByColumns);
399
+ }
400
+ if (apply.having) {
401
+ cqn.having = apply.having
402
+ .map(_remapFunc(cqn.columns))
403
+ .map(_replaceNullRef(cqn.groupBy ?? []))
404
+ }
383
405
 
384
- if (apply.concat && Array.isArray(apply.concat)) {
385
- const additionalQueries = []
386
-
406
+ if (apply.apply) {
407
+ _convertApply(cqn.from.SELECT, apply.apply)
408
+ }
409
+
410
+ if (Array.isArray(apply.concat)) {
411
+ const concatenatedQueries = []
412
+ // Refrain from nesting if no property other than 'from' was transferred from apply to cqn
413
+ // With concat the current level SELECT is the base for multiple queries, and is used as the FROM clause in those,
414
+ // thus it has to be wrapped in a SELECT for a proper CQN structure, like SELECT.from.SELECT.<cqn-props>
415
+ // However if concat is the first transformation the SELECT just has the from and thus SELECT.from.SELECT nesting can be avoided
416
+ const newFrom = Object.keys(cqn).length === 1
417
+ ? cqn.from
418
+ : { SELECT: cqn }
387
419
  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)
420
+ select.from = newFrom
421
+ if (select.apply) {
422
+ const nextCqn = _convertApply(select, select.apply)
423
+ if (Array.isArray(nextCqn)) {
424
+ concatenatedQueries.push(...nextCqn)
425
+ } else {
426
+ concatenatedQueries.push(nextCqn)
427
+ }
428
+ }
397
429
  }
398
-
399
- cqn = additionalQueries
430
+ return concatenatedQueries
400
431
  }
401
-
402
- if (Array.isArray(cqn)) return cqn
403
432
  return { SELECT: cqn }
404
433
  }
405
434
 
@@ -527,7 +556,7 @@
527
556
  = "$count" {count = true}
528
557
  / rv:$("$ref"/"$value") {return !TECHNICAL_OPTS.includes(rv) && {from: {ref: [rv]}}}
529
558
  / head:(
530
- (identifier filter:(OPEN CLOSE/OPEN args CLOSE)? !segment) / val:segment{return [val]}
559
+ (identifier filter:(OPEN CLOSE/OPEN guardedargs CLOSE)? !segment) / val:segment{return [val]}
531
560
  )? tail:((s:"/" {return s;}) path?)? {
532
561
  tail = tail && tail[1]
533
562
  if (!head && !tail) {
@@ -575,6 +604,14 @@
575
604
  return args
576
605
  }
577
606
 
607
+ guardedargs
608
+ = ref:ref o"="o val:guardedval more:( COMMA guardedargs )? {
609
+ const args = [ ref, '=', val ]
610
+ if (more) args.push ('and', ...more[1])
611
+ return args
612
+ }
613
+ / val:guardedval {return [val]}
614
+
578
615
  // `path` cannot be used in hierarchy functions, hence use a simpler definition
579
616
  simplePath
580
617
  = i:identifier filter:(OPEN CLOSE/OPEN a:args CLOSE{ return a })? tail:("/" s:simplePath{ return s })*{
@@ -798,38 +835,6 @@
798
835
  count
799
836
  = val:bool { if(val) SELECT.count = true }
800
837
 
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
838
  deltaToken = "$deltatoken=" o token:([^&]*) { return token }
834
839
 
835
840
  valList "value with double-quoted string"
@@ -840,8 +845,8 @@
840
845
 
841
846
  aliasedParamVal = val / jsonObject / jsonArray / "[" list:innerListParam "]" { return { list } }
842
847
 
843
- custom = k:$([[a-zA-Z0-9-_.~!\[\]]+) "="? v:$([^&]*)? {
844
- _custom(k, v)
848
+ custom = k:$([a-zA-Z0-9_.~!\[\]\-]+) "="? v:$([^&]*)? {
849
+ return _custom(k, v)
845
850
  }
846
851
 
847
852
  aliasedParam "an aliased parameter (@param)" = "@" i:identifier { return "@" + i }
@@ -904,7 +909,7 @@
904
909
  }
905
910
  return { ref:[ head, ...tail ] }
906
911
  }
907
-
912
+
908
913
  val
909
914
  = val:bool {return {val}}
910
915
  / val:date {return {val}}
@@ -916,6 +921,10 @@
916
921
  / val:aliasedParam {return {val}}
917
922
  / null
918
923
 
924
+ guardedval
925
+ = val:val {return val}
926
+ / val:simpleword {throw Object.assign(new Error(`Invalid value: ${val}`), { statusCode: 400 })};
927
+
919
928
  null "null" = "null" {return {val: null }}
920
929
 
921
930
  // REVISIT why not JSON.parse() and return JS object?
@@ -965,8 +974,45 @@
965
974
  //
966
975
  // ---------- Transformations ------------
967
976
  // Odata spec: http://docs.oasis-open.org/odata/odata-data-aggregation-ext/v4.0/odata-data-aggregation-ext-v4.0.html
968
-
969
- trafo
977
+
978
+ transformations
979
+ = mainTransformation:transformation additionalTransformation:("/" t2:transformation {
980
+ return t2
981
+ })* {
982
+ if(mainTransformation === undefined) return
983
+ additionalTransformation = Array.isArray(additionalTransformation) ? additionalTransformation : [additionalTransformation]
984
+ // Loop through each element, add it to current level, if element is already part of result, increase level
985
+ for(let trafos of additionalTransformation) {
986
+ for(const transformation in trafos) {
987
+ if (transformation === 'where' && (mainTransformation.groupBy || mainTransformation.aggregate) && !mainTransformation.having) {
988
+ //When a group by or aggregate preceed a where, the where is a having
989
+ mainTransformation.having = trafos[transformation]
990
+ }
991
+ else if (transformation === 'limit' && mainTransformation.limit?.offset && trafos.limit.rows) {
992
+ //To avoid nesting in case first trafo is skip second is top, than they should be merged
993
+ //Inverse case first top than skip should not be nested as second skip will redcue the amount of returned items
994
+ mainTransformation.limit.rows = trafos.limit.rows
995
+ }
996
+ else if (transformation === 'limit' && mainTransformation.limit?.offset?.val && trafos.limit.offset?.val) {
997
+ //To avoid nesting in case of two subsequent skips as they can be merged
998
+ mainTransformation.limit.offset.val += trafos.limit.offset.val
999
+ }
1000
+ else if(
1001
+ mainTransformation[transformation] ||
1002
+ //Case like aggregation follows with a / after groupBy - if they should be on the same level the aggregation is part of groupBy
1003
+ (transformation === 'aggregate' && mainTransformation.groupBy && !trafos.groupBy)
1004
+ ) {
1005
+ mainTransformation = { apply: mainTransformation }
1006
+ mainTransformation[transformation] = trafos[transformation]
1007
+ } else {
1008
+ mainTransformation[transformation] = trafos[transformation]
1009
+ }
1010
+ }
1011
+ }
1012
+ return {apply: mainTransformation}
1013
+ }
1014
+
1015
+ transformation
970
1016
  = (
971
1017
  "aggregate" agg:aggregateTrafo{return agg} /
972
1018
  "groupby" group:groupbyTrafo{return group} /
@@ -1174,6 +1220,9 @@
1174
1220
 
1175
1221
  word "a string"
1176
1222
  = $([^ \t\n()"&;]+)
1223
+
1224
+ simpleword "a string and/or number"
1225
+ = $([a-zA-Z0-9]+)
1177
1226
 
1178
1227
  time "a time"
1179
1228
  = $([0-9][0-9]":"[0-9][0-9]":"[0-9][0-9])
@@ -1199,7 +1248,7 @@
1199
1248
  = s:$( [+-]? [0-9]+ ) { return parseInt(s) }
1200
1249
 
1201
1250
  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 }
1251
+ = !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
1252
 
1204
1253
  guid "a guid"
1205
1254
  = $( hex16 hex16 "-"? hex16 "-"? hex16 "-"? hex16 "-"? hex16 hex16 hex16 )
@@ -1214,7 +1263,7 @@
1214
1263
  = $( [a-zA-Z0-9-"."_~!$'()*+,;=:@"/""?"]+ )
1215
1264
 
1216
1265
  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') }
1266
+ = "binary'" s:$([a-zA-Z0-9_\-]+ ("=="/"=")?) "'" { return cds.env.features.base64_binaries ? standardBase64(s) : Buffer.from(s, 'base64') }
1218
1267
 
1219
1268
  //
1220
1269
  // ---------- Punctuation ----------