patram 0.0.2 → 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/bin/patram.js +25 -147
- package/lib/build-graph-identity.js +270 -0
- package/lib/build-graph.js +156 -77
- package/lib/check-graph.js +23 -7
- package/lib/claim-helpers.js +55 -0
- package/lib/cli-help-metadata.js +552 -0
- package/lib/command-output.js +83 -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 +361 -0
- package/lib/list-queries.js +18 -0
- package/lib/list-source-files.js +50 -15
- package/lib/load-patram-config.js +505 -18
- package/lib/load-patram-config.types.ts +40 -0
- package/lib/load-project-graph.js +124 -0
- package/lib/output-view.types.ts +88 -0
- package/lib/parse-claims.js +38 -158
- package/lib/parse-claims.types.ts +7 -0
- package/lib/parse-cli-arguments-helpers.js +446 -0
- package/lib/parse-cli-arguments.js +266 -0
- package/lib/parse-cli-arguments.types.ts +69 -0
- package/lib/parse-cli-color-options.js +44 -0
- package/lib/parse-cli-query-pagination.js +49 -0
- package/lib/parse-jsdoc-blocks.js +184 -0
- package/lib/parse-jsdoc-claims.js +280 -0
- package/lib/parse-jsdoc-prose.js +111 -0
- package/lib/parse-markdown-claims.js +242 -0
- package/lib/parse-markdown-directives.js +136 -0
- package/lib/parse-where-clause.js +707 -0
- package/lib/parse-where-clause.types.ts +70 -0
- package/lib/patram-cli.js +464 -0
- package/lib/patram-config.js +3 -1
- package/lib/patram-config.types.ts +2 -1
- package/lib/patram.js +6 -0
- package/lib/query-graph.js +368 -0
- package/lib/query-inspection.js +523 -0
- package/lib/render-check-output.js +315 -0
- package/lib/render-cli-help.js +419 -0
- package/lib/render-json-output.js +161 -0
- package/lib/render-output-view.js +222 -0
- package/lib/render-plain-output.js +182 -0
- package/lib/render-rich-output.js +240 -0
- package/lib/render-rich-source.js +1333 -0
- package/lib/resolve-check-target.js +190 -0
- package/lib/resolve-output-mode.js +60 -0
- package/lib/resolve-patram-graph-config.js +88 -0
- package/lib/resolve-where-clause.js +66 -0
- package/lib/show-document.js +311 -0
- package/lib/source-file-defaults.js +28 -0
- 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/lib/write-paged-output.js +87 -0
- package/package.json +28 -12
- package/bin/patram.test.js +0 -184
- package/lib/build-graph.test.js +0 -141
- package/lib/check-graph.test.js +0 -103
- package/lib/list-source-files.test.js +0 -101
- package/lib/load-patram-config.test.js +0 -211
- package/lib/parse-claims.test.js +0 -113
- package/lib/patram-config.test.js +0 -147
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/* eslint-disable max-lines */
|
|
1
2
|
/**
|
|
2
3
|
* @import { LoadPatramConfigResult, PatramDiagnostic, PatramRepoConfig } from './load-patram-config.types.ts';
|
|
3
4
|
*/
|
|
@@ -8,6 +9,27 @@ import process from 'node:process';
|
|
|
8
9
|
|
|
9
10
|
import { z } from 'zod';
|
|
10
11
|
|
|
12
|
+
import { parsePatramConfig } from './patram-config.js';
|
|
13
|
+
import { parseWhereClause } from './parse-where-clause.js';
|
|
14
|
+
import { resolvePatramGraphConfig } from './resolve-patram-graph-config.js';
|
|
15
|
+
import { DEFAULT_INCLUDE_PATTERNS } from './source-file-defaults.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Repo config loading.
|
|
19
|
+
*
|
|
20
|
+
* Reads `.patram.json`, applies defaults, and validates repo config and graph
|
|
21
|
+
* schema before command execution.
|
|
22
|
+
*
|
|
23
|
+
* Kind: config
|
|
24
|
+
* Status: active
|
|
25
|
+
* Tracked in: ../docs/plans/v0/source-anchor-dogfooding.md
|
|
26
|
+
* Decided by: ../docs/decisions/single-config-file.md
|
|
27
|
+
* Decided by: ../docs/decisions/optional-config-default-scan.md
|
|
28
|
+
* @patram
|
|
29
|
+
* @see {@link ./resolve-patram-graph-config.js}
|
|
30
|
+
* @see {@link ../docs/decisions/single-config-file.md}
|
|
31
|
+
*/
|
|
32
|
+
|
|
11
33
|
const CONFIG_FILE_NAME = '.patram.json';
|
|
12
34
|
|
|
13
35
|
const stored_query_schema = z
|
|
@@ -16,12 +38,71 @@ const stored_query_schema = z
|
|
|
16
38
|
})
|
|
17
39
|
.strict();
|
|
18
40
|
|
|
41
|
+
const derived_summary_scalar_schema = z.union([
|
|
42
|
+
z.boolean(),
|
|
43
|
+
z.number(),
|
|
44
|
+
z.string(),
|
|
45
|
+
z.null(),
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
const derived_summary_count_schema = z
|
|
49
|
+
.object({
|
|
50
|
+
traversal: z
|
|
51
|
+
.string()
|
|
52
|
+
.min(1, 'Derived summary count "traversal" must not be empty.'),
|
|
53
|
+
where: z
|
|
54
|
+
.string()
|
|
55
|
+
.min(1, 'Derived summary count "where" must not be empty.'),
|
|
56
|
+
})
|
|
57
|
+
.strict();
|
|
58
|
+
|
|
59
|
+
const derived_summary_select_case_schema = z
|
|
60
|
+
.object({
|
|
61
|
+
value: derived_summary_scalar_schema,
|
|
62
|
+
when: z.string().min(1, 'Derived summary select "when" must not be empty.'),
|
|
63
|
+
})
|
|
64
|
+
.strict();
|
|
65
|
+
|
|
66
|
+
const derived_summary_field_schema = z
|
|
67
|
+
.object({
|
|
68
|
+
count: derived_summary_count_schema.optional(),
|
|
69
|
+
default: derived_summary_scalar_schema.optional(),
|
|
70
|
+
name: z
|
|
71
|
+
.string()
|
|
72
|
+
.regex(
|
|
73
|
+
/^[a-z][a-z0-9_]*$/du,
|
|
74
|
+
'Derived summary field names must use lower_snake_case.',
|
|
75
|
+
),
|
|
76
|
+
select: z.array(derived_summary_select_case_schema).optional(),
|
|
77
|
+
})
|
|
78
|
+
.strict()
|
|
79
|
+
.superRefine(validateDerivedSummaryFieldDefinition);
|
|
80
|
+
|
|
81
|
+
const derived_summary_schema = z
|
|
82
|
+
.object({
|
|
83
|
+
fields: z
|
|
84
|
+
.array(derived_summary_field_schema)
|
|
85
|
+
.min(1, 'Derived summary "fields" must contain at least one field.'),
|
|
86
|
+
kinds: z
|
|
87
|
+
.array(z.string().min(1))
|
|
88
|
+
.min(1, 'Derived summary "kinds" must contain at least one kind.'),
|
|
89
|
+
})
|
|
90
|
+
.strict()
|
|
91
|
+
.superRefine(validateDerivedSummaryDefinition);
|
|
92
|
+
|
|
19
93
|
const patram_repo_config_schema = z
|
|
20
94
|
.object({
|
|
95
|
+
derived_summaries: z
|
|
96
|
+
.record(z.string().min(1), derived_summary_schema)
|
|
97
|
+
.optional(),
|
|
21
98
|
include: z
|
|
22
99
|
.array(z.string().min(1, 'Include globs must not be empty.'))
|
|
23
|
-
.min(1, 'Include must contain at least one glob.')
|
|
24
|
-
|
|
100
|
+
.min(1, 'Include must contain at least one glob.')
|
|
101
|
+
.default(DEFAULT_INCLUDE_PATTERNS),
|
|
102
|
+
kinds: z.unknown().optional(),
|
|
103
|
+
mappings: z.unknown().optional(),
|
|
104
|
+
queries: z.record(z.string().min(1), stored_query_schema).default({}),
|
|
105
|
+
relations: z.unknown().optional(),
|
|
25
106
|
})
|
|
26
107
|
.strict();
|
|
27
108
|
|
|
@@ -36,7 +117,7 @@ export async function loadPatramConfig(project_directory = process.cwd()) {
|
|
|
36
117
|
const config_source = await readConfigSource(config_file_path);
|
|
37
118
|
|
|
38
119
|
if (config_source === null) {
|
|
39
|
-
return createLoadResult(
|
|
120
|
+
return createLoadResult(createDefaultRepoConfig(), []);
|
|
40
121
|
}
|
|
41
122
|
|
|
42
123
|
const parse_result = parseConfigJson(config_source);
|
|
@@ -54,7 +135,21 @@ export async function loadPatramConfig(project_directory = process.cwd()) {
|
|
|
54
135
|
);
|
|
55
136
|
}
|
|
56
137
|
|
|
57
|
-
|
|
138
|
+
const graph_schema_diagnostics = validateGraphSchema(config_result.data);
|
|
139
|
+
|
|
140
|
+
if (graph_schema_diagnostics.length > 0) {
|
|
141
|
+
return createLoadResult(null, graph_schema_diagnostics);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const normalized_config = normalizeRepoConfig(config_result.data);
|
|
145
|
+
const derived_summary_diagnostics =
|
|
146
|
+
validateDerivedSummaries(normalized_config);
|
|
147
|
+
|
|
148
|
+
if (derived_summary_diagnostics.length > 0) {
|
|
149
|
+
return createLoadResult(null, derived_summary_diagnostics);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return createLoadResult(normalized_config, []);
|
|
58
153
|
}
|
|
59
154
|
|
|
60
155
|
/**
|
|
@@ -108,20 +203,6 @@ function createLoadResult(config, diagnostics) {
|
|
|
108
203
|
};
|
|
109
204
|
}
|
|
110
205
|
|
|
111
|
-
/**
|
|
112
|
-
* @returns {PatramDiagnostic}
|
|
113
|
-
*/
|
|
114
|
-
function createMissingConfigDiagnostic() {
|
|
115
|
-
return {
|
|
116
|
-
code: 'config.not_found',
|
|
117
|
-
column: 1,
|
|
118
|
-
level: 'error',
|
|
119
|
-
line: 1,
|
|
120
|
-
message: 'Config file ".patram.json" was not found.',
|
|
121
|
-
path: CONFIG_FILE_NAME,
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
|
|
125
206
|
/**
|
|
126
207
|
* @param {string} config_source
|
|
127
208
|
* @param {SyntaxError} error
|
|
@@ -168,6 +249,412 @@ function createValidationDiagnostic(issue) {
|
|
|
168
249
|
};
|
|
169
250
|
}
|
|
170
251
|
|
|
252
|
+
/**
|
|
253
|
+
* @param {{ count?: unknown, default?: unknown, select?: unknown }} field_definition
|
|
254
|
+
* @param {import('zod').RefinementCtx} refinement_context
|
|
255
|
+
*/
|
|
256
|
+
function validateDerivedSummaryFieldDefinition(
|
|
257
|
+
field_definition,
|
|
258
|
+
refinement_context,
|
|
259
|
+
) {
|
|
260
|
+
const evaluator_count =
|
|
261
|
+
Number(field_definition.count !== undefined) +
|
|
262
|
+
Number(field_definition.select !== undefined);
|
|
263
|
+
|
|
264
|
+
if (evaluator_count !== 1) {
|
|
265
|
+
refinement_context.addIssue({
|
|
266
|
+
code: 'custom',
|
|
267
|
+
message:
|
|
268
|
+
'Derived summary fields must define exactly one of "count" or "select".',
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (
|
|
273
|
+
field_definition.count !== undefined &&
|
|
274
|
+
field_definition.default !== undefined
|
|
275
|
+
) {
|
|
276
|
+
refinement_context.addIssue({
|
|
277
|
+
code: 'custom',
|
|
278
|
+
message: 'Derived summary count fields must not define "default".',
|
|
279
|
+
path: ['default'],
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (field_definition.select === undefined) {
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (
|
|
288
|
+
Array.isArray(field_definition.select) &&
|
|
289
|
+
field_definition.select.length === 0
|
|
290
|
+
) {
|
|
291
|
+
refinement_context.addIssue({
|
|
292
|
+
code: 'custom',
|
|
293
|
+
message: 'Derived summary "select" must contain at least one case.',
|
|
294
|
+
path: ['select'],
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (field_definition.default === undefined) {
|
|
299
|
+
refinement_context.addIssue({
|
|
300
|
+
code: 'custom',
|
|
301
|
+
message: 'Derived summary select fields must define "default".',
|
|
302
|
+
path: ['default'],
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* @param {{ fields: Array<{ name: string }> }} summary_definition
|
|
309
|
+
* @param {import('zod').RefinementCtx} refinement_context
|
|
310
|
+
*/
|
|
311
|
+
function validateDerivedSummaryDefinition(
|
|
312
|
+
summary_definition,
|
|
313
|
+
refinement_context,
|
|
314
|
+
) {
|
|
315
|
+
const seen_field_names = new Set();
|
|
316
|
+
|
|
317
|
+
for (const [
|
|
318
|
+
field_index,
|
|
319
|
+
field_definition,
|
|
320
|
+
] of summary_definition.fields.entries()) {
|
|
321
|
+
if (!seen_field_names.has(field_definition.name)) {
|
|
322
|
+
seen_field_names.add(field_definition.name);
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
refinement_context.addIssue({
|
|
327
|
+
code: 'custom',
|
|
328
|
+
message: `Duplicate derived summary field "${field_definition.name}".`,
|
|
329
|
+
path: ['fields', field_index, 'name'],
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* @param {{ include: string[], queries: Record<string, { where: string }>, kinds?: unknown, mappings?: unknown, relations?: unknown }} repo_config
|
|
336
|
+
* @returns {PatramDiagnostic[]}
|
|
337
|
+
*/
|
|
338
|
+
function validateGraphSchema(repo_config) {
|
|
339
|
+
if (
|
|
340
|
+
repo_config.kinds === undefined &&
|
|
341
|
+
repo_config.mappings === undefined &&
|
|
342
|
+
repo_config.relations === undefined
|
|
343
|
+
) {
|
|
344
|
+
return [];
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
try {
|
|
348
|
+
parsePatramConfig({
|
|
349
|
+
kinds: repo_config.kinds ?? {},
|
|
350
|
+
mappings: repo_config.mappings ?? {},
|
|
351
|
+
relations: repo_config.relations ?? {},
|
|
352
|
+
});
|
|
353
|
+
} catch (error) {
|
|
354
|
+
if (error instanceof z.ZodError) {
|
|
355
|
+
return error.issues.map(createValidationDiagnostic);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
throw error;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return [];
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* @param {PatramRepoConfig} repo_config
|
|
366
|
+
* @returns {PatramDiagnostic[]}
|
|
367
|
+
*/
|
|
368
|
+
function validateDerivedSummaries(repo_config) {
|
|
369
|
+
if (!repo_config.derived_summaries) {
|
|
370
|
+
return [];
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const graph_config = resolvePatramGraphConfig(repo_config);
|
|
374
|
+
const known_relation_names = new Set(Object.keys(graph_config.relations));
|
|
375
|
+
/** @type {PatramDiagnostic[]} */
|
|
376
|
+
const diagnostics = [];
|
|
377
|
+
const kind_coverage = new Map();
|
|
378
|
+
|
|
379
|
+
for (const [summary_name, summary_definition] of Object.entries(
|
|
380
|
+
repo_config.derived_summaries,
|
|
381
|
+
)) {
|
|
382
|
+
collectDuplicateKindDiagnostics(
|
|
383
|
+
diagnostics,
|
|
384
|
+
kind_coverage,
|
|
385
|
+
summary_definition.kinds,
|
|
386
|
+
summary_name,
|
|
387
|
+
);
|
|
388
|
+
collectDerivedSummaryFieldDiagnostics(
|
|
389
|
+
diagnostics,
|
|
390
|
+
known_relation_names,
|
|
391
|
+
summary_name,
|
|
392
|
+
summary_definition.fields,
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return diagnostics;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* @returns {PatramRepoConfig}
|
|
401
|
+
*/
|
|
402
|
+
function createDefaultRepoConfig() {
|
|
403
|
+
return {
|
|
404
|
+
include: [...DEFAULT_INCLUDE_PATTERNS],
|
|
405
|
+
queries: {},
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* @param {{ derived_summaries?: unknown, include: string[], queries: Record<string, { where: string }>, kinds?: unknown, mappings?: unknown, relations?: unknown }} repo_config
|
|
411
|
+
* @returns {PatramRepoConfig}
|
|
412
|
+
*/
|
|
413
|
+
function normalizeRepoConfig(repo_config) {
|
|
414
|
+
/** @type {PatramRepoConfig} */
|
|
415
|
+
const normalized_config = {
|
|
416
|
+
include: [...repo_config.include],
|
|
417
|
+
queries: { ...repo_config.queries },
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
if (
|
|
421
|
+
repo_config.derived_summaries !== undefined &&
|
|
422
|
+
repo_config.derived_summaries !== null
|
|
423
|
+
) {
|
|
424
|
+
normalized_config.derived_summaries =
|
|
425
|
+
/** @type {PatramRepoConfig['derived_summaries']} */ (
|
|
426
|
+
repo_config.derived_summaries
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (repo_config.kinds !== undefined && repo_config.kinds !== null) {
|
|
431
|
+
normalized_config.kinds = /** @type {PatramRepoConfig['kinds']} */ (
|
|
432
|
+
repo_config.kinds
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (repo_config.mappings !== undefined && repo_config.mappings !== null) {
|
|
437
|
+
normalized_config.mappings = /** @type {PatramRepoConfig['mappings']} */ (
|
|
438
|
+
repo_config.mappings
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (repo_config.relations !== undefined && repo_config.relations !== null) {
|
|
443
|
+
normalized_config.relations = /** @type {PatramRepoConfig['relations']} */ (
|
|
444
|
+
repo_config.relations
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return normalized_config;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* @param {PatramDiagnostic[]} diagnostics
|
|
453
|
+
* @param {Map<string, string>} kind_coverage
|
|
454
|
+
* @param {string[]} kind_names
|
|
455
|
+
* @param {string} summary_name
|
|
456
|
+
*/
|
|
457
|
+
function collectDuplicateKindDiagnostics(
|
|
458
|
+
diagnostics,
|
|
459
|
+
kind_coverage,
|
|
460
|
+
kind_names,
|
|
461
|
+
summary_name,
|
|
462
|
+
) {
|
|
463
|
+
for (const kind_name of kind_names) {
|
|
464
|
+
const existing_summary_name = kind_coverage.get(kind_name);
|
|
465
|
+
|
|
466
|
+
if (!existing_summary_name) {
|
|
467
|
+
kind_coverage.set(kind_name, summary_name);
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
diagnostics.push(
|
|
472
|
+
createConfigDiagnostic(
|
|
473
|
+
`derived_summaries.${summary_name}.kinds`,
|
|
474
|
+
`Kind "${kind_name}" is already covered by derived summary "${existing_summary_name}".`,
|
|
475
|
+
),
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* @param {PatramDiagnostic[]} diagnostics
|
|
482
|
+
* @param {Set<string>} known_relation_names
|
|
483
|
+
* @param {string} summary_name
|
|
484
|
+
* @param {import('./load-patram-config.types.ts').DerivedSummaryFieldConfig[]} field_definitions
|
|
485
|
+
*/
|
|
486
|
+
function collectDerivedSummaryFieldDiagnostics(
|
|
487
|
+
diagnostics,
|
|
488
|
+
known_relation_names,
|
|
489
|
+
summary_name,
|
|
490
|
+
field_definitions,
|
|
491
|
+
) {
|
|
492
|
+
for (const [field_index, field_definition] of field_definitions.entries()) {
|
|
493
|
+
if ('count' in field_definition) {
|
|
494
|
+
collectTraversalDiagnostic(
|
|
495
|
+
diagnostics,
|
|
496
|
+
field_definition.count.traversal,
|
|
497
|
+
known_relation_names,
|
|
498
|
+
`derived_summaries.${summary_name}.fields.${field_index}.count.traversal`,
|
|
499
|
+
);
|
|
500
|
+
collectWhereClauseDiagnostics(
|
|
501
|
+
diagnostics,
|
|
502
|
+
field_definition.count.where,
|
|
503
|
+
known_relation_names,
|
|
504
|
+
`derived_summaries.${summary_name}.fields.${field_index}.count.where`,
|
|
505
|
+
);
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
for (const [case_index, select_case] of field_definition.select.entries()) {
|
|
510
|
+
collectWhereClauseDiagnostics(
|
|
511
|
+
diagnostics,
|
|
512
|
+
select_case.when,
|
|
513
|
+
known_relation_names,
|
|
514
|
+
`derived_summaries.${summary_name}.fields.${field_index}.select.${case_index}.when`,
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* @param {PatramDiagnostic[]} diagnostics
|
|
522
|
+
* @param {string} traversal_text
|
|
523
|
+
* @param {Set<string>} known_relation_names
|
|
524
|
+
* @param {string} diagnostic_path
|
|
525
|
+
*/
|
|
526
|
+
function collectTraversalDiagnostic(
|
|
527
|
+
diagnostics,
|
|
528
|
+
traversal_text,
|
|
529
|
+
known_relation_names,
|
|
530
|
+
diagnostic_path,
|
|
531
|
+
) {
|
|
532
|
+
const traversal_match =
|
|
533
|
+
/^(?<direction>in|out):(?<relation_name>[a-zA-Z0-9_]+)$/du.exec(
|
|
534
|
+
traversal_text,
|
|
535
|
+
);
|
|
536
|
+
|
|
537
|
+
if (!traversal_match?.groups?.relation_name) {
|
|
538
|
+
diagnostics.push(
|
|
539
|
+
createConfigDiagnostic(
|
|
540
|
+
diagnostic_path,
|
|
541
|
+
'Derived summary traversal must use "in:<relation>" or "out:<relation>".',
|
|
542
|
+
),
|
|
543
|
+
);
|
|
544
|
+
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
if (known_relation_names.has(traversal_match.groups.relation_name)) {
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
diagnostics.push(
|
|
553
|
+
createConfigDiagnostic(
|
|
554
|
+
diagnostic_path,
|
|
555
|
+
`Unknown relation "${traversal_match.groups.relation_name}" in derived summary traversal.`,
|
|
556
|
+
),
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* @param {PatramDiagnostic[]} diagnostics
|
|
562
|
+
* @param {string} where_clause
|
|
563
|
+
* @param {Set<string>} known_relation_names
|
|
564
|
+
* @param {string} diagnostic_path
|
|
565
|
+
*/
|
|
566
|
+
function collectWhereClauseDiagnostics(
|
|
567
|
+
diagnostics,
|
|
568
|
+
where_clause,
|
|
569
|
+
known_relation_names,
|
|
570
|
+
diagnostic_path,
|
|
571
|
+
) {
|
|
572
|
+
const parse_result = parseWhereClause(where_clause);
|
|
573
|
+
|
|
574
|
+
if (!parse_result.success) {
|
|
575
|
+
diagnostics.push(
|
|
576
|
+
createConfigDiagnostic(diagnostic_path, parse_result.diagnostic.message),
|
|
577
|
+
);
|
|
578
|
+
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
for (const clause of parse_result.clauses) {
|
|
583
|
+
collectClauseRelationDiagnostics(
|
|
584
|
+
diagnostics,
|
|
585
|
+
clause.term,
|
|
586
|
+
known_relation_names,
|
|
587
|
+
diagnostic_path,
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* @param {PatramDiagnostic[]} diagnostics
|
|
594
|
+
* @param {import('./parse-where-clause.types.ts').ParsedTerm} term
|
|
595
|
+
* @param {Set<string>} known_relation_names
|
|
596
|
+
* @param {string} diagnostic_path
|
|
597
|
+
*/
|
|
598
|
+
function collectClauseRelationDiagnostics(
|
|
599
|
+
diagnostics,
|
|
600
|
+
term,
|
|
601
|
+
known_relation_names,
|
|
602
|
+
diagnostic_path,
|
|
603
|
+
) {
|
|
604
|
+
if (term.kind === 'aggregate') {
|
|
605
|
+
if (!known_relation_names.has(term.traversal.relation_name)) {
|
|
606
|
+
diagnostics.push(
|
|
607
|
+
createConfigDiagnostic(
|
|
608
|
+
diagnostic_path,
|
|
609
|
+
`Unknown relation "${term.traversal.relation_name}" in traversal clause.`,
|
|
610
|
+
),
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
for (const nested_clause of term.clauses) {
|
|
615
|
+
collectClauseRelationDiagnostics(
|
|
616
|
+
diagnostics,
|
|
617
|
+
nested_clause.term,
|
|
618
|
+
known_relation_names,
|
|
619
|
+
diagnostic_path,
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
if (term.kind !== 'relation' && term.kind !== 'relation_target') {
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
if (known_relation_names.has(term.relation_name)) {
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
diagnostics.push(
|
|
635
|
+
createConfigDiagnostic(
|
|
636
|
+
diagnostic_path,
|
|
637
|
+
`Unknown relation "${term.relation_name}" in relation clause.`,
|
|
638
|
+
),
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* @param {string} issue_path
|
|
644
|
+
* @param {string} message
|
|
645
|
+
* @returns {PatramDiagnostic}
|
|
646
|
+
*/
|
|
647
|
+
function createConfigDiagnostic(issue_path, message) {
|
|
648
|
+
return {
|
|
649
|
+
code: 'config.invalid',
|
|
650
|
+
column: 1,
|
|
651
|
+
level: 'error',
|
|
652
|
+
line: 1,
|
|
653
|
+
message: `Invalid config at "${issue_path}": ${message}`,
|
|
654
|
+
path: CONFIG_FILE_NAME,
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
|
|
171
658
|
/**
|
|
172
659
|
* @param {unknown} error
|
|
173
660
|
* @returns {error is NodeJS.ErrnoException}
|
|
@@ -1,10 +1,50 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
KindDefinition,
|
|
3
|
+
MappingDefinition,
|
|
4
|
+
RelationDefinition,
|
|
5
|
+
} from './patram-config.types.ts';
|
|
6
|
+
|
|
1
7
|
export interface StoredQueryConfig {
|
|
2
8
|
where: string;
|
|
3
9
|
}
|
|
4
10
|
|
|
11
|
+
export type DerivedSummaryScalar = boolean | number | string | null;
|
|
12
|
+
|
|
13
|
+
export interface DerivedSummaryCountFieldConfig {
|
|
14
|
+
count: {
|
|
15
|
+
traversal: string;
|
|
16
|
+
where: string;
|
|
17
|
+
};
|
|
18
|
+
name: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface DerivedSummarySelectCaseConfig {
|
|
22
|
+
value: DerivedSummaryScalar;
|
|
23
|
+
when: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface DerivedSummarySelectFieldConfig {
|
|
27
|
+
default: DerivedSummaryScalar;
|
|
28
|
+
name: string;
|
|
29
|
+
select: DerivedSummarySelectCaseConfig[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type DerivedSummaryFieldConfig =
|
|
33
|
+
| DerivedSummaryCountFieldConfig
|
|
34
|
+
| DerivedSummarySelectFieldConfig;
|
|
35
|
+
|
|
36
|
+
export interface DerivedSummaryConfig {
|
|
37
|
+
fields: DerivedSummaryFieldConfig[];
|
|
38
|
+
kinds: string[];
|
|
39
|
+
}
|
|
40
|
+
|
|
5
41
|
export interface PatramRepoConfig {
|
|
42
|
+
derived_summaries?: Record<string, DerivedSummaryConfig>;
|
|
6
43
|
include: string[];
|
|
44
|
+
kinds?: Record<string, KindDefinition>;
|
|
45
|
+
mappings?: Record<string, MappingDefinition>;
|
|
7
46
|
queries: Record<string, StoredQueryConfig>;
|
|
47
|
+
relations?: Record<string, RelationDefinition>;
|
|
8
48
|
}
|
|
9
49
|
|
|
10
50
|
export interface PatramDiagnostic {
|