patram 0.9.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.
Files changed (33) hide show
  1. package/lib/cli/arguments.types.d.ts +64 -0
  2. package/lib/cli/commands/check.js +27 -15
  3. package/lib/cli/commands/queries.js +189 -1
  4. package/lib/cli/commands/query.js +6 -3
  5. package/lib/cli/help-metadata.js +45 -110
  6. package/lib/cli/parse-arguments-helpers.js +295 -39
  7. package/lib/cli/render-help.js +87 -0
  8. package/lib/config/load-patram-config.d.ts +11 -0
  9. package/lib/config/load-patram-config.js +9 -88
  10. package/lib/config/manage-stored-queries-helpers.d.ts +69 -0
  11. package/lib/config/manage-stored-queries-helpers.js +262 -0
  12. package/lib/config/manage-stored-queries-jsonc.d.ts +31 -0
  13. package/lib/config/manage-stored-queries-jsonc.js +95 -0
  14. package/lib/config/manage-stored-queries.d.ts +77 -0
  15. package/lib/config/manage-stored-queries.js +294 -0
  16. package/lib/config/schema.d.ts +2 -0
  17. package/lib/config/schema.js +4 -0
  18. package/lib/config/validate-patram-config-value.d.ts +13 -0
  19. package/lib/config/validate-patram-config-value.js +119 -0
  20. package/lib/find-close-match.d.ts +8 -0
  21. package/lib/find-close-match.js +98 -0
  22. package/lib/graph/query/resolve.d.ts +9 -5
  23. package/lib/graph/query/resolve.js +41 -4
  24. package/lib/output/layout-stored-queries.js +18 -2
  25. package/lib/output/list-queries.js +2 -1
  26. package/lib/output/renderers/json.js +9 -5
  27. package/lib/output/renderers/plain.js +15 -26
  28. package/lib/output/renderers/rich.js +22 -26
  29. package/lib/output/resolve-check-target.js +120 -11
  30. package/lib/output/view-model/index.js +5 -18
  31. package/lib/patram.d.ts +8 -0
  32. package/lib/scan/discover-fields.js +136 -10
  33. package/package.json +2 -1
@@ -0,0 +1,294 @@
1
+ /**
2
+ * @import { CliParseError } from '../cli/arguments.types.ts';
3
+ * @import {
4
+ * PatramDiagnostic,
5
+ * PatramRepoConfig,
6
+ * } from './load-patram-config.types.ts';
7
+ */
8
+
9
+ import { resolve } from 'node:path';
10
+
11
+ import { loadPatramConfig } from './load-patram-config.js';
12
+ import {
13
+ createStoredQueryDefinition,
14
+ createUpdatedStoredQueryDefinition,
15
+ ensureRawQueries,
16
+ loadRawConfig,
17
+ persistStoredQueryMutation,
18
+ rawQueryValueToRecord,
19
+ } from './manage-stored-queries-helpers.js';
20
+ import { CONFIG_FILE_NAME } from './schema.js';
21
+ import { createUnknownStoredQueryError } from '../graph/query/resolve.js';
22
+
23
+ /**
24
+ * @typedef {{
25
+ * action: 'add',
26
+ * description?: string,
27
+ * name: string,
28
+ * where: string,
29
+ * } | {
30
+ * action: 'remove',
31
+ * name: string,
32
+ * } | {
33
+ * action: 'update',
34
+ * description?: string,
35
+ * name: string,
36
+ * next_name?: string,
37
+ * where?: string,
38
+ * }} StoredQueryMutation
39
+ */
40
+
41
+ /**
42
+ * @typedef {{
43
+ * action: 'added',
44
+ * name: string,
45
+ * } | {
46
+ * action: 'removed',
47
+ * name: string,
48
+ * } | {
49
+ * action: 'updated',
50
+ * name: string,
51
+ * previous_name?: string,
52
+ * }} StoredQueryMutationResult
53
+ */
54
+
55
+ /**
56
+ * @param {string} project_directory
57
+ * @param {StoredQueryMutation} stored_query_mutation
58
+ * @returns {Promise<
59
+ * | { success: true, value: StoredQueryMutationResult }
60
+ * | { diagnostics: PatramDiagnostic[], success: false }
61
+ * | { error: CliParseError, success: false }
62
+ * >}
63
+ */
64
+ export async function manageStoredQueries(
65
+ project_directory,
66
+ stored_query_mutation,
67
+ ) {
68
+ const load_result = await loadPatramConfig(project_directory);
69
+
70
+ if (load_result.diagnostics.length > 0) {
71
+ return {
72
+ diagnostics: load_result.diagnostics,
73
+ success: false,
74
+ };
75
+ }
76
+
77
+ const repo_config = load_result.config;
78
+
79
+ if (!repo_config) {
80
+ throw new Error('Expected a valid Patram repo config.');
81
+ }
82
+
83
+ const config_file_path = resolve(project_directory, CONFIG_FILE_NAME);
84
+ const raw_config_result = await loadRawConfig(config_file_path);
85
+
86
+ if (!raw_config_result.success) {
87
+ return {
88
+ diagnostics: [raw_config_result.diagnostic],
89
+ success: false,
90
+ };
91
+ }
92
+
93
+ if (stored_query_mutation.action === 'add') {
94
+ return applyAddStoredQuery(
95
+ config_file_path,
96
+ raw_config_result.value,
97
+ repo_config,
98
+ stored_query_mutation,
99
+ );
100
+ }
101
+
102
+ if (stored_query_mutation.action === 'remove') {
103
+ return applyRemoveStoredQuery(
104
+ config_file_path,
105
+ raw_config_result.value,
106
+ repo_config,
107
+ stored_query_mutation,
108
+ );
109
+ }
110
+
111
+ return applyUpdateStoredQuery(
112
+ config_file_path,
113
+ raw_config_result.value,
114
+ repo_config,
115
+ stored_query_mutation,
116
+ );
117
+ }
118
+
119
+ /**
120
+ * @param {string} config_file_path
121
+ * @param {Record<string, unknown>} raw_config
122
+ * @param {PatramRepoConfig} repo_config
123
+ * @param {{ description?: string, name: string, where: string }} stored_query_mutation
124
+ * @returns {Promise<
125
+ * | { success: true, value: StoredQueryMutationResult }
126
+ * | { diagnostics: PatramDiagnostic[], success: false }
127
+ * | { error: CliParseError, success: false }
128
+ * >}
129
+ */
130
+ async function applyAddStoredQuery(
131
+ config_file_path,
132
+ raw_config,
133
+ repo_config,
134
+ stored_query_mutation,
135
+ ) {
136
+ if (repo_config.queries[stored_query_mutation.name]) {
137
+ return {
138
+ error: {
139
+ code: 'message',
140
+ message: `Stored query already exists: ${stored_query_mutation.name}.`,
141
+ },
142
+ success: false,
143
+ };
144
+ }
145
+
146
+ const raw_queries = ensureRawQueries(raw_config);
147
+ raw_queries[stored_query_mutation.name] = createStoredQueryDefinition(
148
+ stored_query_mutation.where,
149
+ stored_query_mutation.description,
150
+ );
151
+
152
+ return persistStoredQueryMutation(config_file_path, raw_config, {
153
+ action: 'added',
154
+ name: stored_query_mutation.name,
155
+ });
156
+ }
157
+
158
+ /**
159
+ * @param {string} config_file_path
160
+ * @param {Record<string, unknown>} raw_config
161
+ * @param {PatramRepoConfig} repo_config
162
+ * @param {{ name: string }} stored_query_mutation
163
+ * @returns {Promise<
164
+ * | { success: true, value: StoredQueryMutationResult }
165
+ * | { diagnostics: PatramDiagnostic[], success: false }
166
+ * | { error: CliParseError, success: false }
167
+ * >}
168
+ */
169
+ async function applyRemoveStoredQuery(
170
+ config_file_path,
171
+ raw_config,
172
+ repo_config,
173
+ stored_query_mutation,
174
+ ) {
175
+ if (!repo_config.queries[stored_query_mutation.name]) {
176
+ return {
177
+ error: createQueryMutationUnknownStoredQueryError(
178
+ stored_query_mutation.name,
179
+ Object.keys(repo_config.queries),
180
+ 'remove',
181
+ ),
182
+ success: false,
183
+ };
184
+ }
185
+
186
+ const raw_queries = ensureRawQueries(raw_config);
187
+ delete raw_queries[stored_query_mutation.name];
188
+
189
+ return persistStoredQueryMutation(config_file_path, raw_config, {
190
+ action: 'removed',
191
+ name: stored_query_mutation.name,
192
+ });
193
+ }
194
+
195
+ /**
196
+ * @param {string} config_file_path
197
+ * @param {Record<string, unknown>} raw_config
198
+ * @param {PatramRepoConfig} repo_config
199
+ * @param {{ description?: string, name: string, next_name?: string, where?: string }} stored_query_mutation
200
+ * @returns {Promise<
201
+ * | { success: true, value: StoredQueryMutationResult }
202
+ * | { diagnostics: PatramDiagnostic[], success: false }
203
+ * | { error: CliParseError, success: false }
204
+ * >}
205
+ */
206
+ async function applyUpdateStoredQuery(
207
+ config_file_path,
208
+ raw_config,
209
+ repo_config,
210
+ stored_query_mutation,
211
+ ) {
212
+ const existing_query = repo_config.queries[stored_query_mutation.name];
213
+
214
+ if (!existing_query) {
215
+ return {
216
+ error: createQueryMutationUnknownStoredQueryError(
217
+ stored_query_mutation.name,
218
+ Object.keys(repo_config.queries),
219
+ 'update',
220
+ ),
221
+ success: false,
222
+ };
223
+ }
224
+
225
+ const next_name =
226
+ stored_query_mutation.next_name ?? stored_query_mutation.name;
227
+
228
+ if (
229
+ next_name !== stored_query_mutation.name &&
230
+ repo_config.queries[next_name]
231
+ ) {
232
+ return {
233
+ error: {
234
+ code: 'message',
235
+ message: `Stored query already exists: ${next_name}.`,
236
+ },
237
+ success: false,
238
+ };
239
+ }
240
+
241
+ const raw_queries = ensureRawQueries(raw_config);
242
+ const raw_query_value = rawQueryValueToRecord(
243
+ raw_queries[stored_query_mutation.name],
244
+ );
245
+ const next_query = createUpdatedStoredQueryDefinition(
246
+ raw_query_value,
247
+ existing_query,
248
+ stored_query_mutation,
249
+ );
250
+
251
+ if (next_name !== stored_query_mutation.name) {
252
+ delete raw_queries[stored_query_mutation.name];
253
+ }
254
+
255
+ raw_queries[next_name] = next_query;
256
+
257
+ return persistStoredQueryMutation(config_file_path, raw_config, {
258
+ action: 'updated',
259
+ name: next_name,
260
+ previous_name:
261
+ next_name === stored_query_mutation.name
262
+ ? undefined
263
+ : stored_query_mutation.name,
264
+ });
265
+ }
266
+
267
+ /**
268
+ * @param {string} stored_query_name
269
+ * @param {string[]} stored_query_names
270
+ * @param {'remove' | 'update'} subcommand_name
271
+ * @returns {CliParseError}
272
+ */
273
+ function createQueryMutationUnknownStoredQueryError(
274
+ stored_query_name,
275
+ stored_query_names,
276
+ subcommand_name,
277
+ ) {
278
+ const parse_error = createUnknownStoredQueryError(
279
+ stored_query_name,
280
+ stored_query_names,
281
+ );
282
+
283
+ if (
284
+ parse_error.code !== 'unknown_stored_query' ||
285
+ parse_error.suggestion === undefined
286
+ ) {
287
+ return parse_error;
288
+ }
289
+
290
+ return {
291
+ ...parse_error,
292
+ next_path: `patram queries ${subcommand_name} ${parse_error.suggestion}`,
293
+ };
294
+ }
@@ -141,6 +141,7 @@ export const patram_repo_config_schema: z.ZodObject<{
141
141
  prefixes: z.ZodArray<z.ZodString>;
142
142
  }, z.core.$strict>>>;
143
143
  queries: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
144
+ description: z.ZodOptional<z.ZodString>;
144
145
  where: z.ZodString;
145
146
  }, z.core.$strict>>>;
146
147
  relations: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
@@ -167,6 +168,7 @@ import { z } from 'zod';
167
168
  * @typedef {z.output<typeof stored_query_schema>} StoredQueryConfig
168
169
  */
169
170
  declare const stored_query_schema: z.ZodObject<{
171
+ description: z.ZodOptional<z.ZodString>;
170
172
  where: z.ZodString;
171
173
  }, z.core.$strict>;
172
174
  /**
@@ -27,6 +27,10 @@ const MIXED_STYLE_VALUES = new Set(['ignore', 'error']);
27
27
  */
28
28
  const stored_query_schema = z
29
29
  .object({
30
+ description: z
31
+ .string()
32
+ .min(1, 'Stored query "description" must not be empty.')
33
+ .optional(),
30
34
  where: z.string().min(1, 'Stored query "where" must not be empty.'),
31
35
  })
32
36
  .strict();
@@ -0,0 +1,13 @@
1
+ /**
2
+ * @param {unknown} config_value
3
+ * @returns {{ config: PatramRepoConfig, success: true } | { diagnostics: PatramDiagnostic[], success: false }}
4
+ */
5
+ export function validatePatramConfigValue(config_value: unknown): {
6
+ config: PatramRepoConfig;
7
+ success: true;
8
+ } | {
9
+ diagnostics: PatramDiagnostic[];
10
+ success: false;
11
+ };
12
+ import type { PatramRepoConfig } from './load-patram-config.types.d.ts';
13
+ import type { PatramDiagnostic } from './load-patram-config.types.d.ts';
@@ -0,0 +1,119 @@
1
+ /**
2
+ * @import {
3
+ * PatramDiagnostic,
4
+ * PatramRepoConfig,
5
+ * } from './load-patram-config.types.ts';
6
+ */
7
+
8
+ import { patram_repo_config_schema } from './schema.js';
9
+ import { normalizeRepoConfig } from './defaults.js';
10
+ import {
11
+ validateDerivedSummaries,
12
+ validateFieldSchemaConfig,
13
+ validateGraphSchema,
14
+ validateLegacyConfigShape,
15
+ validateStoredQueries,
16
+ } from './validation.js';
17
+
18
+ /**
19
+ * @param {unknown} config_value
20
+ * @returns {{ config: PatramRepoConfig, success: true } | { diagnostics: PatramDiagnostic[], success: false }}
21
+ */
22
+ export function validatePatramConfigValue(config_value) {
23
+ const legacy_config_diagnostics = validateLegacyConfigShape(config_value);
24
+
25
+ if (legacy_config_diagnostics.length > 0) {
26
+ return {
27
+ diagnostics: legacy_config_diagnostics,
28
+ success: false,
29
+ };
30
+ }
31
+
32
+ const config_result = patram_repo_config_schema.safeParse(config_value);
33
+
34
+ if (!config_result.success) {
35
+ return {
36
+ diagnostics: config_result.error.issues.map(createValidationDiagnostic),
37
+ success: false,
38
+ };
39
+ }
40
+
41
+ const graph_schema_diagnostics = validateGraphSchema(config_result.data);
42
+
43
+ if (graph_schema_diagnostics.length > 0) {
44
+ return {
45
+ diagnostics: graph_schema_diagnostics,
46
+ success: false,
47
+ };
48
+ }
49
+
50
+ const normalized_config = normalizeRepoConfig(config_result.data);
51
+ const field_schema_diagnostics = validateFieldSchemaConfig(normalized_config);
52
+
53
+ if (field_schema_diagnostics.length > 0) {
54
+ return {
55
+ diagnostics: field_schema_diagnostics,
56
+ success: false,
57
+ };
58
+ }
59
+
60
+ const stored_query_diagnostics = validateStoredQueries(normalized_config);
61
+
62
+ if (stored_query_diagnostics.length > 0) {
63
+ return {
64
+ diagnostics: stored_query_diagnostics,
65
+ success: false,
66
+ };
67
+ }
68
+
69
+ const derived_summary_diagnostics =
70
+ validateDerivedSummaries(normalized_config);
71
+
72
+ if (derived_summary_diagnostics.length > 0) {
73
+ return {
74
+ diagnostics: derived_summary_diagnostics,
75
+ success: false,
76
+ };
77
+ }
78
+
79
+ return {
80
+ config: normalized_config,
81
+ success: true,
82
+ };
83
+ }
84
+
85
+ /**
86
+ * @param {import('zod').core.$ZodIssue} issue
87
+ * @returns {PatramDiagnostic}
88
+ */
89
+ function createValidationDiagnostic(issue) {
90
+ const issue_path = formatIssuePath(issue.path);
91
+
92
+ if (issue_path) {
93
+ return {
94
+ code: 'config.invalid',
95
+ column: 1,
96
+ level: 'error',
97
+ line: 1,
98
+ message: `Invalid config at "${issue_path}": ${issue.message}`,
99
+ path: '.patram.json',
100
+ };
101
+ }
102
+
103
+ return {
104
+ code: 'config.invalid',
105
+ column: 1,
106
+ level: 'error',
107
+ line: 1,
108
+ message: `Invalid config: ${issue.message}`,
109
+ path: '.patram.json',
110
+ };
111
+ }
112
+
113
+ /**
114
+ * @param {(string | number | symbol | undefined)[]} issue_path
115
+ * @returns {string}
116
+ */
117
+ function formatIssuePath(issue_path) {
118
+ return issue_path.map(String).join('.');
119
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Find the closest candidate above the shared suggestion threshold.
3
+ *
4
+ * @param {string} input_text
5
+ * @param {readonly string[]} candidates
6
+ * @returns {string | undefined}
7
+ */
8
+ export function findCloseMatch(input_text: string, candidates: readonly string[]): string | undefined;
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Find the closest candidate above the shared suggestion threshold.
3
+ *
4
+ * @param {string} input_text
5
+ * @param {readonly string[]} candidates
6
+ * @returns {string | undefined}
7
+ */
8
+ export function findCloseMatch(input_text, candidates) {
9
+ let best_candidate;
10
+ let best_score = 0;
11
+
12
+ for (const candidate of candidates) {
13
+ const score = scoreCandidate(input_text, candidate);
14
+
15
+ if (score > best_score) {
16
+ best_candidate = candidate;
17
+ best_score = score;
18
+ }
19
+ }
20
+
21
+ if (best_score < 0.6) {
22
+ return undefined;
23
+ }
24
+
25
+ return best_candidate;
26
+ }
27
+
28
+ /**
29
+ * @param {string} input_text
30
+ * @param {string} candidate
31
+ * @returns {number}
32
+ */
33
+ function scoreCandidate(input_text, candidate) {
34
+ const max_length = Math.max(input_text.length, candidate.length);
35
+
36
+ if (max_length === 0) {
37
+ return 1;
38
+ }
39
+
40
+ return (
41
+ 1 - calculateDamerauLevenshteinDistance(input_text, candidate) / max_length
42
+ );
43
+ }
44
+
45
+ /**
46
+ * @param {string} left_text
47
+ * @param {string} right_text
48
+ * @returns {number}
49
+ */
50
+ function calculateDamerauLevenshteinDistance(left_text, right_text) {
51
+ /** @type {number[][]} */
52
+ const matrix = Array.from({ length: left_text.length + 1 }, () =>
53
+ Array.from({ length: right_text.length + 1 }, () => 0),
54
+ );
55
+
56
+ for (let left_index = 0; left_index <= left_text.length; left_index += 1) {
57
+ matrix[left_index][0] = left_index;
58
+ }
59
+
60
+ for (
61
+ let right_index = 0;
62
+ right_index <= right_text.length;
63
+ right_index += 1
64
+ ) {
65
+ matrix[0][right_index] = right_index;
66
+ }
67
+
68
+ for (let left_index = 1; left_index <= left_text.length; left_index += 1) {
69
+ for (
70
+ let right_index = 1;
71
+ right_index <= right_text.length;
72
+ right_index += 1
73
+ ) {
74
+ const substitution_cost =
75
+ left_text[left_index - 1] === right_text[right_index - 1] ? 0 : 1;
76
+
77
+ matrix[left_index][right_index] = Math.min(
78
+ matrix[left_index - 1][right_index] + 1,
79
+ matrix[left_index][right_index - 1] + 1,
80
+ matrix[left_index - 1][right_index - 1] + substitution_cost,
81
+ );
82
+
83
+ if (
84
+ left_index > 1 &&
85
+ right_index > 1 &&
86
+ left_text[left_index - 1] === right_text[right_index - 2] &&
87
+ left_text[left_index - 2] === right_text[right_index - 1]
88
+ ) {
89
+ matrix[left_index][right_index] = Math.min(
90
+ matrix[left_index][right_index],
91
+ matrix[left_index - 2][right_index - 2] + substitution_cost,
92
+ );
93
+ }
94
+ }
95
+ }
96
+
97
+ return matrix[left_text.length][right_text.length];
98
+ }
@@ -1,6 +1,3 @@
1
- /**
2
- * @import { PatramRepoConfig } from '../../config/load-patram-config.types.d.ts';
3
- */
4
1
  /**
5
2
  * @typedef {{ kind: 'ad_hoc' } | { kind: 'stored_query', name: string }} QuerySource
6
3
  */
@@ -9,7 +6,7 @@
9
6
  *
10
7
  * @param {PatramRepoConfig} repo_config
11
8
  * @param {string[]} command_arguments
12
- * @returns {{ success: true, value: { query_source: QuerySource, where_clause: string } } | { success: false, message: string }}
9
+ * @returns {{ success: true, value: { query_source: QuerySource, where_clause: string } } | { error: CliParseError, success: false }}
13
10
  */
14
11
  export function resolveWhereClause(repo_config: PatramRepoConfig, command_arguments: string[]): {
15
12
  success: true;
@@ -18,9 +15,15 @@ export function resolveWhereClause(repo_config: PatramRepoConfig, command_argume
18
15
  where_clause: string;
19
16
  };
20
17
  } | {
18
+ error: CliParseError;
21
19
  success: false;
22
- message: string;
23
20
  };
21
+ /**
22
+ * @param {string} stored_query_name
23
+ * @param {string[]} stored_query_names
24
+ * @returns {CliParseError}
25
+ */
26
+ export function createUnknownStoredQueryError(stored_query_name: string, stored_query_names: string[]): CliParseError;
24
27
  export type QuerySource = {
25
28
  kind: "ad_hoc";
26
29
  } | {
@@ -28,3 +31,4 @@ export type QuerySource = {
28
31
  name: string;
29
32
  };
30
33
  import type { PatramRepoConfig } from '../../config/load-patram-config.types.d.ts';
34
+ import type { CliParseError } from '../../cli/arguments.types.d.ts';
@@ -1,7 +1,10 @@
1
1
  /**
2
+ * @import { CliParseError } from '../../cli/arguments.types.ts';
2
3
  * @import { PatramRepoConfig } from '../../config/load-patram-config.types.ts';
3
4
  */
4
5
 
6
+ import { findCloseMatch } from '../../find-close-match.js';
7
+
5
8
  /**
6
9
  * @typedef {{ kind: 'ad_hoc' } | { kind: 'stored_query', name: string }} QuerySource
7
10
  */
@@ -11,7 +14,7 @@
11
14
  *
12
15
  * @param {PatramRepoConfig} repo_config
13
16
  * @param {string[]} command_arguments
14
- * @returns {{ success: true, value: { query_source: QuerySource, where_clause: string } } | { success: false, message: string }}
17
+ * @returns {{ success: true, value: { query_source: QuerySource, where_clause: string } } | { error: CliParseError, success: false }}
15
18
  */
16
19
  export function resolveWhereClause(repo_config, command_arguments) {
17
20
  if (command_arguments[0] === '--where') {
@@ -19,7 +22,10 @@ export function resolveWhereClause(repo_config, command_arguments) {
19
22
 
20
23
  if (where_clause.length === 0) {
21
24
  return {
22
- message: 'Query requires a where clause.',
25
+ error: {
26
+ code: 'message',
27
+ message: 'Query requires a where clause.',
28
+ },
23
29
  success: false,
24
30
  };
25
31
  }
@@ -39,7 +45,10 @@ export function resolveWhereClause(repo_config, command_arguments) {
39
45
 
40
46
  if (!stored_query_name) {
41
47
  return {
42
- message: 'Query requires "--where" or a stored query name.',
48
+ error: {
49
+ code: 'message',
50
+ message: 'Query requires "--where" or a stored query name.',
51
+ },
43
52
  success: false,
44
53
  };
45
54
  }
@@ -48,7 +57,10 @@ export function resolveWhereClause(repo_config, command_arguments) {
48
57
 
49
58
  if (!stored_query) {
50
59
  return {
51
- message: `Stored query "${stored_query_name}" was not found.`,
60
+ error: createUnknownStoredQueryError(
61
+ stored_query_name,
62
+ Object.keys(repo_config.queries),
63
+ ),
52
64
  success: false,
53
65
  };
54
66
  }
@@ -64,3 +76,28 @@ export function resolveWhereClause(repo_config, command_arguments) {
64
76
  },
65
77
  };
66
78
  }
79
+
80
+ /**
81
+ * @param {string} stored_query_name
82
+ * @param {string[]} stored_query_names
83
+ * @returns {CliParseError}
84
+ */
85
+ export function createUnknownStoredQueryError(
86
+ stored_query_name,
87
+ stored_query_names,
88
+ ) {
89
+ const suggestion = findCloseMatch(stored_query_name, stored_query_names);
90
+
91
+ if (!suggestion) {
92
+ return {
93
+ code: 'unknown_stored_query',
94
+ name: stored_query_name,
95
+ };
96
+ }
97
+
98
+ return {
99
+ code: 'unknown_stored_query',
100
+ name: stored_query_name,
101
+ suggestion,
102
+ };
103
+ }