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
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @import { PatramClaim } from './parse-claims.types.ts';
|
|
3
|
+
* @import { MappingDefinition } from './patram-config.types.ts';
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { posix } from 'node:path';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {{
|
|
10
|
+
* class_name: string,
|
|
11
|
+
* id: string,
|
|
12
|
+
* key: string,
|
|
13
|
+
* path: string,
|
|
14
|
+
* }} DocumentNodeReference
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Collect semantic entity keys defined by canonical documents.
|
|
19
|
+
*
|
|
20
|
+
* @param {Record<string, MappingDefinition>} mappings
|
|
21
|
+
* @param {PatramClaim[]} claims
|
|
22
|
+
* @returns {Map<string, string>}
|
|
23
|
+
*/
|
|
24
|
+
export function collectDocumentEntityKeys(mappings, claims) {
|
|
25
|
+
/** @type {Map<string, string>} */
|
|
26
|
+
const document_entity_keys = new Map();
|
|
27
|
+
|
|
28
|
+
for (const claim of claims) {
|
|
29
|
+
const mapping_definition = resolveMappingDefinition(mappings, claim);
|
|
30
|
+
|
|
31
|
+
if (
|
|
32
|
+
mapping_definition?.node?.key !== 'value' ||
|
|
33
|
+
mapping_definition.node.class === 'document'
|
|
34
|
+
) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const source_path = normalizeRepoRelativePath(claim.origin.path);
|
|
39
|
+
const entity_map_key = getDocumentEntityMapKey(
|
|
40
|
+
source_path,
|
|
41
|
+
mapping_definition.node.class,
|
|
42
|
+
);
|
|
43
|
+
const entity_key = getStringClaimValue(claim);
|
|
44
|
+
const existing_entity_key = document_entity_keys.get(entity_map_key);
|
|
45
|
+
|
|
46
|
+
if (existing_entity_key && existing_entity_key !== entity_key) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
`Document "${source_path}" defines multiple ${mapping_definition.node.class} ids.`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
document_entity_keys.set(entity_map_key, entity_key);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return document_entity_keys;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Collect canonical graph identities for document-backed source paths.
|
|
60
|
+
*
|
|
61
|
+
* @param {Record<string, MappingDefinition>} mappings
|
|
62
|
+
* @param {PatramClaim[]} claims
|
|
63
|
+
* @returns {Map<string, DocumentNodeReference>}
|
|
64
|
+
*/
|
|
65
|
+
export function collectDocumentNodeReferences(mappings, claims) {
|
|
66
|
+
/** @type {Map<string, DocumentNodeReference>} */
|
|
67
|
+
const document_node_references = new Map();
|
|
68
|
+
/** @type {Map<string, string>} */
|
|
69
|
+
const pending_document_keys = new Map();
|
|
70
|
+
|
|
71
|
+
for (const claim of claims) {
|
|
72
|
+
const source_path = normalizeRepoRelativePath(claim.origin.path);
|
|
73
|
+
const document_node_reference =
|
|
74
|
+
document_node_references.get(source_path) ??
|
|
75
|
+
createDefaultDocumentNodeReference(source_path);
|
|
76
|
+
const mapping_definition = resolveMappingDefinition(mappings, claim);
|
|
77
|
+
|
|
78
|
+
document_node_references.set(source_path, document_node_reference);
|
|
79
|
+
|
|
80
|
+
if (mapping_definition?.node?.class !== 'document') {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
applyDocumentNodeMapping(
|
|
85
|
+
document_node_reference,
|
|
86
|
+
mapping_definition.node,
|
|
87
|
+
claim,
|
|
88
|
+
pending_document_keys,
|
|
89
|
+
source_path,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return document_node_references;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Resolve the canonical node id for a source document path.
|
|
98
|
+
*
|
|
99
|
+
* @param {Record<string, string> | undefined} document_node_ids
|
|
100
|
+
* @param {string} document_path
|
|
101
|
+
* @returns {string}
|
|
102
|
+
*/
|
|
103
|
+
export function resolveDocumentNodeId(document_node_ids, document_path) {
|
|
104
|
+
return document_node_ids?.[document_path] ?? `doc:${document_path}`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Normalize one repo-relative source path.
|
|
109
|
+
*
|
|
110
|
+
* @param {string} source_path
|
|
111
|
+
* @returns {string}
|
|
112
|
+
*/
|
|
113
|
+
export function normalizeRepoRelativePath(source_path) {
|
|
114
|
+
return posix.normalize(source_path.replaceAll('\\', '/'));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* @param {DocumentNodeReference} document_node_reference
|
|
119
|
+
* @param {{ field: string, key?: 'path' | 'value', class: string }} node_mapping
|
|
120
|
+
* @param {PatramClaim} claim
|
|
121
|
+
* @param {Map<string, string>} pending_document_keys
|
|
122
|
+
* @param {string} source_path
|
|
123
|
+
*/
|
|
124
|
+
function applyDocumentNodeMapping(
|
|
125
|
+
document_node_reference,
|
|
126
|
+
node_mapping,
|
|
127
|
+
claim,
|
|
128
|
+
pending_document_keys,
|
|
129
|
+
source_path,
|
|
130
|
+
) {
|
|
131
|
+
if (node_mapping.field === '$class') {
|
|
132
|
+
assignDocumentNodeClass(
|
|
133
|
+
document_node_reference,
|
|
134
|
+
getStringClaimValue(claim),
|
|
135
|
+
);
|
|
136
|
+
applyPendingDocumentKey(
|
|
137
|
+
document_node_reference,
|
|
138
|
+
pending_document_keys,
|
|
139
|
+
source_path,
|
|
140
|
+
);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (node_mapping.field !== '$id' || node_mapping.key !== 'value') {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const document_node_key = getStringClaimValue(claim);
|
|
149
|
+
|
|
150
|
+
if (document_node_reference.class_name === 'document') {
|
|
151
|
+
assignPendingDocumentKey(
|
|
152
|
+
pending_document_keys,
|
|
153
|
+
source_path,
|
|
154
|
+
document_node_key,
|
|
155
|
+
);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
assignDocumentNodeKey(document_node_reference, document_node_key);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* @param {DocumentNodeReference} document_node_reference
|
|
164
|
+
* @param {Map<string, string>} pending_document_keys
|
|
165
|
+
* @param {string} source_path
|
|
166
|
+
*/
|
|
167
|
+
function applyPendingDocumentKey(
|
|
168
|
+
document_node_reference,
|
|
169
|
+
pending_document_keys,
|
|
170
|
+
source_path,
|
|
171
|
+
) {
|
|
172
|
+
if (document_node_reference.class_name === 'document') {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const pending_document_key = pending_document_keys.get(source_path);
|
|
177
|
+
|
|
178
|
+
if (!pending_document_key) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
assignDocumentNodeKey(document_node_reference, pending_document_key);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* @param {Record<string, MappingDefinition>} mappings
|
|
187
|
+
* @param {PatramClaim} claim
|
|
188
|
+
* @returns {MappingDefinition | null}
|
|
189
|
+
*/
|
|
190
|
+
function resolveMappingDefinition(mappings, claim) {
|
|
191
|
+
if (claim.type === 'directive') {
|
|
192
|
+
return resolveDirectiveMapping(mappings, claim);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return mappings[claim.type] ?? null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* @param {Record<string, MappingDefinition>} mappings
|
|
200
|
+
* @param {PatramClaim} claim
|
|
201
|
+
* @returns {MappingDefinition | null}
|
|
202
|
+
*/
|
|
203
|
+
function resolveDirectiveMapping(mappings, claim) {
|
|
204
|
+
if (!claim.parser || !claim.name) {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return mappings[`${claim.parser}.directive.${claim.name}`] ?? null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* @param {PatramClaim} claim
|
|
213
|
+
* @returns {string}
|
|
214
|
+
*/
|
|
215
|
+
function getStringClaimValue(claim) {
|
|
216
|
+
if (typeof claim.value === 'string') {
|
|
217
|
+
return claim.value;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
throw new Error(`Claim "${claim.id}" does not carry a string value.`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* @param {string} document_path
|
|
225
|
+
* @param {string} class_name
|
|
226
|
+
* @returns {string}
|
|
227
|
+
*/
|
|
228
|
+
function getDocumentEntityMapKey(document_path, class_name) {
|
|
229
|
+
return `${class_name}:${document_path}`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* @param {string} source_path
|
|
234
|
+
* @returns {DocumentNodeReference}
|
|
235
|
+
*/
|
|
236
|
+
function createDefaultDocumentNodeReference(source_path) {
|
|
237
|
+
return {
|
|
238
|
+
class_name: 'document',
|
|
239
|
+
id: `doc:${source_path}`,
|
|
240
|
+
key: source_path,
|
|
241
|
+
path: source_path,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* @param {DocumentNodeReference} document_node_reference
|
|
247
|
+
* @param {string} class_name
|
|
248
|
+
*/
|
|
249
|
+
function assignDocumentNodeClass(document_node_reference, class_name) {
|
|
250
|
+
if (
|
|
251
|
+
document_node_reference.class_name !== 'document' &&
|
|
252
|
+
document_node_reference.class_name !== class_name
|
|
253
|
+
) {
|
|
254
|
+
throw new Error(
|
|
255
|
+
`Document "${document_node_reference.path}" defines multiple semantic classes.`,
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
document_node_reference.class_name = class_name;
|
|
260
|
+
document_node_reference.id = getNodeId(
|
|
261
|
+
document_node_reference.class_name,
|
|
262
|
+
document_node_reference.key,
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* @param {DocumentNodeReference} document_node_reference
|
|
268
|
+
* @param {string} node_key
|
|
269
|
+
*/
|
|
270
|
+
function assignDocumentNodeKey(document_node_reference, node_key) {
|
|
271
|
+
if (
|
|
272
|
+
document_node_reference.key !== document_node_reference.path &&
|
|
273
|
+
document_node_reference.key !== node_key
|
|
274
|
+
) {
|
|
275
|
+
throw new Error(
|
|
276
|
+
`Document "${document_node_reference.path}" defines multiple semantic ids.`,
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
document_node_reference.key = node_key;
|
|
281
|
+
document_node_reference.id = getNodeId(
|
|
282
|
+
document_node_reference.class_name,
|
|
283
|
+
document_node_reference.key,
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* @param {Map<string, string>} pending_document_keys
|
|
289
|
+
* @param {string} source_path
|
|
290
|
+
* @param {string} node_key
|
|
291
|
+
*/
|
|
292
|
+
function assignPendingDocumentKey(
|
|
293
|
+
pending_document_keys,
|
|
294
|
+
source_path,
|
|
295
|
+
node_key,
|
|
296
|
+
) {
|
|
297
|
+
const existing_node_key = pending_document_keys.get(source_path);
|
|
298
|
+
|
|
299
|
+
if (existing_node_key && existing_node_key !== node_key) {
|
|
300
|
+
throw new Error(`Document "${source_path}" defines multiple semantic ids.`);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
pending_document_keys.set(source_path, node_key);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* @param {string} class_name
|
|
308
|
+
* @param {string} node_key
|
|
309
|
+
* @returns {string}
|
|
310
|
+
*/
|
|
311
|
+
function getNodeId(class_name, node_key) {
|
|
312
|
+
if (class_name === 'document') {
|
|
313
|
+
return `doc:${node_key}`;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return `${class_name}:${node_key}`;
|
|
317
|
+
}
|
|
@@ -3,17 +3,19 @@
|
|
|
3
3
|
* @returns {string}
|
|
4
4
|
*/
|
|
5
5
|
export function formatNodeHeader(output_item) {
|
|
6
|
-
if (
|
|
7
|
-
return
|
|
6
|
+
if (output_item.path) {
|
|
7
|
+
return `${output_item.node_kind} ${output_item.path}`;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
return `${output_item.node_kind} ${output_item.id}`;
|
|
10
|
+
return `${output_item.node_kind} ${getOutputNodeKey(output_item.id)}`;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
* @param {
|
|
15
|
-
* @returns {
|
|
14
|
+
* @param {string} node_id
|
|
15
|
+
* @returns {string}
|
|
16
16
|
*/
|
|
17
|
-
|
|
18
|
-
return
|
|
17
|
+
function getOutputNodeKey(node_id) {
|
|
18
|
+
return node_id.includes(':')
|
|
19
|
+
? node_id.split(':').slice(1).join(':')
|
|
20
|
+
: node_id;
|
|
19
21
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { formatDerivedSummaryRow } from './format-derived-summary-row.js';
|
|
2
|
-
import { isDocumentNode } from './format-node-header.js';
|
|
3
2
|
|
|
4
3
|
/**
|
|
5
4
|
* @param {import('./output-view.types.ts').OutputNodeItem} output_item
|
|
@@ -8,18 +7,8 @@ import { isDocumentNode } from './format-node-header.js';
|
|
|
8
7
|
export function formatOutputNodeMetadataRows(output_item) {
|
|
9
8
|
/** @type {string[]} */
|
|
10
9
|
const metadata_rows = [];
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
}
|
|
10
|
+
const stored_metadata_fields =
|
|
11
|
+
output_item.visible_fields.map(formatMetadataField);
|
|
23
12
|
|
|
24
13
|
if (stored_metadata_fields.length > 0) {
|
|
25
14
|
metadata_rows.push(stored_metadata_fields.join(' '));
|
|
@@ -39,16 +28,7 @@ export function formatOutputNodeMetadataRows(output_item) {
|
|
|
39
28
|
export function formatResolvedLinkMetadataRows(target) {
|
|
40
29
|
/** @type {string[]} */
|
|
41
30
|
const metadata_rows = [];
|
|
42
|
-
|
|
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
|
-
}
|
|
31
|
+
const stored_metadata_fields = target.visible_fields.map(formatMetadataField);
|
|
52
32
|
|
|
53
33
|
if (stored_metadata_fields.length > 0) {
|
|
54
34
|
metadata_rows.push(stored_metadata_fields.join(' '));
|
|
@@ -60,3 +40,15 @@ export function formatResolvedLinkMetadataRows(target) {
|
|
|
60
40
|
|
|
61
41
|
return metadata_rows;
|
|
62
42
|
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @param {import('./output-view.types.ts').OutputMetadataField} output_field
|
|
46
|
+
* @returns {string}
|
|
47
|
+
*/
|
|
48
|
+
function formatMetadataField(output_field) {
|
|
49
|
+
const value = Array.isArray(output_field.value)
|
|
50
|
+
? output_field.value.join(', ')
|
|
51
|
+
: output_field.value;
|
|
52
|
+
|
|
53
|
+
return `${output_field.name}: ${value}`;
|
|
54
|
+
}
|
|
@@ -92,65 +92,145 @@ function createStoredQueryPhrases(where_clause) {
|
|
|
92
92
|
return createFallbackPhrases(where_clause);
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
return parse_result.
|
|
96
|
-
|
|
95
|
+
return createExpressionPhrases(parse_result.expression);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* @param {import('./parse-where-clause.types.ts').ParsedExpression} expression
|
|
100
|
+
* @returns {StoredQuerySegment[][]}
|
|
101
|
+
*/
|
|
102
|
+
function createExpressionPhrases(expression) {
|
|
103
|
+
if (expression.kind !== 'and' && expression.kind !== 'or') {
|
|
104
|
+
return [[...createExpressionSegments(expression, 0)]];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return expression.expressions.map((subexpression, expression_index) =>
|
|
108
|
+
createExpressionPhrase(
|
|
109
|
+
subexpression,
|
|
110
|
+
expression.kind,
|
|
111
|
+
expression_index > 0,
|
|
112
|
+
),
|
|
97
113
|
);
|
|
98
114
|
}
|
|
99
115
|
|
|
100
116
|
/**
|
|
101
|
-
* @param {
|
|
102
|
-
*
|
|
103
|
-
*
|
|
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
|
|
117
|
-
* @param {boolean} should_prefix_and
|
|
117
|
+
* @param {import('./parse-where-clause.types.ts').ParsedExpression} expression
|
|
118
|
+
* @param {'and' | 'or'} operator
|
|
119
|
+
* @param {boolean} should_prefix_operator
|
|
118
120
|
* @returns {StoredQuerySegment[]}
|
|
119
121
|
*/
|
|
120
|
-
function
|
|
122
|
+
function createExpressionPhrase(expression, operator, should_prefix_operator) {
|
|
121
123
|
/** @type {StoredQuerySegment[]} */
|
|
122
124
|
const phrase = [];
|
|
123
125
|
|
|
124
|
-
if (
|
|
125
|
-
phrase.push({ kind: 'keyword', text:
|
|
126
|
+
if (should_prefix_operator) {
|
|
127
|
+
phrase.push({ kind: 'keyword', text: operator });
|
|
126
128
|
phrase.push({ kind: 'plain', text: ' ' });
|
|
127
129
|
}
|
|
128
130
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
131
|
+
phrase.push(
|
|
132
|
+
...createExpressionSegments(
|
|
133
|
+
expression,
|
|
134
|
+
getBooleanExpressionPrecedence(operator),
|
|
135
|
+
),
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
return phrase;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* @param {import('./parse-where-clause.types.ts').ParsedExpression} expression
|
|
143
|
+
* @param {number} parent_precedence
|
|
144
|
+
* @returns {StoredQuerySegment[]}
|
|
145
|
+
*/
|
|
146
|
+
function createExpressionSegments(expression, parent_precedence) {
|
|
147
|
+
const expression_precedence = getExpressionPrecedence(expression);
|
|
148
|
+
const expression_segments = createRawExpressionSegments(expression);
|
|
149
|
+
|
|
150
|
+
if (expression_precedence >= parent_precedence) {
|
|
151
|
+
return expression_segments;
|
|
132
152
|
}
|
|
133
153
|
|
|
134
|
-
|
|
154
|
+
return [
|
|
155
|
+
{ kind: 'operator', text: '(' },
|
|
156
|
+
...expression_segments,
|
|
157
|
+
{ kind: 'operator', text: ')' },
|
|
158
|
+
];
|
|
159
|
+
}
|
|
135
160
|
|
|
136
|
-
|
|
161
|
+
/**
|
|
162
|
+
* @param {import('./parse-where-clause.types.ts').ParsedExpression} expression
|
|
163
|
+
* @returns {StoredQuerySegment[]}
|
|
164
|
+
*/
|
|
165
|
+
function createRawExpressionSegments(expression) {
|
|
166
|
+
if (expression.kind === 'and' || expression.kind === 'or') {
|
|
167
|
+
return expression.expressions.flatMap((subexpression, expression_index) => {
|
|
168
|
+
const subexpression_segments = createExpressionSegments(
|
|
169
|
+
subexpression,
|
|
170
|
+
getExpressionPrecedence(expression),
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
if (expression_index === 0) {
|
|
174
|
+
return subexpression_segments;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return [
|
|
178
|
+
{ kind: 'plain', text: ' ' },
|
|
179
|
+
{ kind: 'keyword', text: expression.kind },
|
|
180
|
+
{ kind: 'plain', text: ' ' },
|
|
181
|
+
...subexpression_segments,
|
|
182
|
+
];
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (expression.kind === 'not') {
|
|
187
|
+
return [
|
|
188
|
+
{ kind: 'keyword', text: 'not' },
|
|
189
|
+
{ kind: 'plain', text: ' ' },
|
|
190
|
+
...createExpressionSegments(
|
|
191
|
+
expression.expression,
|
|
192
|
+
getExpressionPrecedence(expression),
|
|
193
|
+
),
|
|
194
|
+
];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (expression.kind === 'term') {
|
|
198
|
+
return createTermSegments(expression.term);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
throw new Error('Unsupported stored-query expression.');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* @param {import('./parse-where-clause.types.ts').ParsedExpression} expression
|
|
206
|
+
* @returns {number}
|
|
207
|
+
*/
|
|
208
|
+
function getExpressionPrecedence(expression) {
|
|
209
|
+
if (expression.kind === 'or') {
|
|
210
|
+
return 1;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (expression.kind === 'and') {
|
|
214
|
+
return 2;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (expression.kind === 'not') {
|
|
218
|
+
return 3;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return 4;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* @param {'and' | 'or'} operator
|
|
226
|
+
* @returns {number}
|
|
227
|
+
*/
|
|
228
|
+
function getBooleanExpressionPrecedence(operator) {
|
|
229
|
+
return operator === 'or' ? 1 : 2;
|
|
137
230
|
}
|
|
138
231
|
|
|
139
232
|
/**
|
|
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
|
|
233
|
+
* @param {import('./parse-where-clause.types.ts').ParsedTerm} term
|
|
154
234
|
* @returns {StoredQuerySegment[]}
|
|
155
235
|
*/
|
|
156
236
|
function createTermSegments(term) {
|
|
@@ -185,7 +265,7 @@ function createTermSegments(term) {
|
|
|
185
265
|
}
|
|
186
266
|
|
|
187
267
|
/**
|
|
188
|
-
* @param {
|
|
268
|
+
* @param {import('./parse-where-clause.types.ts').ParsedFieldSetTerm} term
|
|
189
269
|
* @returns {StoredQuerySegment[]}
|
|
190
270
|
*/
|
|
191
271
|
function createFieldSetSegments(term) {
|
|
@@ -201,14 +281,7 @@ function createFieldSetSegments(term) {
|
|
|
201
281
|
}
|
|
202
282
|
|
|
203
283
|
/**
|
|
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
|
|
284
|
+
* @param {import('./parse-where-clause.types.ts').ParsedAggregateTerm} term
|
|
212
285
|
* @returns {StoredQuerySegment[]}
|
|
213
286
|
*/
|
|
214
287
|
function createAggregateSegments(term) {
|
|
@@ -218,7 +291,7 @@ function createAggregateSegments(term) {
|
|
|
218
291
|
{ kind: 'operator', text: '(' },
|
|
219
292
|
...createTraversalSegments(term.traversal),
|
|
220
293
|
{ kind: 'operator', text: ', ' },
|
|
221
|
-
...
|
|
294
|
+
...createExpressionSegments(term.expression, 0),
|
|
222
295
|
{ kind: 'operator', text: ')' },
|
|
223
296
|
];
|
|
224
297
|
|
|
@@ -244,40 +317,6 @@ function createTraversalSegments(traversal) {
|
|
|
244
317
|
];
|
|
245
318
|
}
|
|
246
319
|
|
|
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
320
|
/**
|
|
282
321
|
* @param {string[]} values
|
|
283
322
|
* @returns {StoredQuerySegment[]}
|