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.
- package/lib/build-graph-identity.js +86 -99
- package/lib/build-graph.js +536 -31
- package/lib/build-graph.types.ts +6 -2
- package/lib/check-directive-metadata.js +534 -0
- package/lib/check-directive-value.js +291 -0
- package/lib/check-graph.js +23 -5
- package/lib/cli-help-metadata.js +56 -16
- package/lib/command-output.js +16 -1
- package/lib/derived-summary.js +10 -8
- package/lib/directive-diagnostics.js +38 -0
- package/lib/directive-type-rules.js +133 -0
- package/lib/discover-fields.js +435 -0
- package/lib/discover-fields.types.ts +52 -0
- package/lib/document-node-identity.js +317 -0
- package/lib/format-node-header.js +9 -7
- package/lib/format-output-metadata.js +15 -23
- package/lib/layout-stored-queries.js +124 -85
- package/lib/load-patram-config.js +433 -96
- package/lib/load-patram-config.types.ts +98 -3
- package/lib/load-project-graph.js +4 -1
- package/lib/output-view.types.ts +14 -6
- package/lib/parse-cli-arguments.types.ts +1 -1
- package/lib/parse-where-clause.js +344 -107
- package/lib/parse-where-clause.types.ts +25 -8
- package/lib/patram-cli.js +68 -4
- package/lib/patram-config.js +31 -31
- package/lib/patram-config.types.ts +10 -4
- package/lib/query-graph.js +269 -40
- package/lib/query-inspection.js +440 -60
- package/lib/render-field-discovery.js +184 -0
- package/lib/render-json-output.js +21 -22
- package/lib/render-output-view.js +301 -34
- package/lib/render-plain-output.js +1 -1
- package/lib/render-rich-output.js +1 -1
- package/lib/render-rich-source.js +245 -14
- package/lib/resolve-patram-graph-config.js +15 -9
- package/lib/show-document.js +66 -9
- package/package.json +5 -5
package/lib/query-graph.js
CHANGED
|
@@ -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
|
-
*
|
|
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(
|
|
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
|
-
|
|
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 {
|
|
119
|
+
* @param {ParsedExpression} expression
|
|
91
120
|
* @param {EvaluationContext} evaluation_context
|
|
92
121
|
* @returns {boolean}
|
|
93
122
|
*/
|
|
94
|
-
function
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
218
|
+
const field_values = getGraphNodeFieldValues(graph_node, term.field_name);
|
|
180
219
|
|
|
181
|
-
if (
|
|
182
|
-
|
|
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 (
|
|
191
|
-
return
|
|
224
|
+
if (term.operator === '!=') {
|
|
225
|
+
return field_values.every((field_value) => field_value !== term.value);
|
|
192
226
|
}
|
|
193
227
|
|
|
194
|
-
if (
|
|
195
|
-
return
|
|
228
|
+
if (term.operator === '^=') {
|
|
229
|
+
return field_values.some((field_value) =>
|
|
230
|
+
field_value.startsWith(term.value),
|
|
231
|
+
);
|
|
196
232
|
}
|
|
197
233
|
|
|
198
|
-
if (
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
|
214
|
-
const is_member =
|
|
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(
|
|
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
|
+
}
|