patram 0.2.0 → 0.4.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 (38) hide show
  1. package/lib/build-graph-identity.js +86 -99
  2. package/lib/build-graph.js +536 -31
  3. package/lib/build-graph.types.ts +6 -2
  4. package/lib/check-directive-metadata.js +534 -0
  5. package/lib/check-directive-value.js +291 -0
  6. package/lib/check-graph.js +23 -5
  7. package/lib/cli-help-metadata.js +56 -16
  8. package/lib/command-output.js +16 -1
  9. package/lib/derived-summary.js +10 -8
  10. package/lib/directive-diagnostics.js +38 -0
  11. package/lib/directive-type-rules.js +133 -0
  12. package/lib/discover-fields.js +435 -0
  13. package/lib/discover-fields.types.ts +52 -0
  14. package/lib/document-node-identity.js +317 -0
  15. package/lib/format-node-header.js +9 -7
  16. package/lib/format-output-metadata.js +15 -23
  17. package/lib/layout-stored-queries.js +124 -85
  18. package/lib/load-patram-config.js +433 -96
  19. package/lib/load-patram-config.types.ts +98 -3
  20. package/lib/load-project-graph.js +4 -1
  21. package/lib/output-view.types.ts +14 -6
  22. package/lib/parse-cli-arguments.types.ts +1 -1
  23. package/lib/parse-where-clause.js +344 -107
  24. package/lib/parse-where-clause.types.ts +25 -8
  25. package/lib/patram-cli.js +68 -4
  26. package/lib/patram-config.js +31 -31
  27. package/lib/patram-config.types.ts +10 -4
  28. package/lib/query-graph.js +269 -40
  29. package/lib/query-inspection.js +440 -60
  30. package/lib/render-field-discovery.js +184 -0
  31. package/lib/render-json-output.js +21 -22
  32. package/lib/render-output-view.js +301 -34
  33. package/lib/render-plain-output.js +1 -1
  34. package/lib/render-rich-output.js +1 -1
  35. package/lib/render-rich-source.js +245 -14
  36. package/lib/resolve-patram-graph-config.js +15 -9
  37. package/lib/show-document.js +66 -9
  38. package/package.json +5 -5
@@ -1,10 +1,10 @@
1
1
  /* eslint-disable max-lines */
2
2
  /**
3
3
  * @import { BuildGraphResult, GraphNode } from './build-graph.types.ts';
4
- * @import { PatramDiagnostic } from './load-patram-config.types.ts';
4
+ * @import { PatramDiagnostic, PatramRepoConfig } from './load-patram-config.types.ts';
5
5
  * @import {
6
6
  * ParsedAggregateTerm,
7
- * ParsedClause,
7
+ * ParsedExpression,
8
8
  * ParsedFieldSetTerm,
9
9
  * ParsedFieldTerm,
10
10
  * ParsedRelationTargetTerm,
@@ -14,6 +14,7 @@
14
14
  */
15
15
 
16
16
  import { parseWhereClause } from './parse-where-clause.js';
17
+ import { getQuerySemanticDiagnostics } from './query-inspection.js';
17
18
 
18
19
  /**
19
20
  * Query graph filtering.
@@ -54,10 +55,16 @@ export const DEFAULT_QUERY_LIMIT = 25;
54
55
  *
55
56
  * @param {BuildGraphResult} graph
56
57
  * @param {string} where_clause
58
+ * @param {PatramRepoConfig | { limit?: number, offset?: number }=} repo_config_or_pagination
57
59
  * @param {{ limit?: number, offset?: number }=} pagination_options
58
60
  * @returns {{ diagnostics: PatramDiagnostic[], nodes: GraphNode[], total_count: number }}
59
61
  */
60
- export function queryGraph(graph, where_clause, pagination_options = {}) {
62
+ export function queryGraph(
63
+ graph,
64
+ where_clause,
65
+ repo_config_or_pagination = {},
66
+ pagination_options = {},
67
+ ) {
61
68
  const parse_result = parseWhereClause(where_clause);
62
69
 
63
70
  if (!parse_result.success) {
@@ -68,15 +75,37 @@ export function queryGraph(graph, where_clause, pagination_options = {}) {
68
75
  };
69
76
  }
70
77
 
78
+ const { pagination_options: resolved_pagination_options, repo_config } =
79
+ resolveQueryGraphOptions(repo_config_or_pagination, pagination_options);
80
+
81
+ if (repo_config) {
82
+ const diagnostics = getQuerySemanticDiagnostics(
83
+ repo_config,
84
+ { kind: 'ad_hoc' },
85
+ parse_result.expression,
86
+ );
87
+
88
+ if (diagnostics.length > 0) {
89
+ return {
90
+ diagnostics,
91
+ nodes: [],
92
+ total_count: 0,
93
+ };
94
+ }
95
+ }
96
+
71
97
  const evaluation_context = {
72
98
  nodes: graph.nodes,
73
99
  relation_indexes: createRelationIndexes(graph.edges),
74
100
  };
75
101
  const graph_nodes = Object.values(graph.nodes).sort(compareGraphNodes);
76
102
  const matching_nodes = graph_nodes.filter((graph_node) =>
77
- matchesClauses(graph_node, parse_result.clauses, evaluation_context),
103
+ matchesExpression(graph_node, parse_result.expression, evaluation_context),
104
+ );
105
+ const paginated_nodes = paginateNodes(
106
+ matching_nodes,
107
+ resolved_pagination_options,
78
108
  );
79
- const paginated_nodes = paginateNodes(matching_nodes, pagination_options);
80
109
 
81
110
  return {
82
111
  diagnostics: [],
@@ -87,26 +116,36 @@ export function queryGraph(graph, where_clause, pagination_options = {}) {
87
116
 
88
117
  /**
89
118
  * @param {GraphNode} graph_node
90
- * @param {ParsedClause[]} clauses
119
+ * @param {ParsedExpression} expression
91
120
  * @param {EvaluationContext} evaluation_context
92
121
  * @returns {boolean}
93
122
  */
94
- function matchesClauses(graph_node, clauses, evaluation_context) {
95
- return clauses.every((clause) =>
96
- matchesClause(graph_node, clause, evaluation_context),
97
- );
98
- }
123
+ function matchesExpression(graph_node, expression, evaluation_context) {
124
+ if (expression.kind === 'and') {
125
+ return expression.expressions.every((subexpression) =>
126
+ matchesExpression(graph_node, subexpression, evaluation_context),
127
+ );
128
+ }
99
129
 
100
- /**
101
- * @param {GraphNode} graph_node
102
- * @param {ParsedClause} clause
103
- * @param {EvaluationContext} evaluation_context
104
- * @returns {boolean}
105
- */
106
- function matchesClause(graph_node, clause, evaluation_context) {
107
- const is_match = matchesTerm(graph_node, clause.term, evaluation_context);
130
+ if (expression.kind === 'or') {
131
+ return expression.expressions.some((subexpression) =>
132
+ matchesExpression(graph_node, subexpression, evaluation_context),
133
+ );
134
+ }
135
+
136
+ if (expression.kind === 'not') {
137
+ return !matchesExpression(
138
+ graph_node,
139
+ expression.expression,
140
+ evaluation_context,
141
+ );
142
+ }
143
+
144
+ if (expression.kind === 'term') {
145
+ return matchesTerm(graph_node, expression.term, evaluation_context);
146
+ }
108
147
 
109
- return clause.is_negated ? !is_match : is_match;
148
+ throw new Error('Unsupported parsed boolean expression.');
110
149
  }
111
150
 
112
151
  /**
@@ -152,7 +191,7 @@ function matchesAggregateTerm(graph_node, term, evaluation_context) {
152
191
  evaluation_context,
153
192
  );
154
193
  const matching_count = related_nodes.filter((related_node) =>
155
- matchesClauses(related_node, term.clauses, evaluation_context),
194
+ matchesExpression(related_node, term.expression, evaluation_context),
156
195
  ).length;
157
196
 
158
197
  if (term.aggregate_name === 'any') {
@@ -176,29 +215,48 @@ function matchesAggregateTerm(graph_node, term, evaluation_context) {
176
215
  * @returns {boolean}
177
216
  */
178
217
  function matchesFieldTerm(graph_node, term) {
179
- const term_key = `${term.field_name}${term.operator}`;
218
+ const field_values = getGraphNodeFieldValues(graph_node, term.field_name);
180
219
 
181
- if (
182
- term_key === 'id=' ||
183
- term_key === 'kind=' ||
184
- term_key === 'path=' ||
185
- term_key === 'status='
186
- ) {
187
- return graph_node[term.field_name] === term.value;
220
+ if (term.operator === '=') {
221
+ return field_values.some((field_value) => field_value === term.value);
188
222
  }
189
223
 
190
- if (term_key === 'id^=') {
191
- return graph_node.id.startsWith(term.value);
224
+ if (term.operator === '!=') {
225
+ return field_values.every((field_value) => field_value !== term.value);
192
226
  }
193
227
 
194
- if (term_key === 'path^=') {
195
- return (graph_node.path ?? '').startsWith(term.value);
228
+ if (term.operator === '^=') {
229
+ return field_values.some((field_value) =>
230
+ field_value.startsWith(term.value),
231
+ );
196
232
  }
197
233
 
198
- if (term_key === 'title~') {
199
- return (graph_node.title ?? '')
200
- .toLocaleLowerCase('en')
201
- .includes(term.value.toLocaleLowerCase('en'));
234
+ if (term.operator === '~') {
235
+ const term_value = term.value.toLocaleLowerCase('en');
236
+
237
+ return field_values.some((field_value) =>
238
+ field_value.toLocaleLowerCase('en').includes(term_value),
239
+ );
240
+ }
241
+
242
+ if (term.operator === '<' || term.operator === '<=') {
243
+ return field_values.some((field_value) =>
244
+ compareFieldValues(
245
+ field_value,
246
+ /** @type {'<' | '<='} */ (term.operator),
247
+ term.value,
248
+ ),
249
+ );
250
+ }
251
+
252
+ if (term.operator === '>' || term.operator === '>=') {
253
+ return field_values.some((field_value) =>
254
+ compareFieldValues(
255
+ field_value,
256
+ /** @type {'>' | '>='} */ (term.operator),
257
+ term.value,
258
+ ),
259
+ );
202
260
  }
203
261
 
204
262
  throw new Error('Unsupported parsed where clause.');
@@ -210,8 +268,10 @@ function matchesFieldTerm(graph_node, term) {
210
268
  * @returns {boolean}
211
269
  */
212
270
  function matchesFieldSetTerm(graph_node, term) {
213
- const actual_value = graph_node[term.field_name];
214
- const is_member = actual_value ? term.values.includes(actual_value) : false;
271
+ const field_values = getGraphNodeFieldValues(graph_node, term.field_name);
272
+ const is_member = field_values.some((field_value) =>
273
+ term.values.includes(field_value),
274
+ );
215
275
 
216
276
  return term.operator === 'in' ? is_member : !is_member;
217
277
  }
@@ -352,7 +412,10 @@ function createDirectionalRelationIndex(graph_edges, source_key, target_key) {
352
412
  * @returns {number}
353
413
  */
354
414
  function compareGraphNodes(left_node, right_node) {
355
- return left_node.id.localeCompare(right_node.id, 'en');
415
+ return (left_node.$id ?? left_node.id).localeCompare(
416
+ right_node.$id ?? right_node.id,
417
+ 'en',
418
+ );
356
419
  }
357
420
 
358
421
  /**
@@ -366,3 +429,169 @@ function paginateNodes(matching_nodes, pagination_options) {
366
429
 
367
430
  return matching_nodes.slice(offset, offset + limit);
368
431
  }
432
+
433
+ /**
434
+ * @param {GraphNode} graph_node
435
+ * @param {string} field_name
436
+ * @returns {string[]}
437
+ */
438
+ function getGraphNodeFieldValues(graph_node, field_name) {
439
+ const structural_value = getStructuralFieldValue(graph_node, field_name);
440
+
441
+ if (structural_value !== undefined) {
442
+ return [structural_value];
443
+ }
444
+
445
+ if (field_name === '$path' || field_name === 'title') {
446
+ return [];
447
+ }
448
+
449
+ return normalizeFieldValues(graph_node[field_name]);
450
+ }
451
+
452
+ /**
453
+ * @param {GraphNode} graph_node
454
+ * @param {string} field_name
455
+ * @returns {string | undefined}
456
+ */
457
+ function getStructuralFieldValue(graph_node, field_name) {
458
+ if (field_name === '$id') {
459
+ return graph_node.$id ?? graph_node.id;
460
+ }
461
+
462
+ if (field_name === '$class') {
463
+ return graph_node.$class ?? graph_node.kind;
464
+ }
465
+
466
+ if (field_name === '$path') {
467
+ return graph_node.$path ?? graph_node.path;
468
+ }
469
+
470
+ if (field_name === 'title') {
471
+ return graph_node.title;
472
+ }
473
+ }
474
+
475
+ /**
476
+ * @param {unknown} field_value
477
+ * @returns {string[]}
478
+ */
479
+ function normalizeFieldValues(field_value) {
480
+ if (Array.isArray(field_value)) {
481
+ return field_value.flatMap(getScalarFieldValue);
482
+ }
483
+
484
+ return getScalarFieldValue(field_value);
485
+ }
486
+
487
+ /**
488
+ * @param {unknown} field_value
489
+ * @returns {string[]}
490
+ */
491
+ function getScalarFieldValue(field_value) {
492
+ if (
493
+ typeof field_value === 'string' ||
494
+ typeof field_value === 'number' ||
495
+ typeof field_value === 'boolean'
496
+ ) {
497
+ return [String(field_value)];
498
+ }
499
+
500
+ return [];
501
+ }
502
+
503
+ /**
504
+ * @param {PatramRepoConfig | { limit?: number, offset?: number }} repo_config_or_pagination
505
+ * @param {{ limit?: number, offset?: number }} pagination_options
506
+ * @returns {{ pagination_options: { limit?: number, offset?: number }, repo_config: PatramRepoConfig | null }}
507
+ */
508
+ function resolveQueryGraphOptions(
509
+ repo_config_or_pagination,
510
+ pagination_options,
511
+ ) {
512
+ if (isRepoConfig(repo_config_or_pagination)) {
513
+ return {
514
+ pagination_options,
515
+ repo_config: repo_config_or_pagination,
516
+ };
517
+ }
518
+
519
+ return {
520
+ pagination_options: repo_config_or_pagination,
521
+ repo_config: null,
522
+ };
523
+ }
524
+
525
+ /**
526
+ * @param {PatramRepoConfig | { limit?: number, offset?: number }} value
527
+ * @returns {value is PatramRepoConfig}
528
+ */
529
+ function isRepoConfig(value) {
530
+ return 'include' in value || 'queries' in value;
531
+ }
532
+
533
+ /**
534
+ * @param {string} left_value
535
+ * @param {'<' | '<=' | '>' | '>='} comparison
536
+ * @param {string} right_value
537
+ * @returns {boolean}
538
+ */
539
+ function compareFieldValues(left_value, comparison, right_value) {
540
+ const numeric_comparison = compareNumericFieldValues(
541
+ left_value,
542
+ comparison,
543
+ right_value,
544
+ );
545
+
546
+ if (numeric_comparison !== null) {
547
+ return numeric_comparison;
548
+ }
549
+
550
+ return compareComparableValues(
551
+ left_value.localeCompare(right_value, 'en'),
552
+ comparison,
553
+ );
554
+ }
555
+
556
+ /**
557
+ * @param {string} left_value
558
+ * @param {'<' | '<=' | '>' | '>='} comparison
559
+ * @param {string} right_value
560
+ * @returns {boolean | null}
561
+ */
562
+ function compareNumericFieldValues(left_value, comparison, right_value) {
563
+ const left_number = Number(left_value);
564
+ const right_number = Number(right_value);
565
+
566
+ if (
567
+ !Number.isFinite(left_number) ||
568
+ !Number.isFinite(right_number) ||
569
+ left_value.trim().length === 0 ||
570
+ right_value.trim().length === 0
571
+ ) {
572
+ return null;
573
+ }
574
+
575
+ return compareComparableValues(left_number - right_number, comparison);
576
+ }
577
+
578
+ /**
579
+ * @param {number} comparison_result
580
+ * @param {'<' | '<=' | '>' | '>='} comparison
581
+ * @returns {boolean}
582
+ */
583
+ function compareComparableValues(comparison_result, comparison) {
584
+ if (comparison === '<') {
585
+ return comparison_result < 0;
586
+ }
587
+
588
+ if (comparison === '<=') {
589
+ return comparison_result <= 0;
590
+ }
591
+
592
+ if (comparison === '>') {
593
+ return comparison_result > 0;
594
+ }
595
+
596
+ return comparison_result >= 0;
597
+ }