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,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';
@@ -0,0 +1,262 @@
1
+ /**
2
+ * @import {
3
+ * PatramDiagnostic,
4
+ * StoredQueryConfig,
5
+ * } from './load-patram-config.types.ts';
6
+ */
7
+
8
+ import { readFile, writeFile } from 'node:fs/promises';
9
+
10
+ import { parsePatramConfigSource } from './load-patram-config.js';
11
+ import { applyStoredQueryMutationToConfigSource } from './manage-stored-queries-jsonc.js';
12
+ import { CONFIG_FILE_NAME } from './schema.js';
13
+ import { validatePatramConfigValue } from './validate-patram-config-value.js';
14
+
15
+ /**
16
+ * @param {string} config_file_path
17
+ * @returns {Promise<
18
+ * | { success: true, value: Record<string, unknown> }
19
+ * | { diagnostic: PatramDiagnostic, success: false }
20
+ * >}
21
+ */
22
+ export async function loadRawConfig(config_file_path) {
23
+ const config_source = await readConfigSource(config_file_path);
24
+
25
+ if (config_source === null) {
26
+ return {
27
+ success: true,
28
+ value: {},
29
+ };
30
+ }
31
+
32
+ const parse_result = parsePatramConfigSource(config_source);
33
+
34
+ if (!parse_result.success) {
35
+ return parse_result;
36
+ }
37
+
38
+ if (
39
+ parse_result.value === null ||
40
+ typeof parse_result.value !== 'object' ||
41
+ Array.isArray(parse_result.value)
42
+ ) {
43
+ return {
44
+ diagnostic: createInvalidTopLevelConfigDiagnostic(),
45
+ success: false,
46
+ };
47
+ }
48
+
49
+ return {
50
+ success: true,
51
+ value: /** @type {Record<string, unknown>} */ (parse_result.value),
52
+ };
53
+ }
54
+
55
+ /**
56
+ * @param {string} config_file_path
57
+ * @param {Record<string, unknown>} raw_config
58
+ * @param {StoredQueryMutationResult} mutation_result
59
+ * @returns {Promise<
60
+ * | { success: true, value: StoredQueryMutationResult }
61
+ * | { diagnostics: PatramDiagnostic[], success: false }
62
+ * >}
63
+ */
64
+ export async function persistStoredQueryMutation(
65
+ config_file_path,
66
+ raw_config,
67
+ mutation_result,
68
+ ) {
69
+ const validation_result = validatePatramConfigValue(raw_config);
70
+
71
+ if (!validation_result.success) {
72
+ return {
73
+ diagnostics: validation_result.diagnostics,
74
+ success: false,
75
+ };
76
+ }
77
+
78
+ const config_source = (await readConfigSource(config_file_path)) ?? '{}\n';
79
+ const raw_queries = ensureRawQueries(raw_config);
80
+ const next_config_source = applyStoredQueryMutationToConfigSource(
81
+ config_source,
82
+ raw_queries,
83
+ mutation_result,
84
+ );
85
+
86
+ await writeFile(config_file_path, next_config_source);
87
+
88
+ return {
89
+ success: true,
90
+ value: mutation_result,
91
+ };
92
+ }
93
+
94
+ /**
95
+ * @param {string} where_clause
96
+ * @param {string | undefined} description
97
+ * @returns {StoredQueryConfig}
98
+ */
99
+ export function createStoredQueryDefinition(where_clause, description) {
100
+ /** @type {StoredQueryConfig} */
101
+ const stored_query = {
102
+ where: where_clause,
103
+ };
104
+ const normalized_description = normalizeDescription(description);
105
+
106
+ if (normalized_description !== undefined) {
107
+ stored_query.description = normalized_description;
108
+ }
109
+
110
+ return stored_query;
111
+ }
112
+
113
+ /**
114
+ * @param {Record<string, unknown> | null} raw_query_value
115
+ * @param {StoredQueryConfig} existing_query
116
+ * @param {{ description?: string, where?: string }} stored_query_mutation
117
+ * @returns {StoredQueryConfig}
118
+ */
119
+ export function createUpdatedStoredQueryDefinition(
120
+ raw_query_value,
121
+ existing_query,
122
+ stored_query_mutation,
123
+ ) {
124
+ /** @type {StoredQueryConfig} */
125
+ const next_query = {
126
+ where: existing_query.where,
127
+ };
128
+
129
+ if (
130
+ raw_query_value &&
131
+ Object.hasOwn(raw_query_value, 'description') &&
132
+ typeof raw_query_value.description === 'string' &&
133
+ raw_query_value.description.length > 0
134
+ ) {
135
+ next_query.description = raw_query_value.description;
136
+ } else if (
137
+ typeof existing_query.description === 'string' &&
138
+ existing_query.description.length > 0
139
+ ) {
140
+ next_query.description = existing_query.description;
141
+ }
142
+
143
+ if (stored_query_mutation.where !== undefined) {
144
+ next_query.where = stored_query_mutation.where;
145
+ }
146
+
147
+ if (stored_query_mutation.description !== undefined) {
148
+ if (stored_query_mutation.description.length === 0) {
149
+ delete next_query.description;
150
+ } else {
151
+ next_query.description = stored_query_mutation.description;
152
+ }
153
+ }
154
+
155
+ return next_query;
156
+ }
157
+
158
+ /**
159
+ * @param {Record<string, unknown>} raw_config
160
+ * @returns {Record<string, unknown>}
161
+ */
162
+ export function ensureRawQueries(raw_config) {
163
+ const raw_queries_value = raw_config.queries;
164
+
165
+ if (
166
+ raw_queries_value !== null &&
167
+ typeof raw_queries_value === 'object' &&
168
+ !Array.isArray(raw_queries_value)
169
+ ) {
170
+ return /** @type {Record<string, unknown>} */ (raw_queries_value);
171
+ }
172
+
173
+ /** @type {Record<string, unknown>} */
174
+ const raw_queries = {};
175
+ raw_config.queries = raw_queries;
176
+
177
+ return raw_queries;
178
+ }
179
+
180
+ /**
181
+ * @param {unknown} raw_query_value
182
+ * @returns {Record<string, unknown> | null}
183
+ */
184
+ export function rawQueryValueToRecord(raw_query_value) {
185
+ if (
186
+ raw_query_value === null ||
187
+ typeof raw_query_value !== 'object' ||
188
+ Array.isArray(raw_query_value)
189
+ ) {
190
+ return null;
191
+ }
192
+
193
+ return /** @type {Record<string, unknown>} */ (raw_query_value);
194
+ }
195
+
196
+ /**
197
+ * @param {string | undefined} description
198
+ * @returns {string | undefined}
199
+ */
200
+ function normalizeDescription(description) {
201
+ if (description === undefined || description.length === 0) {
202
+ return undefined;
203
+ }
204
+
205
+ return description;
206
+ }
207
+
208
+ /**
209
+ * @returns {PatramDiagnostic}
210
+ */
211
+ function createInvalidTopLevelConfigDiagnostic() {
212
+ return {
213
+ code: 'config.invalid',
214
+ column: 1,
215
+ level: 'error',
216
+ line: 1,
217
+ message: 'Invalid config: Expected a top-level object.',
218
+ path: CONFIG_FILE_NAME,
219
+ };
220
+ }
221
+
222
+ /**
223
+ * @param {string} config_file_path
224
+ * @returns {Promise<string | null>}
225
+ */
226
+ async function readConfigSource(config_file_path) {
227
+ try {
228
+ return await readFile(config_file_path, 'utf8');
229
+ } catch (error) {
230
+ if (isMissingFileError(error)) {
231
+ return null;
232
+ }
233
+
234
+ throw error;
235
+ }
236
+ }
237
+
238
+ /**
239
+ * @param {unknown} error
240
+ * @returns {error is NodeJS.ErrnoException}
241
+ */
242
+ function isMissingFileError(error) {
243
+ if (!(error instanceof Error)) {
244
+ return false;
245
+ }
246
+
247
+ return 'code' in error && error.code === 'ENOENT';
248
+ }
249
+
250
+ /**
251
+ * @typedef {{
252
+ * action: 'added',
253
+ * name: string,
254
+ * } | {
255
+ * action: 'removed',
256
+ * name: string,
257
+ * } | {
258
+ * action: 'updated',
259
+ * name: string,
260
+ * previous_name?: string,
261
+ * }} StoredQueryMutationResult
262
+ */
@@ -0,0 +1,31 @@
1
+ /**
2
+ * @typedef {{
3
+ * action: 'added',
4
+ * name: string,
5
+ * } | {
6
+ * action: 'removed',
7
+ * name: string,
8
+ * } | {
9
+ * action: 'updated',
10
+ * name: string,
11
+ * previous_name?: string,
12
+ * }} StoredQueryMutationResult
13
+ */
14
+ /**
15
+ * @param {string} config_source
16
+ * @param {Record<string, unknown>} raw_queries
17
+ * @param {StoredQueryMutationResult} mutation_result
18
+ * @returns {string}
19
+ */
20
+ export function applyStoredQueryMutationToConfigSource(config_source: string, raw_queries: Record<string, unknown>, mutation_result: StoredQueryMutationResult): string;
21
+ export type StoredQueryMutationResult = {
22
+ action: "added";
23
+ name: string;
24
+ } | {
25
+ action: "removed";
26
+ name: string;
27
+ } | {
28
+ action: "updated";
29
+ name: string;
30
+ previous_name?: string;
31
+ };
@@ -0,0 +1,95 @@
1
+ /**
2
+ * @import { FormattingOptions } from 'jsonc-parser';
3
+ */
4
+
5
+ import { applyEdits, modify } from 'jsonc-parser';
6
+
7
+ /**
8
+ * @typedef {{
9
+ * action: 'added',
10
+ * name: string,
11
+ * } | {
12
+ * action: 'removed',
13
+ * name: string,
14
+ * } | {
15
+ * action: 'updated',
16
+ * name: string,
17
+ * previous_name?: string,
18
+ * }} StoredQueryMutationResult
19
+ */
20
+
21
+ /**
22
+ * @param {string} config_source
23
+ * @param {Record<string, unknown>} raw_queries
24
+ * @param {StoredQueryMutationResult} mutation_result
25
+ * @returns {string}
26
+ */
27
+ export function applyStoredQueryMutationToConfigSource(
28
+ config_source,
29
+ raw_queries,
30
+ mutation_result,
31
+ ) {
32
+ const formatting_options = inferFormattingOptions(config_source);
33
+ let next_config_source = config_source;
34
+
35
+ if (
36
+ mutation_result.action === 'updated' &&
37
+ mutation_result.previous_name !== undefined
38
+ ) {
39
+ next_config_source = applyConfigEdit(
40
+ next_config_source,
41
+ ['queries', mutation_result.previous_name],
42
+ undefined,
43
+ formatting_options,
44
+ );
45
+ }
46
+
47
+ if (mutation_result.action === 'removed') {
48
+ return applyConfigEdit(
49
+ next_config_source,
50
+ ['queries', mutation_result.name],
51
+ undefined,
52
+ formatting_options,
53
+ );
54
+ }
55
+
56
+ return applyConfigEdit(
57
+ next_config_source,
58
+ ['queries', mutation_result.name],
59
+ raw_queries[mutation_result.name],
60
+ formatting_options,
61
+ );
62
+ }
63
+
64
+ /**
65
+ * @param {string} config_source
66
+ * @returns {FormattingOptions}
67
+ */
68
+ function inferFormattingOptions(config_source) {
69
+ const indentation_match = config_source.match(/^(?<indentation>[ \t]+)\S/mu);
70
+ const indentation = indentation_match?.groups?.indentation ?? ' ';
71
+ const insert_spaces = !indentation.includes('\t');
72
+
73
+ return {
74
+ eol: config_source.includes('\r\n') ? '\r\n' : '\n',
75
+ insertFinalNewline: config_source.endsWith('\n'),
76
+ insertSpaces: insert_spaces,
77
+ tabSize: insert_spaces ? indentation.length : 1,
78
+ };
79
+ }
80
+
81
+ /**
82
+ * @param {string} config_source
83
+ * @param {(string | number)[]} path
84
+ * @param {unknown} value
85
+ * @param {FormattingOptions} formatting_options
86
+ * @returns {string}
87
+ */
88
+ function applyConfigEdit(config_source, path, value, formatting_options) {
89
+ return applyEdits(
90
+ config_source,
91
+ modify(config_source, path, value, {
92
+ formattingOptions: formatting_options,
93
+ }),
94
+ );
95
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * @typedef {{
3
+ * action: 'add',
4
+ * description?: string,
5
+ * name: string,
6
+ * where: string,
7
+ * } | {
8
+ * action: 'remove',
9
+ * name: string,
10
+ * } | {
11
+ * action: 'update',
12
+ * description?: string,
13
+ * name: string,
14
+ * next_name?: string,
15
+ * where?: string,
16
+ * }} StoredQueryMutation
17
+ */
18
+ /**
19
+ * @typedef {{
20
+ * action: 'added',
21
+ * name: string,
22
+ * } | {
23
+ * action: 'removed',
24
+ * name: string,
25
+ * } | {
26
+ * action: 'updated',
27
+ * name: string,
28
+ * previous_name?: string,
29
+ * }} StoredQueryMutationResult
30
+ */
31
+ /**
32
+ * @param {string} project_directory
33
+ * @param {StoredQueryMutation} stored_query_mutation
34
+ * @returns {Promise<
35
+ * | { success: true, value: StoredQueryMutationResult }
36
+ * | { diagnostics: PatramDiagnostic[], success: false }
37
+ * | { error: CliParseError, success: false }
38
+ * >}
39
+ */
40
+ export function manageStoredQueries(project_directory: string, stored_query_mutation: StoredQueryMutation): Promise<{
41
+ success: true;
42
+ value: StoredQueryMutationResult;
43
+ } | {
44
+ diagnostics: PatramDiagnostic[];
45
+ success: false;
46
+ } | {
47
+ error: CliParseError;
48
+ success: false;
49
+ }>;
50
+ export type StoredQueryMutation = {
51
+ action: "add";
52
+ description?: string;
53
+ name: string;
54
+ where: string;
55
+ } | {
56
+ action: "remove";
57
+ name: string;
58
+ } | {
59
+ action: "update";
60
+ description?: string;
61
+ name: string;
62
+ next_name?: string;
63
+ where?: string;
64
+ };
65
+ export type StoredQueryMutationResult = {
66
+ action: "added";
67
+ name: string;
68
+ } | {
69
+ action: "removed";
70
+ name: string;
71
+ } | {
72
+ action: "updated";
73
+ name: string;
74
+ previous_name?: string;
75
+ };
76
+ import type { PatramDiagnostic } from './load-patram-config.types.d.ts';
77
+ import type { CliParseError } from '../cli/arguments.types.d.ts';