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
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @import { BuildGraphResult, GraphNode } from './build-graph.types.ts';
|
|
3
|
+
* @import { OutputDerivedSummary } from './output-view.types.ts';
|
|
4
|
+
* @import { DerivedSummaryConfig, DerivedSummaryFieldConfig, DerivedSummaryScalar, PatramRepoConfig } from './load-patram-config.types.ts';
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { queryGraph } from './query-graph.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Derived summary evaluation.
|
|
11
|
+
*
|
|
12
|
+
* Evaluates repo-configured output-only metadata for graph nodes without
|
|
13
|
+
* mutating graph state.
|
|
14
|
+
*
|
|
15
|
+
* Kind: output
|
|
16
|
+
* Status: active
|
|
17
|
+
* Tracked in: ../docs/plans/v0/declarative-derived-summaries.md
|
|
18
|
+
* Decided by: ../docs/decisions/declarative-derived-summary-config.md
|
|
19
|
+
* Decided by: ../docs/decisions/declarative-derived-summary-side-effects.md
|
|
20
|
+
* @patram
|
|
21
|
+
* @see {@link ./load-patram-config.js}
|
|
22
|
+
* @see {@link ./render-output-view.js}
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @typedef {{
|
|
27
|
+
* evaluate: (graph_node: GraphNode) => OutputDerivedSummary | null,
|
|
28
|
+
* }} DerivedSummaryEvaluator
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {PatramRepoConfig} repo_config
|
|
33
|
+
* @param {BuildGraphResult} graph
|
|
34
|
+
* @returns {DerivedSummaryEvaluator}
|
|
35
|
+
*/
|
|
36
|
+
export function createDerivedSummaryEvaluator(repo_config, graph) {
|
|
37
|
+
const summary_by_kind = createSummaryByKind(repo_config.derived_summaries);
|
|
38
|
+
/** @type {Map<string, Set<string>>} */
|
|
39
|
+
const matching_node_id_cache = new Map();
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
evaluate(graph_node) {
|
|
43
|
+
const configured_summary = summary_by_kind.get(graph_node.kind);
|
|
44
|
+
|
|
45
|
+
if (!configured_summary) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
fields: configured_summary.definition.fields.map(
|
|
51
|
+
(field_definition) => ({
|
|
52
|
+
name: field_definition.name,
|
|
53
|
+
value: evaluateFieldValue(
|
|
54
|
+
field_definition,
|
|
55
|
+
configured_summary.definition,
|
|
56
|
+
graph,
|
|
57
|
+
graph_node,
|
|
58
|
+
matching_node_id_cache,
|
|
59
|
+
),
|
|
60
|
+
}),
|
|
61
|
+
),
|
|
62
|
+
name: configured_summary.name,
|
|
63
|
+
};
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @param {DerivedSummaryFieldConfig} field_definition
|
|
70
|
+
* @param {DerivedSummaryConfig} summary_definition
|
|
71
|
+
* @param {BuildGraphResult} graph
|
|
72
|
+
* @param {GraphNode} graph_node
|
|
73
|
+
* @param {Map<string, Set<string>>} matching_node_id_cache
|
|
74
|
+
* @returns {DerivedSummaryScalar}
|
|
75
|
+
*/
|
|
76
|
+
function evaluateFieldValue(
|
|
77
|
+
field_definition,
|
|
78
|
+
summary_definition,
|
|
79
|
+
graph,
|
|
80
|
+
graph_node,
|
|
81
|
+
matching_node_id_cache,
|
|
82
|
+
) {
|
|
83
|
+
if ('count' in field_definition) {
|
|
84
|
+
return countMatchingTraversalNodes(
|
|
85
|
+
graph,
|
|
86
|
+
graph_node,
|
|
87
|
+
field_definition.count.traversal,
|
|
88
|
+
field_definition.count.where,
|
|
89
|
+
matching_node_id_cache,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return selectFieldValue(
|
|
94
|
+
graph,
|
|
95
|
+
graph_node,
|
|
96
|
+
summary_definition,
|
|
97
|
+
field_definition,
|
|
98
|
+
matching_node_id_cache,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* @param {BuildGraphResult} graph
|
|
104
|
+
* @param {GraphNode} graph_node
|
|
105
|
+
* @param {string} traversal_text
|
|
106
|
+
* @param {string} where_clause
|
|
107
|
+
* @param {Map<string, Set<string>>} matching_node_id_cache
|
|
108
|
+
* @returns {number}
|
|
109
|
+
*/
|
|
110
|
+
function countMatchingTraversalNodes(
|
|
111
|
+
graph,
|
|
112
|
+
graph_node,
|
|
113
|
+
traversal_text,
|
|
114
|
+
where_clause,
|
|
115
|
+
matching_node_id_cache,
|
|
116
|
+
) {
|
|
117
|
+
const target_node_ids = getTraversedNodeIds(
|
|
118
|
+
graph,
|
|
119
|
+
graph_node.id,
|
|
120
|
+
traversal_text,
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
if (target_node_ids.size === 0) {
|
|
124
|
+
return 0;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const matching_node_ids = getMatchingNodeIds(
|
|
128
|
+
graph,
|
|
129
|
+
where_clause,
|
|
130
|
+
matching_node_id_cache,
|
|
131
|
+
);
|
|
132
|
+
let matching_count = 0;
|
|
133
|
+
|
|
134
|
+
for (const target_node_id of target_node_ids) {
|
|
135
|
+
if (matching_node_ids.has(target_node_id)) {
|
|
136
|
+
matching_count += 1;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return matching_count;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* @param {BuildGraphResult} graph
|
|
145
|
+
* @param {GraphNode} graph_node
|
|
146
|
+
* @param {DerivedSummaryConfig} summary_definition
|
|
147
|
+
* @param {Extract<DerivedSummaryFieldConfig, { select: unknown }>} field_definition
|
|
148
|
+
* @param {Map<string, Set<string>>} matching_node_id_cache
|
|
149
|
+
* @returns {DerivedSummaryScalar}
|
|
150
|
+
*/
|
|
151
|
+
function selectFieldValue(
|
|
152
|
+
graph,
|
|
153
|
+
graph_node,
|
|
154
|
+
summary_definition,
|
|
155
|
+
field_definition,
|
|
156
|
+
matching_node_id_cache,
|
|
157
|
+
) {
|
|
158
|
+
for (const select_case of field_definition.select) {
|
|
159
|
+
const matching_node_ids = getMatchingNodeIds(
|
|
160
|
+
graph,
|
|
161
|
+
select_case.when,
|
|
162
|
+
matching_node_id_cache,
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
if (matching_node_ids.has(graph_node.id)) {
|
|
166
|
+
return select_case.value;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return field_definition.default;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* @param {BuildGraphResult} graph
|
|
175
|
+
* @param {string} where_clause
|
|
176
|
+
* @param {Map<string, Set<string>>} matching_node_id_cache
|
|
177
|
+
* @returns {Set<string>}
|
|
178
|
+
*/
|
|
179
|
+
function getMatchingNodeIds(graph, where_clause, matching_node_id_cache) {
|
|
180
|
+
const cached_node_ids = matching_node_id_cache.get(where_clause);
|
|
181
|
+
|
|
182
|
+
if (cached_node_ids) {
|
|
183
|
+
return cached_node_ids;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const query_result = queryGraph(graph, where_clause);
|
|
187
|
+
|
|
188
|
+
if (query_result.diagnostics.length > 0) {
|
|
189
|
+
throw new Error(
|
|
190
|
+
`Expected derived summary query "${where_clause}" to be valid.`,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const matching_node_ids = new Set(
|
|
195
|
+
query_result.nodes.map((matching_node) => matching_node.id),
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
matching_node_id_cache.set(where_clause, matching_node_ids);
|
|
199
|
+
|
|
200
|
+
return matching_node_ids;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* @param {BuildGraphResult} graph
|
|
205
|
+
* @param {string} node_id
|
|
206
|
+
* @param {string} traversal_text
|
|
207
|
+
* @returns {Set<string>}
|
|
208
|
+
*/
|
|
209
|
+
function getTraversedNodeIds(graph, node_id, traversal_text) {
|
|
210
|
+
const traversal = parseTraversal(traversal_text);
|
|
211
|
+
/** @type {Set<string>} */
|
|
212
|
+
const target_node_ids = new Set();
|
|
213
|
+
|
|
214
|
+
for (const graph_edge of graph.edges) {
|
|
215
|
+
if (graph_edge.relation !== traversal.relation_name) {
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (traversal.direction === 'in' && graph_edge.to === node_id) {
|
|
220
|
+
target_node_ids.add(graph_edge.from);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (traversal.direction === 'out' && graph_edge.from === node_id) {
|
|
224
|
+
target_node_ids.add(graph_edge.to);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return target_node_ids;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* @param {string} traversal_text
|
|
233
|
+
* @returns {{ direction: 'in' | 'out', relation_name: string }}
|
|
234
|
+
*/
|
|
235
|
+
function parseTraversal(traversal_text) {
|
|
236
|
+
const traversal_match =
|
|
237
|
+
/^(?<direction>in|out):(?<relation_name>[a-zA-Z0-9_]+)$/du.exec(
|
|
238
|
+
traversal_text,
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
if (
|
|
242
|
+
!traversal_match?.groups?.direction ||
|
|
243
|
+
!traversal_match.groups.relation_name
|
|
244
|
+
) {
|
|
245
|
+
throw new Error(`Invalid derived summary traversal "${traversal_text}".`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
direction: /** @type {'in' | 'out'} */ (traversal_match.groups.direction),
|
|
250
|
+
relation_name: traversal_match.groups.relation_name,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* @param {PatramRepoConfig['derived_summaries']} derived_summaries
|
|
256
|
+
* @returns {Map<string, { definition: DerivedSummaryConfig, name: string }>}
|
|
257
|
+
*/
|
|
258
|
+
function createSummaryByKind(derived_summaries) {
|
|
259
|
+
/** @type {Map<string, { definition: DerivedSummaryConfig, name: string }>} */
|
|
260
|
+
const summary_by_kind = new Map();
|
|
261
|
+
|
|
262
|
+
if (!derived_summaries) {
|
|
263
|
+
return summary_by_kind;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
for (const [summary_name, summary_definition] of Object.entries(
|
|
267
|
+
derived_summaries,
|
|
268
|
+
)) {
|
|
269
|
+
for (const kind_name of summary_definition.kinds) {
|
|
270
|
+
summary_by_kind.set(kind_name, {
|
|
271
|
+
definition: summary_definition,
|
|
272
|
+
name: summary_name,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return summary_by_kind;
|
|
278
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {import('./output-view.types.ts').OutputDerivedSummary} derived_summary
|
|
3
|
+
* @returns {string}
|
|
4
|
+
*/
|
|
5
|
+
export function formatDerivedSummaryRow(derived_summary) {
|
|
6
|
+
return derived_summary.fields
|
|
7
|
+
.map((field) => `${field.name}: ${String(field.value)}`)
|
|
8
|
+
.join(' ');
|
|
9
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {import('./output-view.types.ts').OutputNodeItem} output_item
|
|
3
|
+
* @returns {string}
|
|
4
|
+
*/
|
|
5
|
+
export function formatNodeHeader(output_item) {
|
|
6
|
+
if (isDocumentNode(output_item)) {
|
|
7
|
+
return `document ${output_item.path}`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return `${output_item.node_kind} ${output_item.id}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {import('./output-view.types.ts').OutputNodeItem} output_item
|
|
15
|
+
* @returns {boolean}
|
|
16
|
+
*/
|
|
17
|
+
export function isDocumentNode(output_item) {
|
|
18
|
+
return output_item.id === `doc:${output_item.path}`;
|
|
19
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {{ header: string, metadata_rows: string[], metadata_indent?: string, title: string, title_indent?: string }} options
|
|
3
|
+
* @returns {string}
|
|
4
|
+
*/
|
|
5
|
+
export function formatOutputItemBlock(options) {
|
|
6
|
+
const metadata_indent = options.metadata_indent ?? '';
|
|
7
|
+
const title_indent = options.title_indent ?? ' ';
|
|
8
|
+
/** @type {string[]} */
|
|
9
|
+
const lines = [options.header];
|
|
10
|
+
|
|
11
|
+
if (options.metadata_rows.length > 0) {
|
|
12
|
+
lines.push(
|
|
13
|
+
...options.metadata_rows.map(
|
|
14
|
+
(metadata_row) => `${metadata_indent}${metadata_row}`,
|
|
15
|
+
),
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
lines.push('', `${title_indent}${options.title}`);
|
|
20
|
+
|
|
21
|
+
return lines.join('\n');
|
|
22
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { formatDerivedSummaryRow } from './format-derived-summary-row.js';
|
|
2
|
+
import { isDocumentNode } from './format-node-header.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @param {import('./output-view.types.ts').OutputNodeItem} output_item
|
|
6
|
+
* @returns {string[]}
|
|
7
|
+
*/
|
|
8
|
+
export function formatOutputNodeMetadataRows(output_item) {
|
|
9
|
+
/** @type {string[]} */
|
|
10
|
+
const metadata_rows = [];
|
|
11
|
+
/** @type {string[]} */
|
|
12
|
+
const stored_metadata_fields = [];
|
|
13
|
+
|
|
14
|
+
if (isDocumentNode(output_item)) {
|
|
15
|
+
stored_metadata_fields.push(`kind: ${output_item.node_kind}`);
|
|
16
|
+
} else {
|
|
17
|
+
stored_metadata_fields.push(`path: ${output_item.path}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (output_item.status) {
|
|
21
|
+
stored_metadata_fields.push(`status: ${output_item.status}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (stored_metadata_fields.length > 0) {
|
|
25
|
+
metadata_rows.push(stored_metadata_fields.join(' '));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (output_item.derived_summary) {
|
|
29
|
+
metadata_rows.push(formatDerivedSummaryRow(output_item.derived_summary));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return metadata_rows;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @param {import('./output-view.types.ts').OutputResolvedLinkTarget} target
|
|
37
|
+
* @returns {string[]}
|
|
38
|
+
*/
|
|
39
|
+
export function formatResolvedLinkMetadataRows(target) {
|
|
40
|
+
/** @type {string[]} */
|
|
41
|
+
const metadata_rows = [];
|
|
42
|
+
/** @type {string[]} */
|
|
43
|
+
const stored_metadata_fields = [];
|
|
44
|
+
|
|
45
|
+
if (target.kind) {
|
|
46
|
+
stored_metadata_fields.push(`kind: ${target.kind}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (target.status) {
|
|
50
|
+
stored_metadata_fields.push(`status: ${target.status}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (stored_metadata_fields.length > 0) {
|
|
54
|
+
metadata_rows.push(stored_metadata_fields.join(' '));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (target.derived_summary) {
|
|
58
|
+
metadata_rows.push(formatDerivedSummaryRow(target.derived_summary));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return metadata_rows;
|
|
62
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/* eslint-disable max-lines */
|
|
1
2
|
/**
|
|
2
3
|
* @import { OutputStoredQueryItem } from './output-view.types.ts';
|
|
3
4
|
*/
|
|
@@ -97,7 +98,22 @@ function createStoredQueryPhrases(where_clause) {
|
|
|
97
98
|
}
|
|
98
99
|
|
|
99
100
|
/**
|
|
100
|
-
* @param {{
|
|
101
|
+
* @param {{
|
|
102
|
+
* is_negated: boolean,
|
|
103
|
+
* term:
|
|
104
|
+
* | { field_name: 'id' | 'kind' | 'path' | 'status' | 'title', kind: 'field', operator: '=' | '^=' | '~', value: string }
|
|
105
|
+
* | { field_name: 'id' | 'kind' | 'path' | 'status' | 'title', kind: 'field_set', operator: 'in' | 'not in', values: string[] }
|
|
106
|
+
* | { kind: 'relation', relation_name: string }
|
|
107
|
+
* | { kind: 'relation_target', relation_name: string, target_id: string }
|
|
108
|
+
* | {
|
|
109
|
+
* aggregate_name: 'any' | 'count' | 'none',
|
|
110
|
+
* clauses: unknown[],
|
|
111
|
+
* comparison?: '!=' | '<' | '<=' | '=' | '>' | '>=',
|
|
112
|
+
* kind: 'aggregate',
|
|
113
|
+
* traversal: { direction: 'in' | 'out', relation_name: string },
|
|
114
|
+
* value?: number,
|
|
115
|
+
* },
|
|
116
|
+
* }} clause
|
|
101
117
|
* @param {boolean} should_prefix_and
|
|
102
118
|
* @returns {StoredQuerySegment[]}
|
|
103
119
|
*/
|
|
@@ -121,10 +137,27 @@ function createClausePhrase(clause, should_prefix_and) {
|
|
|
121
137
|
}
|
|
122
138
|
|
|
123
139
|
/**
|
|
124
|
-
* @param {
|
|
140
|
+
* @param {
|
|
141
|
+
* | { field_name: 'id' | 'kind' | 'path' | 'status' | 'title', kind: 'field', operator: '=' | '^=' | '~', value: string }
|
|
142
|
+
* | { field_name: 'id' | 'kind' | 'path' | 'status' | 'title', kind: 'field_set', operator: 'in' | 'not in', values: string[] }
|
|
143
|
+
* | { kind: 'relation', relation_name: string }
|
|
144
|
+
* | { kind: 'relation_target', relation_name: string, target_id: string }
|
|
145
|
+
* | {
|
|
146
|
+
* aggregate_name: 'any' | 'count' | 'none',
|
|
147
|
+
* clauses: { is_negated: boolean, term: unknown }[],
|
|
148
|
+
* comparison?: '!=' | '<' | '<=' | '=' | '>' | '>=',
|
|
149
|
+
* kind: 'aggregate',
|
|
150
|
+
* traversal: { direction: 'in' | 'out', relation_name: string },
|
|
151
|
+
* value?: number,
|
|
152
|
+
* }
|
|
153
|
+
* } term
|
|
125
154
|
* @returns {StoredQuerySegment[]}
|
|
126
155
|
*/
|
|
127
156
|
function createTermSegments(term) {
|
|
157
|
+
if (term.kind === 'aggregate') {
|
|
158
|
+
return createAggregateSegments(term);
|
|
159
|
+
}
|
|
160
|
+
|
|
128
161
|
if (term.kind === 'field') {
|
|
129
162
|
return [
|
|
130
163
|
{ kind: 'field_name', text: term.field_name },
|
|
@@ -133,6 +166,10 @@ function createTermSegments(term) {
|
|
|
133
166
|
];
|
|
134
167
|
}
|
|
135
168
|
|
|
169
|
+
if (term.kind === 'field_set') {
|
|
170
|
+
return createFieldSetSegments(term);
|
|
171
|
+
}
|
|
172
|
+
|
|
136
173
|
if (term.kind === 'relation_target') {
|
|
137
174
|
return [
|
|
138
175
|
{ kind: 'field_name', text: term.relation_name },
|
|
@@ -147,6 +184,117 @@ function createTermSegments(term) {
|
|
|
147
184
|
];
|
|
148
185
|
}
|
|
149
186
|
|
|
187
|
+
/**
|
|
188
|
+
* @param {{ field_name: 'id' | 'kind' | 'path' | 'status' | 'title', kind: 'field_set', operator: 'in' | 'not in', values: string[] }} term
|
|
189
|
+
* @returns {StoredQuerySegment[]}
|
|
190
|
+
*/
|
|
191
|
+
function createFieldSetSegments(term) {
|
|
192
|
+
return [
|
|
193
|
+
{ kind: 'field_name', text: term.field_name },
|
|
194
|
+
{ kind: 'plain', text: ' ' },
|
|
195
|
+
{ kind: 'operator', text: term.operator },
|
|
196
|
+
{ kind: 'plain', text: ' ' },
|
|
197
|
+
{ kind: 'operator', text: '[' },
|
|
198
|
+
...createListSegments(term.values),
|
|
199
|
+
{ kind: 'operator', text: ']' },
|
|
200
|
+
];
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* @param {{
|
|
205
|
+
* aggregate_name: 'any' | 'count' | 'none',
|
|
206
|
+
* clauses: { is_negated: boolean, term: unknown }[],
|
|
207
|
+
* comparison?: '!=' | '<' | '<=' | '=' | '>' | '>=',
|
|
208
|
+
* kind: 'aggregate',
|
|
209
|
+
* traversal: { direction: 'in' | 'out', relation_name: string },
|
|
210
|
+
* value?: number,
|
|
211
|
+
* }} term
|
|
212
|
+
* @returns {StoredQuerySegment[]}
|
|
213
|
+
*/
|
|
214
|
+
function createAggregateSegments(term) {
|
|
215
|
+
/** @type {StoredQuerySegment[]} */
|
|
216
|
+
const segments = [
|
|
217
|
+
{ kind: 'field_name', text: term.aggregate_name },
|
|
218
|
+
{ kind: 'operator', text: '(' },
|
|
219
|
+
...createTraversalSegments(term.traversal),
|
|
220
|
+
{ kind: 'operator', text: ', ' },
|
|
221
|
+
...createNestedClauseSegments(term.clauses),
|
|
222
|
+
{ kind: 'operator', text: ')' },
|
|
223
|
+
];
|
|
224
|
+
|
|
225
|
+
if (term.aggregate_name === 'count') {
|
|
226
|
+
segments.push({ kind: 'plain', text: ' ' });
|
|
227
|
+
segments.push({ kind: 'operator', text: term.comparison ?? '=' });
|
|
228
|
+
segments.push({ kind: 'plain', text: ' ' });
|
|
229
|
+
segments.push({ kind: 'literal', text: String(term.value ?? 0) });
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return segments;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* @param {{ direction: 'in' | 'out', relation_name: string }} traversal
|
|
237
|
+
* @returns {StoredQuerySegment[]}
|
|
238
|
+
*/
|
|
239
|
+
function createTraversalSegments(traversal) {
|
|
240
|
+
return [
|
|
241
|
+
{ kind: 'field_name', text: traversal.direction },
|
|
242
|
+
{ kind: 'operator', text: ':' },
|
|
243
|
+
{ kind: 'field_name', text: traversal.relation_name },
|
|
244
|
+
];
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* @param {{ is_negated: boolean, term: unknown }[]} clauses
|
|
249
|
+
* @returns {StoredQuerySegment[]}
|
|
250
|
+
*/
|
|
251
|
+
function createNestedClauseSegments(clauses) {
|
|
252
|
+
return clauses.flatMap((clause, clause_index) => {
|
|
253
|
+
const clause_phrase = createClausePhrase(
|
|
254
|
+
/** @type {{
|
|
255
|
+
* is_negated: boolean,
|
|
256
|
+
* term:
|
|
257
|
+
* | { field_name: 'id' | 'kind' | 'path' | 'status' | 'title', kind: 'field', operator: '=' | '^=' | '~', value: string }
|
|
258
|
+
* | { field_name: 'id' | 'kind' | 'path' | 'status' | 'title', kind: 'field_set', operator: 'in' | 'not in', values: string[] }
|
|
259
|
+
* | { kind: 'relation', relation_name: string }
|
|
260
|
+
* | { kind: 'relation_target', relation_name: string, target_id: string }
|
|
261
|
+
* | {
|
|
262
|
+
* aggregate_name: 'any' | 'count' | 'none',
|
|
263
|
+
* clauses: { is_negated: boolean, term: unknown }[],
|
|
264
|
+
* comparison?: '!=' | '<' | '<=' | '=' | '>' | '>=',
|
|
265
|
+
* kind: 'aggregate',
|
|
266
|
+
* traversal: { direction: 'in' | 'out', relation_name: string },
|
|
267
|
+
* value?: number,
|
|
268
|
+
* },
|
|
269
|
+
* }} */ (clause),
|
|
270
|
+
clause_index > 0,
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
if (clause_index === 0) {
|
|
274
|
+
return clause_phrase;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return [{ kind: 'plain', text: ' ' }, ...clause_phrase];
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* @param {string[]} values
|
|
283
|
+
* @returns {StoredQuerySegment[]}
|
|
284
|
+
*/
|
|
285
|
+
function createListSegments(values) {
|
|
286
|
+
return values.flatMap((value, value_index) => {
|
|
287
|
+
if (value_index === 0) {
|
|
288
|
+
return [{ kind: 'literal', text: value }];
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return [
|
|
292
|
+
{ kind: 'operator', text: ', ' },
|
|
293
|
+
{ kind: 'literal', text: value },
|
|
294
|
+
];
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
150
298
|
/**
|
|
151
299
|
* @param {string} where_clause
|
|
152
300
|
* @returns {StoredQuerySegment[][]}
|