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.
Files changed (67) hide show
  1. package/bin/patram.js +25 -147
  2. package/lib/build-graph-identity.js +270 -0
  3. package/lib/build-graph.js +156 -77
  4. package/lib/check-graph.js +23 -7
  5. package/lib/claim-helpers.js +55 -0
  6. package/lib/cli-help-metadata.js +552 -0
  7. package/lib/command-output.js +83 -0
  8. package/lib/derived-summary.js +278 -0
  9. package/lib/format-derived-summary-row.js +9 -0
  10. package/lib/format-node-header.js +19 -0
  11. package/lib/format-output-item-block.js +22 -0
  12. package/lib/format-output-metadata.js +62 -0
  13. package/lib/layout-stored-queries.js +361 -0
  14. package/lib/list-queries.js +18 -0
  15. package/lib/list-source-files.js +50 -15
  16. package/lib/load-patram-config.js +505 -18
  17. package/lib/load-patram-config.types.ts +40 -0
  18. package/lib/load-project-graph.js +124 -0
  19. package/lib/output-view.types.ts +88 -0
  20. package/lib/parse-claims.js +38 -158
  21. package/lib/parse-claims.types.ts +7 -0
  22. package/lib/parse-cli-arguments-helpers.js +446 -0
  23. package/lib/parse-cli-arguments.js +266 -0
  24. package/lib/parse-cli-arguments.types.ts +69 -0
  25. package/lib/parse-cli-color-options.js +44 -0
  26. package/lib/parse-cli-query-pagination.js +49 -0
  27. package/lib/parse-jsdoc-blocks.js +184 -0
  28. package/lib/parse-jsdoc-claims.js +280 -0
  29. package/lib/parse-jsdoc-prose.js +111 -0
  30. package/lib/parse-markdown-claims.js +242 -0
  31. package/lib/parse-markdown-directives.js +136 -0
  32. package/lib/parse-where-clause.js +707 -0
  33. package/lib/parse-where-clause.types.ts +70 -0
  34. package/lib/patram-cli.js +464 -0
  35. package/lib/patram-config.js +3 -1
  36. package/lib/patram-config.types.ts +2 -1
  37. package/lib/patram.js +6 -0
  38. package/lib/query-graph.js +368 -0
  39. package/lib/query-inspection.js +523 -0
  40. package/lib/render-check-output.js +315 -0
  41. package/lib/render-cli-help.js +419 -0
  42. package/lib/render-json-output.js +161 -0
  43. package/lib/render-output-view.js +222 -0
  44. package/lib/render-plain-output.js +182 -0
  45. package/lib/render-rich-output.js +240 -0
  46. package/lib/render-rich-source.js +1333 -0
  47. package/lib/resolve-check-target.js +190 -0
  48. package/lib/resolve-output-mode.js +60 -0
  49. package/lib/resolve-patram-graph-config.js +88 -0
  50. package/lib/resolve-where-clause.js +66 -0
  51. package/lib/show-document.js +311 -0
  52. package/lib/source-file-defaults.js +28 -0
  53. package/lib/tagged-fenced-block-error.js +17 -0
  54. package/lib/tagged-fenced-block-markdown.js +111 -0
  55. package/lib/tagged-fenced-block-metadata.js +97 -0
  56. package/lib/tagged-fenced-block-parser.js +292 -0
  57. package/lib/tagged-fenced-blocks.js +100 -0
  58. package/lib/tagged-fenced-blocks.types.ts +38 -0
  59. package/lib/write-paged-output.js +87 -0
  60. package/package.json +28 -12
  61. package/bin/patram.test.js +0 -184
  62. package/lib/build-graph.test.js +0 -141
  63. package/lib/check-graph.test.js +0 -103
  64. package/lib/list-source-files.test.js +0 -101
  65. package/lib/load-patram-config.test.js +0 -211
  66. package/lib/parse-claims.test.js +0 -113
  67. package/lib/patram-config.test.js +0 -147
@@ -0,0 +1,523 @@
1
+ /* eslint-disable max-lines */
2
+ /**
3
+ * @import { PatramDiagnostic, PatramRepoConfig } from './load-patram-config.types.ts';
4
+ * @import { ResolvedOutputMode } from './output-view.types.ts';
5
+ * @import {
6
+ * ParsedClause,
7
+ * ParsedRelationTargetTerm,
8
+ * ParsedRelationTerm,
9
+ * ParsedTerm,
10
+ * ParsedTraversalTerm,
11
+ * } from './parse-where-clause.types.ts';
12
+ */
13
+
14
+ import { Ansis } from 'ansis';
15
+
16
+ import { parseWhereClause } from './parse-where-clause.js';
17
+
18
+ /**
19
+ * @typedef {{ kind: 'ad_hoc' } | { kind: 'stored_query', name: string }} QuerySource
20
+ */
21
+
22
+ /**
23
+ * @typedef {{ query_source: QuerySource, where_clause: string }} ResolvedWhereClause
24
+ */
25
+
26
+ /**
27
+ * @typedef {{ inspection_mode: 'explain' | 'lint', limit: number | null, offset: number }} QueryInspectionOptions
28
+ */
29
+
30
+ /**
31
+ * @typedef {{
32
+ * execution?: {
33
+ * limit: number | null,
34
+ * offset: number,
35
+ * },
36
+ * inspection_mode: 'explain' | 'lint',
37
+ * query_source: QuerySource,
38
+ * where_clause: string,
39
+ * clauses?: ParsedClause[],
40
+ * }} QueryInspectionSuccess
41
+ */
42
+
43
+ /**
44
+ * Inspect one resolved query without executing it.
45
+ *
46
+ * @param {PatramRepoConfig} repo_config
47
+ * @param {ResolvedWhereClause} resolved_where_clause
48
+ * @param {QueryInspectionOptions} inspection_options
49
+ * @returns {{ success: true, value: QueryInspectionSuccess } | { diagnostics: PatramDiagnostic[], success: false }}
50
+ */
51
+ export function inspectQuery(
52
+ repo_config,
53
+ resolved_where_clause,
54
+ inspection_options,
55
+ ) {
56
+ const parse_result = parseWhereClause(resolved_where_clause.where_clause);
57
+
58
+ if (!parse_result.success) {
59
+ return {
60
+ diagnostics: [
61
+ replaceDiagnosticPath(
62
+ parse_result.diagnostic,
63
+ formatQueryDiagnosticPath(resolved_where_clause.query_source),
64
+ ),
65
+ ],
66
+ success: false,
67
+ };
68
+ }
69
+
70
+ const diagnostics = collectRelationDiagnostics(
71
+ repo_config,
72
+ resolved_where_clause.query_source,
73
+ parse_result.clauses,
74
+ );
75
+
76
+ if (diagnostics.length > 0) {
77
+ return {
78
+ diagnostics,
79
+ success: false,
80
+ };
81
+ }
82
+
83
+ if (inspection_options.inspection_mode === 'lint') {
84
+ return {
85
+ success: true,
86
+ value: {
87
+ inspection_mode: 'lint',
88
+ query_source: resolved_where_clause.query_source,
89
+ where_clause: resolved_where_clause.where_clause,
90
+ },
91
+ };
92
+ }
93
+
94
+ return {
95
+ success: true,
96
+ value: {
97
+ clauses: parse_result.clauses,
98
+ execution: {
99
+ limit: inspection_options.limit,
100
+ offset: inspection_options.offset,
101
+ },
102
+ inspection_mode: 'explain',
103
+ query_source: resolved_where_clause.query_source,
104
+ where_clause: resolved_where_clause.where_clause,
105
+ },
106
+ };
107
+ }
108
+
109
+ /**
110
+ * Render a successful query inspection in one output mode.
111
+ *
112
+ * @param {QueryInspectionSuccess} query_inspection
113
+ * @param {ResolvedOutputMode} output_mode
114
+ * @returns {string}
115
+ */
116
+ export function renderQueryInspection(query_inspection, output_mode) {
117
+ if (output_mode.renderer_name === 'json') {
118
+ return `${JSON.stringify(formatJsonQueryInspection(query_inspection), null, 2)}\n`;
119
+ }
120
+
121
+ if (output_mode.renderer_name === 'plain') {
122
+ return renderTextQueryInspection(query_inspection, {
123
+ header: identity,
124
+ label: identity,
125
+ });
126
+ }
127
+
128
+ const ansi = new Ansis(output_mode.color_enabled ? 3 : 0);
129
+
130
+ return renderTextQueryInspection(query_inspection, {
131
+ header(value) {
132
+ return ansi.green(value);
133
+ },
134
+ label(value) {
135
+ return ansi.gray(value);
136
+ },
137
+ });
138
+ }
139
+
140
+ /**
141
+ * @param {PatramRepoConfig} repo_config
142
+ * @param {QuerySource} query_source
143
+ * @param {ParsedClause[]} clauses
144
+ * @returns {PatramDiagnostic[]}
145
+ */
146
+ function collectRelationDiagnostics(repo_config, query_source, clauses) {
147
+ if (!repo_config.relations) {
148
+ return [];
149
+ }
150
+
151
+ const known_relation_names = new Set(Object.keys(repo_config.relations));
152
+ /** @type {PatramDiagnostic[]} */
153
+ const diagnostics = [];
154
+
155
+ collectClauseDiagnostics(
156
+ clauses,
157
+ diagnostics,
158
+ known_relation_names,
159
+ formatQueryDiagnosticPath(query_source),
160
+ );
161
+
162
+ return diagnostics;
163
+ }
164
+
165
+ /**
166
+ * @param {ParsedClause[]} clauses
167
+ * @param {PatramDiagnostic[]} diagnostics
168
+ * @param {Set<string>} known_relation_names
169
+ * @param {string} diagnostic_path
170
+ */
171
+ function collectClauseDiagnostics(
172
+ clauses,
173
+ diagnostics,
174
+ known_relation_names,
175
+ diagnostic_path,
176
+ ) {
177
+ for (const clause of clauses) {
178
+ collectTermDiagnostics(
179
+ clause.term,
180
+ diagnostics,
181
+ known_relation_names,
182
+ diagnostic_path,
183
+ );
184
+ }
185
+ }
186
+
187
+ /**
188
+ * @param {ParsedTerm} term
189
+ * @param {PatramDiagnostic[]} diagnostics
190
+ * @param {Set<string>} known_relation_names
191
+ * @param {string} diagnostic_path
192
+ */
193
+ function collectTermDiagnostics(
194
+ term,
195
+ diagnostics,
196
+ known_relation_names,
197
+ diagnostic_path,
198
+ ) {
199
+ if (term.kind === 'aggregate') {
200
+ collectTraversalDiagnostic(
201
+ term.traversal,
202
+ diagnostics,
203
+ known_relation_names,
204
+ diagnostic_path,
205
+ );
206
+ collectClauseDiagnostics(
207
+ term.clauses,
208
+ diagnostics,
209
+ known_relation_names,
210
+ diagnostic_path,
211
+ );
212
+
213
+ return;
214
+ }
215
+
216
+ if (term.kind === 'relation') {
217
+ collectRelationDiagnostic(
218
+ term,
219
+ diagnostics,
220
+ known_relation_names,
221
+ diagnostic_path,
222
+ 'relation clause',
223
+ );
224
+
225
+ return;
226
+ }
227
+
228
+ if (term.kind === 'relation_target') {
229
+ collectRelationDiagnostic(
230
+ term,
231
+ diagnostics,
232
+ known_relation_names,
233
+ diagnostic_path,
234
+ 'relation clause',
235
+ );
236
+ }
237
+ }
238
+
239
+ /**
240
+ * @param {ParsedRelationTerm | ParsedRelationTargetTerm} term
241
+ * @param {PatramDiagnostic[]} diagnostics
242
+ * @param {Set<string>} known_relation_names
243
+ * @param {string} diagnostic_path
244
+ * @param {string} clause_kind
245
+ */
246
+ function collectRelationDiagnostic(
247
+ term,
248
+ diagnostics,
249
+ known_relation_names,
250
+ diagnostic_path,
251
+ clause_kind,
252
+ ) {
253
+ if (known_relation_names.has(term.relation_name)) {
254
+ return;
255
+ }
256
+
257
+ diagnostics.push(
258
+ createQueryDiagnostic(
259
+ diagnostic_path,
260
+ term.column,
261
+ `Unknown relation "${term.relation_name}" in ${clause_kind}.`,
262
+ ),
263
+ );
264
+ }
265
+
266
+ /**
267
+ * @param {ParsedTraversalTerm} traversal
268
+ * @param {PatramDiagnostic[]} diagnostics
269
+ * @param {Set<string>} known_relation_names
270
+ * @param {string} diagnostic_path
271
+ */
272
+ function collectTraversalDiagnostic(
273
+ traversal,
274
+ diagnostics,
275
+ known_relation_names,
276
+ diagnostic_path,
277
+ ) {
278
+ if (known_relation_names.has(traversal.relation_name)) {
279
+ return;
280
+ }
281
+
282
+ diagnostics.push(
283
+ createQueryDiagnostic(
284
+ diagnostic_path,
285
+ traversal.column,
286
+ `Unknown relation "${traversal.relation_name}" in traversal clause.`,
287
+ ),
288
+ );
289
+ }
290
+
291
+ /**
292
+ * @param {QueryInspectionSuccess} query_inspection
293
+ * @returns {object}
294
+ */
295
+ function formatJsonQueryInspection(query_inspection) {
296
+ if (query_inspection.inspection_mode === 'lint') {
297
+ return {
298
+ diagnostics: [],
299
+ mode: 'lint',
300
+ source: query_inspection.query_source,
301
+ where: query_inspection.where_clause,
302
+ };
303
+ }
304
+
305
+ return {
306
+ clauses: query_inspection.clauses,
307
+ diagnostics: [],
308
+ execution: query_inspection.execution,
309
+ mode: 'explain',
310
+ source: query_inspection.query_source,
311
+ where: query_inspection.where_clause,
312
+ };
313
+ }
314
+
315
+ /**
316
+ * @param {QueryInspectionSuccess} query_inspection
317
+ * @param {{ header: (value: string) => string, label: (value: string) => string }} render_options
318
+ * @returns {string}
319
+ */
320
+ function renderTextQueryInspection(query_inspection, render_options) {
321
+ /** @type {string[]} */
322
+ const output_lines = [
323
+ render_options.header(
324
+ query_inspection.inspection_mode === 'lint'
325
+ ? 'Query is valid.'
326
+ : 'Query explanation',
327
+ ),
328
+ formatLabeledLine(
329
+ render_options,
330
+ 'source',
331
+ formatQuerySource(query_inspection.query_source),
332
+ ),
333
+ formatLabeledLine(render_options, 'where', query_inspection.where_clause),
334
+ ];
335
+
336
+ if (query_inspection.inspection_mode === 'lint') {
337
+ return `${output_lines.join('\n')}\n`;
338
+ }
339
+
340
+ output_lines.push(
341
+ formatLabeledLine(
342
+ render_options,
343
+ 'offset',
344
+ String(query_inspection.execution?.offset ?? 0),
345
+ ),
346
+ formatLabeledLine(
347
+ render_options,
348
+ 'limit',
349
+ query_inspection.execution?.limit === null
350
+ ? 'none'
351
+ : String(query_inspection.execution?.limit ?? ''),
352
+ ),
353
+ '',
354
+ `${render_options.label('clauses:')}`,
355
+ ...formatExplainClauseBlock(
356
+ query_inspection.clauses ?? [],
357
+ render_options,
358
+ '',
359
+ ),
360
+ );
361
+
362
+ return `${output_lines.join('\n')}\n`;
363
+ }
364
+
365
+ /**
366
+ * @param {ParsedClause[]} clauses
367
+ * @param {{ header: (value: string) => string, label: (value: string) => string }} render_options
368
+ * @param {string} indentation
369
+ * @returns {string[]}
370
+ */
371
+ function formatExplainClauseBlock(clauses, render_options, indentation) {
372
+ /** @type {string[]} */
373
+ const output_lines = [];
374
+
375
+ clauses.forEach((clause, clause_index) => {
376
+ const clause_number = clause_index + 1;
377
+ const clause_text = formatClauseSummary(clause);
378
+
379
+ output_lines.push(`${indentation}${clause_number}. ${clause_text}`);
380
+
381
+ if (clause.term.kind !== 'aggregate') {
382
+ return;
383
+ }
384
+
385
+ output_lines.push(
386
+ `${indentation} ${render_options.label('traversal:')} ${formatTraversal(clause.term.traversal)}`,
387
+ );
388
+
389
+ if (clause.term.aggregate_name === 'count') {
390
+ output_lines.push(
391
+ `${indentation} ${render_options.label('comparison:')} ${clause.term.comparison} ${clause.term.value}`,
392
+ );
393
+ }
394
+
395
+ output_lines.push(
396
+ `${indentation} ${render_options.label('nested clauses:')}`,
397
+ ...formatExplainClauseBlock(
398
+ clause.term.clauses,
399
+ render_options,
400
+ `${indentation} `,
401
+ ),
402
+ );
403
+ });
404
+
405
+ return output_lines;
406
+ }
407
+
408
+ /**
409
+ * @param {ParsedClause} clause
410
+ * @returns {string}
411
+ */
412
+ function formatClauseSummary(clause) {
413
+ const clause_prefix = clause.is_negated ? 'not ' : '';
414
+
415
+ if (clause.term.kind === 'aggregate') {
416
+ return `${clause_prefix}aggregate ${clause.term.aggregate_name}`;
417
+ }
418
+
419
+ return `${clause_prefix}${formatTermSummary(clause.term)}`;
420
+ }
421
+
422
+ /**
423
+ * @param {ParsedTerm} term
424
+ * @returns {string}
425
+ */
426
+ function formatTermSummary(term) {
427
+ if (term.kind === 'field') {
428
+ return `${term.field_name} ${term.operator} ${term.value}`;
429
+ }
430
+
431
+ if (term.kind === 'field_set') {
432
+ return `${term.field_name} ${term.operator} [${term.values.join(', ')}]`;
433
+ }
434
+
435
+ if (term.kind === 'relation') {
436
+ return `${term.relation_name} exists`;
437
+ }
438
+
439
+ if (term.kind === 'relation_target') {
440
+ return `${term.relation_name} = ${term.target_id}`;
441
+ }
442
+
443
+ throw new Error('Expected a non-aggregate query term.');
444
+ }
445
+
446
+ /**
447
+ * @param {ParsedTraversalTerm} traversal
448
+ * @returns {string}
449
+ */
450
+ function formatTraversal(traversal) {
451
+ return `${traversal.direction}:${traversal.relation_name}`;
452
+ }
453
+
454
+ /**
455
+ * @param {{ header: (value: string) => string, label: (value: string) => string }} render_options
456
+ * @param {string} label
457
+ * @param {string} value
458
+ * @returns {string}
459
+ */
460
+ function formatLabeledLine(render_options, label, value) {
461
+ return `${render_options.label(`${label}:`)} ${value}`;
462
+ }
463
+
464
+ /**
465
+ * @param {QuerySource} query_source
466
+ * @returns {string}
467
+ */
468
+ function formatQuerySource(query_source) {
469
+ if (query_source.kind === 'stored_query') {
470
+ return `stored query "${query_source.name}"`;
471
+ }
472
+
473
+ return 'ad hoc query';
474
+ }
475
+
476
+ /**
477
+ * @param {QuerySource} query_source
478
+ * @returns {string}
479
+ */
480
+ function formatQueryDiagnosticPath(query_source) {
481
+ if (query_source.kind === 'stored_query') {
482
+ return `<query:${query_source.name}>`;
483
+ }
484
+
485
+ return '<query>';
486
+ }
487
+
488
+ /**
489
+ * @param {string} diagnostic_path
490
+ * @param {number} column
491
+ * @param {string} message
492
+ * @returns {PatramDiagnostic}
493
+ */
494
+ function createQueryDiagnostic(diagnostic_path, column, message) {
495
+ return {
496
+ code: 'query.unknown_relation',
497
+ column,
498
+ level: 'error',
499
+ line: 1,
500
+ message,
501
+ path: diagnostic_path,
502
+ };
503
+ }
504
+
505
+ /**
506
+ * @param {PatramDiagnostic} diagnostic
507
+ * @param {string} diagnostic_path
508
+ * @returns {PatramDiagnostic}
509
+ */
510
+ function replaceDiagnosticPath(diagnostic, diagnostic_path) {
511
+ return {
512
+ ...diagnostic,
513
+ path: diagnostic_path,
514
+ };
515
+ }
516
+
517
+ /**
518
+ * @param {string} value
519
+ * @returns {string}
520
+ */
521
+ function identity(value) {
522
+ return value;
523
+ }