patram 0.11.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 (110) hide show
  1. package/bin/patram.js +4 -4
  2. package/lib/cli/commands/fields.js +0 -4
  3. package/lib/cli/commands/queries.js +10 -20
  4. package/lib/cli/commands/query.js +1 -8
  5. package/lib/cli/commands/refs.js +3 -10
  6. package/lib/cli/commands/show.js +1 -8
  7. package/lib/cli/help-metadata.js +71 -106
  8. package/lib/cli/main.js +10 -10
  9. package/lib/cli/parse-arguments-helpers.js +165 -59
  10. package/lib/cli/parse-arguments.js +4 -4
  11. package/lib/cli/render-help.js +2 -2
  12. package/lib/config/defaults.js +33 -25
  13. package/lib/config/load-patram-config.d.ts +8 -33
  14. package/lib/config/load-patram-config.js +9 -33
  15. package/lib/config/load-patram-config.types.d.ts +3 -40
  16. package/lib/config/manage-stored-queries-helpers.d.ts +4 -4
  17. package/lib/config/manage-stored-queries-helpers.js +91 -33
  18. package/lib/config/manage-stored-queries.d.ts +4 -4
  19. package/lib/config/manage-stored-queries.js +11 -5
  20. package/lib/config/patram-config.d.ts +34 -34
  21. package/lib/config/patram-config.js +3 -3
  22. package/lib/config/patram-config.types.d.ts +5 -11
  23. package/lib/config/resolve-patram-graph-config.d.ts +5 -1
  24. package/lib/config/resolve-patram-graph-config.js +3 -119
  25. package/lib/config/schema.d.ts +158 -269
  26. package/lib/config/schema.js +72 -210
  27. package/lib/config/validate-patram-config-value.js +6 -31
  28. package/lib/config/validation.d.ts +2 -12
  29. package/lib/config/validation.js +125 -483
  30. package/lib/find-close-match.d.ts +4 -1
  31. package/lib/graph/build-graph-identity.d.ts +1 -32
  32. package/lib/graph/build-graph-identity.js +5 -269
  33. package/lib/graph/build-graph.d.ts +13 -4
  34. package/lib/graph/build-graph.js +347 -488
  35. package/lib/graph/build-graph.types.d.ts +8 -9
  36. package/lib/graph/check-directive-metadata-helpers.d.ts +30 -0
  37. package/lib/graph/check-directive-metadata-helpers.js +126 -0
  38. package/lib/graph/check-directive-metadata.d.ts +8 -9
  39. package/lib/graph/check-directive-metadata.js +70 -561
  40. package/lib/graph/check-directive-path-target.d.ts +6 -13
  41. package/lib/graph/check-directive-path-target.js +26 -57
  42. package/lib/graph/check-directive-value.d.ts +1 -5
  43. package/lib/graph/check-directive-value.js +40 -180
  44. package/lib/graph/check-graph.d.ts +5 -5
  45. package/lib/graph/check-graph.js +8 -6
  46. package/lib/graph/document-node-identity.d.ts +23 -7
  47. package/lib/graph/document-node-identity.js +417 -160
  48. package/lib/graph/graph-node.d.ts +42 -0
  49. package/lib/graph/graph-node.js +83 -0
  50. package/lib/graph/inspect-reverse-references.js +16 -11
  51. package/lib/graph/load-project-graph.d.ts +7 -7
  52. package/lib/graph/load-project-graph.js +7 -7
  53. package/lib/graph/parse-where-clause.types.d.ts +3 -2
  54. package/lib/graph/query/cypher-reader.d.ts +59 -0
  55. package/lib/graph/query/cypher-reader.js +151 -0
  56. package/lib/graph/query/cypher-support.d.ts +79 -0
  57. package/lib/graph/query/cypher-support.js +213 -0
  58. package/lib/graph/query/cypher-tokenize.d.ts +13 -0
  59. package/lib/graph/query/cypher-tokenize.js +225 -0
  60. package/lib/graph/query/cypher.types.d.ts +43 -0
  61. package/lib/graph/query/execute.d.ts +7 -7
  62. package/lib/graph/query/execute.js +71 -33
  63. package/lib/graph/query/inspect.js +58 -24
  64. package/lib/graph/query/parse-cypher-patterns.d.ts +27 -0
  65. package/lib/graph/query/parse-cypher-patterns.js +382 -0
  66. package/lib/graph/query/parse-cypher.d.ts +7 -0
  67. package/lib/graph/query/parse-cypher.js +580 -0
  68. package/lib/graph/query/parse-query.d.ts +13 -0
  69. package/lib/graph/query/parse-query.js +97 -0
  70. package/lib/graph/query/resolve.js +77 -23
  71. package/lib/output/command-output.js +12 -5
  72. package/lib/output/compact-layout.js +221 -0
  73. package/lib/output/format-output-item-block.js +31 -1
  74. package/lib/output/format-output-metadata.js +16 -29
  75. package/lib/output/format-stored-query-block.js +95 -0
  76. package/lib/output/layout-incoming-references.js +101 -19
  77. package/lib/output/layout-stored-queries.js +23 -330
  78. package/lib/output/list-queries.js +1 -1
  79. package/lib/output/render-field-discovery.js +11 -2
  80. package/lib/output/render-output-view.js +9 -5
  81. package/lib/output/renderers/json.js +5 -26
  82. package/lib/output/renderers/plain.js +155 -35
  83. package/lib/output/renderers/rich.js +250 -36
  84. package/lib/output/resolved-link-layout.js +43 -0
  85. package/lib/output/rich-source/render.js +193 -35
  86. package/lib/output/show-document.js +25 -18
  87. package/lib/output/view-model/index.js +124 -103
  88. package/lib/parse/jsdoc/parse-jsdoc-blocks.js +1 -1
  89. package/lib/parse/jsdoc/parse-jsdoc-claims.js +12 -6
  90. package/lib/parse/markdown/parse-markdown-claims.js +99 -62
  91. package/lib/parse/markdown/parse-markdown-directives.d.ts +10 -6
  92. package/lib/parse/markdown/parse-markdown-directives.js +104 -18
  93. package/lib/parse/markdown/parse-markdown-prose.d.ts +27 -0
  94. package/lib/parse/markdown/parse-markdown-prose.js +243 -0
  95. package/lib/parse/parse-claims.d.ts +2 -6
  96. package/lib/parse/parse-claims.js +11 -53
  97. package/lib/parse/tagged-fenced/tagged-fenced-blocks.d.ts +4 -4
  98. package/lib/parse/tagged-fenced/tagged-fenced-blocks.js +4 -4
  99. package/lib/parse/yaml/parse-yaml-claims.js +4 -4
  100. package/lib/patram.d.ts +3 -5
  101. package/lib/patram.js +1 -1
  102. package/lib/scan/discover-fields.js +194 -55
  103. package/lib/scan/list-source-files.d.ts +4 -4
  104. package/lib/scan/list-source-files.js +4 -4
  105. package/package.json +1 -1
  106. package/lib/directive-validation-test-helpers.js +0 -87
  107. package/lib/graph/query/parse.d.ts +0 -75
  108. package/lib/graph/query/parse.js +0 -1064
  109. package/lib/output/derived-summary.js +0 -280
  110. package/lib/output/format-derived-summary-row.js +0 -9
@@ -0,0 +1,580 @@
1
+ /* eslint-disable max-lines */
2
+ /**
3
+ * @import {
4
+ * ParsedAggregateComparison,
5
+ * ParsedAggregateTerm,
6
+ * ParsedExpression,
7
+ * ParsedFieldTerm,
8
+ * ParseWhereClauseResult,
9
+ * } from '../parse-where-clause.types.ts';
10
+ * @import { CypherExpressionResult, CypherParserState, CypherToken } from './cypher.types.ts';
11
+ */
12
+
13
+ import {
14
+ collapseAndExpressions,
15
+ collapseBooleanExpression,
16
+ createFieldExpression,
17
+ createFieldSetExpression,
18
+ createNodeLabelExpression,
19
+ isAggregateComparison,
20
+ } from './cypher-support.js';
21
+ import {
22
+ consumeKeyword,
23
+ consumeSymbol,
24
+ consumeToken,
25
+ expectKeyword,
26
+ expectSymbol,
27
+ failAtCurrent,
28
+ peekKeyword,
29
+ peekToken,
30
+ } from './cypher-reader.js';
31
+ import {
32
+ parseCypherListValue,
33
+ parseCypherScalarValue,
34
+ parseNodePattern,
35
+ parseSubqueryAggregate,
36
+ } from './parse-cypher-patterns.js';
37
+
38
+ /**
39
+ * @param {CypherParserState} parser_state
40
+ * @returns {ParseWhereClauseResult}
41
+ */
42
+ export function parseCypherExpression(parser_state) {
43
+ expectKeyword(parser_state, 'MATCH');
44
+ const root_node = parseNodePattern(parser_state);
45
+
46
+ if (!root_node.variable_name) {
47
+ return failAtCurrent(
48
+ parser_state,
49
+ 'Cypher root MATCH requires a variable.',
50
+ );
51
+ }
52
+
53
+ parser_state.root_variable_name = root_node.variable_name;
54
+
55
+ /** @type {ParsedExpression[]} */
56
+ const expressions = [];
57
+ const label_expression = createNodeLabelExpression(
58
+ root_node,
59
+ parser_state.repo_config,
60
+ );
61
+
62
+ if (label_expression) {
63
+ expressions.push(label_expression);
64
+ }
65
+
66
+ if (consumeKeyword(parser_state, 'WHERE')) {
67
+ const where_result = parseCypherBooleanExpression(parser_state);
68
+
69
+ if (!where_result.success) {
70
+ return where_result;
71
+ }
72
+
73
+ expressions.push(where_result.expression);
74
+ }
75
+
76
+ expectKeyword(parser_state, 'RETURN');
77
+ return resolveReturnExpression(
78
+ parser_state,
79
+ root_node.variable_name,
80
+ expressions,
81
+ );
82
+ }
83
+
84
+ /**
85
+ * @param {CypherParserState} parser_state
86
+ * @returns {CypherExpressionResult}
87
+ */
88
+ function parseCypherBooleanExpression(parser_state) {
89
+ return parseCypherOrExpression(parser_state);
90
+ }
91
+
92
+ /**
93
+ * @param {CypherParserState} parser_state
94
+ * @returns {CypherExpressionResult}
95
+ */
96
+ function parseCypherOrExpression(parser_state) {
97
+ return parseBooleanExpression(
98
+ parser_state,
99
+ 'OR',
100
+ 'or',
101
+ parseCypherAndExpression,
102
+ );
103
+ }
104
+
105
+ /**
106
+ * @param {CypherParserState} parser_state
107
+ * @returns {CypherExpressionResult}
108
+ */
109
+ function parseCypherAndExpression(parser_state) {
110
+ return parseBooleanExpression(
111
+ parser_state,
112
+ 'AND',
113
+ 'and',
114
+ parseCypherUnaryExpression,
115
+ );
116
+ }
117
+
118
+ /**
119
+ * @param {CypherParserState} parser_state
120
+ * @returns {CypherExpressionResult}
121
+ */
122
+ function parseCypherUnaryExpression(parser_state) {
123
+ if (!consumeKeyword(parser_state, 'NOT')) {
124
+ return parseCypherPrimaryExpression(parser_state);
125
+ }
126
+
127
+ const expression_result = parseCypherUnaryExpression(parser_state);
128
+
129
+ if (!expression_result.success) {
130
+ return expression_result;
131
+ }
132
+
133
+ return {
134
+ expression: {
135
+ expression: expression_result.expression,
136
+ kind: 'not',
137
+ },
138
+ success: true,
139
+ };
140
+ }
141
+
142
+ /**
143
+ * @param {CypherParserState} parser_state
144
+ * @returns {CypherExpressionResult}
145
+ */
146
+ function parseCypherPrimaryExpression(parser_state) {
147
+ if (consumeSymbol(parser_state, '(')) {
148
+ return parseGroupedExpression(parser_state);
149
+ }
150
+
151
+ if (peekKeyword(parser_state, 'EXISTS')) {
152
+ return parseExistsSubquery(parser_state);
153
+ }
154
+
155
+ if (peekKeyword(parser_state, 'COUNT')) {
156
+ return parseCountSubquery(parser_state);
157
+ }
158
+
159
+ return parsePropertyPredicate(parser_state);
160
+ }
161
+
162
+ /**
163
+ * @param {CypherParserState} parser_state
164
+ * @returns {CypherExpressionResult}
165
+ */
166
+ function parseExistsSubquery(parser_state) {
167
+ const exists_token = consumeToken(parser_state);
168
+
169
+ if (!exists_token) {
170
+ return failAtCurrent(parser_state, 'Expected EXISTS.');
171
+ }
172
+
173
+ const aggregate_result = parseSubqueryAggregate(
174
+ parser_state,
175
+ exists_token.column,
176
+ 'any',
177
+ parseCypherBooleanExpression,
178
+ );
179
+
180
+ if (!aggregate_result.success) {
181
+ return aggregate_result;
182
+ }
183
+
184
+ return createAggregateExpression(aggregate_result.term);
185
+ }
186
+
187
+ /**
188
+ * @param {CypherParserState} parser_state
189
+ * @returns {CypherExpressionResult}
190
+ */
191
+ function parseCountSubquery(parser_state) {
192
+ const count_token = consumeToken(parser_state);
193
+
194
+ if (!count_token) {
195
+ return failAtCurrent(parser_state, 'Expected COUNT.');
196
+ }
197
+
198
+ const aggregate_result = parseSubqueryAggregate(
199
+ parser_state,
200
+ count_token.column,
201
+ 'count',
202
+ parseCypherBooleanExpression,
203
+ );
204
+
205
+ if (!aggregate_result.success) {
206
+ return aggregate_result;
207
+ }
208
+
209
+ const comparison_result = parseCountComparison(parser_state);
210
+
211
+ if (!comparison_result.success) {
212
+ return comparison_result;
213
+ }
214
+
215
+ assignCountComparison(aggregate_result.term, comparison_result);
216
+ return createAggregateExpression(aggregate_result.term);
217
+ }
218
+
219
+ /**
220
+ * @param {CypherParserState} parser_state
221
+ * @returns {CypherExpressionResult}
222
+ */
223
+ function parsePropertyPredicate(parser_state) {
224
+ const field_reference = parsePredicateLeftHandSide(parser_state);
225
+
226
+ if (!field_reference.success) {
227
+ return field_reference;
228
+ }
229
+
230
+ const operator_result = parsePredicateOperator(parser_state);
231
+
232
+ if (!operator_result.success) {
233
+ return operator_result;
234
+ }
235
+
236
+ if (operator_result.kind === 'scalar') {
237
+ return createFieldExpression(
238
+ field_reference.field_name,
239
+ operator_result.operator,
240
+ parseCypherScalarValue(parser_state),
241
+ field_reference.column,
242
+ );
243
+ }
244
+
245
+ return createFieldSetExpression(
246
+ field_reference.field_name,
247
+ operator_result.operator,
248
+ parseCypherListValue(parser_state),
249
+ field_reference.column,
250
+ );
251
+ }
252
+
253
+ /**
254
+ * @param {CypherParserState} parser_state
255
+ * @returns {{ success: true, column: number, field_name: string } | ReturnType<typeof failAtCurrent>}
256
+ */
257
+ function parsePredicateLeftHandSide(parser_state) {
258
+ const identifier_token = consumeToken(parser_state);
259
+
260
+ if (!identifier_token || identifier_token.kind !== 'identifier') {
261
+ return failAtCurrent(parser_state, 'Expected a Cypher predicate.');
262
+ }
263
+
264
+ if (peekToken(parser_state)?.value === '(') {
265
+ return parseStructuralFunctionPredicate(parser_state, identifier_token);
266
+ }
267
+
268
+ if (peekToken(parser_state)?.value !== '.') {
269
+ return failAtCurrent(parser_state, 'Expected a Cypher predicate.');
270
+ }
271
+
272
+ expectSymbol(parser_state, '.');
273
+ const field_token = consumeToken(parser_state);
274
+
275
+ if (!field_token || field_token.kind !== 'identifier') {
276
+ return failAtCurrent(parser_state, 'Expected a property name.');
277
+ }
278
+
279
+ return {
280
+ column: field_token.column,
281
+ field_name: field_token.value,
282
+ success: true,
283
+ };
284
+ }
285
+
286
+ /**
287
+ * @param {CypherParserState} parser_state
288
+ * @param {CypherToken} identifier_token
289
+ * @returns {{ success: true, column: number, field_name: string } | ReturnType<typeof failAtCurrent>}
290
+ */
291
+ function parseStructuralFunctionPredicate(parser_state, identifier_token) {
292
+ const field_name = resolveStructuralFunctionFieldName(identifier_token.value);
293
+
294
+ if (!field_name) {
295
+ return failAtCurrent(parser_state, 'Expected a Cypher predicate.');
296
+ }
297
+
298
+ expectSymbol(parser_state, '(');
299
+
300
+ const variable_token = consumeToken(parser_state);
301
+
302
+ if (!variable_token || variable_token.kind !== 'identifier') {
303
+ return failAtCurrent(parser_state, 'Expected a variable name.');
304
+ }
305
+
306
+ expectSymbol(parser_state, ')');
307
+
308
+ return {
309
+ column: identifier_token.column,
310
+ field_name,
311
+ success: true,
312
+ };
313
+ }
314
+
315
+ /**
316
+ * @param {CypherParserState} parser_state
317
+ * @param {string} keyword
318
+ * @param {'and' | 'or'} kind
319
+ * @param {(parser_state: CypherParserState) => CypherExpressionResult} parse_next_expression
320
+ * @returns {CypherExpressionResult}
321
+ */
322
+ function parseBooleanExpression(
323
+ parser_state,
324
+ keyword,
325
+ kind,
326
+ parse_next_expression,
327
+ ) {
328
+ const first_result = parse_next_expression(parser_state);
329
+
330
+ if (!first_result.success) {
331
+ return first_result;
332
+ }
333
+
334
+ /** @type {ParsedExpression[]} */
335
+ const expressions = [first_result.expression];
336
+
337
+ while (consumeKeyword(parser_state, keyword)) {
338
+ const next_result = parse_next_expression(parser_state);
339
+
340
+ if (!next_result.success) {
341
+ return next_result;
342
+ }
343
+
344
+ expressions.push(next_result.expression);
345
+ }
346
+
347
+ return {
348
+ expression: collapseBooleanExpression(kind, expressions),
349
+ success: true,
350
+ };
351
+ }
352
+
353
+ /**
354
+ * @param {CypherParserState} parser_state
355
+ * @returns {CypherExpressionResult}
356
+ */
357
+ function parseGroupedExpression(parser_state) {
358
+ const expression_result = parseCypherBooleanExpression(parser_state);
359
+
360
+ if (!expression_result.success) {
361
+ return expression_result;
362
+ }
363
+
364
+ expectSymbol(parser_state, ')');
365
+ return expression_result;
366
+ }
367
+
368
+ /**
369
+ * @param {CypherParserState} parser_state
370
+ * @returns {{ success: true, comparison: ParsedAggregateComparison, value: number } | ReturnType<typeof failAtCurrent>}
371
+ */
372
+ function parseCountComparison(parser_state) {
373
+ const comparison = resolveAggregateComparison(consumeToken(parser_state));
374
+
375
+ if (!comparison) {
376
+ return failAtCurrent(parser_state, 'Expected a count comparison operator.');
377
+ }
378
+
379
+ const value_token = consumeToken(parser_state);
380
+
381
+ if (!value_token || value_token.kind !== 'number') {
382
+ return failAtCurrent(parser_state, 'Expected a count comparison value.');
383
+ }
384
+
385
+ return {
386
+ comparison,
387
+ success: true,
388
+ value: Number.parseInt(value_token.value, 10),
389
+ };
390
+ }
391
+
392
+ /**
393
+ * @param {CypherParserState} parser_state
394
+ * @returns {{
395
+ * success: true,
396
+ * kind: 'scalar',
397
+ * operator: ParsedFieldTerm['operator'],
398
+ * } | {
399
+ * success: true,
400
+ * kind: 'set',
401
+ * operator: 'in' | 'not in',
402
+ * } | ReturnType<typeof failAtCurrent>}
403
+ */
404
+ function parsePredicateOperator(parser_state) {
405
+ if (consumeKeyword(parser_state, 'STARTS')) {
406
+ expectKeyword(parser_state, 'WITH');
407
+ return {
408
+ kind: 'scalar',
409
+ operator: '^=',
410
+ success: true,
411
+ };
412
+ }
413
+
414
+ if (consumeKeyword(parser_state, 'ENDS')) {
415
+ expectKeyword(parser_state, 'WITH');
416
+ return {
417
+ kind: 'scalar',
418
+ operator: '$=',
419
+ success: true,
420
+ };
421
+ }
422
+
423
+ if (consumeKeyword(parser_state, 'CONTAINS')) {
424
+ return {
425
+ kind: 'scalar',
426
+ operator: '~',
427
+ success: true,
428
+ };
429
+ }
430
+
431
+ if (consumeKeyword(parser_state, 'NOT')) {
432
+ expectKeyword(parser_state, 'IN');
433
+ return {
434
+ kind: 'set',
435
+ operator: 'not in',
436
+ success: true,
437
+ };
438
+ }
439
+
440
+ if (consumeKeyword(parser_state, 'IN')) {
441
+ return {
442
+ kind: 'set',
443
+ operator: 'in',
444
+ success: true,
445
+ };
446
+ }
447
+
448
+ const operator = resolveFieldComparisonOperator(peekToken(parser_state));
449
+
450
+ if (!operator) {
451
+ return failAtCurrent(parser_state, 'Expected a Cypher predicate operator.');
452
+ }
453
+
454
+ consumeToken(parser_state);
455
+
456
+ return {
457
+ kind: 'scalar',
458
+ operator,
459
+ success: true,
460
+ };
461
+ }
462
+
463
+ /**
464
+ * @param {CypherParserState} parser_state
465
+ * @param {string} root_variable_name
466
+ * @param {ParsedExpression[]} expressions
467
+ * @returns {ParseWhereClauseResult}
468
+ */
469
+ function resolveReturnExpression(
470
+ parser_state,
471
+ root_variable_name,
472
+ expressions,
473
+ ) {
474
+ const return_token = consumeToken(parser_state);
475
+
476
+ if (!return_token || return_token.kind !== 'identifier') {
477
+ return failAtCurrent(parser_state, 'Expected a return variable.');
478
+ }
479
+
480
+ if (return_token.value !== root_variable_name) {
481
+ return failAtCurrent(
482
+ parser_state,
483
+ 'Cypher RETURN must return the root MATCH variable.',
484
+ );
485
+ }
486
+
487
+ return {
488
+ expression: collapseAndExpressions(expressions),
489
+ success: true,
490
+ };
491
+ }
492
+
493
+ /**
494
+ * @param {ParsedAggregateTerm} aggregate_term
495
+ * @param {{ success: true, comparison: ParsedAggregateComparison, value: number }} comparison_result
496
+ */
497
+ function assignCountComparison(aggregate_term, comparison_result) {
498
+ aggregate_term.comparison = comparison_result.comparison;
499
+ aggregate_term.value = comparison_result.value;
500
+ }
501
+
502
+ /**
503
+ * @param {ParsedAggregateTerm} aggregate_term
504
+ * @returns {CypherExpressionResult}
505
+ */
506
+ function createAggregateExpression(aggregate_term) {
507
+ return {
508
+ expression: {
509
+ kind: 'term',
510
+ term: aggregate_term,
511
+ },
512
+ success: true,
513
+ };
514
+ }
515
+
516
+ /**
517
+ * @param {CypherToken | undefined} token
518
+ * @returns {ParsedAggregateComparison | undefined}
519
+ */
520
+ function resolveAggregateComparison(token) {
521
+ const operator = resolveComparisonOperator(token);
522
+
523
+ if (!operator || !isAggregateComparison(operator)) {
524
+ return undefined;
525
+ }
526
+
527
+ return operator;
528
+ }
529
+
530
+ /**
531
+ * @param {CypherToken | undefined} token
532
+ * @returns {ParsedFieldTerm['operator'] | undefined}
533
+ */
534
+ function resolveFieldComparisonOperator(token) {
535
+ const operator = resolveComparisonOperator(token);
536
+
537
+ if (!operator) {
538
+ return undefined;
539
+ }
540
+
541
+ return /** @type {ParsedFieldTerm['operator']} */ (operator);
542
+ }
543
+
544
+ /**
545
+ * @param {string} function_name
546
+ * @returns {'$id' | '$path' | undefined}
547
+ */
548
+ function resolveStructuralFunctionFieldName(function_name) {
549
+ if (function_name === 'id') {
550
+ return '$id';
551
+ }
552
+
553
+ if (function_name === 'path') {
554
+ return '$path';
555
+ }
556
+
557
+ return undefined;
558
+ }
559
+
560
+ /**
561
+ * @param {CypherToken | undefined} token
562
+ * @returns {string | undefined}
563
+ */
564
+ function resolveComparisonOperator(token) {
565
+ if (!token || token.kind !== 'symbol') {
566
+ return undefined;
567
+ }
568
+
569
+ switch (token.value) {
570
+ case '<':
571
+ case '<=':
572
+ case '<>':
573
+ case '=':
574
+ case '>':
575
+ case '>=':
576
+ return token.value;
577
+ default:
578
+ return undefined;
579
+ }
580
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Parse one query string as the constrained Cypher subset.
3
+ *
4
+ * @param {string} query_text
5
+ * @param {PatramRepoConfig | null} repo_config
6
+ * @param {{ bindings?: Record<string, string> }=} options
7
+ * @returns {ParseWhereClauseResult}
8
+ */
9
+ export function parseQueryExpression(query_text: string, repo_config: PatramRepoConfig | null, options?: {
10
+ bindings?: Record<string, string>;
11
+ } | undefined): ParseWhereClauseResult;
12
+ import type { PatramRepoConfig } from '../../config/load-patram-config.types.d.ts';
13
+ import type { ParseWhereClauseResult } from '../parse-where-clause.types.d.ts';
@@ -0,0 +1,97 @@
1
+ /**
2
+ * @import { PatramRepoConfig } from '../../config/load-patram-config.types.ts';
3
+ * @import { ParseWhereClauseResult } from '../parse-where-clause.types.ts';
4
+ * @import { CypherParserState } from './cypher.types.ts';
5
+ */
6
+
7
+ import { tokenizeCypher } from './cypher-tokenize.js';
8
+ import { parseCypherExpression } from './parse-cypher.js';
9
+
10
+ /**
11
+ * Parse one query string as the constrained Cypher subset.
12
+ *
13
+ * @param {string} query_text
14
+ * @param {PatramRepoConfig | null} repo_config
15
+ * @param {{ bindings?: Record<string, string> }=} options
16
+ * @returns {ParseWhereClauseResult}
17
+ */
18
+ export function parseQueryExpression(query_text, repo_config, options = {}) {
19
+ const tokenize_result = tokenizeCypher(query_text);
20
+
21
+ if (!tokenize_result.success) {
22
+ return tokenize_result;
23
+ }
24
+
25
+ /** @type {CypherParserState} */
26
+ const parser_state = {
27
+ bindings: options.bindings ?? {},
28
+ index: 0,
29
+ query_text,
30
+ repo_config,
31
+ root_variable_name: null,
32
+ tokens: tokenize_result.tokens,
33
+ };
34
+
35
+ try {
36
+ const expression_result = parseCypherExpression(parser_state);
37
+
38
+ if (!expression_result.success) {
39
+ return expression_result;
40
+ }
41
+
42
+ return resolveTrailingTokens(parser_state, expression_result);
43
+ } catch (error) {
44
+ return resolveCypherSyntaxError(query_text, parser_state, error);
45
+ }
46
+ }
47
+
48
+ /**
49
+ * @param {CypherParserState} parser_state
50
+ * @param {ParseWhereClauseResult & { success: true }} expression_result
51
+ * @returns {ParseWhereClauseResult}
52
+ */
53
+ function resolveTrailingTokens(parser_state, expression_result) {
54
+ if (parser_state.index >= parser_state.tokens.length) {
55
+ return expression_result;
56
+ }
57
+
58
+ const token = parser_state.tokens[parser_state.index];
59
+
60
+ return {
61
+ diagnostic: {
62
+ code: 'query.invalid',
63
+ column: token.column,
64
+ level: 'error',
65
+ line: 1,
66
+ message: `Unsupported query token "${token.value}".`,
67
+ path: '<query>',
68
+ },
69
+ success: false,
70
+ };
71
+ }
72
+
73
+ /**
74
+ * @param {string} query_text
75
+ * @param {CypherParserState} parser_state
76
+ * @param {unknown} error
77
+ * @returns {ParseWhereClauseResult}
78
+ */
79
+ function resolveCypherSyntaxError(query_text, parser_state, error) {
80
+ if (!(error instanceof Error)) {
81
+ throw error;
82
+ }
83
+
84
+ return {
85
+ diagnostic: {
86
+ code: 'query.invalid',
87
+ column:
88
+ parser_state.tokens[parser_state.index]?.column ??
89
+ query_text.length + 1,
90
+ level: 'error',
91
+ line: 1,
92
+ message: error.message,
93
+ path: '<query>',
94
+ },
95
+ success: false,
96
+ };
97
+ }