@nymphjs/query-parser 1.0.0-beta.10 → 1.0.0-beta.101

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.
@@ -1,10 +1,10 @@
1
1
  import type { EntityConstructor, Options, Selector } from '@nymphjs/client';
2
- import splitn from '@sciactive/splitn';
2
+ import { splitn } from '@sciactive/splitn';
3
3
 
4
4
  export type BareQueryHandler = (
5
5
  input: string,
6
6
  entityClass?: EntityConstructor,
7
- defaultFields?: string[]
7
+ defaultFields?: string[],
8
8
  ) => Partial<Selector>;
9
9
 
10
10
  export type QRefMap = {
@@ -12,7 +12,7 @@ export type QRefMap = {
12
12
  };
13
13
 
14
14
  export default function queryParser<
15
- T extends EntityConstructor = EntityConstructor
15
+ T extends EntityConstructor = EntityConstructor,
16
16
  >({
17
17
  query,
18
18
  entityClass,
@@ -27,7 +27,7 @@ export default function queryParser<
27
27
  type: '|',
28
28
  ilike: defaultFields.map((field) => [field, input]) as [
29
29
  string,
30
- string
30
+ string,
31
31
  ][],
32
32
  };
33
33
  }
@@ -90,7 +90,8 @@ function selectorsParser({
90
90
  } else if (inQuote) {
91
91
  continue;
92
92
  } else if (curQuery[i] === '(') {
93
- if (currentStart == null) {
93
+ const searchClause = i !== 0 && !curQuery[i - 1].match(/\s/);
94
+ if (currentStart == null && !searchClause) {
94
95
  currentStart = i;
95
96
  } else {
96
97
  nesting++;
@@ -144,7 +145,7 @@ function selectorsParser({
144
145
  defaultFields,
145
146
  qrefMap,
146
147
  bareHandler,
147
- })
148
+ }),
148
149
  );
149
150
  }
150
151
  }
@@ -171,6 +172,16 @@ function selectorsParser({
171
172
  }
172
173
  curQuery = curQuery.replace(offsetRegex, '');
173
174
 
175
+ // JavaScript variable names are ridiculously infeasable to check
176
+ // thoroughly, so this is a "best attempt".
177
+ const sortRegex =
178
+ /(?: |^)sort:([_$a-zA-Z\xA0-\uFFFF][_$a-zA-Z0-9\xA0-\uFFFF]*)(?= |$)/;
179
+ const sortMatch = curQuery.match(sortRegex);
180
+ if (sortMatch) {
181
+ options.sort = sortMatch[1];
182
+ }
183
+ curQuery = curQuery.replace(sortRegex, '');
184
+
174
185
  const reverseRegex = /(?: |^)reverse:(true|false|1|0)(?= |$)/;
175
186
  const reverseMatch = curQuery.match(reverseRegex);
176
187
  if (reverseMatch) {
@@ -222,8 +233,35 @@ function selectorParser({
222
233
  }): string {
223
234
  let curQuery = query;
224
235
 
236
+ // eg. prop(some search string) or prop!(some search string)
237
+ const searchRegex = /(?: |^)([^\s=\[\]<>{}]+?)!?\([^\)]+\)(?= |$)/g;
238
+ const searchMatch = curQuery.match(searchRegex);
239
+ if (searchMatch) {
240
+ selector.search = [];
241
+ selector['!search'] = [];
242
+ for (let match of searchMatch) {
243
+ try {
244
+ let [name, value] = splitn(match.trim().slice(0, -1), '(', 2);
245
+ if (name.endsWith('!')) {
246
+ selector['!search'].push([name.slice(0, -1), value]);
247
+ } else {
248
+ selector.search.push([name, value]);
249
+ }
250
+ } catch (e: any) {
251
+ continue;
252
+ }
253
+ }
254
+ if (!selector.search.length) {
255
+ delete selector.search;
256
+ }
257
+ if (!selector['!search'].length) {
258
+ delete selector['!search'];
259
+ }
260
+ }
261
+ curQuery = curQuery.replace(searchRegex, '');
262
+
225
263
  // eg. user<{User name="Hunter"}> or user!<{User name="Hunter"}>
226
- const qrefRegex = /(?: |^)(\w+)!?<\{(\w+) (.*?[^\\])\}>(?= |$)/g;
264
+ const qrefRegex = /(?: |^)([^\s=\[\]<>{}]+?)!?<\{(\w+) (.*?[^\\])\}>(?= |$)/g;
227
265
  const qrefMatch = curQuery.match(qrefRegex);
228
266
  if (qrefMatch) {
229
267
  selector.qref = [];
@@ -232,7 +270,7 @@ function selectorParser({
232
270
  try {
233
271
  let [name, value] = splitn(match.trim().slice(0, -1), '<', 2);
234
272
  value = unQuoteCurlies(value.slice(1, -1));
235
- let [className, qrefQuery] = value.split(' ', 2);
273
+ let [className, qrefQuery] = splitn(value, ' ', 2);
236
274
  const EntityClass = qrefMap[className].class;
237
275
  if (EntityClass == null) {
238
276
  continue;
@@ -262,50 +300,114 @@ function selectorParser({
262
300
  }
263
301
  curQuery = curQuery.replace(qrefRegex, '');
264
302
 
265
- // eg. name=Marty or name="Marty McFly" or enabled=true or someArray=[1,2]
266
- const equalRegex = /(?: |^)(\w+)!?=(""|".*?[^\\]"|[^ ]+)(?= |$)/g;
303
+ // eg. someArray=[1,2] or someObject={"prop":"some value"}
304
+ const equalJsonRegex = /(?: |^)([^\s=\[\]<>{}]+?)!?=(\{|\[)/g;
305
+ const equalJsonMatch = [...curQuery.matchAll(equalJsonRegex)];
306
+ if (equalJsonMatch) {
307
+ if (!('equal' in selector)) {
308
+ selector.equal = [];
309
+ }
310
+ if (!('!equal' in selector)) {
311
+ selector['!equal'] = [];
312
+ }
313
+ // Work backward to find all long JSON values.
314
+ for (let i = equalJsonMatch.length - 1; i >= 0; i--) {
315
+ const match = equalJsonMatch[i];
316
+ let [name, opener] = splitn(match[0].trim(), '=', 2);
317
+ let start = match.index + match[0].length - 1;
318
+ let nextEndToken = curQuery.indexOf(opener === '{' ? '}' : ']', start);
319
+ while (nextEndToken !== -1) {
320
+ try {
321
+ if (name.endsWith('!')) {
322
+ (selector['!equal'] as [string, any][]).unshift([
323
+ name.slice(0, -1),
324
+ JSON.parse(curQuery.substring(start, nextEndToken + 1)),
325
+ ]);
326
+ } else {
327
+ (selector.equal as [string, any][]).unshift([
328
+ name,
329
+ JSON.parse(curQuery.substring(start, nextEndToken + 1)),
330
+ ]);
331
+ }
332
+
333
+ curQuery =
334
+ curQuery.substring(0, match.index) +
335
+ curQuery.substring(nextEndToken + 1);
336
+ break;
337
+ } catch (e: any) {
338
+ nextEndToken = curQuery.indexOf(
339
+ opener === '{' ? '}' : ']',
340
+ nextEndToken + 1,
341
+ );
342
+ }
343
+ }
344
+ }
345
+ if (selector.equal == null || !selector.equal.length) {
346
+ delete selector.equal;
347
+ }
348
+ if (selector['!equal'] == null || !selector['!equal'].length) {
349
+ delete selector['!equal'];
350
+ }
351
+ }
352
+
353
+ // eg. name=Marty or name="Marty McFly" or enabled=true
354
+ const equalRegex =
355
+ /(?: |^)([^\s=\[\]<>{}]+?)!?=(""|".*?[^\\]"|[^ ]+)(?= |$)/g;
267
356
  const equalMatch = curQuery.match(equalRegex);
268
357
  if (equalMatch) {
269
- selector.equal = [];
270
- selector['!equal'] = [];
358
+ if (!('equal' in selector)) {
359
+ selector.equal = [];
360
+ }
361
+ if (!('!equal' in selector)) {
362
+ selector['!equal'] = [];
363
+ }
271
364
  for (let match of equalMatch) {
272
365
  try {
273
- let [name, value] = match.trim().split('=', 2);
366
+ let [name, value] = splitn(match.trim(), '=', 2);
274
367
  try {
275
368
  if (name.endsWith('!')) {
276
- selector['!equal'].push([name.slice(0, -1), JSON.parse(value)]);
369
+ (selector['!equal'] as [string, any][]).push([
370
+ name.slice(0, -1),
371
+ JSON.parse(value),
372
+ ]);
277
373
  } else {
278
- selector.equal.push([name, JSON.parse(value)]);
374
+ (selector.equal as [string, any][]).push([name, JSON.parse(value)]);
279
375
  }
280
376
  } catch (e: any) {
281
377
  if (name.endsWith('!')) {
282
- selector['!equal'].push([name.slice(0, -1), unQuoteString(value)]);
378
+ (selector['!equal'] as [string, any][]).push([
379
+ name.slice(0, -1),
380
+ unQuoteString(value),
381
+ ]);
283
382
  } else {
284
- selector.equal.push([name, unQuoteString(value)]);
383
+ (selector.equal as [string, any][]).push([
384
+ name,
385
+ unQuoteString(value),
386
+ ]);
285
387
  }
286
388
  }
287
389
  } catch (e: any) {
288
390
  continue;
289
391
  }
290
392
  }
291
- if (!selector.equal.length) {
393
+ if (selector.equal == null || !selector.equal.length) {
292
394
  delete selector.equal;
293
395
  }
294
- if (!selector['!equal'].length) {
396
+ if (selector['!equal'] == null || !selector['!equal'].length) {
295
397
  delete selector['!equal'];
296
398
  }
297
399
  }
298
400
  curQuery = curQuery.replace(equalRegex, '');
299
401
 
300
402
  // eg. user<{790274347f9b3a018c2cedee}> or user!<{790274347f9b3a018c2cedee}>
301
- const refRegex = /(?: |^)(\w+)!?<\{([0-9a-f]{24})\}>(?= |$)/g;
403
+ const refRegex = /(?: |^)([^\s=\[\]<>{}]+?)!?<\{([0-9a-f]{24})\}>(?= |$)/g;
302
404
  const refMatch = curQuery.match(refRegex);
303
405
  if (refMatch) {
304
406
  selector.ref = [];
305
407
  selector['!ref'] = [];
306
408
  for (let match of refMatch) {
307
409
  try {
308
- let [name, value] = match.trim().slice(0, -1).split('<', 2);
410
+ let [name, value] = splitn(match.trim().slice(0, -1), '<', 2);
309
411
  if (name.endsWith('!')) {
310
412
  selector['!ref'].push([name.slice(0, -1), value.slice(1, -1)]);
311
413
  } else {
@@ -325,31 +427,32 @@ function selectorParser({
325
427
  curQuery = curQuery.replace(refRegex, '');
326
428
 
327
429
  // eg. someArrayOfNumbers<10> or someObject!<"some string">
328
- const containRegex = /(?: |^)(\w+)!?(<.*?[^\\]>)(?= |$)/g;
430
+ const containRegex =
431
+ /(?: |^)([^\s=\[\]<>{}]+?)!?(<(?:[^"][^>]*?|".*?[^\\]"))>(?= |$)/g;
329
432
  const containMatch = curQuery.match(containRegex);
330
433
  if (containMatch) {
331
434
  selector.contain = [];
332
435
  selector['!contain'] = [];
333
436
  for (let match of containMatch) {
334
437
  try {
335
- let [name, value] = match.trim().slice(0, -1).split('<', 2);
438
+ let [name, value] = splitn(match.trim().slice(0, -1), '<', 2);
336
439
  try {
337
440
  if (name.endsWith('!')) {
338
441
  selector['!contain'].push([
339
442
  name.slice(0, -1),
340
- JSON.parse(unQuoteAngles(value)),
443
+ JSON.parse(unQuoteString(value)),
341
444
  ]);
342
445
  } else {
343
- selector.contain.push([name, JSON.parse(unQuoteAngles(value))]);
446
+ selector.contain.push([name, JSON.parse(unQuoteString(value))]);
344
447
  }
345
448
  } catch (e: any) {
346
449
  if (name.endsWith('!')) {
347
450
  selector['!contain'].push([
348
451
  name.slice(0, -1),
349
- unQuoteAngles(value),
452
+ unQuoteString(value),
350
453
  ]);
351
454
  } else {
352
- selector.contain.push([name, unQuoteAngles(value)]);
455
+ selector.contain.push([name, unQuoteString(value)]);
353
456
  }
354
457
  }
355
458
  } catch (e: any) {
@@ -366,7 +469,8 @@ function selectorParser({
366
469
  curQuery = curQuery.replace(containRegex, '');
367
470
 
368
471
  // eg. name~/Hunter/ or name!~/hunter/i
369
- const posixRegex = /(?: |^)(\w+)!?~(\/\/|\/.*?[^\\]\/)i?(?= |$)/g;
472
+ const posixRegex =
473
+ /(?: |^)([^\s=\[\]<>{}]+?)!?~(\/\/|\/.*?[^\\]\/)i?(?= |$)/g;
370
474
  const posixMatch = curQuery.match(posixRegex);
371
475
  if (posixMatch) {
372
476
  selector.match = [];
@@ -375,7 +479,7 @@ function selectorParser({
375
479
  selector['!imatch'] = [];
376
480
  for (let match of posixMatch) {
377
481
  try {
378
- let [name, value] = match.trim().split('~', 2);
482
+ let [name, value] = splitn(match.trim(), '~', 2);
379
483
  if (name.endsWith('!')) {
380
484
  if (value.endsWith('i')) {
381
485
  selector['!imatch'].push([
@@ -415,7 +519,8 @@ function selectorParser({
415
519
  curQuery = curQuery.replace(posixRegex, '');
416
520
 
417
521
  // eg. name~Hunter or name!~"hunter"i
418
- const likeRegex = /(?: |^)(\w+)!?~(""i?|".*?[^\\]"i?|[^ ]+)(?= |$)/g;
522
+ const likeRegex =
523
+ /(?: |^)([^\s=\[\]<>{}]+?)!?~(""i?|".*?[^\\]"i?|[^ ]+)(?= |$)/g;
419
524
  const likeMatch = curQuery.match(likeRegex);
420
525
  if (likeMatch) {
421
526
  selector.like = [];
@@ -424,7 +529,7 @@ function selectorParser({
424
529
  selector['!ilike'] = [];
425
530
  for (let match of likeMatch) {
426
531
  try {
427
- let [name, value] = match.trim().split('~', 2);
532
+ let [name, value] = splitn(match.trim(), '~', 2);
428
533
  if (name.endsWith('!')) {
429
534
  if (value.endsWith('"i')) {
430
535
  selector['!ilike'].push([
@@ -488,7 +593,7 @@ function selectorParser({
488
593
  curQuery = curQuery.replace(guidRegex, '');
489
594
 
490
595
  // eg. [enabled] or [!defaultPrimaryGroup]
491
- const truthyRegex = /(?: |^)\[(!?\w+)\](?= |$)/g;
596
+ const truthyRegex = /(?: |^)\[(!?[^\s=\[\]<>{}]+?)\](?= |$)/g;
492
597
  const truthyMatch = curQuery.match(truthyRegex);
493
598
  if (truthyMatch) {
494
599
  selector.truthy = [];
@@ -542,13 +647,13 @@ function selectorParser({
542
647
  curQuery = curQuery.replace(tagRegex, '');
543
648
 
544
649
  // eg. cdate>15
545
- const gtRegex = /(?: |^)(\w+)>(-?\d+(?:\.\d+)?)(?= |$)/g;
650
+ const gtRegex = /(?: |^)([^\s=\[\]<>{}]+?)>(-?\d+(?:\.\d+)?)(?= |$)/g;
546
651
  const gtMatch = curQuery.match(gtRegex);
547
652
  if (gtMatch) {
548
653
  selector.gt = [];
549
654
  for (let match of gtMatch) {
550
655
  try {
551
- let [name, value] = match.trim().split('>', 2);
656
+ let [name, value] = splitn(match.trim(), '>', 2);
552
657
  selector.gt.push([name, Number(value)]);
553
658
  } catch (e: any) {
554
659
  continue;
@@ -561,7 +666,7 @@ function selectorParser({
561
666
  curQuery = curQuery.replace(gtRegex, '');
562
667
 
563
668
  // eg. cdate>yesterday or cdate>"2 days ago"
564
- const gtRelativeRegex = /(?: |^)(\w+)>(\w+|"[^"]+")(?= |$)/g;
669
+ const gtRelativeRegex = /(?: |^)([^\s=\[\]<>{}]+?)>(\w+|"[^"]+")(?= |$)/g;
565
670
  const gtRelativeMatch = curQuery.match(gtRelativeRegex);
566
671
  if (gtRelativeMatch) {
567
672
  if (selector.gt == null) {
@@ -569,7 +674,7 @@ function selectorParser({
569
674
  }
570
675
  for (let match of gtRelativeMatch) {
571
676
  try {
572
- let [name, value] = match.trim().split('>', 2);
677
+ let [name, value] = splitn(match.trim(), '>', 2);
573
678
  (selector.gt as [string, null, string][]).push([
574
679
  name,
575
680
  null,
@@ -586,13 +691,13 @@ function selectorParser({
586
691
  curQuery = curQuery.replace(gtRelativeRegex, '');
587
692
 
588
693
  // eg. cdate>=15
589
- const gteRegex = /(?: |^)(\w+)>=(-?\d+(?:\.\d+)?)(?= |$)/g;
694
+ const gteRegex = /(?: |^)([^\s=\[\]<>{}]+?)>=(-?\d+(?:\.\d+)?)(?= |$)/g;
590
695
  const gteMatch = curQuery.match(gteRegex);
591
696
  if (gteMatch) {
592
697
  selector.gte = [];
593
698
  for (let match of gteMatch) {
594
699
  try {
595
- let [name, value] = match.trim().split('>=', 2);
700
+ let [name, value] = splitn(match.trim(), '>=', 2);
596
701
  selector.gte.push([name, Number(value)]);
597
702
  } catch (e: any) {
598
703
  continue;
@@ -605,7 +710,7 @@ function selectorParser({
605
710
  curQuery = curQuery.replace(gteRegex, '');
606
711
 
607
712
  // eg. cdate>=yesterday or cdate>="2 days ago"
608
- const gteRelativeRegex = /(?: |^)(\w+)>=(\w+|"[^"]+")(?= |$)/g;
713
+ const gteRelativeRegex = /(?: |^)([^\s=\[\]<>{}]+?)>=(\w+|"[^"]+")(?= |$)/g;
609
714
  const gteRelativeMatch = curQuery.match(gteRelativeRegex);
610
715
  if (gteRelativeMatch) {
611
716
  if (selector.gte == null) {
@@ -613,7 +718,7 @@ function selectorParser({
613
718
  }
614
719
  for (let match of gteRelativeMatch) {
615
720
  try {
616
- let [name, value] = match.trim().split('>=', 2);
721
+ let [name, value] = splitn(match.trim(), '>=', 2);
617
722
  (selector.gte as [string, null, string][]).push([
618
723
  name,
619
724
  null,
@@ -630,13 +735,13 @@ function selectorParser({
630
735
  curQuery = curQuery.replace(gteRelativeRegex, '');
631
736
 
632
737
  // eg. cdate<15
633
- const ltRegex = /(?: |^)(\w+)<(-?\d+(?:\.\d+)?)(?= |$)/g;
738
+ const ltRegex = /(?: |^)([^\s=\[\]<>{}]+?)<(-?\d+(?:\.\d+)?)(?= |$)/g;
634
739
  const ltMatch = curQuery.match(ltRegex);
635
740
  if (ltMatch) {
636
741
  selector.lt = [];
637
742
  for (let match of ltMatch) {
638
743
  try {
639
- let [name, value] = match.trim().split('<', 2);
744
+ let [name, value] = splitn(match.trim(), '<', 2);
640
745
  selector.lt.push([name, Number(value)]);
641
746
  } catch (e: any) {
642
747
  continue;
@@ -649,7 +754,7 @@ function selectorParser({
649
754
  curQuery = curQuery.replace(ltRegex, '');
650
755
 
651
756
  // eg. cdate<yesterday or cdate<"2 days ago"
652
- const ltRelativeRegex = /(?: |^)(\w+)<(\w+|"[^"]+")(?= |$)/g;
757
+ const ltRelativeRegex = /(?: |^)([^\s=\[\]<>{}]+?)<(\w+|"[^"]+")(?= |$)/g;
653
758
  const ltRelativeMatch = curQuery.match(ltRelativeRegex);
654
759
  if (ltRelativeMatch) {
655
760
  if (selector.lt == null) {
@@ -657,7 +762,7 @@ function selectorParser({
657
762
  }
658
763
  for (let match of ltRelativeMatch) {
659
764
  try {
660
- let [name, value] = match.trim().split('<', 2);
765
+ let [name, value] = splitn(match.trim(), '<', 2);
661
766
  (selector.lt as [string, null, string][]).push([
662
767
  name,
663
768
  null,
@@ -674,13 +779,13 @@ function selectorParser({
674
779
  curQuery = curQuery.replace(ltRelativeRegex, '');
675
780
 
676
781
  // eg. cdate<=15
677
- const lteRegex = /(?: |^)(\w+)<=(-?\d+(?:\.\d+)?)(?= |$)/g;
782
+ const lteRegex = /(?: |^)([^\s=\[\]<>{}]+?)<=(-?\d+(?:\.\d+)?)(?= |$)/g;
678
783
  const lteMatch = curQuery.match(lteRegex);
679
784
  if (lteMatch) {
680
785
  selector.lte = [];
681
786
  for (let match of lteMatch) {
682
787
  try {
683
- let [name, value] = match.trim().split('<=', 2);
788
+ let [name, value] = splitn(match.trim(), '<=', 2);
684
789
  selector.lte.push([name, Number(value)]);
685
790
  } catch (e: any) {
686
791
  continue;
@@ -693,7 +798,7 @@ function selectorParser({
693
798
  curQuery = curQuery.replace(lteRegex, '');
694
799
 
695
800
  // eg. cdate<=yesterday or cdate<="2 days ago"
696
- const lteRelativeRegex = /(?: |^)(\w+)<=(\w+|"[^"]+")(?= |$)/g;
801
+ const lteRelativeRegex = /(?: |^)([^\s=\[\]<>{}]+?)<=(\w+|"[^"]+")(?= |$)/g;
697
802
  const lteRelativeMatch = curQuery.match(lteRelativeRegex);
698
803
  if (lteRelativeMatch) {
699
804
  if (selector.lte == null) {
@@ -701,7 +806,7 @@ function selectorParser({
701
806
  }
702
807
  for (let match of lteRelativeMatch) {
703
808
  try {
704
- let [name, value] = match.trim().split('<=', 2);
809
+ let [name, value] = splitn(match.trim(), '<=', 2);
705
810
  (selector.lte as [string, null, string][]).push([
706
811
  name,
707
812
  null,
package/tsconfig.json CHANGED
@@ -2,12 +2,14 @@
2
2
  "extends": "@tsconfig/recommended/tsconfig.json",
3
3
 
4
4
  "compilerOptions": {
5
- "lib": ["DOM", "ES2021"],
6
- "target": "ES2021",
5
+ "module": "ES2022",
6
+ "moduleResolution": "node",
7
+ "lib": ["DOM", "ES2022"],
8
+ "target": "ES2022",
7
9
  "noImplicitAny": true,
8
- "removeComments": true,
10
+ "removeComments": false,
9
11
  "sourceMap": true,
10
- "outDir": "lib",
12
+ "outDir": "dist",
11
13
  "resolveJsonModule": true,
12
14
  "rootDir": "src/",
13
15
  "declaration": true
package/typedoc.json ADDED
@@ -0,0 +1,4 @@
1
+ {
2
+ "extends": ["../../typedoc.base.json"],
3
+ "entryPoints": ["src/index.ts"]
4
+ }
package/webpack.config.js DELETED
@@ -1,28 +0,0 @@
1
- const path = require('path');
2
-
3
- module.exports = {
4
- mode: 'production',
5
- devtool: 'source-map',
6
- entry: {
7
- NymphClient: './lib/index.js',
8
- },
9
- output: {
10
- path: path.resolve(__dirname, 'dist'),
11
- filename: 'index.js',
12
- library: ['@nymphjs/client'],
13
- libraryTarget: 'umd',
14
- globalObject: 'this',
15
- },
16
- resolve: {
17
- extensions: ['.tsx', '.ts', '.js'],
18
- },
19
- module: {
20
- rules: [
21
- {
22
- test: /\.tsx?$/,
23
- use: 'ts-loader',
24
- exclude: /node_modules/,
25
- },
26
- ],
27
- },
28
- };