patram 0.10.0 → 0.12.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 (117) hide show
  1. package/bin/patram.js +4 -4
  2. package/lib/cli/arguments.types.d.ts +1 -0
  3. package/lib/cli/commands/check.js +27 -15
  4. package/lib/cli/commands/fields.js +0 -4
  5. package/lib/cli/commands/queries.js +179 -1
  6. package/lib/cli/commands/query.js +1 -8
  7. package/lib/cli/commands/refs.js +3 -10
  8. package/lib/cli/commands/show.js +1 -8
  9. package/lib/cli/help-metadata.js +106 -111
  10. package/lib/cli/main.js +10 -10
  11. package/lib/cli/parse-arguments-helpers.js +416 -66
  12. package/lib/cli/parse-arguments.js +4 -4
  13. package/lib/cli/render-help.js +10 -4
  14. package/lib/config/defaults.js +33 -25
  15. package/lib/config/load-patram-config.d.ts +19 -33
  16. package/lib/config/load-patram-config.js +18 -121
  17. package/lib/config/load-patram-config.types.d.ts +3 -40
  18. package/lib/config/manage-stored-queries-helpers.d.ts +69 -0
  19. package/lib/config/manage-stored-queries-helpers.js +320 -0
  20. package/lib/config/manage-stored-queries-jsonc.d.ts +31 -0
  21. package/lib/config/manage-stored-queries-jsonc.js +95 -0
  22. package/lib/config/manage-stored-queries.d.ts +77 -0
  23. package/lib/config/manage-stored-queries.js +300 -0
  24. package/lib/config/patram-config.d.ts +34 -34
  25. package/lib/config/patram-config.js +3 -3
  26. package/lib/config/patram-config.types.d.ts +5 -11
  27. package/lib/config/resolve-patram-graph-config.d.ts +5 -1
  28. package/lib/config/resolve-patram-graph-config.js +3 -119
  29. package/lib/config/schema.d.ts +158 -269
  30. package/lib/config/schema.js +72 -210
  31. package/lib/config/validate-patram-config-value.d.ts +13 -0
  32. package/lib/config/validate-patram-config-value.js +94 -0
  33. package/lib/config/validation.d.ts +2 -12
  34. package/lib/config/validation.js +125 -483
  35. package/lib/find-close-match.d.ts +4 -1
  36. package/lib/graph/build-graph-identity.d.ts +1 -32
  37. package/lib/graph/build-graph-identity.js +5 -269
  38. package/lib/graph/build-graph.d.ts +13 -4
  39. package/lib/graph/build-graph.js +347 -488
  40. package/lib/graph/build-graph.types.d.ts +8 -9
  41. package/lib/graph/check-directive-metadata-helpers.d.ts +30 -0
  42. package/lib/graph/check-directive-metadata-helpers.js +126 -0
  43. package/lib/graph/check-directive-metadata.d.ts +8 -9
  44. package/lib/graph/check-directive-metadata.js +70 -561
  45. package/lib/graph/check-directive-path-target.d.ts +6 -13
  46. package/lib/graph/check-directive-path-target.js +26 -57
  47. package/lib/graph/check-directive-value.d.ts +1 -5
  48. package/lib/graph/check-directive-value.js +40 -180
  49. package/lib/graph/check-graph.d.ts +5 -5
  50. package/lib/graph/check-graph.js +8 -6
  51. package/lib/graph/document-node-identity.d.ts +23 -7
  52. package/lib/graph/document-node-identity.js +417 -160
  53. package/lib/graph/graph-node.d.ts +42 -0
  54. package/lib/graph/graph-node.js +83 -0
  55. package/lib/graph/inspect-reverse-references.js +16 -11
  56. package/lib/graph/load-project-graph.d.ts +7 -7
  57. package/lib/graph/load-project-graph.js +7 -7
  58. package/lib/graph/parse-where-clause.types.d.ts +3 -2
  59. package/lib/graph/query/cypher-reader.d.ts +59 -0
  60. package/lib/graph/query/cypher-reader.js +151 -0
  61. package/lib/graph/query/cypher-support.d.ts +79 -0
  62. package/lib/graph/query/cypher-support.js +213 -0
  63. package/lib/graph/query/cypher-tokenize.d.ts +13 -0
  64. package/lib/graph/query/cypher-tokenize.js +225 -0
  65. package/lib/graph/query/cypher.types.d.ts +43 -0
  66. package/lib/graph/query/execute.d.ts +7 -7
  67. package/lib/graph/query/execute.js +71 -33
  68. package/lib/graph/query/inspect.js +58 -24
  69. package/lib/graph/query/parse-cypher-patterns.d.ts +27 -0
  70. package/lib/graph/query/parse-cypher-patterns.js +382 -0
  71. package/lib/graph/query/parse-cypher.d.ts +7 -0
  72. package/lib/graph/query/parse-cypher.js +580 -0
  73. package/lib/graph/query/parse-query.d.ts +13 -0
  74. package/lib/graph/query/parse-query.js +97 -0
  75. package/lib/graph/query/resolve.d.ts +6 -0
  76. package/lib/graph/query/resolve.js +81 -24
  77. package/lib/output/command-output.js +12 -5
  78. package/lib/output/compact-layout.js +221 -0
  79. package/lib/output/format-output-item-block.js +31 -1
  80. package/lib/output/format-output-metadata.js +16 -29
  81. package/lib/output/format-stored-query-block.js +95 -0
  82. package/lib/output/layout-incoming-references.js +101 -19
  83. package/lib/output/layout-stored-queries.js +23 -330
  84. package/lib/output/list-queries.js +1 -1
  85. package/lib/output/render-field-discovery.js +11 -2
  86. package/lib/output/render-output-view.js +9 -5
  87. package/lib/output/renderers/json.js +5 -26
  88. package/lib/output/renderers/plain.js +155 -35
  89. package/lib/output/renderers/rich.js +250 -36
  90. package/lib/output/resolve-check-target.js +120 -11
  91. package/lib/output/resolved-link-layout.js +43 -0
  92. package/lib/output/rich-source/render.js +193 -35
  93. package/lib/output/show-document.js +25 -18
  94. package/lib/output/view-model/index.js +124 -103
  95. package/lib/parse/jsdoc/parse-jsdoc-blocks.js +1 -1
  96. package/lib/parse/jsdoc/parse-jsdoc-claims.js +12 -6
  97. package/lib/parse/markdown/parse-markdown-claims.js +99 -62
  98. package/lib/parse/markdown/parse-markdown-directives.d.ts +10 -6
  99. package/lib/parse/markdown/parse-markdown-directives.js +104 -18
  100. package/lib/parse/markdown/parse-markdown-prose.d.ts +27 -0
  101. package/lib/parse/markdown/parse-markdown-prose.js +243 -0
  102. package/lib/parse/parse-claims.d.ts +2 -6
  103. package/lib/parse/parse-claims.js +11 -53
  104. package/lib/parse/tagged-fenced/tagged-fenced-blocks.d.ts +4 -4
  105. package/lib/parse/tagged-fenced/tagged-fenced-blocks.js +4 -4
  106. package/lib/parse/yaml/parse-yaml-claims.js +4 -4
  107. package/lib/patram.d.ts +9 -3
  108. package/lib/patram.js +1 -1
  109. package/lib/scan/discover-fields.js +194 -55
  110. package/lib/scan/list-source-files.d.ts +4 -4
  111. package/lib/scan/list-source-files.js +4 -4
  112. package/package.json +2 -1
  113. package/lib/directive-validation-test-helpers.js +0 -87
  114. package/lib/graph/query/parse.d.ts +0 -75
  115. package/lib/graph/query/parse.js +0 -1064
  116. package/lib/output/derived-summary.js +0 -280
  117. package/lib/output/format-derived-summary-row.js +0 -9
@@ -1,1064 +0,0 @@
1
- /* eslint-disable max-lines */
2
- /**
3
- * @import { PatramDiagnostic } from '../../config/load-patram-config.types.ts';
4
- * @import {
5
- * ParseWhereClauseResult,
6
- * ParsedAggregateComparison,
7
- * ParsedAggregateName,
8
- * ParsedAggregateTerm,
9
- * ParsedExpression,
10
- * ParsedFieldName,
11
- * ParsedTerm,
12
- * ParsedTraversalTerm,
13
- * } from '../parse-where-clause.types.ts';
14
- */
15
-
16
- /**
17
- * @typedef {{ bindings: Record<string, string>, index: number, where_clause: string }} ParserState
18
- */
19
-
20
- /**
21
- * @typedef {{
22
- * diagnostic: PatramDiagnostic,
23
- * success: false,
24
- * }} ParseFailureResult
25
- */
26
-
27
- /**
28
- * @typedef {{
29
- * expression: ParsedExpression,
30
- * success: true,
31
- * } | ParseFailureResult} ParseExpressionResult
32
- */
33
-
34
- /**
35
- * @typedef {{
36
- * success: true,
37
- * term: ParsedTerm,
38
- * } | ParseFailureResult} ParseTermResult
39
- */
40
-
41
- /**
42
- * @typedef {{
43
- * success: true,
44
- * value: string,
45
- * } | ParseFailureResult} ParseValueResult
46
- */
47
-
48
- /**
49
- * Parse one where clause into a structured boolean expression.
50
- *
51
- * @param {string} where_clause
52
- * @param {{ bindings?: Record<string, string> }=} options
53
- * @returns {ParseWhereClauseResult}
54
- */
55
- export function parseWhereClause(where_clause, options = {}) {
56
- /** @type {ParserState} */
57
- const parser_state = {
58
- bindings: options.bindings ?? {},
59
- index: 0,
60
- where_clause,
61
- };
62
-
63
- skipWhitespace(parser_state);
64
-
65
- if (isAtEnd(parser_state)) {
66
- return fail(1, 'Query must not be empty.');
67
- }
68
-
69
- const expression_result = parseExpression(parser_state, null);
70
-
71
- if (!expression_result.success) {
72
- return expression_result;
73
- }
74
-
75
- skipWhitespace(parser_state);
76
-
77
- if (!isAtEnd(parser_state)) {
78
- return failToken(parser_state);
79
- }
80
-
81
- return {
82
- expression: expression_result.expression,
83
- success: true,
84
- };
85
- }
86
-
87
- /**
88
- * @param {ParserState} parser_state
89
- * @param {')' | null} stop_character
90
- * @returns {ParseExpressionResult}
91
- */
92
- function parseExpression(parser_state, stop_character) {
93
- return parseOrExpression(parser_state, stop_character);
94
- }
95
-
96
- /**
97
- * @param {ParserState} parser_state
98
- * @param {')' | null} stop_character
99
- * @returns {ParseExpressionResult}
100
- */
101
- function parseOrExpression(parser_state, stop_character) {
102
- const first_expression_result = parseAndExpression(
103
- parser_state,
104
- stop_character,
105
- );
106
-
107
- if (!first_expression_result.success) {
108
- return first_expression_result;
109
- }
110
-
111
- /** @type {ParsedExpression[]} */
112
- const expressions = [first_expression_result.expression];
113
-
114
- while (true) {
115
- skipWhitespace(parser_state);
116
-
117
- if (
118
- currentCharacter(parser_state) === stop_character ||
119
- isAtEnd(parser_state)
120
- ) {
121
- return collapseBooleanExpression('or', expressions);
122
- }
123
-
124
- if (!consumeKeyword(parser_state, 'or')) {
125
- return collapseBooleanExpression('or', expressions);
126
- }
127
-
128
- skipWhitespace(parser_state);
129
-
130
- if (
131
- currentCharacter(parser_state) === stop_character ||
132
- isAtEnd(parser_state)
133
- ) {
134
- return failExpectedTerm(parser_state);
135
- }
136
-
137
- const next_expression_result = parseAndExpression(
138
- parser_state,
139
- stop_character,
140
- );
141
-
142
- if (!next_expression_result.success) {
143
- return next_expression_result;
144
- }
145
-
146
- expressions.push(next_expression_result.expression);
147
- }
148
- }
149
-
150
- /**
151
- * @param {ParserState} parser_state
152
- * @param {')' | null} stop_character
153
- * @returns {ParseExpressionResult}
154
- */
155
- function parseAndExpression(parser_state, stop_character) {
156
- const first_expression_result = parseUnaryExpression(
157
- parser_state,
158
- stop_character,
159
- );
160
-
161
- if (!first_expression_result.success) {
162
- return first_expression_result;
163
- }
164
-
165
- /** @type {ParsedExpression[]} */
166
- const expressions = [first_expression_result.expression];
167
-
168
- while (true) {
169
- skipWhitespace(parser_state);
170
-
171
- if (
172
- currentCharacter(parser_state) === stop_character ||
173
- isAtEnd(parser_state)
174
- ) {
175
- return collapseBooleanExpression('and', expressions);
176
- }
177
-
178
- if (!consumeKeyword(parser_state, 'and')) {
179
- return collapseBooleanExpression('and', expressions);
180
- }
181
-
182
- skipWhitespace(parser_state);
183
-
184
- if (
185
- currentCharacter(parser_state) === stop_character ||
186
- isAtEnd(parser_state)
187
- ) {
188
- return failExpectedTerm(parser_state);
189
- }
190
-
191
- const next_expression_result = parseUnaryExpression(
192
- parser_state,
193
- stop_character,
194
- );
195
-
196
- if (!next_expression_result.success) {
197
- return next_expression_result;
198
- }
199
-
200
- expressions.push(next_expression_result.expression);
201
- }
202
- }
203
-
204
- /**
205
- * @param {ParserState} parser_state
206
- * @param {')' | null} stop_character
207
- * @returns {ParseExpressionResult}
208
- */
209
- function parseUnaryExpression(parser_state, stop_character) {
210
- skipWhitespace(parser_state);
211
- const start_index = parser_state.index;
212
-
213
- if (!consumeKeyword(parser_state, 'not')) {
214
- return parsePrimaryExpression(parser_state, stop_character);
215
- }
216
-
217
- const whitespace_length = skipWhitespace(parser_state);
218
-
219
- if (whitespace_length === 0 && currentCharacter(parser_state) !== '(') {
220
- return fail(
221
- start_index + 1,
222
- `Unsupported query token "${readToken(parser_state, start_index)}".`,
223
- );
224
- }
225
-
226
- if (
227
- currentCharacter(parser_state) === stop_character ||
228
- isAtEnd(parser_state)
229
- ) {
230
- return failExpectedTerm(parser_state);
231
- }
232
-
233
- const expression_result = parseUnaryExpression(parser_state, stop_character);
234
-
235
- if (!expression_result.success) {
236
- return expression_result;
237
- }
238
-
239
- return successExpression({
240
- expression: expression_result.expression,
241
- kind: 'not',
242
- });
243
- }
244
-
245
- /**
246
- * @param {ParserState} parser_state
247
- * @param {')' | null} stop_character
248
- * @returns {ParseExpressionResult}
249
- */
250
- function parsePrimaryExpression(parser_state, stop_character) {
251
- skipWhitespace(parser_state);
252
-
253
- if (
254
- currentCharacter(parser_state) === stop_character ||
255
- isAtEnd(parser_state)
256
- ) {
257
- return failExpectedTerm(parser_state);
258
- }
259
-
260
- if (consumeOperator(parser_state, '(')) {
261
- const expression_result = parseExpression(parser_state, ')');
262
-
263
- if (!expression_result.success) {
264
- return expression_result;
265
- }
266
-
267
- if (!consumeOperator(parser_state, ')')) {
268
- return failToken(parser_state);
269
- }
270
-
271
- return expression_result;
272
- }
273
-
274
- const term_result = parseTerm(parser_state);
275
-
276
- if (!term_result.success) {
277
- return term_result;
278
- }
279
-
280
- return successExpression({
281
- kind: 'term',
282
- term: term_result.term,
283
- });
284
- }
285
-
286
- /**
287
- * @param {'and' | 'or'} kind
288
- * @param {ParsedExpression[]} expressions
289
- * @returns {ParseExpressionResult}
290
- */
291
- function collapseBooleanExpression(kind, expressions) {
292
- if (expressions.length === 1) {
293
- return successExpression(expressions[0]);
294
- }
295
-
296
- return successExpression({
297
- expressions,
298
- kind,
299
- });
300
- }
301
-
302
- /**
303
- * @param {ParserState} parser_state
304
- * @returns {ParseTermResult}
305
- */
306
- function parseTerm(parser_state) {
307
- return parseAggregate(parser_state) ?? parseAtomicTerm(parser_state);
308
- }
309
-
310
- /**
311
- * @param {ParserState} parser_state
312
- * @returns {ParseTermResult | null}
313
- */
314
- function parseAggregate(parser_state) {
315
- const start_index = parser_state.index;
316
- const aggregate_name = parseIdentifier(parser_state);
317
-
318
- if (
319
- aggregate_name !== 'any' &&
320
- aggregate_name !== 'count' &&
321
- aggregate_name !== 'none'
322
- ) {
323
- parser_state.index = start_index;
324
- return null;
325
- }
326
-
327
- skipWhitespace(parser_state);
328
-
329
- if (currentCharacter(parser_state) !== '(') {
330
- parser_state.index = start_index;
331
- return null;
332
- }
333
-
334
- parser_state.index += 1;
335
- skipWhitespace(parser_state);
336
-
337
- const traversal_result = parseTraversal(parser_state);
338
-
339
- if (!traversal_result.success) {
340
- return traversal_result;
341
- }
342
-
343
- if (!consumeOperator(parser_state, ',')) {
344
- return failToken(parser_state);
345
- }
346
-
347
- skipWhitespace(parser_state);
348
- const expression_result = parseExpression(parser_state, ')');
349
-
350
- if (!expression_result.success) {
351
- return expression_result;
352
- }
353
-
354
- if (!consumeOperator(parser_state, ')')) {
355
- return failToken(parser_state);
356
- }
357
-
358
- return createAggregateTerm(
359
- parser_state,
360
- aggregate_name,
361
- traversal_result.traversal,
362
- expression_result.expression,
363
- );
364
- }
365
-
366
- /**
367
- * @param {ParserState} parser_state
368
- * @returns {ParseTermResult}
369
- */
370
- function parseAtomicTerm(parser_state) {
371
- const start_index = parser_state.index;
372
- const field_or_relation_name = parseIdentifier(parser_state);
373
-
374
- if (!field_or_relation_name) {
375
- return failToken(parser_state);
376
- }
377
-
378
- return parseOperatorTerm(parser_state, start_index, field_or_relation_name);
379
- }
380
-
381
- /**
382
- * @param {ParserState} parser_state
383
- * @param {ParsedAggregateName} aggregate_name
384
- * @param {ParsedTraversalTerm} traversal
385
- * @param {ParsedExpression} expression
386
- * @returns {ParseTermResult}
387
- */
388
- function createAggregateTerm(
389
- parser_state,
390
- aggregate_name,
391
- traversal,
392
- expression,
393
- ) {
394
- /** @type {ParsedAggregateTerm} */
395
- const aggregate_term = {
396
- aggregate_name,
397
- expression,
398
- kind: 'aggregate',
399
- traversal,
400
- };
401
-
402
- if (aggregate_name !== 'count') {
403
- return successTerm(aggregate_term);
404
- }
405
-
406
- const count_tail = parseCountTail(parser_state);
407
-
408
- if (!count_tail) {
409
- return failToken(parser_state);
410
- }
411
-
412
- return successTerm({
413
- ...aggregate_term,
414
- comparison: count_tail.comparison,
415
- value: count_tail.value,
416
- });
417
- }
418
-
419
- /**
420
- * @param {ParserState} parser_state
421
- * @param {number} start_index
422
- * @param {ParsedFieldName | string} field_name
423
- * @returns {ParseTermResult | null}
424
- */
425
- function parseFieldSet(parser_state, start_index, field_name) {
426
- const operator_start_index = parser_state.index;
427
-
428
- if (!consumeRequiredWhitespace(parser_state)) {
429
- return null;
430
- }
431
-
432
- const operator = parseSetOperator(parser_state);
433
-
434
- if (!operator) {
435
- parser_state.index = operator_start_index;
436
- return null;
437
- }
438
-
439
- if (!consumeRequiredWhitespace(parser_state)) {
440
- return failToken(parser_state);
441
- }
442
-
443
- const values = parseList(parser_state);
444
-
445
- if (!values) {
446
- return failToken(parser_state);
447
- }
448
-
449
- if (!Array.isArray(values)) {
450
- return values;
451
- }
452
-
453
- return successTerm({
454
- column: start_index + 1,
455
- field_name,
456
- kind: 'field_set',
457
- operator,
458
- values,
459
- });
460
- }
461
-
462
- /**
463
- * @param {ParserState} parser_state
464
- * @param {number} start_index
465
- * @param {string} field_or_relation_name
466
- * @returns {ParseTermResult}
467
- */
468
- function parseOperatorTerm(parser_state, start_index, field_or_relation_name) {
469
- const field_set = parseFieldSet(
470
- parser_state,
471
- start_index,
472
- field_or_relation_name,
473
- );
474
-
475
- if (field_set) {
476
- return field_set;
477
- }
478
-
479
- const prefix_term = parsePrefixTerm(
480
- parser_state,
481
- start_index,
482
- field_or_relation_name,
483
- );
484
-
485
- if (prefix_term) {
486
- return prefix_term;
487
- }
488
-
489
- const contains_term = parseContainsTerm(
490
- parser_state,
491
- start_index,
492
- field_or_relation_name,
493
- );
494
-
495
- if (contains_term) {
496
- return contains_term;
497
- }
498
-
499
- const comparison_term = parseFieldComparisonTerm(
500
- parser_state,
501
- start_index,
502
- field_or_relation_name,
503
- );
504
-
505
- if (comparison_term) {
506
- return comparison_term;
507
- }
508
-
509
- const equality_term = parseEqualityTerm(
510
- parser_state,
511
- start_index,
512
- field_or_relation_name,
513
- );
514
-
515
- if (equality_term) {
516
- return equality_term;
517
- }
518
-
519
- if (consumeOperator(parser_state, ':*')) {
520
- return successTerm({
521
- column: start_index + 1,
522
- kind: 'relation',
523
- relation_name: field_or_relation_name,
524
- });
525
- }
526
-
527
- parser_state.index = start_index;
528
- return failToken(parser_state);
529
- }
530
-
531
- /**
532
- * @param {ParserState} parser_state
533
- * @returns {{ comparison: ParsedAggregateComparison, value: number } | null}
534
- */
535
- function parseCountTail(parser_state) {
536
- if (!consumeRequiredWhitespace(parser_state)) {
537
- return null;
538
- }
539
-
540
- const comparison = parseComparison(parser_state);
541
-
542
- if (!comparison || !consumeRequiredWhitespace(parser_state)) {
543
- return null;
544
- }
545
-
546
- const value = parseInteger(parser_state);
547
-
548
- return value === null ? null : { comparison, value };
549
- }
550
-
551
- /**
552
- * @param {ParserState} parser_state
553
- * @returns {{ success: true, traversal: ParsedTraversalTerm } | { diagnostic: PatramDiagnostic, success: false }}
554
- */
555
- function parseTraversal(parser_state) {
556
- const column = parser_state.index + 1;
557
- const direction = parseIdentifier(parser_state);
558
-
559
- if (
560
- (direction !== 'in' && direction !== 'out') ||
561
- !consumeOperator(parser_state, ':')
562
- ) {
563
- return failToken(parser_state);
564
- }
565
-
566
- const relation_name = parseIdentifier(parser_state);
567
-
568
- return relation_name
569
- ? { success: true, traversal: { column, direction, relation_name } }
570
- : failToken(parser_state);
571
- }
572
-
573
- /**
574
- * @param {ParserState} parser_state
575
- * @returns {'in' | 'not in' | null}
576
- */
577
- function parseSetOperator(parser_state) {
578
- if (consumeKeyword(parser_state, 'in')) {
579
- return 'in';
580
- }
581
-
582
- if (
583
- !consumeKeyword(parser_state, 'not') ||
584
- !consumeRequiredWhitespace(parser_state)
585
- ) {
586
- return null;
587
- }
588
-
589
- return consumeKeyword(parser_state, 'in') ? 'not in' : null;
590
- }
591
-
592
- /**
593
- * @param {ParserState} parser_state
594
- * @param {number} start_index
595
- * @param {string} field_name
596
- * @returns {ParseTermResult | null}
597
- */
598
- function parsePrefixTerm(parser_state, start_index, field_name) {
599
- if (!consumeOperator(parser_state, '^=')) {
600
- return null;
601
- }
602
-
603
- skipWhitespace(parser_state);
604
- const value_result = parseResolvedBareValue(parser_state);
605
-
606
- if (!value_result) {
607
- return failToken(parser_state);
608
- }
609
-
610
- if (!value_result.success) {
611
- return value_result;
612
- }
613
-
614
- return value_result.value
615
- ? successTerm({
616
- column: start_index + 1,
617
- field_name,
618
- kind: 'field',
619
- operator: '^=',
620
- value: value_result.value,
621
- })
622
- : failToken(parser_state);
623
- }
624
-
625
- /**
626
- * @param {ParserState} parser_state
627
- * @param {number} start_index
628
- * @param {string} field_name
629
- * @returns {ParseTermResult | null}
630
- */
631
- function parseContainsTerm(parser_state, start_index, field_name) {
632
- if (!consumeOperator(parser_state, '~')) {
633
- return null;
634
- }
635
-
636
- skipWhitespace(parser_state);
637
- const value_result = parseResolvedBareValue(parser_state);
638
-
639
- if (!value_result) {
640
- return failToken(parser_state);
641
- }
642
-
643
- if (!value_result.success) {
644
- return value_result;
645
- }
646
-
647
- return value_result.value
648
- ? successTerm({
649
- column: start_index + 1,
650
- field_name,
651
- kind: 'field',
652
- operator: '~',
653
- value: value_result.value,
654
- })
655
- : failToken(parser_state);
656
- }
657
-
658
- /**
659
- * @param {ParserState} parser_state
660
- * @param {number} start_index
661
- * @param {string} field_or_relation_name
662
- * @returns {ParseTermResult | null}
663
- */
664
- function parseEqualityTerm(parser_state, start_index, field_or_relation_name) {
665
- if (!consumeOperator(parser_state, '=')) {
666
- return null;
667
- }
668
-
669
- skipWhitespace(parser_state);
670
- const value_result = parseResolvedBareValue(parser_state);
671
-
672
- if (!value_result) {
673
- return failToken(parser_state);
674
- }
675
-
676
- if (!value_result.success) {
677
- return value_result;
678
- }
679
-
680
- const value = value_result.value;
681
-
682
- if (
683
- field_or_relation_name.startsWith('$') ||
684
- field_or_relation_name === 'title' ||
685
- !value.includes(':')
686
- ) {
687
- return successTerm({
688
- column: start_index + 1,
689
- field_name: field_or_relation_name,
690
- kind: 'field',
691
- operator: '=',
692
- value,
693
- });
694
- }
695
-
696
- return successTerm({
697
- column: start_index + 1,
698
- kind: 'relation_target',
699
- relation_name: field_or_relation_name,
700
- target_id: value,
701
- });
702
- }
703
-
704
- /**
705
- * @param {ParserState} parser_state
706
- * @param {number} start_index
707
- * @param {string} field_name
708
- * @returns {ParseTermResult | null}
709
- */
710
- function parseFieldComparisonTerm(parser_state, start_index, field_name) {
711
- const operator = parseFieldComparisonOperator(parser_state);
712
-
713
- if (!operator) {
714
- return null;
715
- }
716
-
717
- skipWhitespace(parser_state);
718
- const value_result = parseResolvedBareValue(parser_state);
719
-
720
- if (!value_result) {
721
- return failToken(parser_state);
722
- }
723
-
724
- if (!value_result.success) {
725
- return value_result;
726
- }
727
-
728
- return successTerm({
729
- column: start_index + 1,
730
- field_name,
731
- kind: 'field',
732
- operator,
733
- value: value_result.value,
734
- });
735
- }
736
-
737
- /**
738
- * @param {ParserState} parser_state
739
- * @returns {'!=' | '<=' | '>=' | '<' | '>' | null}
740
- */
741
- function parseFieldComparisonOperator(parser_state) {
742
- /** @type {Array<'!=' | '<=' | '>=' | '<' | '>'>} */
743
- const comparisons = ['!=', '<=', '>=', '<', '>'];
744
-
745
- return (
746
- comparisons.find((value) => consumeOperator(parser_state, value)) ?? null
747
- );
748
- }
749
-
750
- /**
751
- * @param {ParserState} parser_state
752
- * @returns {string[] | ParseFailureResult | null}
753
- */
754
- function parseList(parser_state) {
755
- if (!consumeOperator(parser_state, '[')) {
756
- return null;
757
- }
758
-
759
- /** @type {string[]} */
760
- const values = [];
761
-
762
- while (true) {
763
- skipWhitespace(parser_state);
764
-
765
- if (consumeOperator(parser_state, ']')) {
766
- return values.length > 0 ? values : null;
767
- }
768
-
769
- const list_value_result = parseResolvedListValue(parser_state);
770
-
771
- if (!list_value_result) {
772
- return null;
773
- }
774
-
775
- if (!list_value_result.success) {
776
- return list_value_result;
777
- }
778
-
779
- values.push(list_value_result.value);
780
- skipWhitespace(parser_state);
781
-
782
- if (consumeOperator(parser_state, ']')) {
783
- return values;
784
- }
785
-
786
- if (!consumeOperator(parser_state, ',')) {
787
- return null;
788
- }
789
- }
790
- }
791
-
792
- /**
793
- * @param {ParserState} parser_state
794
- * @returns {ParsedAggregateComparison | null}
795
- */
796
- function parseComparison(parser_state) {
797
- /** @type {ParsedAggregateComparison[]} */
798
- const comparisons = ['>=', '<=', '!=', '=', '>', '<'];
799
-
800
- return (
801
- comparisons.find((value) => consumeOperator(parser_state, value)) ?? null
802
- );
803
- }
804
-
805
- /**
806
- * @param {ParserState} parser_state
807
- * @returns {string | null}
808
- */
809
- function parseIdentifier(parser_state) {
810
- return readMatch(parser_state, /^\$?[a-z_][a-z0-9_]*/u);
811
- }
812
-
813
- /**
814
- * @param {ParserState} parser_state
815
- * @returns {number | null}
816
- */
817
- function parseInteger(parser_state) {
818
- const numeric_text = readMatch(parser_state, /^\d+/u);
819
- return numeric_text ? Number.parseInt(numeric_text, 10) : null;
820
- }
821
-
822
- /**
823
- * @param {ParserState} parser_state
824
- * @returns {string | null}
825
- */
826
- function parseBareValue(parser_state) {
827
- return readMatch(parser_state, /^[^\s\],)]+/u);
828
- }
829
-
830
- /**
831
- * @param {ParserState} parser_state
832
- * @returns {string | null}
833
- */
834
- function parseListValue(parser_state) {
835
- const list_value = readMatch(parser_state, /^[^\s\],][^\],)]*/u);
836
- return list_value?.trim() || null;
837
- }
838
-
839
- /**
840
- * @param {ParserState} parser_state
841
- * @returns {ParseValueResult | null}
842
- */
843
- function parseResolvedBareValue(parser_state) {
844
- const start_index = parser_state.index;
845
- const raw_value = parseBareValue(parser_state);
846
-
847
- if (!raw_value) {
848
- return null;
849
- }
850
-
851
- return resolveBindingValue(parser_state, raw_value, start_index);
852
- }
853
-
854
- /**
855
- * @param {ParserState} parser_state
856
- * @returns {ParseValueResult | null}
857
- */
858
- function parseResolvedListValue(parser_state) {
859
- const start_index = parser_state.index;
860
- const raw_value = parseListValue(parser_state);
861
-
862
- if (!raw_value) {
863
- return null;
864
- }
865
-
866
- return resolveBindingValue(parser_state, raw_value, start_index);
867
- }
868
-
869
- /**
870
- * @param {ParserState} parser_state
871
- * @param {string} raw_value
872
- * @param {number} start_index
873
- * @returns {ParseValueResult}
874
- */
875
- function resolveBindingValue(parser_state, raw_value, start_index) {
876
- if (!raw_value.startsWith('@')) {
877
- return {
878
- success: true,
879
- value: raw_value,
880
- };
881
- }
882
-
883
- const binding_name = raw_value.slice(1);
884
-
885
- if (!/^[a-z_][a-z0-9_]*$/u.test(binding_name)) {
886
- return fail(
887
- start_index + 1,
888
- `Unsupported query binding "${raw_value}".`,
889
- 'query.invalid',
890
- );
891
- }
892
-
893
- const binding_value = parser_state.bindings[binding_name];
894
-
895
- if (binding_value === undefined) {
896
- return fail(
897
- start_index + 1,
898
- `Missing query binding "${binding_name}".`,
899
- 'query.binding_missing',
900
- );
901
- }
902
-
903
- return {
904
- success: true,
905
- value: binding_value,
906
- };
907
- }
908
-
909
- /**
910
- * @param {ParserState} parser_state
911
- * @param {RegExp} pattern
912
- * @returns {string | null}
913
- */
914
- function readMatch(parser_state, pattern) {
915
- const match = parser_state.where_clause
916
- .slice(parser_state.index)
917
- .match(pattern);
918
-
919
- if (!match) {
920
- return null;
921
- }
922
-
923
- parser_state.index += match[0].length;
924
- return match[0];
925
- }
926
-
927
- /**
928
- * @param {ParserState} parser_state
929
- * @param {string} operator
930
- * @returns {boolean}
931
- */
932
- function consumeOperator(parser_state, operator) {
933
- skipWhitespace(parser_state);
934
-
935
- if (!parser_state.where_clause.startsWith(operator, parser_state.index)) {
936
- return false;
937
- }
938
-
939
- parser_state.index += operator.length;
940
- return true;
941
- }
942
-
943
- /**
944
- * @param {ParserState} parser_state
945
- * @param {string} keyword
946
- * @returns {boolean}
947
- */
948
- function consumeKeyword(parser_state, keyword) {
949
- if (!parser_state.where_clause.startsWith(keyword, parser_state.index)) {
950
- return false;
951
- }
952
-
953
- const next_character =
954
- parser_state.where_clause[parser_state.index + keyword.length];
955
-
956
- if (next_character && /[a-z_]/u.test(next_character)) {
957
- return false;
958
- }
959
-
960
- parser_state.index += keyword.length;
961
- return true;
962
- }
963
-
964
- /**
965
- * @param {ParserState} parser_state
966
- * @returns {boolean}
967
- */
968
- function consumeRequiredWhitespace(parser_state) {
969
- return skipWhitespace(parser_state) > 0;
970
- }
971
-
972
- /**
973
- * @param {ParserState} parser_state
974
- * @returns {number}
975
- */
976
- function skipWhitespace(parser_state) {
977
- const whitespace = readMatch(parser_state, /^\s+/u);
978
- return whitespace?.length ?? 0;
979
- }
980
-
981
- /**
982
- * @param {ParserState} parser_state
983
- * @returns {string | undefined}
984
- */
985
- function currentCharacter(parser_state) {
986
- return parser_state.where_clause[parser_state.index];
987
- }
988
-
989
- /**
990
- * @param {ParserState} parser_state
991
- * @returns {boolean}
992
- */
993
- function isAtEnd(parser_state) {
994
- return parser_state.index >= parser_state.where_clause.length;
995
- }
996
-
997
- /**
998
- * @param {ParsedExpression} expression
999
- * @returns {ParseExpressionResult}
1000
- */
1001
- function successExpression(expression) {
1002
- return { expression, success: true };
1003
- }
1004
-
1005
- /**
1006
- * @param {ParsedTerm} term
1007
- * @returns {ParseTermResult}
1008
- */
1009
- function successTerm(term) {
1010
- return { success: true, term };
1011
- }
1012
-
1013
- /**
1014
- * @param {ParserState} parser_state
1015
- * @returns {{ diagnostic: PatramDiagnostic, success: false }}
1016
- */
1017
- function failToken(parser_state) {
1018
- return fail(
1019
- parser_state.index + 1,
1020
- `Unsupported query token "${readToken(parser_state, parser_state.index)}".`,
1021
- );
1022
- }
1023
-
1024
- /**
1025
- * @param {ParserState} parser_state
1026
- * @returns {{ diagnostic: PatramDiagnostic, success: false }}
1027
- */
1028
- function failExpectedTerm(parser_state) {
1029
- return fail(
1030
- isAtEnd(parser_state)
1031
- ? parser_state.where_clause.length + 1
1032
- : parser_state.index + 1,
1033
- 'Expected a query term.',
1034
- );
1035
- }
1036
-
1037
- /**
1038
- * @param {number} column
1039
- * @param {string} message
1040
- * @param {string} code
1041
- * @returns {{ diagnostic: PatramDiagnostic, success: false }}
1042
- */
1043
- function fail(column, message, code = 'query.invalid') {
1044
- return {
1045
- diagnostic: {
1046
- code,
1047
- column,
1048
- level: 'error',
1049
- line: 1,
1050
- message,
1051
- path: '<query>',
1052
- },
1053
- success: false,
1054
- };
1055
- }
1056
-
1057
- /**
1058
- * @param {ParserState} parser_state
1059
- * @param {number} start_index
1060
- * @returns {string}
1061
- */
1062
- function readToken(parser_state, start_index) {
1063
- return parser_state.where_clause.slice(start_index).match(/^\S+/u)?.[0] ?? '';
1064
- }