patram 0.1.1 → 0.3.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 +57 -24
- package/lib/build-graph.js +383 -17
- package/lib/build-graph.types.ts +5 -2
- package/lib/check-directive-metadata.js +516 -0
- package/lib/check-directive-value.js +282 -0
- package/lib/check-graph.js +24 -5
- package/lib/cli-help-metadata.js +580 -0
- package/lib/derived-summary.js +280 -0
- package/lib/directive-diagnostics.js +38 -0
- package/lib/directive-type-rules.js +133 -0
- package/lib/discover-fields.js +427 -0
- package/lib/discover-fields.types.ts +52 -0
- package/lib/format-derived-summary-row.js +9 -0
- package/lib/format-node-header.js +21 -0
- package/lib/format-output-item-block.js +22 -0
- package/lib/format-output-metadata.js +54 -0
- package/lib/layout-stored-queries.js +96 -2
- package/lib/load-patram-config.js +754 -18
- package/lib/load-patram-config.types.ts +128 -2
- package/lib/load-project-graph.js +4 -1
- package/lib/output-view.types.ts +29 -6
- package/lib/parse-cli-arguments-helpers.js +263 -90
- package/lib/parse-cli-arguments.js +160 -8
- package/lib/parse-cli-arguments.types.ts +49 -4
- package/lib/parse-where-clause.js +670 -209
- package/lib/parse-where-clause.types.ts +72 -0
- package/lib/patram-cli.js +180 -21
- package/lib/patram-config.js +31 -31
- package/lib/patram-config.types.ts +10 -4
- package/lib/patram.js +6 -0
- package/lib/query-graph.js +444 -113
- package/lib/query-inspection.js +798 -0
- package/lib/render-check-output.js +1 -1
- package/lib/render-cli-help.js +419 -0
- package/lib/render-field-discovery.js +148 -0
- package/lib/render-json-output.js +66 -14
- package/lib/render-output-view.js +272 -22
- package/lib/render-plain-output.js +31 -86
- package/lib/render-rich-output.js +34 -87
- package/lib/resolve-patram-graph-config.js +15 -9
- package/lib/resolve-where-clause.js +18 -3
- package/lib/show-document.js +51 -7
- 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 +12 -7
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
/* eslint-disable max-lines */
|
|
2
2
|
/**
|
|
3
|
-
* @import {
|
|
3
|
+
* @import {
|
|
4
|
+
* ClassSchemaConfig,
|
|
5
|
+
* LoadPatramConfigResult,
|
|
6
|
+
* MetadataFieldConfig,
|
|
7
|
+
* PatramDiagnostic,
|
|
8
|
+
* PatramRepoConfig,
|
|
9
|
+
* } from './load-patram-config.types.ts';
|
|
4
10
|
*/
|
|
5
11
|
|
|
6
12
|
import { readFile } from 'node:fs/promises';
|
|
@@ -10,6 +16,9 @@ import process from 'node:process';
|
|
|
10
16
|
import { z } from 'zod';
|
|
11
17
|
|
|
12
18
|
import { parsePatramConfig } from './patram-config.js';
|
|
19
|
+
import { parseWhereClause } from './parse-where-clause.js';
|
|
20
|
+
import { getQuerySemanticDiagnostics } from './query-inspection.js';
|
|
21
|
+
import { resolvePatramGraphConfig } from './resolve-patram-graph-config.js';
|
|
13
22
|
import { DEFAULT_INCLUDE_PATTERNS } from './source-file-defaults.js';
|
|
14
23
|
|
|
15
24
|
/**
|
|
@@ -29,6 +38,7 @@ import { DEFAULT_INCLUDE_PATTERNS } from './source-file-defaults.js';
|
|
|
29
38
|
*/
|
|
30
39
|
|
|
31
40
|
const CONFIG_FILE_NAME = '.patram.json';
|
|
41
|
+
const RESERVED_STRUCTURAL_FIELD_NAMES = new Set(['$class', '$id', '$path']);
|
|
32
42
|
|
|
33
43
|
const stored_query_schema = z
|
|
34
44
|
.object({
|
|
@@ -36,18 +46,168 @@ const stored_query_schema = z
|
|
|
36
46
|
})
|
|
37
47
|
.strict();
|
|
38
48
|
|
|
49
|
+
const derived_summary_scalar_schema = z.union([
|
|
50
|
+
z.boolean(),
|
|
51
|
+
z.number(),
|
|
52
|
+
z.string(),
|
|
53
|
+
z.null(),
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
const derived_summary_count_schema = z
|
|
57
|
+
.object({
|
|
58
|
+
traversal: z
|
|
59
|
+
.string()
|
|
60
|
+
.min(1, 'Derived summary count "traversal" must not be empty.'),
|
|
61
|
+
where: z
|
|
62
|
+
.string()
|
|
63
|
+
.min(1, 'Derived summary count "where" must not be empty.'),
|
|
64
|
+
})
|
|
65
|
+
.strict();
|
|
66
|
+
|
|
67
|
+
const derived_summary_select_case_schema = z
|
|
68
|
+
.object({
|
|
69
|
+
value: derived_summary_scalar_schema,
|
|
70
|
+
when: z.string().min(1, 'Derived summary select "when" must not be empty.'),
|
|
71
|
+
})
|
|
72
|
+
.strict();
|
|
73
|
+
|
|
74
|
+
const derived_summary_field_schema = z
|
|
75
|
+
.object({
|
|
76
|
+
count: derived_summary_count_schema.optional(),
|
|
77
|
+
default: derived_summary_scalar_schema.optional(),
|
|
78
|
+
name: z
|
|
79
|
+
.string()
|
|
80
|
+
.regex(
|
|
81
|
+
/^[a-z][a-z0-9_]*$/du,
|
|
82
|
+
'Derived summary field names must use lower_snake_case.',
|
|
83
|
+
),
|
|
84
|
+
select: z.array(derived_summary_select_case_schema).optional(),
|
|
85
|
+
})
|
|
86
|
+
.strict()
|
|
87
|
+
.superRefine(validateDerivedSummaryFieldDefinition);
|
|
88
|
+
|
|
89
|
+
const derived_summary_schema = z
|
|
90
|
+
.object({
|
|
91
|
+
classes: z
|
|
92
|
+
.array(z.string().min(1))
|
|
93
|
+
.min(1, 'Derived summary "classes" must contain at least one class.'),
|
|
94
|
+
fields: z
|
|
95
|
+
.array(derived_summary_field_schema)
|
|
96
|
+
.min(1, 'Derived summary "fields" must contain at least one field.'),
|
|
97
|
+
})
|
|
98
|
+
.strict()
|
|
99
|
+
.superRefine(validateDerivedSummaryDefinition);
|
|
100
|
+
|
|
101
|
+
const field_display_schema = z
|
|
102
|
+
.object({
|
|
103
|
+
hidden: z.boolean().optional(),
|
|
104
|
+
order: z.number().optional(),
|
|
105
|
+
})
|
|
106
|
+
.strict();
|
|
107
|
+
|
|
108
|
+
const field_query_schema = z
|
|
109
|
+
.object({
|
|
110
|
+
contains: z.boolean().optional(),
|
|
111
|
+
prefix: z.boolean().optional(),
|
|
112
|
+
})
|
|
113
|
+
.strict();
|
|
114
|
+
|
|
115
|
+
const field_base_shape = {
|
|
116
|
+
display: field_display_schema.optional(),
|
|
117
|
+
multiple: z.boolean().optional(),
|
|
118
|
+
path_class: z.string().min(1).optional(),
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const metadata_field_schema = z.discriminatedUnion('type', [
|
|
122
|
+
z
|
|
123
|
+
.object({
|
|
124
|
+
...field_base_shape,
|
|
125
|
+
query: field_query_schema.optional(),
|
|
126
|
+
type: z.literal('string'),
|
|
127
|
+
})
|
|
128
|
+
.strict(),
|
|
129
|
+
z
|
|
130
|
+
.object({
|
|
131
|
+
...field_base_shape,
|
|
132
|
+
type: z.literal('integer'),
|
|
133
|
+
})
|
|
134
|
+
.strict(),
|
|
135
|
+
z
|
|
136
|
+
.object({
|
|
137
|
+
...field_base_shape,
|
|
138
|
+
type: z.literal('enum'),
|
|
139
|
+
values: z
|
|
140
|
+
.array(z.string().min(1, 'Field enum values must not be empty.'))
|
|
141
|
+
.min(1, 'Field enum values must contain at least one value.'),
|
|
142
|
+
})
|
|
143
|
+
.strict(),
|
|
144
|
+
z
|
|
145
|
+
.object({
|
|
146
|
+
...field_base_shape,
|
|
147
|
+
type: z.literal('path'),
|
|
148
|
+
})
|
|
149
|
+
.strict(),
|
|
150
|
+
z
|
|
151
|
+
.object({
|
|
152
|
+
...field_base_shape,
|
|
153
|
+
type: z.literal('glob'),
|
|
154
|
+
})
|
|
155
|
+
.strict(),
|
|
156
|
+
z
|
|
157
|
+
.object({
|
|
158
|
+
...field_base_shape,
|
|
159
|
+
type: z.literal('date'),
|
|
160
|
+
})
|
|
161
|
+
.strict(),
|
|
162
|
+
z
|
|
163
|
+
.object({
|
|
164
|
+
...field_base_shape,
|
|
165
|
+
type: z.literal('date_time'),
|
|
166
|
+
})
|
|
167
|
+
.strict(),
|
|
168
|
+
]);
|
|
169
|
+
|
|
170
|
+
const class_field_rule_schema = z
|
|
171
|
+
.object({
|
|
172
|
+
presence: z.enum(['required', 'optional', 'forbidden']),
|
|
173
|
+
})
|
|
174
|
+
.strict();
|
|
175
|
+
|
|
176
|
+
const class_schema_schema = z
|
|
177
|
+
.object({
|
|
178
|
+
document_path_class: z.string().min(1).optional(),
|
|
179
|
+
fields: z.record(z.string().min(1), class_field_rule_schema).default({}),
|
|
180
|
+
unknown_fields: z.enum(['ignore', 'error']).optional(),
|
|
181
|
+
})
|
|
182
|
+
.strict();
|
|
183
|
+
|
|
184
|
+
const path_class_schema = z
|
|
185
|
+
.object({
|
|
186
|
+
prefixes: z
|
|
187
|
+
.array(z.string().min(1, 'Path class prefixes must not be empty.'))
|
|
188
|
+
.min(1, 'Path classes must contain at least one prefix.'),
|
|
189
|
+
})
|
|
190
|
+
.strict();
|
|
191
|
+
|
|
39
192
|
const patram_repo_config_schema = z
|
|
40
193
|
.object({
|
|
194
|
+
class_schemas: z.record(z.string().min(1), class_schema_schema).optional(),
|
|
195
|
+
classes: z.unknown().optional(),
|
|
196
|
+
derived_summaries: z
|
|
197
|
+
.record(z.string().min(1), derived_summary_schema)
|
|
198
|
+
.optional(),
|
|
199
|
+
fields: z.record(z.string().min(1), metadata_field_schema).optional(),
|
|
41
200
|
include: z
|
|
42
201
|
.array(z.string().min(1, 'Include globs must not be empty.'))
|
|
43
202
|
.min(1, 'Include must contain at least one glob.')
|
|
44
203
|
.default(DEFAULT_INCLUDE_PATTERNS),
|
|
45
|
-
kinds: z.unknown().optional(),
|
|
46
204
|
mappings: z.unknown().optional(),
|
|
205
|
+
path_classes: z.record(z.string().min(1), path_class_schema).optional(),
|
|
47
206
|
queries: z.record(z.string().min(1), stored_query_schema).default({}),
|
|
48
207
|
relations: z.unknown().optional(),
|
|
49
208
|
})
|
|
50
|
-
.strict()
|
|
209
|
+
.strict()
|
|
210
|
+
.superRefine(validateFieldDefinitionKeys);
|
|
51
211
|
|
|
52
212
|
/**
|
|
53
213
|
* Load and validate the repo Patram config.
|
|
@@ -84,7 +244,27 @@ export async function loadPatramConfig(project_directory = process.cwd()) {
|
|
|
84
244
|
return createLoadResult(null, graph_schema_diagnostics);
|
|
85
245
|
}
|
|
86
246
|
|
|
87
|
-
|
|
247
|
+
const normalized_config = normalizeRepoConfig(config_result.data);
|
|
248
|
+
const field_schema_diagnostics = validateFieldSchemaConfig(normalized_config);
|
|
249
|
+
|
|
250
|
+
if (field_schema_diagnostics.length > 0) {
|
|
251
|
+
return createLoadResult(null, field_schema_diagnostics);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const stored_query_diagnostics = validateStoredQueries(normalized_config);
|
|
255
|
+
|
|
256
|
+
if (stored_query_diagnostics.length > 0) {
|
|
257
|
+
return createLoadResult(null, stored_query_diagnostics);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const derived_summary_diagnostics =
|
|
261
|
+
validateDerivedSummaries(normalized_config);
|
|
262
|
+
|
|
263
|
+
if (derived_summary_diagnostics.length > 0) {
|
|
264
|
+
return createLoadResult(null, derived_summary_diagnostics);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return createLoadResult(normalized_config, []);
|
|
88
268
|
}
|
|
89
269
|
|
|
90
270
|
/**
|
|
@@ -185,12 +365,112 @@ function createValidationDiagnostic(issue) {
|
|
|
185
365
|
}
|
|
186
366
|
|
|
187
367
|
/**
|
|
188
|
-
* @param {{
|
|
368
|
+
* @param {{ fields?: Record<string, MetadataFieldConfig> }} repo_config
|
|
369
|
+
* @param {import('zod').RefinementCtx} refinement_context
|
|
370
|
+
*/
|
|
371
|
+
function validateFieldDefinitionKeys(repo_config, refinement_context) {
|
|
372
|
+
for (const field_name of Object.keys(repo_config.fields ?? {})) {
|
|
373
|
+
if (!field_name.startsWith('$')) {
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
refinement_context.addIssue({
|
|
378
|
+
code: 'custom',
|
|
379
|
+
message: 'Metadata field names must not start with "$".',
|
|
380
|
+
path: ['fields', field_name],
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* @param {{ count?: unknown, default?: unknown, select?: unknown }} field_definition
|
|
387
|
+
* @param {import('zod').RefinementCtx} refinement_context
|
|
388
|
+
*/
|
|
389
|
+
function validateDerivedSummaryFieldDefinition(
|
|
390
|
+
field_definition,
|
|
391
|
+
refinement_context,
|
|
392
|
+
) {
|
|
393
|
+
const evaluator_count =
|
|
394
|
+
Number(field_definition.count !== undefined) +
|
|
395
|
+
Number(field_definition.select !== undefined);
|
|
396
|
+
|
|
397
|
+
if (evaluator_count !== 1) {
|
|
398
|
+
refinement_context.addIssue({
|
|
399
|
+
code: 'custom',
|
|
400
|
+
message:
|
|
401
|
+
'Derived summary fields must define exactly one of "count" or "select".',
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (
|
|
406
|
+
field_definition.count !== undefined &&
|
|
407
|
+
field_definition.default !== undefined
|
|
408
|
+
) {
|
|
409
|
+
refinement_context.addIssue({
|
|
410
|
+
code: 'custom',
|
|
411
|
+
message: 'Derived summary count fields must not define "default".',
|
|
412
|
+
path: ['default'],
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (field_definition.select === undefined) {
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (
|
|
421
|
+
Array.isArray(field_definition.select) &&
|
|
422
|
+
field_definition.select.length === 0
|
|
423
|
+
) {
|
|
424
|
+
refinement_context.addIssue({
|
|
425
|
+
code: 'custom',
|
|
426
|
+
message: 'Derived summary "select" must contain at least one case.',
|
|
427
|
+
path: ['select'],
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (field_definition.default === undefined) {
|
|
432
|
+
refinement_context.addIssue({
|
|
433
|
+
code: 'custom',
|
|
434
|
+
message: 'Derived summary select fields must define "default".',
|
|
435
|
+
path: ['default'],
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* @param {{ fields: Array<{ name: string }> }} summary_definition
|
|
442
|
+
* @param {import('zod').RefinementCtx} refinement_context
|
|
443
|
+
*/
|
|
444
|
+
function validateDerivedSummaryDefinition(
|
|
445
|
+
summary_definition,
|
|
446
|
+
refinement_context,
|
|
447
|
+
) {
|
|
448
|
+
const seen_field_names = new Set();
|
|
449
|
+
|
|
450
|
+
for (const [
|
|
451
|
+
field_index,
|
|
452
|
+
field_definition,
|
|
453
|
+
] of summary_definition.fields.entries()) {
|
|
454
|
+
if (!seen_field_names.has(field_definition.name)) {
|
|
455
|
+
seen_field_names.add(field_definition.name);
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
refinement_context.addIssue({
|
|
460
|
+
code: 'custom',
|
|
461
|
+
message: `Duplicate derived summary field "${field_definition.name}".`,
|
|
462
|
+
path: ['fields', field_index, 'name'],
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* @param {{ include: string[], queries: Record<string, { where: string }>, classes?: unknown, mappings?: unknown, relations?: unknown }} repo_config
|
|
189
469
|
* @returns {PatramDiagnostic[]}
|
|
190
470
|
*/
|
|
191
471
|
function validateGraphSchema(repo_config) {
|
|
192
472
|
if (
|
|
193
|
-
repo_config.
|
|
473
|
+
repo_config.classes === undefined &&
|
|
194
474
|
repo_config.mappings === undefined &&
|
|
195
475
|
repo_config.relations === undefined
|
|
196
476
|
) {
|
|
@@ -199,7 +479,7 @@ function validateGraphSchema(repo_config) {
|
|
|
199
479
|
|
|
200
480
|
try {
|
|
201
481
|
parsePatramConfig({
|
|
202
|
-
|
|
482
|
+
classes: repo_config.classes ?? {},
|
|
203
483
|
mappings: repo_config.mappings ?? {},
|
|
204
484
|
relations: repo_config.relations ?? {},
|
|
205
485
|
});
|
|
@@ -214,6 +494,87 @@ function validateGraphSchema(repo_config) {
|
|
|
214
494
|
return [];
|
|
215
495
|
}
|
|
216
496
|
|
|
497
|
+
/**
|
|
498
|
+
* @param {PatramRepoConfig} repo_config
|
|
499
|
+
* @returns {PatramDiagnostic[]}
|
|
500
|
+
*/
|
|
501
|
+
function validateFieldSchemaConfig(repo_config) {
|
|
502
|
+
const path_classes = repo_config.path_classes ?? {};
|
|
503
|
+
const classes = repo_config.classes ?? {};
|
|
504
|
+
const fields = repo_config.fields ?? {};
|
|
505
|
+
/** @type {PatramDiagnostic[]} */
|
|
506
|
+
const diagnostics = [];
|
|
507
|
+
|
|
508
|
+
collectFieldConfigDiagnostics(diagnostics, path_classes, fields);
|
|
509
|
+
collectClassSchemaConfigDiagnostics(
|
|
510
|
+
diagnostics,
|
|
511
|
+
path_classes,
|
|
512
|
+
classes,
|
|
513
|
+
fields,
|
|
514
|
+
repo_config.class_schemas,
|
|
515
|
+
);
|
|
516
|
+
|
|
517
|
+
return diagnostics;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* @param {PatramRepoConfig} repo_config
|
|
522
|
+
* @returns {PatramDiagnostic[]}
|
|
523
|
+
*/
|
|
524
|
+
function validateStoredQueries(repo_config) {
|
|
525
|
+
/** @type {PatramDiagnostic[]} */
|
|
526
|
+
const diagnostics = [];
|
|
527
|
+
|
|
528
|
+
for (const [query_name, stored_query] of Object.entries(
|
|
529
|
+
repo_config.queries,
|
|
530
|
+
)) {
|
|
531
|
+
collectWhereClauseDiagnostics(
|
|
532
|
+
diagnostics,
|
|
533
|
+
repo_config,
|
|
534
|
+
stored_query.where,
|
|
535
|
+
`queries.${query_name}.where`,
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
return diagnostics;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* @param {PatramRepoConfig} repo_config
|
|
544
|
+
* @returns {PatramDiagnostic[]}
|
|
545
|
+
*/
|
|
546
|
+
function validateDerivedSummaries(repo_config) {
|
|
547
|
+
if (!repo_config.derived_summaries) {
|
|
548
|
+
return [];
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
const graph_config = resolvePatramGraphConfig(repo_config);
|
|
552
|
+
const known_relation_names = new Set(Object.keys(graph_config.relations));
|
|
553
|
+
/** @type {PatramDiagnostic[]} */
|
|
554
|
+
const diagnostics = [];
|
|
555
|
+
const class_coverage = new Map();
|
|
556
|
+
|
|
557
|
+
for (const [summary_name, summary_definition] of Object.entries(
|
|
558
|
+
repo_config.derived_summaries,
|
|
559
|
+
)) {
|
|
560
|
+
collectDuplicateClassDiagnostics(
|
|
561
|
+
diagnostics,
|
|
562
|
+
class_coverage,
|
|
563
|
+
summary_definition.classes,
|
|
564
|
+
summary_name,
|
|
565
|
+
);
|
|
566
|
+
collectDerivedSummaryFieldDiagnostics(
|
|
567
|
+
diagnostics,
|
|
568
|
+
known_relation_names,
|
|
569
|
+
repo_config,
|
|
570
|
+
summary_name,
|
|
571
|
+
summary_definition.fields,
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
return diagnostics;
|
|
576
|
+
}
|
|
577
|
+
|
|
217
578
|
/**
|
|
218
579
|
* @returns {PatramRepoConfig}
|
|
219
580
|
*/
|
|
@@ -225,7 +586,7 @@ function createDefaultRepoConfig() {
|
|
|
225
586
|
}
|
|
226
587
|
|
|
227
588
|
/**
|
|
228
|
-
* @param {{ include: string[], queries: Record<string, { where: string }>,
|
|
589
|
+
* @param {{ class_schemas?: unknown, classes?: unknown, derived_summaries?: unknown, fields?: unknown, include: string[], mappings?: unknown, path_classes?: unknown, queries: Record<string, { where: string }>, relations?: unknown }} repo_config
|
|
229
590
|
* @returns {PatramRepoConfig}
|
|
230
591
|
*/
|
|
231
592
|
function normalizeRepoConfig(repo_config) {
|
|
@@ -235,25 +596,400 @@ function normalizeRepoConfig(repo_config) {
|
|
|
235
596
|
queries: { ...repo_config.queries },
|
|
236
597
|
};
|
|
237
598
|
|
|
238
|
-
|
|
239
|
-
normalized_config
|
|
240
|
-
|
|
599
|
+
assignOptionalRepoConfigField(
|
|
600
|
+
normalized_config,
|
|
601
|
+
'class_schemas',
|
|
602
|
+
repo_config.class_schemas,
|
|
603
|
+
);
|
|
604
|
+
assignOptionalRepoConfigField(
|
|
605
|
+
normalized_config,
|
|
606
|
+
'classes',
|
|
607
|
+
repo_config.classes,
|
|
608
|
+
);
|
|
609
|
+
assignOptionalRepoConfigField(
|
|
610
|
+
normalized_config,
|
|
611
|
+
'derived_summaries',
|
|
612
|
+
repo_config.derived_summaries,
|
|
613
|
+
);
|
|
614
|
+
assignOptionalRepoConfigField(
|
|
615
|
+
normalized_config,
|
|
616
|
+
'fields',
|
|
617
|
+
repo_config.fields,
|
|
618
|
+
);
|
|
619
|
+
assignOptionalRepoConfigField(
|
|
620
|
+
normalized_config,
|
|
621
|
+
'mappings',
|
|
622
|
+
repo_config.mappings,
|
|
623
|
+
);
|
|
624
|
+
assignOptionalRepoConfigField(
|
|
625
|
+
normalized_config,
|
|
626
|
+
'path_classes',
|
|
627
|
+
repo_config.path_classes,
|
|
628
|
+
);
|
|
629
|
+
assignOptionalRepoConfigField(
|
|
630
|
+
normalized_config,
|
|
631
|
+
'relations',
|
|
632
|
+
repo_config.relations,
|
|
633
|
+
);
|
|
634
|
+
|
|
635
|
+
return normalized_config;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* @param {PatramDiagnostic[]} diagnostics
|
|
640
|
+
* @param {Map<string, string>} class_coverage
|
|
641
|
+
* @param {string[]} class_names
|
|
642
|
+
* @param {string} summary_name
|
|
643
|
+
*/
|
|
644
|
+
function collectDuplicateClassDiagnostics(
|
|
645
|
+
diagnostics,
|
|
646
|
+
class_coverage,
|
|
647
|
+
class_names,
|
|
648
|
+
summary_name,
|
|
649
|
+
) {
|
|
650
|
+
for (const class_name of class_names) {
|
|
651
|
+
const existing_summary_name = class_coverage.get(class_name);
|
|
652
|
+
|
|
653
|
+
if (!existing_summary_name) {
|
|
654
|
+
class_coverage.set(class_name, summary_name);
|
|
655
|
+
continue;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
diagnostics.push(
|
|
659
|
+
createConfigDiagnostic(
|
|
660
|
+
`derived_summaries.${summary_name}.classes`,
|
|
661
|
+
`Class "${class_name}" is already covered by derived summary "${existing_summary_name}".`,
|
|
662
|
+
),
|
|
241
663
|
);
|
|
242
664
|
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* @param {PatramDiagnostic[]} diagnostics
|
|
669
|
+
* @param {Set<string>} known_relation_names
|
|
670
|
+
* @param {PatramRepoConfig} repo_config
|
|
671
|
+
* @param {string} summary_name
|
|
672
|
+
* @param {import('./load-patram-config.types.ts').DerivedSummaryFieldConfig[]} field_definitions
|
|
673
|
+
*/
|
|
674
|
+
function collectDerivedSummaryFieldDiagnostics(
|
|
675
|
+
diagnostics,
|
|
676
|
+
known_relation_names,
|
|
677
|
+
repo_config,
|
|
678
|
+
summary_name,
|
|
679
|
+
field_definitions,
|
|
680
|
+
) {
|
|
681
|
+
for (const [field_index, field_definition] of field_definitions.entries()) {
|
|
682
|
+
if ('count' in field_definition) {
|
|
683
|
+
collectTraversalDiagnostic(
|
|
684
|
+
diagnostics,
|
|
685
|
+
field_definition.count.traversal,
|
|
686
|
+
known_relation_names,
|
|
687
|
+
`derived_summaries.${summary_name}.fields.${field_index}.count.traversal`,
|
|
688
|
+
);
|
|
689
|
+
collectWhereClauseDiagnostics(
|
|
690
|
+
diagnostics,
|
|
691
|
+
repo_config,
|
|
692
|
+
field_definition.count.where,
|
|
693
|
+
`derived_summaries.${summary_name}.fields.${field_index}.count.where`,
|
|
694
|
+
);
|
|
695
|
+
continue;
|
|
696
|
+
}
|
|
243
697
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
698
|
+
for (const [case_index, select_case] of field_definition.select.entries()) {
|
|
699
|
+
collectWhereClauseDiagnostics(
|
|
700
|
+
diagnostics,
|
|
701
|
+
repo_config,
|
|
702
|
+
select_case.when,
|
|
703
|
+
`derived_summaries.${summary_name}.fields.${field_index}.select.${case_index}.when`,
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/**
|
|
710
|
+
* @param {PatramDiagnostic[]} diagnostics
|
|
711
|
+
* @param {string} traversal_text
|
|
712
|
+
* @param {Set<string>} known_relation_names
|
|
713
|
+
* @param {string} diagnostic_path
|
|
714
|
+
*/
|
|
715
|
+
function collectTraversalDiagnostic(
|
|
716
|
+
diagnostics,
|
|
717
|
+
traversal_text,
|
|
718
|
+
known_relation_names,
|
|
719
|
+
diagnostic_path,
|
|
720
|
+
) {
|
|
721
|
+
const traversal_match =
|
|
722
|
+
/^(?<direction>in|out):(?<relation_name>[a-zA-Z0-9_]+)$/du.exec(
|
|
723
|
+
traversal_text,
|
|
247
724
|
);
|
|
725
|
+
|
|
726
|
+
if (!traversal_match?.groups?.relation_name) {
|
|
727
|
+
diagnostics.push(
|
|
728
|
+
createConfigDiagnostic(
|
|
729
|
+
diagnostic_path,
|
|
730
|
+
'Derived summary traversal must use "in:<relation>" or "out:<relation>".',
|
|
731
|
+
),
|
|
732
|
+
);
|
|
733
|
+
|
|
734
|
+
return;
|
|
248
735
|
}
|
|
249
736
|
|
|
250
|
-
if (
|
|
251
|
-
|
|
252
|
-
|
|
737
|
+
if (known_relation_names.has(traversal_match.groups.relation_name)) {
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
diagnostics.push(
|
|
742
|
+
createConfigDiagnostic(
|
|
743
|
+
diagnostic_path,
|
|
744
|
+
`Unknown relation "${traversal_match.groups.relation_name}" in derived summary traversal.`,
|
|
745
|
+
),
|
|
746
|
+
);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* @param {PatramDiagnostic[]} diagnostics
|
|
751
|
+
* @param {Record<string, { prefixes: string[] }>} path_classes
|
|
752
|
+
* @param {Record<string, MetadataFieldConfig>} fields
|
|
753
|
+
*/
|
|
754
|
+
function collectFieldConfigDiagnostics(diagnostics, path_classes, fields) {
|
|
755
|
+
for (const [field_name, field_definition] of Object.entries(fields)) {
|
|
756
|
+
if (collectReservedFieldDiagnostic(diagnostics, field_name)) {
|
|
757
|
+
continue;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
collectDisplayOrderDiagnostic(diagnostics, field_name, field_definition);
|
|
761
|
+
collectFieldPathClassDiagnostic(
|
|
762
|
+
diagnostics,
|
|
763
|
+
path_classes,
|
|
764
|
+
field_name,
|
|
765
|
+
field_definition,
|
|
253
766
|
);
|
|
254
767
|
}
|
|
768
|
+
}
|
|
255
769
|
|
|
256
|
-
|
|
770
|
+
/**
|
|
771
|
+
* @param {PatramDiagnostic[]} diagnostics
|
|
772
|
+
* @param {string} field_name
|
|
773
|
+
* @returns {boolean}
|
|
774
|
+
*/
|
|
775
|
+
function collectReservedFieldDiagnostic(diagnostics, field_name) {
|
|
776
|
+
if (
|
|
777
|
+
!field_name.startsWith('$') ||
|
|
778
|
+
!RESERVED_STRUCTURAL_FIELD_NAMES.has(field_name)
|
|
779
|
+
) {
|
|
780
|
+
return false;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
diagnostics.push(
|
|
784
|
+
createConfigDiagnostic(
|
|
785
|
+
`fields.${field_name}`,
|
|
786
|
+
'Metadata field names must not start with "$".',
|
|
787
|
+
),
|
|
788
|
+
);
|
|
789
|
+
|
|
790
|
+
return true;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
/**
|
|
794
|
+
* @param {PatramDiagnostic[]} diagnostics
|
|
795
|
+
* @param {string} field_name
|
|
796
|
+
* @param {MetadataFieldConfig} field_definition
|
|
797
|
+
*/
|
|
798
|
+
function collectDisplayOrderDiagnostic(
|
|
799
|
+
diagnostics,
|
|
800
|
+
field_name,
|
|
801
|
+
field_definition,
|
|
802
|
+
) {
|
|
803
|
+
if (
|
|
804
|
+
field_definition.display?.order === undefined ||
|
|
805
|
+
(Number.isInteger(field_definition.display.order) &&
|
|
806
|
+
field_definition.display.order >= 0)
|
|
807
|
+
) {
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
diagnostics.push(
|
|
812
|
+
createConfigDiagnostic(
|
|
813
|
+
`fields.${field_name}.display.order`,
|
|
814
|
+
'Display order must be a non-negative integer.',
|
|
815
|
+
),
|
|
816
|
+
);
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
/**
|
|
820
|
+
* @param {PatramDiagnostic[]} diagnostics
|
|
821
|
+
* @param {Record<string, { prefixes: string[] }>} path_classes
|
|
822
|
+
* @param {string} field_name
|
|
823
|
+
* @param {MetadataFieldConfig} field_definition
|
|
824
|
+
*/
|
|
825
|
+
function collectFieldPathClassDiagnostic(
|
|
826
|
+
diagnostics,
|
|
827
|
+
path_classes,
|
|
828
|
+
field_name,
|
|
829
|
+
field_definition,
|
|
830
|
+
) {
|
|
831
|
+
if (
|
|
832
|
+
!('path_class' in field_definition) ||
|
|
833
|
+
field_definition.path_class === undefined
|
|
834
|
+
) {
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
if (field_definition.type !== 'path') {
|
|
839
|
+
diagnostics.push(
|
|
840
|
+
createConfigDiagnostic(
|
|
841
|
+
`fields.${field_name}.path_class`,
|
|
842
|
+
'Path classes are only valid for path fields.',
|
|
843
|
+
),
|
|
844
|
+
);
|
|
845
|
+
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
if (path_classes[field_definition.path_class]) {
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
diagnostics.push(
|
|
854
|
+
createConfigDiagnostic(
|
|
855
|
+
`fields.${field_name}.path_class`,
|
|
856
|
+
`Unknown path class "${field_definition.path_class}".`,
|
|
857
|
+
),
|
|
858
|
+
);
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
/**
|
|
862
|
+
* @param {PatramDiagnostic[]} diagnostics
|
|
863
|
+
* @param {Record<string, { prefixes: string[] }>} path_classes
|
|
864
|
+
* @param {Record<string, unknown>} classes
|
|
865
|
+
* @param {Record<string, MetadataFieldConfig>} fields
|
|
866
|
+
* @param {PatramRepoConfig['class_schemas']} class_schemas
|
|
867
|
+
*/
|
|
868
|
+
function collectClassSchemaConfigDiagnostics(
|
|
869
|
+
diagnostics,
|
|
870
|
+
path_classes,
|
|
871
|
+
classes,
|
|
872
|
+
fields,
|
|
873
|
+
class_schemas,
|
|
874
|
+
) {
|
|
875
|
+
if (!class_schemas) {
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
for (const class_name of Object.keys(class_schemas)) {
|
|
880
|
+
if (classes[class_name]) {
|
|
881
|
+
continue;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
diagnostics.push(
|
|
885
|
+
createConfigDiagnostic(
|
|
886
|
+
`class_schemas.${class_name}`,
|
|
887
|
+
`Unknown class "${class_name}".`,
|
|
888
|
+
),
|
|
889
|
+
);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
for (const [class_name, schema_definition] of Object.entries(class_schemas)) {
|
|
893
|
+
for (const field_name of Object.keys(schema_definition.fields)) {
|
|
894
|
+
if (fields[field_name]) {
|
|
895
|
+
continue;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
diagnostics.push(
|
|
899
|
+
createConfigDiagnostic(
|
|
900
|
+
`class_schemas.${class_name}.fields.${field_name}`,
|
|
901
|
+
`Unknown field "${field_name}".`,
|
|
902
|
+
),
|
|
903
|
+
);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
for (const [class_name, schema_definition] of Object.entries(class_schemas)) {
|
|
908
|
+
if (
|
|
909
|
+
schema_definition.document_path_class === undefined ||
|
|
910
|
+
path_classes[schema_definition.document_path_class]
|
|
911
|
+
) {
|
|
912
|
+
continue;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
diagnostics.push(
|
|
916
|
+
createConfigDiagnostic(
|
|
917
|
+
`class_schemas.${class_name}.document_path_class`,
|
|
918
|
+
`Unknown path class "${schema_definition.document_path_class}".`,
|
|
919
|
+
),
|
|
920
|
+
);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
/**
|
|
925
|
+
* @template {Exclude<keyof PatramRepoConfig, 'include' | 'queries'>} TKey
|
|
926
|
+
* @param {PatramRepoConfig} normalized_config
|
|
927
|
+
* @param {TKey} field_name
|
|
928
|
+
* @param {unknown} field_value
|
|
929
|
+
*/
|
|
930
|
+
function assignOptionalRepoConfigField(
|
|
931
|
+
normalized_config,
|
|
932
|
+
field_name,
|
|
933
|
+
field_value,
|
|
934
|
+
) {
|
|
935
|
+
if (field_value === undefined || field_value === null) {
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
normalized_config[field_name] = /** @type {PatramRepoConfig[TKey]} */ (
|
|
940
|
+
field_value
|
|
941
|
+
);
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
/**
|
|
945
|
+
* @param {PatramDiagnostic[]} diagnostics
|
|
946
|
+
* @param {PatramRepoConfig} repo_config
|
|
947
|
+
* @param {string} where_clause
|
|
948
|
+
* @param {string} diagnostic_path
|
|
949
|
+
*/
|
|
950
|
+
function collectWhereClauseDiagnostics(
|
|
951
|
+
diagnostics,
|
|
952
|
+
repo_config,
|
|
953
|
+
where_clause,
|
|
954
|
+
diagnostic_path,
|
|
955
|
+
) {
|
|
956
|
+
const parse_result = parseWhereClause(where_clause);
|
|
957
|
+
|
|
958
|
+
if (!parse_result.success) {
|
|
959
|
+
diagnostics.push(
|
|
960
|
+
createConfigDiagnostic(diagnostic_path, parse_result.diagnostic.message),
|
|
961
|
+
);
|
|
962
|
+
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
const semantic_diagnostics = getQuerySemanticDiagnostics(
|
|
967
|
+
repo_config,
|
|
968
|
+
{ kind: 'ad_hoc' },
|
|
969
|
+
parse_result.clauses,
|
|
970
|
+
);
|
|
971
|
+
|
|
972
|
+
for (const semantic_diagnostic of semantic_diagnostics) {
|
|
973
|
+
diagnostics.push(
|
|
974
|
+
createConfigDiagnostic(diagnostic_path, semantic_diagnostic.message),
|
|
975
|
+
);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
/**
|
|
980
|
+
* @param {string} issue_path
|
|
981
|
+
* @param {string} message
|
|
982
|
+
* @returns {PatramDiagnostic}
|
|
983
|
+
*/
|
|
984
|
+
function createConfigDiagnostic(issue_path, message) {
|
|
985
|
+
return {
|
|
986
|
+
code: 'config.invalid',
|
|
987
|
+
column: 1,
|
|
988
|
+
level: 'error',
|
|
989
|
+
line: 1,
|
|
990
|
+
message: `Invalid config at "${issue_path}": ${message}`,
|
|
991
|
+
path: CONFIG_FILE_NAME,
|
|
992
|
+
};
|
|
257
993
|
}
|
|
258
994
|
|
|
259
995
|
/**
|