patram 0.3.0 → 0.5.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.
@@ -1,5 +1,4 @@
1
1
  /**
2
- * @import { PatramConfig } from './patram-config.types.ts';
3
2
  * @import { RefinementCtx } from 'zod';
4
3
  */
5
4
 
@@ -11,14 +10,20 @@ const CLAIM_TYPE_SCHEMA = z.string().min(1);
11
10
  const KEY_SOURCE_SCHEMA = z.enum(['path', 'value']);
12
11
  const TARGET_SCHEMA = z.enum(['path', 'value']);
13
12
 
14
- const class_definition_schema = z
13
+ /**
14
+ * @typedef {z.output<typeof class_definition_schema>} ClassDefinition
15
+ */
16
+ export const class_definition_schema = z
15
17
  .object({
16
18
  builtin: z.boolean().optional(),
17
19
  label: z.string().min(1).optional(),
18
20
  })
19
21
  .strict();
20
22
 
21
- const relation_definition_schema = z
23
+ /**
24
+ * @typedef {z.output<typeof relation_definition_schema>} RelationDefinition
25
+ */
26
+ export const relation_definition_schema = z
22
27
  .object({
23
28
  builtin: z.boolean().optional(),
24
29
  from: z.array(CLASS_NAME_SCHEMA).min(1),
@@ -26,6 +31,9 @@ const relation_definition_schema = z
26
31
  })
27
32
  .strict();
28
33
 
34
+ /**
35
+ * @typedef {z.output<typeof mapping_node_schema>} MappingNodeDefinition
36
+ */
29
37
  const mapping_node_schema = z
30
38
  .object({
31
39
  class: CLASS_NAME_SCHEMA,
@@ -34,6 +42,9 @@ const mapping_node_schema = z
34
42
  })
35
43
  .strict();
36
44
 
45
+ /**
46
+ * @typedef {z.output<typeof mapping_emit_schema>} MappingEmitDefinition
47
+ */
37
48
  const mapping_emit_schema = z
38
49
  .object({
39
50
  relation: RELATION_NAME_SCHEMA,
@@ -42,7 +53,10 @@ const mapping_emit_schema = z
42
53
  })
43
54
  .strict();
44
55
 
45
- const mapping_definition_schema = z
56
+ /**
57
+ * @typedef {z.output<typeof mapping_definition_schema>} MappingDefinition
58
+ */
59
+ export const mapping_definition_schema = z
46
60
  .object({
47
61
  emit: mapping_emit_schema.optional(),
48
62
  node: mapping_node_schema.optional(),
@@ -50,6 +64,9 @@ const mapping_definition_schema = z
50
64
  .strict()
51
65
  .superRefine(validateMappingDefinition);
52
66
 
67
+ /**
68
+ * @typedef {z.output<typeof patramConfigSchema>} PatramGraphConfig
69
+ */
53
70
  export const patramConfigSchema = z
54
71
  .object({
55
72
  $schema: z.url().optional(),
@@ -64,7 +81,7 @@ export const patramConfigSchema = z
64
81
  * Parse and validate Patram JSON configuration.
65
82
  *
66
83
  * @param {unknown} config_json
67
- * @returns {PatramConfig}
84
+ * @returns {PatramGraphConfig}
68
85
  */
69
86
  export function parsePatramConfig(config_json) {
70
87
  return patramConfigSchema.parse(config_json);
@@ -86,7 +103,7 @@ function validateMappingDefinition(mapping_definition, refinement_context) {
86
103
  }
87
104
 
88
105
  /**
89
- * @param {PatramConfig} config_json
106
+ * @param {PatramGraphConfig} config_json
90
107
  * @param {RefinementCtx} refinement_context
91
108
  */
92
109
  function validatePatramConfigReferences(config_json, refinement_context) {
@@ -96,7 +113,7 @@ function validatePatramConfigReferences(config_json, refinement_context) {
96
113
  }
97
114
 
98
115
  /**
99
- * @param {PatramConfig} config_json
116
+ * @param {PatramGraphConfig} config_json
100
117
  * @param {RefinementCtx} refinement_context
101
118
  */
102
119
  function validateRelationClasses(config_json, refinement_context) {
@@ -119,7 +136,7 @@ function validateRelationClasses(config_json, refinement_context) {
119
136
  }
120
137
 
121
138
  /**
122
- * @param {PatramConfig} config_json
139
+ * @param {PatramGraphConfig} config_json
123
140
  * @param {RefinementCtx} refinement_context
124
141
  */
125
142
  function validateMappingClasses(config_json, refinement_context) {
@@ -147,7 +164,7 @@ function validateMappingClasses(config_json, refinement_context) {
147
164
  }
148
165
 
149
166
  /**
150
- * @param {PatramConfig} config_json
167
+ * @param {PatramGraphConfig} config_json
151
168
  * @param {RefinementCtx} refinement_context
152
169
  */
153
170
  function validateMappingRelations(config_json, refinement_context) {
@@ -1,40 +1,22 @@
1
- export interface ClassDefinition {
2
- builtin?: boolean;
3
- label?: string;
4
- }
5
-
6
- export interface RelationDefinition {
7
- builtin?: boolean;
8
- from: string[];
9
- to: string[];
10
- }
11
-
12
- export interface MappingNodeDefinition {
13
- class: string;
14
- field: string;
15
- key?: 'path' | 'value';
16
- }
17
-
18
- export interface MappingEmitDefinition {
19
- relation: string;
20
- target: 'path' | 'value';
21
- target_class: string;
22
- }
23
-
24
- export interface MappingDefinition {
25
- emit?: MappingEmitDefinition;
26
- node?: MappingNodeDefinition;
27
- }
28
-
29
- export interface PatramConfig {
30
- $schema?: string;
31
- classes: Record<string, ClassDefinition>;
32
- class_schemas?: Record<string, ClassSchemaConfig>;
33
- fields?: Record<string, MetadataFieldConfig>;
34
- mappings: Record<string, MappingDefinition>;
35
- relations: Record<string, RelationDefinition>;
36
- }
37
1
  import type {
38
2
  ClassSchemaConfig,
39
3
  MetadataFieldConfig,
40
4
  } from './load-patram-config.types.ts';
5
+
6
+ export type ClassDefinition = import('./patram-config.js').ClassDefinition;
7
+ export type RelationDefinition =
8
+ import('./patram-config.js').RelationDefinition;
9
+ export type MappingNodeDefinition =
10
+ import('./patram-config.js').MappingNodeDefinition;
11
+ export type MappingEmitDefinition =
12
+ import('./patram-config.js').MappingEmitDefinition;
13
+ export type MappingDefinition = import('./patram-config.js').MappingDefinition;
14
+ export type PatramGraphConfig = import('./patram-config.js').PatramGraphConfig;
15
+ export type PatramClassConfig = ClassDefinition & {
16
+ schema?: ClassSchemaConfig;
17
+ };
18
+
19
+ export type PatramConfig = Omit<PatramGraphConfig, 'classes'> & {
20
+ classes: Record<string, PatramClassConfig>;
21
+ fields?: Record<string, MetadataFieldConfig>;
22
+ };
@@ -4,7 +4,7 @@
4
4
  * @import { PatramDiagnostic, PatramRepoConfig } from './load-patram-config.types.ts';
5
5
  * @import {
6
6
  * ParsedAggregateTerm,
7
- * ParsedClause,
7
+ * ParsedExpression,
8
8
  * ParsedFieldSetTerm,
9
9
  * ParsedFieldTerm,
10
10
  * ParsedRelationTargetTerm,
@@ -82,7 +82,7 @@ export function queryGraph(
82
82
  const diagnostics = getQuerySemanticDiagnostics(
83
83
  repo_config,
84
84
  { kind: 'ad_hoc' },
85
- parse_result.clauses,
85
+ parse_result.expression,
86
86
  );
87
87
 
88
88
  if (diagnostics.length > 0) {
@@ -100,7 +100,7 @@ export function queryGraph(
100
100
  };
101
101
  const graph_nodes = Object.values(graph.nodes).sort(compareGraphNodes);
102
102
  const matching_nodes = graph_nodes.filter((graph_node) =>
103
- matchesClauses(graph_node, parse_result.clauses, evaluation_context),
103
+ matchesExpression(graph_node, parse_result.expression, evaluation_context),
104
104
  );
105
105
  const paginated_nodes = paginateNodes(
106
106
  matching_nodes,
@@ -116,26 +116,36 @@ export function queryGraph(
116
116
 
117
117
  /**
118
118
  * @param {GraphNode} graph_node
119
- * @param {ParsedClause[]} clauses
119
+ * @param {ParsedExpression} expression
120
120
  * @param {EvaluationContext} evaluation_context
121
121
  * @returns {boolean}
122
122
  */
123
- function matchesClauses(graph_node, clauses, evaluation_context) {
124
- return clauses.every((clause) =>
125
- matchesClause(graph_node, clause, evaluation_context),
126
- );
127
- }
123
+ function matchesExpression(graph_node, expression, evaluation_context) {
124
+ if (expression.kind === 'and') {
125
+ return expression.expressions.every((subexpression) =>
126
+ matchesExpression(graph_node, subexpression, evaluation_context),
127
+ );
128
+ }
128
129
 
129
- /**
130
- * @param {GraphNode} graph_node
131
- * @param {ParsedClause} clause
132
- * @param {EvaluationContext} evaluation_context
133
- * @returns {boolean}
134
- */
135
- function matchesClause(graph_node, clause, evaluation_context) {
136
- const is_match = matchesTerm(graph_node, clause.term, evaluation_context);
130
+ if (expression.kind === 'or') {
131
+ return expression.expressions.some((subexpression) =>
132
+ matchesExpression(graph_node, subexpression, evaluation_context),
133
+ );
134
+ }
135
+
136
+ if (expression.kind === 'not') {
137
+ return !matchesExpression(
138
+ graph_node,
139
+ expression.expression,
140
+ evaluation_context,
141
+ );
142
+ }
143
+
144
+ if (expression.kind === 'term') {
145
+ return matchesTerm(graph_node, expression.term, evaluation_context);
146
+ }
137
147
 
138
- return clause.is_negated ? !is_match : is_match;
148
+ throw new Error('Unsupported parsed boolean expression.');
139
149
  }
140
150
 
141
151
  /**
@@ -181,7 +191,7 @@ function matchesAggregateTerm(graph_node, term, evaluation_context) {
181
191
  evaluation_context,
182
192
  );
183
193
  const matching_count = related_nodes.filter((related_node) =>
184
- matchesClauses(related_node, term.clauses, evaluation_context),
194
+ matchesExpression(related_node, term.expression, evaluation_context),
185
195
  ).length;
186
196
 
187
197
  if (term.aggregate_name === 'any') {
@@ -1,10 +1,9 @@
1
- /** @import * as $k$$l$parse$j$where$j$clause$k$types$k$ts from './parse-where-clause.types.ts'; */
2
1
  /* eslint-disable max-lines */
3
2
  /**
4
3
  * @import { PatramDiagnostic, PatramRepoConfig } from './load-patram-config.types.ts';
5
4
  * @import { ResolvedOutputMode } from './output-view.types.ts';
6
5
  * @import {
7
- * ParsedClause,
6
+ * ParsedExpression,
8
7
  * ParsedRelationTargetTerm,
9
8
  * ParsedRelationTerm,
10
9
  * ParsedTerm,
@@ -38,10 +37,10 @@ import { parseWhereClause } from './parse-where-clause.js';
38
37
  * limit: number | null,
39
38
  * offset: number,
40
39
  * },
40
+ * expression?: ParsedExpression,
41
41
  * inspection_mode: 'explain' | 'lint',
42
42
  * query_source: QuerySource,
43
43
  * where_clause: string,
44
- * clauses?: ParsedClause[],
45
44
  * }} QueryInspectionSuccess
46
45
  */
47
46
 
@@ -75,7 +74,7 @@ export function inspectQuery(
75
74
  const diagnostics = getQuerySemanticDiagnostics(
76
75
  repo_config,
77
76
  resolved_where_clause.query_source,
78
- parse_result.clauses,
77
+ parse_result.expression,
79
78
  );
80
79
 
81
80
  if (diagnostics.length > 0) {
@@ -99,11 +98,11 @@ export function inspectQuery(
99
98
  return {
100
99
  success: true,
101
100
  value: {
102
- clauses: parse_result.clauses,
103
101
  execution: {
104
102
  limit: inspection_options.limit,
105
103
  offset: inspection_options.offset,
106
104
  },
105
+ expression: parse_result.expression,
107
106
  inspection_mode: 'explain',
108
107
  query_source: resolved_where_clause.query_source,
109
108
  where_clause: resolved_where_clause.where_clause,
@@ -143,20 +142,26 @@ export function renderQueryInspection(query_inspection, output_mode) {
143
142
  }
144
143
 
145
144
  /**
145
+ * Collect schema-aware diagnostics for one parsed where clause.
146
+ *
146
147
  * @param {PatramRepoConfig} repo_config
147
148
  * @param {QuerySource} query_source
148
- * @param {ParsedClause[]} clauses
149
+ * @param {ParsedExpression} expression
149
150
  * @returns {PatramDiagnostic[]}
150
151
  */
151
- function collectSemanticDiagnostics(repo_config, query_source, clauses) {
152
+ export function getQuerySemanticDiagnostics(
153
+ repo_config,
154
+ query_source,
155
+ expression,
156
+ ) {
152
157
  const known_relation_names = new Set(
153
158
  Object.keys(repo_config.relations ?? {}),
154
159
  );
155
160
  /** @type {PatramDiagnostic[]} */
156
161
  const diagnostics = [];
157
162
 
158
- collectClauseDiagnostics(
159
- clauses,
163
+ collectExpressionDiagnostics(
164
+ expression,
160
165
  diagnostics,
161
166
  known_relation_names,
162
167
  repo_config.fields ?? {},
@@ -167,44 +172,58 @@ function collectSemanticDiagnostics(repo_config, query_source, clauses) {
167
172
  }
168
173
 
169
174
  /**
170
- * Collect schema-aware diagnostics for one parsed where clause.
171
- *
172
- * @param {PatramRepoConfig} repo_config
173
- * @param {QuerySource} query_source
174
- * @param {ParsedClause[]} clauses
175
- * @returns {PatramDiagnostic[]}
176
- */
177
- export function getQuerySemanticDiagnostics(
178
- repo_config,
179
- query_source,
180
- clauses,
181
- ) {
182
- return collectSemanticDiagnostics(repo_config, query_source, clauses);
183
- }
184
-
185
- /**
186
- * @param {ParsedClause[]} clauses
175
+ * @param {ParsedExpression} expression
187
176
  * @param {PatramDiagnostic[]} diagnostics
188
177
  * @param {Set<string>} known_relation_names
189
178
  * @param {Record<string, import('./load-patram-config.types.ts').MetadataFieldConfig>} known_field_definitions
190
179
  * @param {string} diagnostic_path
191
180
  */
192
- function collectClauseDiagnostics(
193
- clauses,
181
+ function collectExpressionDiagnostics(
182
+ expression,
194
183
  diagnostics,
195
184
  known_relation_names,
196
185
  known_field_definitions,
197
186
  diagnostic_path,
198
187
  ) {
199
- for (const clause of clauses) {
188
+ if (expression.kind === 'and' || expression.kind === 'or') {
189
+ for (const subexpression of expression.expressions) {
190
+ collectExpressionDiagnostics(
191
+ subexpression,
192
+ diagnostics,
193
+ known_relation_names,
194
+ known_field_definitions,
195
+ diagnostic_path,
196
+ );
197
+ }
198
+
199
+ return;
200
+ }
201
+
202
+ if (expression.kind === 'not') {
203
+ collectExpressionDiagnostics(
204
+ expression.expression,
205
+ diagnostics,
206
+ known_relation_names,
207
+ known_field_definitions,
208
+ diagnostic_path,
209
+ );
210
+
211
+ return;
212
+ }
213
+
214
+ if (expression.kind === 'term') {
200
215
  collectTermDiagnostics(
201
- clause.term,
216
+ expression.term,
202
217
  diagnostics,
203
218
  known_relation_names,
204
219
  known_field_definitions,
205
220
  diagnostic_path,
206
221
  );
222
+
223
+ return;
207
224
  }
225
+
226
+ throw new Error('Unsupported query inspection expression.');
208
227
  }
209
228
 
210
229
  /**
@@ -228,8 +247,8 @@ function collectTermDiagnostics(
228
247
  known_relation_names,
229
248
  diagnostic_path,
230
249
  );
231
- collectClauseDiagnostics(
232
- term.clauses,
250
+ collectExpressionDiagnostics(
251
+ term.expression,
233
252
  diagnostics,
234
253
  known_relation_names,
235
254
  known_field_definitions,
@@ -577,9 +596,9 @@ function formatJsonQueryInspection(query_inspection) {
577
596
  }
578
597
 
579
598
  return {
580
- clauses: query_inspection.clauses,
581
599
  diagnostics: [],
582
600
  execution: query_inspection.execution,
601
+ expression: query_inspection.expression,
583
602
  mode: 'explain',
584
603
  source: query_inspection.query_source,
585
604
  where: query_inspection.where_clause,
@@ -625,9 +644,12 @@ function renderTextQueryInspection(query_inspection, render_options) {
625
644
  : String(query_inspection.execution?.limit ?? ''),
626
645
  ),
627
646
  '',
628
- `${render_options.label('clauses:')}`,
629
- ...formatExplainClauseBlock(
630
- query_inspection.clauses ?? [],
647
+ `${render_options.label('expression:')}`,
648
+ ...formatExplainExpressionBlock(
649
+ query_inspection.expression ?? {
650
+ expressions: [],
651
+ kind: 'and',
652
+ },
631
653
  render_options,
632
654
  '',
633
655
  ),
@@ -637,42 +659,49 @@ function renderTextQueryInspection(query_inspection, render_options) {
637
659
  }
638
660
 
639
661
  /**
640
- * @param {ParsedClause[]} clauses
662
+ * @param {ParsedExpression} expression
641
663
  * @param {{ header: (value: string) => string, label: (value: string) => string }} render_options
642
664
  * @param {string} indentation
643
665
  * @returns {string[]}
644
666
  */
645
- function formatExplainClauseBlock(clauses, render_options, indentation) {
646
- /** @type {string[]} */
647
- const output_lines = [];
667
+ function formatExplainExpressionBlock(expression, render_options, indentation) {
668
+ if (expression.kind === 'and') {
669
+ return formatExplainExpressionItems(
670
+ expression.expressions,
671
+ render_options,
672
+ indentation,
673
+ );
674
+ }
648
675
 
649
- clauses.forEach((clause, clause_index) => {
650
- const clause_number = clause_index + 1;
651
- const clause_text = formatClauseSummary(clause);
676
+ return formatExplainExpressionItems(
677
+ [expression],
678
+ render_options,
679
+ indentation,
680
+ );
681
+ }
652
682
 
653
- output_lines.push(`${indentation}${clause_number}. ${clause_text}`);
683
+ /**
684
+ * @param {ParsedExpression[]} expressions
685
+ * @param {{ header: (value: string) => string, label: (value: string) => string }} render_options
686
+ * @param {string} indentation
687
+ * @returns {string[]}
688
+ */
689
+ function formatExplainExpressionItems(
690
+ expressions,
691
+ render_options,
692
+ indentation,
693
+ ) {
694
+ /** @type {string[]} */
695
+ const output_lines = [];
654
696
 
655
- if (clause.term.kind !== 'aggregate') {
656
- return;
657
- }
697
+ expressions.forEach((expression, expression_index) => {
698
+ const expression_number = expression_index + 1;
658
699
 
659
700
  output_lines.push(
660
- `${indentation} ${render_options.label('traversal:')} ${formatTraversal(clause.term.traversal)}`,
701
+ `${indentation}${expression_number}. ${formatExpressionSummary(expression)}`,
661
702
  );
662
-
663
- if (clause.term.aggregate_name === 'count') {
664
- output_lines.push(
665
- `${indentation} ${render_options.label('comparison:')} ${clause.term.comparison} ${clause.term.value}`,
666
- );
667
- }
668
-
669
703
  output_lines.push(
670
- `${indentation} ${render_options.label('nested clauses:')}`,
671
- ...formatExplainClauseBlock(
672
- clause.term.clauses,
673
- render_options,
674
- `${indentation} `,
675
- ),
704
+ ...formatExpressionDetailLines(expression, render_options, indentation),
676
705
  );
677
706
  });
678
707
 
@@ -680,17 +709,89 @@ function formatExplainClauseBlock(clauses, render_options, indentation) {
680
709
  }
681
710
 
682
711
  /**
683
- * @param {ParsedClause} clause
712
+ * @param {ParsedExpression} expression
684
713
  * @returns {string}
685
714
  */
686
- function formatClauseSummary(clause) {
687
- const clause_prefix = clause.is_negated ? 'not ' : '';
715
+ function formatExpressionSummary(expression) {
716
+ if (expression.kind === 'and') {
717
+ return 'all of';
718
+ }
688
719
 
689
- if (clause.term.kind === 'aggregate') {
690
- return `${clause_prefix}aggregate ${clause.term.aggregate_name}`;
720
+ if (expression.kind === 'or') {
721
+ return 'any of';
691
722
  }
692
723
 
693
- return `${clause_prefix}${formatTermSummary(clause.term)}`;
724
+ if (expression.kind === 'not') {
725
+ if (expression.expression.kind === 'term') {
726
+ return `not ${formatTermSummary(expression.expression.term)}`;
727
+ }
728
+
729
+ return 'not';
730
+ }
731
+
732
+ if (expression.kind === 'term') {
733
+ return formatTermSummary(expression.term);
734
+ }
735
+
736
+ throw new Error('Unsupported explain expression.');
737
+ }
738
+
739
+ /**
740
+ * @param {ParsedExpression} expression
741
+ * @param {{ header: (value: string) => string, label: (value: string) => string }} render_options
742
+ * @param {string} indentation
743
+ * @returns {string[]}
744
+ */
745
+ function formatExpressionDetailLines(expression, render_options, indentation) {
746
+ if (expression.kind === 'and' || expression.kind === 'or') {
747
+ return formatExplainExpressionItems(
748
+ expression.expressions,
749
+ render_options,
750
+ `${indentation} `,
751
+ );
752
+ }
753
+
754
+ if (expression.kind === 'not') {
755
+ if (expression.expression.kind === 'term') {
756
+ return [];
757
+ }
758
+
759
+ return formatExplainExpressionBlock(
760
+ expression.expression,
761
+ render_options,
762
+ `${indentation} `,
763
+ );
764
+ }
765
+
766
+ if (expression.kind !== 'term') {
767
+ throw new Error('Unsupported explain expression details.');
768
+ }
769
+
770
+ if (expression.term.kind !== 'aggregate') {
771
+ return [];
772
+ }
773
+
774
+ /** @type {string[]} */
775
+ const output_lines = [
776
+ `${indentation} ${render_options.label('traversal:')} ${formatTraversal(expression.term.traversal)}`,
777
+ ];
778
+
779
+ if (expression.term.aggregate_name === 'count') {
780
+ output_lines.push(
781
+ `${indentation} ${render_options.label('comparison:')} ${expression.term.comparison} ${expression.term.value}`,
782
+ );
783
+ }
784
+
785
+ output_lines.push(
786
+ `${indentation} ${render_options.label('nested expression:')}`,
787
+ ...formatExplainExpressionBlock(
788
+ expression.term.expression,
789
+ render_options,
790
+ `${indentation} `,
791
+ ),
792
+ );
793
+
794
+ return output_lines;
694
795
  }
695
796
 
696
797
  /**
@@ -698,6 +799,10 @@ function formatClauseSummary(clause) {
698
799
  * @returns {string}
699
800
  */
700
801
  function formatTermSummary(term) {
802
+ if (term.kind === 'aggregate') {
803
+ return `aggregate ${term.aggregate_name}`;
804
+ }
805
+
701
806
  if (term.kind === 'field') {
702
807
  return `${term.field_name} ${term.operator} ${term.value}`;
703
808
  }
@@ -714,7 +819,7 @@ function formatTermSummary(term) {
714
819
  return `${term.relation_name} = ${term.target_id}`;
715
820
  }
716
821
 
717
- throw new Error('Expected a non-aggregate query term.');
822
+ throw new Error('Expected a parsed query term.');
718
823
  }
719
824
 
720
825
  /**