patram 0.10.0 → 0.11.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.
@@ -5,7 +5,7 @@
5
5
  * @typedef {import('./arguments.types.ts').CliParseError} CliParseError
6
6
  * @typedef {import('./arguments.types.ts').ParsedCliHelpRequest} ParsedCliHelpRequest
7
7
  * @typedef {{ kind: string, name?: string, rawName?: string, value?: string | boolean }} CliOptionToken
8
- * @typedef {{ color?: string, explain?: boolean, help?: boolean, json?: boolean, limit?: string, lint?: boolean, 'no-color'?: boolean, offset?: string, plain?: boolean, where?: string }} CliOptionValues
8
+ * @typedef {{ color?: string, desc?: string, explain?: boolean, help?: boolean, json?: boolean, limit?: string, lint?: boolean, name?: string, 'no-color'?: boolean, offset?: string, plain?: boolean, query?: string, where?: string }} CliOptionValues
9
9
  * @typedef {{ option_tokens: CliOptionToken[], positionals: string[], values: CliOptionValues }} ParsedCommandLine
10
10
  */
11
11
 
@@ -23,14 +23,17 @@ import { findInvalidQueryPagination } from './query-pagination.js';
23
23
 
24
24
  export const CLI_OPTIONS = /** @type {const} */ ({
25
25
  color: { type: 'string' },
26
+ desc: { type: 'string' },
26
27
  explain: { type: 'boolean' },
27
28
  help: { type: 'boolean' },
28
29
  json: { type: 'boolean' },
29
30
  limit: { type: 'string' },
30
31
  lint: { type: 'boolean' },
32
+ name: { type: 'string' },
31
33
  'no-color': { type: 'boolean' },
32
34
  offset: { type: 'string' },
33
35
  plain: { type: 'boolean' },
36
+ query: { type: 'string' },
34
37
  where: { type: 'string' },
35
38
  });
36
39
 
@@ -123,6 +126,11 @@ export function validateParsedCommand(command_name, command_line) {
123
126
  command_line.values,
124
127
  command_positionals,
125
128
  ) ??
129
+ findInvalidQueriesMutation(
130
+ command_name,
131
+ command_line.values,
132
+ command_positionals,
133
+ ) ??
126
134
  validateCommandPositionals(command_name, command_positionals)
127
135
  );
128
136
  }
@@ -162,6 +170,25 @@ export function buildCommandArguments(
162
170
  return [command_positionals[0], '--where', parsed_values.where];
163
171
  }
164
172
 
173
+ if (command_name === 'queries') {
174
+ /** @type {string[]} */
175
+ const command_arguments = [...command_positionals];
176
+
177
+ if (parsed_values.name !== undefined) {
178
+ command_arguments.push('--name', parsed_values.name);
179
+ }
180
+
181
+ if (parsed_values.query !== undefined) {
182
+ command_arguments.push('--query', parsed_values.query);
183
+ }
184
+
185
+ if (parsed_values.desc !== undefined) {
186
+ command_arguments.push('--desc', parsed_values.desc);
187
+ }
188
+
189
+ return command_arguments;
190
+ }
191
+
165
192
  return command_positionals;
166
193
  }
167
194
 
@@ -305,24 +332,10 @@ function findInvalidCommandOption(command_name, option_tokens) {
305
332
  */
306
333
  function findMissingOptionValue(option_tokens) {
307
334
  for (const token of option_tokens) {
308
- if (token.name === 'where' && typeof token.value !== 'string') {
309
- return {
310
- argument_label: "<name> or --where '<clause>'",
311
- code: 'missing_required_argument',
312
- command_name: 'query',
313
- };
314
- }
315
-
316
- if (token.name === 'offset' && typeof token.value !== 'string') {
317
- return createMessageParseError('Offset requires a value.');
318
- }
335
+ const missing_value_error = findMissingOptionValueError(token);
319
336
 
320
- if (token.name === 'limit' && typeof token.value !== 'string') {
321
- return createMessageParseError('Limit requires a value.');
322
- }
323
-
324
- if (token.name === 'color' && typeof token.value !== 'string') {
325
- return createMessageParseError('Color requires a value.');
337
+ if (missing_value_error) {
338
+ return missing_value_error;
326
339
  }
327
340
  }
328
341
 
@@ -386,6 +399,42 @@ function findInvalidQueryInspection(command_name, parsed_values) {
386
399
  return null;
387
400
  }
388
401
 
402
+ /**
403
+ * @param {CliCommandName} command_name
404
+ * @param {CliOptionValues} parsed_values
405
+ * @param {string[]} command_positionals
406
+ * @returns {CliParseError | null}
407
+ */
408
+ function findInvalidQueriesMutation(
409
+ command_name,
410
+ parsed_values,
411
+ command_positionals,
412
+ ) {
413
+ if (command_name !== 'queries' || command_positionals.length === 0) {
414
+ return null;
415
+ }
416
+
417
+ const subcommand_name = command_positionals[0];
418
+
419
+ if (
420
+ subcommand_name !== 'add' &&
421
+ subcommand_name !== 'remove' &&
422
+ subcommand_name !== 'update'
423
+ ) {
424
+ return createUnexpectedArgumentError('queries', subcommand_name);
425
+ }
426
+
427
+ if (subcommand_name === 'add') {
428
+ return validateQueriesAddMutation(parsed_values, command_positionals);
429
+ }
430
+
431
+ if (subcommand_name === 'remove') {
432
+ return validateQueriesRemoveMutation(parsed_values, command_positionals);
433
+ }
434
+
435
+ return validateQueriesUpdateMutation(parsed_values, command_positionals);
436
+ }
437
+
389
438
  /**
390
439
  * @param {CliCommandName} command_name
391
440
  * @param {string[]} command_positionals
@@ -394,24 +443,18 @@ function findInvalidQueryInspection(command_name, parsed_values) {
394
443
  function validateCommandPositionals(command_name, command_positionals) {
395
444
  const command_definition = getCommandDefinition(command_name);
396
445
 
446
+ if (allowsQueriesMutationPositionals(command_name, command_positionals)) {
447
+ return null;
448
+ }
449
+
397
450
  if (command_positionals.length < command_definition.min_positionals) {
398
- if (command_name === 'query' && command_definition.missing_argument_label) {
399
- return {
400
- argument_label: command_definition.missing_argument_label,
401
- code: 'missing_required_argument',
402
- command_name: 'query',
403
- };
404
- }
451
+ const missing_argument_error = createMissingArgumentError(
452
+ command_name,
453
+ command_definition.missing_argument_label,
454
+ );
405
455
 
406
- if (
407
- (command_name === 'refs' || command_name === 'show') &&
408
- command_definition.missing_argument_label
409
- ) {
410
- return {
411
- argument_label: command_definition.missing_argument_label,
412
- code: 'missing_required_argument',
413
- command_name,
414
- };
456
+ if (missing_argument_error) {
457
+ return missing_argument_error;
415
458
  }
416
459
 
417
460
  return createMessageParseError(
@@ -443,14 +486,215 @@ function validateCommandPositionals(command_name, command_positionals) {
443
486
  */
444
487
  function isKnownCommandOptionName(option_name) {
445
488
  return (
489
+ option_name === 'desc' ||
446
490
  option_name === 'explain' ||
447
491
  option_name === 'limit' ||
448
492
  option_name === 'lint' ||
493
+ option_name === 'name' ||
449
494
  option_name === 'offset' ||
495
+ option_name === 'query' ||
450
496
  option_name === 'where'
451
497
  );
452
498
  }
453
499
 
500
+ /**
501
+ * @param {CliOptionToken} option_token
502
+ * @returns {CliParseError | null}
503
+ */
504
+ function findMissingOptionValueError(option_token) {
505
+ if (typeof option_token.value === 'string') {
506
+ return null;
507
+ }
508
+
509
+ if (option_token.name === 'where') {
510
+ return {
511
+ argument_label: "<name> or --where '<clause>'",
512
+ code: 'missing_required_argument',
513
+ command_name: 'query',
514
+ };
515
+ }
516
+
517
+ const message = getMissingOptionValueMessage(option_token.name);
518
+
519
+ if (!message) {
520
+ return null;
521
+ }
522
+
523
+ return createMessageParseError(message);
524
+ }
525
+
526
+ /**
527
+ * @param {string | undefined} option_name
528
+ * @returns {string | null}
529
+ */
530
+ function getMissingOptionValueMessage(option_name) {
531
+ /** @type {Record<string, string>} */
532
+ const option_messages = {
533
+ color: 'Color requires a value.',
534
+ desc: 'Desc requires a value.',
535
+ limit: 'Limit requires a value.',
536
+ name: 'Name requires a value.',
537
+ offset: 'Offset requires a value.',
538
+ query: 'Query requires a value.',
539
+ };
540
+
541
+ if (!option_name || !Object.hasOwn(option_messages, option_name)) {
542
+ return null;
543
+ }
544
+
545
+ return option_messages[option_name];
546
+ }
547
+
548
+ /**
549
+ * @param {CliOptionValues} parsed_values
550
+ * @param {string[]} command_positionals
551
+ * @returns {CliParseError | null}
552
+ */
553
+ function validateQueriesAddMutation(parsed_values, command_positionals) {
554
+ const positional_error = validateQueriesMutationPositionals(
555
+ command_positionals,
556
+ 'Queries add requires a stored query name.',
557
+ );
558
+
559
+ if (positional_error) {
560
+ return positional_error;
561
+ }
562
+
563
+ if (parsed_values.name !== undefined) {
564
+ return createMessageParseError('Queries add does not accept "--name".');
565
+ }
566
+
567
+ if (parsed_values.query === undefined) {
568
+ return createMessageParseError('Queries add requires "--query <clause>".');
569
+ }
570
+
571
+ return null;
572
+ }
573
+
574
+ /**
575
+ * @param {CliOptionValues} parsed_values
576
+ * @param {string[]} command_positionals
577
+ * @returns {CliParseError | null}
578
+ */
579
+ function validateQueriesRemoveMutation(parsed_values, command_positionals) {
580
+ const positional_error = validateQueriesMutationPositionals(
581
+ command_positionals,
582
+ 'Queries remove requires a stored query name.',
583
+ );
584
+
585
+ if (positional_error) {
586
+ return positional_error;
587
+ }
588
+
589
+ if (
590
+ parsed_values.desc !== undefined ||
591
+ parsed_values.name !== undefined ||
592
+ parsed_values.query !== undefined
593
+ ) {
594
+ return createMessageParseError(
595
+ 'Queries remove does not accept mutation options.',
596
+ );
597
+ }
598
+
599
+ return null;
600
+ }
601
+
602
+ /**
603
+ * @param {CliOptionValues} parsed_values
604
+ * @param {string[]} command_positionals
605
+ * @returns {CliParseError | null}
606
+ */
607
+ function validateQueriesUpdateMutation(parsed_values, command_positionals) {
608
+ const positional_error = validateQueriesMutationPositionals(
609
+ command_positionals,
610
+ 'Queries update requires a stored query name.',
611
+ );
612
+
613
+ if (positional_error) {
614
+ return positional_error;
615
+ }
616
+
617
+ if (
618
+ parsed_values.desc === undefined &&
619
+ parsed_values.name === undefined &&
620
+ parsed_values.query === undefined
621
+ ) {
622
+ return createMessageParseError(
623
+ 'Queries update requires at least one of "--name", "--query", or "--desc".',
624
+ );
625
+ }
626
+
627
+ return null;
628
+ }
629
+
630
+ /**
631
+ * @param {string[]} command_positionals
632
+ * @param {string} missing_name_message
633
+ * @returns {CliParseError | null}
634
+ */
635
+ function validateQueriesMutationPositionals(
636
+ command_positionals,
637
+ missing_name_message,
638
+ ) {
639
+ if (command_positionals.length < 2) {
640
+ return createMessageParseError(missing_name_message);
641
+ }
642
+
643
+ if (command_positionals.length > 2) {
644
+ return createUnexpectedArgumentError('queries', command_positionals[2]);
645
+ }
646
+
647
+ return null;
648
+ }
649
+
650
+ /**
651
+ * @param {CliCommandName} command_name
652
+ * @param {string[]} command_positionals
653
+ * @returns {boolean}
654
+ */
655
+ function allowsQueriesMutationPositionals(command_name, command_positionals) {
656
+ if (command_name !== 'queries' || command_positionals.length === 0) {
657
+ return false;
658
+ }
659
+
660
+ const subcommand_name = command_positionals[0];
661
+
662
+ return (
663
+ subcommand_name === 'add' ||
664
+ subcommand_name === 'remove' ||
665
+ subcommand_name === 'update'
666
+ );
667
+ }
668
+
669
+ /**
670
+ * @param {CliCommandName} command_name
671
+ * @param {string | null} missing_argument_label
672
+ * @returns {CliParseError | null}
673
+ */
674
+ function createMissingArgumentError(command_name, missing_argument_label) {
675
+ if (!missing_argument_label) {
676
+ return null;
677
+ }
678
+
679
+ if (command_name === 'query') {
680
+ return {
681
+ argument_label: missing_argument_label,
682
+ code: 'missing_required_argument',
683
+ command_name: 'query',
684
+ };
685
+ }
686
+
687
+ if (command_name === 'refs' || command_name === 'show') {
688
+ return {
689
+ argument_label: missing_argument_label,
690
+ code: 'missing_required_argument',
691
+ command_name,
692
+ };
693
+ }
694
+
695
+ return null;
696
+ }
697
+
454
698
  /**
455
699
  * @param {'help' | CliCommandName} command_name
456
700
  * @param {string | undefined} token
@@ -85,6 +85,7 @@ export function renderCliParseError(parse_error) {
85
85
  if (parse_error.code === 'unknown_stored_query') {
86
86
  return renderUnknownStoredQueryError(
87
87
  parse_error.name,
88
+ parse_error.next_path,
88
89
  parse_error.suggestion,
89
90
  );
90
91
  }
@@ -359,10 +360,15 @@ function renderUnexpectedArgumentError(command_name, invalid_token) {
359
360
 
360
361
  /**
361
362
  * @param {string} stored_query_name
363
+ * @param {string | undefined} next_path
362
364
  * @param {string | undefined} suggestion
363
365
  * @returns {string}
364
366
  */
365
- function renderUnknownStoredQueryError(stored_query_name, suggestion) {
367
+ function renderUnknownStoredQueryError(
368
+ stored_query_name,
369
+ next_path,
370
+ suggestion,
371
+ ) {
366
372
  if (suggestion) {
367
373
  return joinOutputLines([
368
374
  `Unknown stored query: ${stored_query_name}`,
@@ -371,7 +377,7 @@ function renderUnknownStoredQueryError(stored_query_name, suggestion) {
371
377
  ` ${suggestion}`,
372
378
  '',
373
379
  'Next:',
374
- ` patram query ${suggestion}`,
380
+ ` ${next_path ?? `patram query ${suggestion}`}`,
375
381
  ]);
376
382
  }
377
383
 
@@ -49,6 +49,17 @@
49
49
  * @returns {Promise<LoadPatramConfigResult>}
50
50
  */
51
51
  export function loadPatramConfig(project_directory?: string): Promise<LoadPatramConfigResult>;
52
+ /**
53
+ * @param {string} config_source
54
+ * @returns {{ success: true, value: unknown } | { success: false, diagnostic: PatramDiagnostic }}
55
+ */
56
+ export function parsePatramConfigSource(config_source: string): {
57
+ success: true;
58
+ value: unknown;
59
+ } | {
60
+ success: false;
61
+ diagnostic: PatramDiagnostic;
62
+ };
52
63
  export type PatramDiagnostic = {
53
64
  code: string;
54
65
  column: number;
@@ -18,15 +18,9 @@ import { readFile } from 'node:fs/promises';
18
18
  import { resolve } from 'node:path';
19
19
  import process from 'node:process';
20
20
 
21
- import { CONFIG_FILE_NAME, patram_repo_config_schema } from './schema.js';
22
- import { createDefaultRepoConfig, normalizeRepoConfig } from './defaults.js';
23
- import {
24
- validateDerivedSummaries,
25
- validateFieldSchemaConfig,
26
- validateGraphSchema,
27
- validateLegacyConfigShape,
28
- validateStoredQueries,
29
- } from './validation.js';
21
+ import { CONFIG_FILE_NAME } from './schema.js';
22
+ import { createDefaultRepoConfig } from './defaults.js';
23
+ import { validatePatramConfigValue } from './validate-patram-config-value.js';
30
24
 
31
25
  /**
32
26
  * Repo config loading.
@@ -90,56 +84,19 @@ export async function loadPatramConfig(project_directory = process.cwd()) {
90
84
  return createLoadResult(createDefaultRepoConfig(), []);
91
85
  }
92
86
 
93
- const parse_result = parseConfigJson(config_source);
87
+ const parse_result = parsePatramConfigSource(config_source);
94
88
 
95
89
  if (!parse_result.success) {
96
90
  return createLoadResult(null, [parse_result.diagnostic]);
97
91
  }
98
92
 
99
- const legacy_config_diagnostics = validateLegacyConfigShape(
100
- parse_result.value,
101
- );
93
+ const validation_result = validatePatramConfigValue(parse_result.value);
102
94
 
103
- if (legacy_config_diagnostics.length > 0) {
104
- return createLoadResult(null, legacy_config_diagnostics);
95
+ if (!validation_result.success) {
96
+ return createLoadResult(null, validation_result.diagnostics);
105
97
  }
106
98
 
107
- const config_result = patram_repo_config_schema.safeParse(parse_result.value);
108
-
109
- if (!config_result.success) {
110
- return createLoadResult(
111
- null,
112
- config_result.error.issues.map(createValidationDiagnostic),
113
- );
114
- }
115
-
116
- const graph_schema_diagnostics = validateGraphSchema(config_result.data);
117
-
118
- if (graph_schema_diagnostics.length > 0) {
119
- return createLoadResult(null, graph_schema_diagnostics);
120
- }
121
-
122
- const normalized_config = normalizeRepoConfig(config_result.data);
123
- const field_schema_diagnostics = validateFieldSchemaConfig(normalized_config);
124
-
125
- if (field_schema_diagnostics.length > 0) {
126
- return createLoadResult(null, field_schema_diagnostics);
127
- }
128
-
129
- const stored_query_diagnostics = validateStoredQueries(normalized_config);
130
-
131
- if (stored_query_diagnostics.length > 0) {
132
- return createLoadResult(null, stored_query_diagnostics);
133
- }
134
-
135
- const derived_summary_diagnostics =
136
- validateDerivedSummaries(normalized_config);
137
-
138
- if (derived_summary_diagnostics.length > 0) {
139
- return createLoadResult(null, derived_summary_diagnostics);
140
- }
141
-
142
- return createLoadResult(normalized_config, []);
99
+ return createLoadResult(validation_result.config, []);
143
100
  }
144
101
 
145
102
  /**
@@ -162,7 +119,7 @@ async function readConfigSource(config_file_path) {
162
119
  * @param {string} config_source
163
120
  * @returns {{ success: true, value: unknown } | { success: false, diagnostic: PatramDiagnostic }}
164
121
  */
165
- function parseConfigJson(config_source) {
122
+ export function parsePatramConfigSource(config_source) {
166
123
  try {
167
124
  return {
168
125
  success: true,
@@ -211,34 +168,6 @@ function createInvalidJsonDiagnostic(config_source, error) {
211
168
  };
212
169
  }
213
170
 
214
- /**
215
- * @param {import('zod').core.$ZodIssue} issue
216
- * @returns {PatramDiagnostic}
217
- */
218
- function createValidationDiagnostic(issue) {
219
- const issue_path = formatIssuePath(issue.path);
220
-
221
- if (issue_path) {
222
- return {
223
- code: 'config.invalid',
224
- column: 1,
225
- level: 'error',
226
- line: 1,
227
- message: `Invalid config at "${issue_path}": ${issue.message}`,
228
- path: CONFIG_FILE_NAME,
229
- };
230
- }
231
-
232
- return {
233
- code: 'config.invalid',
234
- column: 1,
235
- level: 'error',
236
- line: 1,
237
- message: `Invalid config: ${issue.message}`,
238
- path: CONFIG_FILE_NAME,
239
- };
240
- }
241
-
242
171
  /**
243
172
  * @param {unknown} error
244
173
  * @returns {error is NodeJS.ErrnoException}
@@ -281,14 +210,6 @@ function getJsonSyntaxOrigin(config_source, error_message) {
281
210
  };
282
211
  }
283
212
 
284
- /**
285
- * @param {(string | number | symbol | undefined)[]} issue_path
286
- * @returns {string}
287
- */
288
- function formatIssuePath(issue_path) {
289
- return issue_path.map(String).join('.');
290
- }
291
-
292
213
  /**
293
214
  * @param {string} source_text
294
215
  * @param {number} offset
@@ -0,0 +1,69 @@
1
+ /**
2
+ * @param {string} config_file_path
3
+ * @returns {Promise<
4
+ * | { success: true, value: Record<string, unknown> }
5
+ * | { diagnostic: PatramDiagnostic, success: false }
6
+ * >}
7
+ */
8
+ export function loadRawConfig(config_file_path: string): Promise<{
9
+ success: true;
10
+ value: Record<string, unknown>;
11
+ } | {
12
+ diagnostic: PatramDiagnostic;
13
+ success: false;
14
+ }>;
15
+ /**
16
+ * @param {string} config_file_path
17
+ * @param {Record<string, unknown>} raw_config
18
+ * @param {StoredQueryMutationResult} mutation_result
19
+ * @returns {Promise<
20
+ * | { success: true, value: StoredQueryMutationResult }
21
+ * | { diagnostics: PatramDiagnostic[], success: false }
22
+ * >}
23
+ */
24
+ export function persistStoredQueryMutation(config_file_path: string, raw_config: Record<string, unknown>, mutation_result: StoredQueryMutationResult): Promise<{
25
+ success: true;
26
+ value: StoredQueryMutationResult;
27
+ } | {
28
+ diagnostics: PatramDiagnostic[];
29
+ success: false;
30
+ }>;
31
+ /**
32
+ * @param {string} where_clause
33
+ * @param {string | undefined} description
34
+ * @returns {StoredQueryConfig}
35
+ */
36
+ export function createStoredQueryDefinition(where_clause: string, description: string | undefined): StoredQueryConfig;
37
+ /**
38
+ * @param {Record<string, unknown> | null} raw_query_value
39
+ * @param {StoredQueryConfig} existing_query
40
+ * @param {{ description?: string, where?: string }} stored_query_mutation
41
+ * @returns {StoredQueryConfig}
42
+ */
43
+ export function createUpdatedStoredQueryDefinition(raw_query_value: Record<string, unknown> | null, existing_query: StoredQueryConfig, stored_query_mutation: {
44
+ description?: string;
45
+ where?: string;
46
+ }): StoredQueryConfig;
47
+ /**
48
+ * @param {Record<string, unknown>} raw_config
49
+ * @returns {Record<string, unknown>}
50
+ */
51
+ export function ensureRawQueries(raw_config: Record<string, unknown>): Record<string, unknown>;
52
+ /**
53
+ * @param {unknown} raw_query_value
54
+ * @returns {Record<string, unknown> | null}
55
+ */
56
+ export function rawQueryValueToRecord(raw_query_value: unknown): Record<string, unknown> | null;
57
+ export type StoredQueryMutationResult = {
58
+ action: "added";
59
+ name: string;
60
+ } | {
61
+ action: "removed";
62
+ name: string;
63
+ } | {
64
+ action: "updated";
65
+ name: string;
66
+ previous_name?: string;
67
+ };
68
+ import type { PatramDiagnostic } from './load-patram-config.types.d.ts';
69
+ import type { StoredQueryConfig } from './load-patram-config.types.d.ts';