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.
@@ -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
+ }
@@ -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
+ }
@@ -18,6 +18,12 @@ export function resolveWhereClause(repo_config: PatramRepoConfig, command_argume
18
18
  error: CliParseError;
19
19
  success: false;
20
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;
21
27
  export type QuerySource = {
22
28
  kind: "ad_hoc";
23
29
  } | {
@@ -82,7 +82,10 @@ export function resolveWhereClause(repo_config, command_arguments) {
82
82
  * @param {string[]} stored_query_names
83
83
  * @returns {CliParseError}
84
84
  */
85
- function createUnknownStoredQueryError(stored_query_name, stored_query_names) {
85
+ export function createUnknownStoredQueryError(
86
+ stored_query_name,
87
+ stored_query_names,
88
+ ) {
86
89
  const suggestion = findCloseMatch(stored_query_name, stored_query_names);
87
90
 
88
91
  if (!suggestion) {
@@ -21,7 +21,7 @@ const CONFIG_FILE_NAME = '.patram.json';
21
21
  * @param {string | undefined} target_argument
22
22
  * @returns {Promise<ResolvedCheckTarget>}
23
23
  */
24
- export async function resolveCheckTarget(target_argument) {
24
+ async function resolveCheckTarget(target_argument) {
25
25
  if (target_argument === undefined) {
26
26
  return {
27
27
  project_directory: process.cwd(),
@@ -35,23 +35,19 @@ export async function resolveCheckTarget(target_argument) {
35
35
  ? absolute_target_path
36
36
  : dirname(absolute_target_path);
37
37
  const project_directory = await findProjectDirectory(target_directory);
38
-
39
38
  if (target_stats.isFile()) {
40
39
  const target_path = normalizeRepoRelativePath(
41
40
  relative(project_directory, absolute_target_path),
42
41
  );
43
-
44
42
  return {
45
43
  project_directory,
46
44
  target_kind: 'file',
47
45
  target_path,
48
46
  };
49
47
  }
50
-
51
48
  const target_path = normalizeRepoRelativePath(
52
49
  relative(project_directory, absolute_target_path),
53
50
  );
54
-
55
51
  if (target_path.length === 0) {
56
52
  return {
57
53
  project_directory,
@@ -65,7 +61,36 @@ export async function resolveCheckTarget(target_argument) {
65
61
  target_path,
66
62
  };
67
63
  }
64
+ /**
65
+ * @param {string[]} target_arguments
66
+ * @returns {Promise<ResolvedCheckTarget[]>}
67
+ */
68
+ export async function resolveCheckTargets(target_arguments) {
69
+ if (target_arguments.length === 0) {
70
+ return [await resolveCheckTarget(undefined)];
71
+ }
68
72
 
73
+ return Promise.all(
74
+ target_arguments.map((target_argument) =>
75
+ resolveCheckTarget(target_argument),
76
+ ),
77
+ );
78
+ }
79
+ /** @param {ResolvedCheckTarget[]} resolved_targets
80
+ * @returns {string | null}
81
+ */
82
+ export function resolveCheckTargetProjectDirectory(resolved_targets) {
83
+ const project_directory = resolved_targets[0]?.project_directory;
84
+ if (!project_directory) {
85
+ return null;
86
+ }
87
+ for (const resolved_target of resolved_targets) {
88
+ if (resolved_target.project_directory !== project_directory) {
89
+ return null;
90
+ }
91
+ }
92
+ return project_directory;
93
+ }
69
94
  /**
70
95
  * Select the source files covered by one resolved `check` target.
71
96
  *
@@ -73,10 +98,7 @@ export async function resolveCheckTarget(target_argument) {
73
98
  * @param {ResolvedCheckTarget} resolved_target
74
99
  * @returns {string[]}
75
100
  */
76
- export function selectCheckTargetSourceFiles(
77
- source_file_paths,
78
- resolved_target,
79
- ) {
101
+ function selectCheckTargetSourceFiles(source_file_paths, resolved_target) {
80
102
  if (resolved_target.target_kind === 'project') {
81
103
  return source_file_paths;
82
104
  }
@@ -91,7 +113,22 @@ export function selectCheckTargetSourceFiles(
91
113
  isPathInsideDirectory(source_file_path, resolved_target.target_path),
92
114
  );
93
115
  }
94
-
116
+ /**
117
+ * @param {string[]} source_file_paths
118
+ * @param {ResolvedCheckTarget[]} resolved_targets
119
+ * @returns {string[]}
120
+ */
121
+ export function selectCheckTargetsSourceFiles(
122
+ source_file_paths,
123
+ resolved_targets,
124
+ ) {
125
+ return selectCheckTargetValues(
126
+ source_file_paths,
127
+ resolved_targets,
128
+ selectCheckTargetSourceFiles,
129
+ (source_file_path) => source_file_path,
130
+ );
131
+ }
95
132
  /**
96
133
  * Filter diagnostics to one resolved `check` target.
97
134
  *
@@ -99,7 +136,7 @@ export function selectCheckTargetSourceFiles(
99
136
  * @param {ResolvedCheckTarget} resolved_target
100
137
  * @returns {PatramDiagnostic[]}
101
138
  */
102
- export function selectCheckTargetDiagnostics(diagnostics, resolved_target) {
139
+ function selectCheckTargetDiagnostics(diagnostics, resolved_target) {
103
140
  if (resolved_target.target_kind === 'project') {
104
141
  return diagnostics;
105
142
  }
@@ -115,6 +152,20 @@ export function selectCheckTargetDiagnostics(diagnostics, resolved_target) {
115
152
  );
116
153
  }
117
154
 
155
+ /**
156
+ * @param {PatramDiagnostic[]} diagnostics
157
+ * @param {ResolvedCheckTarget[]} resolved_targets
158
+ * @returns {PatramDiagnostic[]}
159
+ */
160
+ export function selectCheckTargetsDiagnostics(diagnostics, resolved_targets) {
161
+ return selectCheckTargetValues(
162
+ diagnostics,
163
+ resolved_targets,
164
+ selectCheckTargetDiagnostics,
165
+ createDiagnosticKey,
166
+ );
167
+ }
168
+
118
169
  /**
119
170
  * @param {string} start_directory
120
171
  * @returns {Promise<string>}
@@ -155,6 +206,49 @@ async function hasConfigFile(directory_path) {
155
206
  return true;
156
207
  }
157
208
 
209
+ /**
210
+ * @template ValueType
211
+ * @param {ValueType[]} values
212
+ * @param {ResolvedCheckTarget[]} resolved_targets
213
+ * @param {(values: ValueType[], resolved_target: ResolvedCheckTarget) => ValueType[]} select_values
214
+ * @param {(value: ValueType) => string} get_value_key
215
+ * @returns {ValueType[]}
216
+ */
217
+ function selectCheckTargetValues(
218
+ values,
219
+ resolved_targets,
220
+ select_values,
221
+ get_value_key,
222
+ ) {
223
+ if (
224
+ resolved_targets.some(
225
+ (resolved_target) => resolved_target.target_kind === 'project',
226
+ )
227
+ ) {
228
+ return values;
229
+ }
230
+
231
+ /** @type {Set<string>} */
232
+ const selected_keys = new Set();
233
+ /** @type {ValueType[]} */
234
+ const selected_values = [];
235
+
236
+ for (const resolved_target of resolved_targets) {
237
+ for (const value of select_values(values, resolved_target)) {
238
+ const value_key = get_value_key(value);
239
+
240
+ if (selected_keys.has(value_key)) {
241
+ continue;
242
+ }
243
+
244
+ selected_keys.add(value_key);
245
+ selected_values.push(value);
246
+ }
247
+ }
248
+
249
+ return selected_values;
250
+ }
251
+
158
252
  /**
159
253
  * @param {string} source_path
160
254
  * @param {string} directory_path
@@ -175,6 +269,21 @@ function normalizeRepoRelativePath(source_path) {
175
269
  return source_path.replaceAll('\\', '/');
176
270
  }
177
271
 
272
+ /**
273
+ * @param {PatramDiagnostic} diagnostic
274
+ * @returns {string}
275
+ */
276
+ function createDiagnosticKey(diagnostic) {
277
+ return [
278
+ diagnostic.path,
279
+ diagnostic.line,
280
+ diagnostic.column,
281
+ diagnostic.level,
282
+ diagnostic.code,
283
+ diagnostic.message,
284
+ ].join(':');
285
+ }
286
+
178
287
  /**
179
288
  * @param {unknown} error
180
289
  * @returns {error is NodeJS.ErrnoException}
package/lib/patram.d.ts CHANGED
@@ -48,6 +48,8 @@ export type PatramParsedTerm =
48
48
  import('./graph/parse-where-clause.types.d.ts').ParsedTerm;
49
49
  export type PatramParsedExpression =
50
50
  import('./graph/parse-where-clause.types.d.ts').ParsedExpression;
51
+ export type PatramParseResult =
52
+ import('./graph/parse-where-clause.types.d.ts').ParseWhereClauseResult;
51
53
  export type PatramParseWhereClauseResult =
52
54
  import('./graph/parse-where-clause.types.d.ts').ParseWhereClauseResult;
53
55
  export type PatramQuerySource =
@@ -59,6 +61,12 @@ export type PatramQuerySource =
59
61
  name: string;
60
62
  };
61
63
 
64
+ export interface PatramQueryGraphOptions {
65
+ bindings?: Record<string, string>;
66
+ limit?: number;
67
+ offset?: number;
68
+ }
69
+
62
70
  export interface PatramProjectGraphResult {
63
71
  claims: import('./parse/parse-claims.types.d.ts').PatramClaim[];
64
72
  config: import('./config/load-patram-config.types.d.ts').PatramRepoConfig;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patram",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "type": "module",
5
5
  "main": "./lib/patram.js",
6
6
  "types": "./lib/patram.d.ts",
@@ -67,6 +67,7 @@
67
67
  "ansis": "^4.2.0",
68
68
  "beautiful-mermaid": "^1.1.3",
69
69
  "globby": "^16.1.1",
70
+ "jsonc-parser": "^3.3.1",
70
71
  "md4x": "^0.0.25",
71
72
  "shiki": "^4.0.2",
72
73
  "string-width": "^8.2.0",