patram 0.1.1 → 0.2.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 +39 -7
- package/lib/build-graph.js +14 -1
- package/lib/cli-help-metadata.js +552 -0
- package/lib/derived-summary.js +278 -0
- package/lib/format-derived-summary-row.js +9 -0
- package/lib/format-node-header.js +19 -0
- package/lib/format-output-item-block.js +22 -0
- package/lib/format-output-metadata.js +62 -0
- package/lib/layout-stored-queries.js +150 -2
- package/lib/load-patram-config.js +401 -2
- package/lib/load-patram-config.types.ts +31 -0
- package/lib/output-view.types.ts +15 -0
- package/lib/parse-cli-arguments-helpers.js +263 -90
- package/lib/parse-cli-arguments.js +160 -8
- package/lib/parse-cli-arguments.types.ts +48 -3
- package/lib/parse-where-clause.js +604 -209
- package/lib/parse-where-clause.types.ts +70 -0
- package/lib/patram-cli.js +144 -17
- package/lib/patram.js +6 -0
- package/lib/query-graph.js +231 -119
- package/lib/query-inspection.js +523 -0
- package/lib/render-check-output.js +1 -1
- package/lib/render-cli-help.js +419 -0
- package/lib/render-json-output.js +57 -4
- package/lib/render-output-view.js +37 -8
- package/lib/render-plain-output.js +31 -86
- package/lib/render-rich-output.js +34 -87
- package/lib/resolve-where-clause.js +18 -3
- package/lib/tagged-fenced-block-error.js +17 -0
- package/lib/tagged-fenced-block-markdown.js +111 -0
- package/lib/tagged-fenced-block-metadata.js +97 -0
- package/lib/tagged-fenced-block-parser.js +292 -0
- package/lib/tagged-fenced-blocks.js +100 -0
- package/lib/tagged-fenced-blocks.types.ts +38 -0
- package/package.json +8 -3
package/lib/query-graph.js
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
|
+
/* eslint-disable max-lines */
|
|
1
2
|
/**
|
|
2
3
|
* @import { BuildGraphResult, GraphNode } from './build-graph.types.ts';
|
|
3
4
|
* @import { PatramDiagnostic } from './load-patram-config.types.ts';
|
|
5
|
+
* @import {
|
|
6
|
+
* ParsedAggregateTerm,
|
|
7
|
+
* ParsedClause,
|
|
8
|
+
* ParsedFieldSetTerm,
|
|
9
|
+
* ParsedFieldTerm,
|
|
10
|
+
* ParsedRelationTargetTerm,
|
|
11
|
+
* ParsedTerm,
|
|
12
|
+
* ParsedTraversalTerm,
|
|
13
|
+
* } from './parse-where-clause.types.ts';
|
|
4
14
|
*/
|
|
5
15
|
|
|
6
16
|
import { parseWhereClause } from './parse-where-clause.js';
|
|
@@ -26,7 +36,21 @@ import { parseWhereClause } from './parse-where-clause.js';
|
|
|
26
36
|
export const DEFAULT_QUERY_LIMIT = 25;
|
|
27
37
|
|
|
28
38
|
/**
|
|
29
|
-
*
|
|
39
|
+
* @typedef {{
|
|
40
|
+
* incoming: Map<string, Map<string, Set<string>>>,
|
|
41
|
+
* outgoing: Map<string, Map<string, Set<string>>>,
|
|
42
|
+
* }} RelationIndexes
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @typedef {{
|
|
47
|
+
* nodes: BuildGraphResult['nodes'],
|
|
48
|
+
* relation_indexes: RelationIndexes,
|
|
49
|
+
* }} EvaluationContext
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Filter graph nodes with the query language.
|
|
30
54
|
*
|
|
31
55
|
* @param {BuildGraphResult} graph
|
|
32
56
|
* @param {string} where_clause
|
|
@@ -44,11 +68,13 @@ export function queryGraph(graph, where_clause, pagination_options = {}) {
|
|
|
44
68
|
};
|
|
45
69
|
}
|
|
46
70
|
|
|
47
|
-
const
|
|
48
|
-
|
|
71
|
+
const evaluation_context = {
|
|
72
|
+
nodes: graph.nodes,
|
|
73
|
+
relation_indexes: createRelationIndexes(graph.edges),
|
|
74
|
+
};
|
|
49
75
|
const graph_nodes = Object.values(graph.nodes).sort(compareGraphNodes);
|
|
50
76
|
const matching_nodes = graph_nodes.filter((graph_node) =>
|
|
51
|
-
|
|
77
|
+
matchesClauses(graph_node, parse_result.clauses, evaluation_context),
|
|
52
78
|
);
|
|
53
79
|
const paginated_nodes = paginateNodes(matching_nodes, pagination_options);
|
|
54
80
|
|
|
@@ -60,178 +86,264 @@ export function queryGraph(graph, where_clause, pagination_options = {}) {
|
|
|
60
86
|
}
|
|
61
87
|
|
|
62
88
|
/**
|
|
63
|
-
* @param {
|
|
64
|
-
* @
|
|
89
|
+
* @param {GraphNode} graph_node
|
|
90
|
+
* @param {ParsedClause[]} clauses
|
|
91
|
+
* @param {EvaluationContext} evaluation_context
|
|
92
|
+
* @returns {boolean}
|
|
65
93
|
*/
|
|
66
|
-
function
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
);
|
|
72
|
-
}
|
|
94
|
+
function matchesClauses(graph_node, clauses, evaluation_context) {
|
|
95
|
+
return clauses.every((clause) =>
|
|
96
|
+
matchesClause(graph_node, clause, evaluation_context),
|
|
97
|
+
);
|
|
98
|
+
}
|
|
73
99
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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);
|
|
81
108
|
|
|
82
|
-
return
|
|
109
|
+
return clause.is_negated ? !is_match : is_match;
|
|
83
110
|
}
|
|
84
111
|
|
|
85
112
|
/**
|
|
86
|
-
* @param {
|
|
87
|
-
* @param {
|
|
88
|
-
* @
|
|
113
|
+
* @param {GraphNode} graph_node
|
|
114
|
+
* @param {ParsedTerm} term
|
|
115
|
+
* @param {EvaluationContext} evaluation_context
|
|
116
|
+
* @returns {boolean}
|
|
89
117
|
*/
|
|
90
|
-
function
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
118
|
+
function matchesTerm(graph_node, term, evaluation_context) {
|
|
119
|
+
if (term.kind === 'aggregate') {
|
|
120
|
+
return matchesAggregateTerm(graph_node, term, evaluation_context);
|
|
121
|
+
}
|
|
94
122
|
|
|
95
|
-
|
|
96
|
-
|
|
123
|
+
if (term.kind === 'field') {
|
|
124
|
+
return matchesFieldTerm(graph_node, term);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (term.kind === 'field_set') {
|
|
128
|
+
return matchesFieldSetTerm(graph_node, term);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (term.kind === 'relation') {
|
|
132
|
+
return hasOutgoingRelation(
|
|
133
|
+
graph_node.id,
|
|
134
|
+
term.relation_name,
|
|
135
|
+
evaluation_context.relation_indexes,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return matchesRelationTargetTerm(graph_node, term, evaluation_context);
|
|
97
140
|
}
|
|
98
141
|
|
|
99
142
|
/**
|
|
100
|
-
* @param {
|
|
101
|
-
* @param {
|
|
102
|
-
* @param {
|
|
103
|
-
* @returns {
|
|
143
|
+
* @param {GraphNode} graph_node
|
|
144
|
+
* @param {ParsedAggregateTerm} term
|
|
145
|
+
* @param {EvaluationContext} evaluation_context
|
|
146
|
+
* @returns {boolean}
|
|
104
147
|
*/
|
|
105
|
-
function
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
148
|
+
function matchesAggregateTerm(graph_node, term, evaluation_context) {
|
|
149
|
+
const related_nodes = getRelatedNodes(
|
|
150
|
+
graph_node.id,
|
|
151
|
+
term.traversal,
|
|
152
|
+
evaluation_context,
|
|
153
|
+
);
|
|
154
|
+
const matching_count = related_nodes.filter((related_node) =>
|
|
155
|
+
matchesClauses(related_node, term.clauses, evaluation_context),
|
|
156
|
+
).length;
|
|
110
157
|
|
|
111
|
-
|
|
112
|
-
|
|
158
|
+
if (term.aggregate_name === 'any') {
|
|
159
|
+
return matching_count > 0;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (term.aggregate_name === 'none') {
|
|
163
|
+
return matching_count === 0;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return compareNumbers(
|
|
167
|
+
matching_count,
|
|
168
|
+
term.comparison ?? '=',
|
|
169
|
+
term.value ?? 0,
|
|
170
|
+
);
|
|
113
171
|
}
|
|
114
172
|
|
|
115
173
|
/**
|
|
116
|
-
* @param {
|
|
117
|
-
* @
|
|
174
|
+
* @param {GraphNode} graph_node
|
|
175
|
+
* @param {ParsedFieldTerm} term
|
|
176
|
+
* @returns {boolean}
|
|
118
177
|
*/
|
|
119
|
-
function
|
|
120
|
-
|
|
121
|
-
const relation_index = new Map();
|
|
122
|
-
|
|
123
|
-
for (const graph_edge of graph_edges) {
|
|
124
|
-
let relation_targets = relation_index.get(graph_edge.from);
|
|
178
|
+
function matchesFieldTerm(graph_node, term) {
|
|
179
|
+
const term_key = `${term.field_name}${term.operator}`;
|
|
125
180
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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;
|
|
188
|
+
}
|
|
130
189
|
|
|
131
|
-
|
|
190
|
+
if (term_key === 'id^=') {
|
|
191
|
+
return graph_node.id.startsWith(term.value);
|
|
192
|
+
}
|
|
132
193
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
194
|
+
if (term_key === 'path^=') {
|
|
195
|
+
return (graph_node.path ?? '').startsWith(term.value);
|
|
196
|
+
}
|
|
137
197
|
|
|
138
|
-
|
|
198
|
+
if (term_key === 'title~') {
|
|
199
|
+
return (graph_node.title ?? '')
|
|
200
|
+
.toLocaleLowerCase('en')
|
|
201
|
+
.includes(term.value.toLocaleLowerCase('en'));
|
|
139
202
|
}
|
|
140
203
|
|
|
141
|
-
|
|
204
|
+
throw new Error('Unsupported parsed where clause.');
|
|
142
205
|
}
|
|
143
206
|
|
|
144
207
|
/**
|
|
145
|
-
* @param {
|
|
146
|
-
* @param {
|
|
147
|
-
* @
|
|
148
|
-
* @returns {(graph_node: GraphNode) => boolean}
|
|
208
|
+
* @param {GraphNode} graph_node
|
|
209
|
+
* @param {ParsedFieldSetTerm} term
|
|
210
|
+
* @returns {boolean}
|
|
149
211
|
*/
|
|
150
|
-
function
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const is_match = actual_value === expected_value;
|
|
212
|
+
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;
|
|
154
215
|
|
|
155
|
-
|
|
156
|
-
};
|
|
216
|
+
return term.operator === 'in' ? is_member : !is_member;
|
|
157
217
|
}
|
|
158
218
|
|
|
159
219
|
/**
|
|
160
|
-
* @param {
|
|
161
|
-
* @param {
|
|
162
|
-
* @
|
|
220
|
+
* @param {GraphNode} graph_node
|
|
221
|
+
* @param {ParsedRelationTargetTerm} term
|
|
222
|
+
* @param {EvaluationContext} evaluation_context
|
|
223
|
+
* @returns {boolean}
|
|
163
224
|
*/
|
|
164
|
-
function
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
225
|
+
function matchesRelationTargetTerm(graph_node, term, evaluation_context) {
|
|
226
|
+
const relation_targets = evaluation_context.relation_indexes.outgoing.get(
|
|
227
|
+
graph_node.id,
|
|
228
|
+
);
|
|
229
|
+
const matching_targets = relation_targets?.get(term.relation_name);
|
|
168
230
|
|
|
169
|
-
|
|
170
|
-
};
|
|
231
|
+
return matching_targets?.has(term.target_id) ?? false;
|
|
171
232
|
}
|
|
172
233
|
|
|
173
234
|
/**
|
|
174
|
-
* @param {string}
|
|
175
|
-
* @param {
|
|
176
|
-
* @
|
|
235
|
+
* @param {string} node_id
|
|
236
|
+
* @param {string} relation_name
|
|
237
|
+
* @param {RelationIndexes} relation_indexes
|
|
238
|
+
* @returns {boolean}
|
|
177
239
|
*/
|
|
178
|
-
function
|
|
179
|
-
|
|
180
|
-
const path_value = graph_node.path ?? '';
|
|
181
|
-
const is_match = path_value.startsWith(path_prefix);
|
|
240
|
+
function hasOutgoingRelation(node_id, relation_name, relation_indexes) {
|
|
241
|
+
const relation_targets = relation_indexes.outgoing.get(node_id);
|
|
182
242
|
|
|
183
|
-
|
|
184
|
-
};
|
|
243
|
+
return relation_targets?.has(relation_name) ?? false;
|
|
185
244
|
}
|
|
186
245
|
|
|
187
246
|
/**
|
|
188
|
-
* @param {string}
|
|
189
|
-
* @param {
|
|
190
|
-
* @
|
|
247
|
+
* @param {string} node_id
|
|
248
|
+
* @param {ParsedTraversalTerm} traversal
|
|
249
|
+
* @param {EvaluationContext} evaluation_context
|
|
250
|
+
* @returns {GraphNode[]}
|
|
191
251
|
*/
|
|
192
|
-
function
|
|
193
|
-
const
|
|
252
|
+
function getRelatedNodes(node_id, traversal, evaluation_context) {
|
|
253
|
+
const relation_index =
|
|
254
|
+
traversal.direction === 'in'
|
|
255
|
+
? evaluation_context.relation_indexes.incoming
|
|
256
|
+
: evaluation_context.relation_indexes.outgoing;
|
|
257
|
+
const relation_targets = relation_index.get(node_id);
|
|
258
|
+
const target_ids = relation_targets?.get(traversal.relation_name);
|
|
259
|
+
|
|
260
|
+
if (!target_ids) {
|
|
261
|
+
return [];
|
|
262
|
+
}
|
|
194
263
|
|
|
195
|
-
return (
|
|
196
|
-
const
|
|
197
|
-
const is_match = title_value
|
|
198
|
-
.toLocaleLowerCase('en')
|
|
199
|
-
.includes(normalized_title_text);
|
|
264
|
+
return [...target_ids].flatMap((target_id) => {
|
|
265
|
+
const target_node = evaluation_context.nodes[target_id];
|
|
200
266
|
|
|
201
|
-
return
|
|
202
|
-
};
|
|
267
|
+
return target_node ? [target_node] : [];
|
|
268
|
+
});
|
|
203
269
|
}
|
|
204
270
|
|
|
205
271
|
/**
|
|
206
|
-
* @param {
|
|
207
|
-
* @param {
|
|
208
|
-
* @
|
|
272
|
+
* @param {number} left_value
|
|
273
|
+
* @param {'!=' | '<' | '<=' | '=' | '>' | '>='} comparison
|
|
274
|
+
* @param {number} right_value
|
|
275
|
+
* @returns {boolean}
|
|
209
276
|
*/
|
|
210
|
-
function
|
|
211
|
-
|
|
277
|
+
function compareNumbers(left_value, comparison, right_value) {
|
|
278
|
+
if (comparison === '!=') {
|
|
279
|
+
return left_value !== right_value;
|
|
280
|
+
}
|
|
212
281
|
|
|
213
|
-
if (
|
|
214
|
-
|
|
215
|
-
term_key === 'kind=' ||
|
|
216
|
-
term_key === 'status=' ||
|
|
217
|
-
term_key === 'path='
|
|
218
|
-
) {
|
|
219
|
-
return createFieldPredicate(term.field_name, term.value, is_negated);
|
|
282
|
+
if (comparison === '<') {
|
|
283
|
+
return left_value < right_value;
|
|
220
284
|
}
|
|
221
285
|
|
|
222
|
-
if (
|
|
223
|
-
return
|
|
286
|
+
if (comparison === '<=') {
|
|
287
|
+
return left_value <= right_value;
|
|
224
288
|
}
|
|
225
289
|
|
|
226
|
-
if (
|
|
227
|
-
return
|
|
290
|
+
if (comparison === '=') {
|
|
291
|
+
return left_value === right_value;
|
|
228
292
|
}
|
|
229
293
|
|
|
230
|
-
if (
|
|
231
|
-
return
|
|
294
|
+
if (comparison === '>') {
|
|
295
|
+
return left_value > right_value;
|
|
232
296
|
}
|
|
233
297
|
|
|
234
|
-
|
|
298
|
+
if (comparison === '>=') {
|
|
299
|
+
return left_value >= right_value;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
throw new Error('Unsupported aggregate comparison.');
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* @param {BuildGraphResult['edges']} graph_edges
|
|
307
|
+
* @returns {RelationIndexes}
|
|
308
|
+
*/
|
|
309
|
+
function createRelationIndexes(graph_edges) {
|
|
310
|
+
return {
|
|
311
|
+
incoming: createDirectionalRelationIndex(graph_edges, 'to', 'from'),
|
|
312
|
+
outgoing: createDirectionalRelationIndex(graph_edges, 'from', 'to'),
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* @param {BuildGraphResult['edges']} graph_edges
|
|
318
|
+
* @param {'from' | 'to'} source_key
|
|
319
|
+
* @param {'from' | 'to'} target_key
|
|
320
|
+
* @returns {Map<string, Map<string, Set<string>>>}
|
|
321
|
+
*/
|
|
322
|
+
function createDirectionalRelationIndex(graph_edges, source_key, target_key) {
|
|
323
|
+
/** @type {Map<string, Map<string, Set<string>>>} */
|
|
324
|
+
const relation_index = new Map();
|
|
325
|
+
|
|
326
|
+
for (const graph_edge of graph_edges) {
|
|
327
|
+
const source_id = graph_edge[source_key];
|
|
328
|
+
const target_id = graph_edge[target_key];
|
|
329
|
+
let relation_targets = relation_index.get(source_id);
|
|
330
|
+
|
|
331
|
+
if (!relation_targets) {
|
|
332
|
+
relation_targets = new Map();
|
|
333
|
+
relation_index.set(source_id, relation_targets);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
let target_ids = relation_targets.get(graph_edge.relation);
|
|
337
|
+
|
|
338
|
+
if (!target_ids) {
|
|
339
|
+
target_ids = new Set();
|
|
340
|
+
relation_targets.set(graph_edge.relation, target_ids);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
target_ids.add(target_id);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return relation_index;
|
|
235
347
|
}
|
|
236
348
|
|
|
237
349
|
/**
|