@memberjunction/query-gen 0.0.1 → 2.126.1

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 (138) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/CHANGELOG.md +34 -0
  3. package/COORDINATOR.md +768 -0
  4. package/IMPLEMENTATION_PLAN.md +1753 -0
  5. package/LLM_ENTITY_GROUPING_PLAN.md +977 -0
  6. package/README.md +675 -29
  7. package/dist/cli/commands/export.d.ts +15 -0
  8. package/dist/cli/commands/export.d.ts.map +1 -0
  9. package/dist/cli/commands/export.js +178 -0
  10. package/dist/cli/commands/export.js.map +1 -0
  11. package/dist/cli/commands/generate.d.ts +19 -0
  12. package/dist/cli/commands/generate.d.ts.map +1 -0
  13. package/dist/cli/commands/generate.js +282 -0
  14. package/dist/cli/commands/generate.js.map +1 -0
  15. package/dist/cli/commands/validate.d.ts +17 -0
  16. package/dist/cli/commands/validate.d.ts.map +1 -0
  17. package/dist/cli/commands/validate.js +193 -0
  18. package/dist/cli/commands/validate.js.map +1 -0
  19. package/dist/cli/config.d.ts +51 -0
  20. package/dist/cli/config.d.ts.map +1 -0
  21. package/dist/cli/config.js +142 -0
  22. package/dist/cli/config.js.map +1 -0
  23. package/dist/cli/index.d.ts +13 -0
  24. package/dist/cli/index.d.ts.map +1 -0
  25. package/dist/cli/index.js +57 -0
  26. package/dist/cli/index.js.map +1 -0
  27. package/dist/core/EntityGrouper.d.ts +74 -0
  28. package/dist/core/EntityGrouper.d.ts.map +1 -0
  29. package/dist/core/EntityGrouper.js +246 -0
  30. package/dist/core/EntityGrouper.js.map +1 -0
  31. package/dist/core/MetadataExporter.d.ts +59 -0
  32. package/dist/core/MetadataExporter.d.ts.map +1 -0
  33. package/dist/core/MetadataExporter.js +151 -0
  34. package/dist/core/MetadataExporter.js.map +1 -0
  35. package/dist/core/QueryDatabaseWriter.d.ts +50 -0
  36. package/dist/core/QueryDatabaseWriter.d.ts.map +1 -0
  37. package/dist/core/QueryDatabaseWriter.js +152 -0
  38. package/dist/core/QueryDatabaseWriter.js.map +1 -0
  39. package/dist/core/QueryFixer.d.ts +48 -0
  40. package/dist/core/QueryFixer.d.ts.map +1 -0
  41. package/dist/core/QueryFixer.js +115 -0
  42. package/dist/core/QueryFixer.js.map +1 -0
  43. package/dist/core/QueryRefiner.d.ts +94 -0
  44. package/dist/core/QueryRefiner.d.ts.map +1 -0
  45. package/dist/core/QueryRefiner.js +267 -0
  46. package/dist/core/QueryRefiner.js.map +1 -0
  47. package/dist/core/QueryTester.d.ts +70 -0
  48. package/dist/core/QueryTester.d.ts.map +1 -0
  49. package/dist/core/QueryTester.js +243 -0
  50. package/dist/core/QueryTester.js.map +1 -0
  51. package/dist/core/QueryWriter.d.ts +57 -0
  52. package/dist/core/QueryWriter.d.ts.map +1 -0
  53. package/dist/core/QueryWriter.js +184 -0
  54. package/dist/core/QueryWriter.js.map +1 -0
  55. package/dist/core/QuestionGenerator.d.ts +58 -0
  56. package/dist/core/QuestionGenerator.d.ts.map +1 -0
  57. package/dist/core/QuestionGenerator.js +145 -0
  58. package/dist/core/QuestionGenerator.js.map +1 -0
  59. package/dist/data/schema.d.ts +230 -0
  60. package/dist/data/schema.d.ts.map +1 -0
  61. package/dist/data/schema.js +6 -0
  62. package/dist/data/schema.js.map +1 -0
  63. package/dist/index.d.ts +28 -0
  64. package/dist/index.d.ts.map +1 -0
  65. package/dist/index.js +77 -0
  66. package/dist/index.js.map +1 -0
  67. package/dist/prompts/PromptNames.d.ts +32 -0
  68. package/dist/prompts/PromptNames.d.ts.map +1 -0
  69. package/dist/prompts/PromptNames.js +35 -0
  70. package/dist/prompts/PromptNames.js.map +1 -0
  71. package/dist/utils/category-builder.d.ts +28 -0
  72. package/dist/utils/category-builder.d.ts.map +1 -0
  73. package/dist/utils/category-builder.js +90 -0
  74. package/dist/utils/category-builder.js.map +1 -0
  75. package/dist/utils/entity-helpers.d.ts +49 -0
  76. package/dist/utils/entity-helpers.d.ts.map +1 -0
  77. package/dist/utils/entity-helpers.js +189 -0
  78. package/dist/utils/entity-helpers.js.map +1 -0
  79. package/dist/utils/error-handlers.d.ts +19 -0
  80. package/dist/utils/error-handlers.d.ts.map +1 -0
  81. package/dist/utils/error-handlers.js +41 -0
  82. package/dist/utils/error-handlers.js.map +1 -0
  83. package/dist/utils/graph-helpers.d.ts +51 -0
  84. package/dist/utils/graph-helpers.d.ts.map +1 -0
  85. package/dist/utils/graph-helpers.js +82 -0
  86. package/dist/utils/graph-helpers.js.map +1 -0
  87. package/dist/utils/prompt-helpers.d.ts +25 -0
  88. package/dist/utils/prompt-helpers.d.ts.map +1 -0
  89. package/dist/utils/prompt-helpers.js +66 -0
  90. package/dist/utils/prompt-helpers.js.map +1 -0
  91. package/dist/utils/query-helpers.d.ts +23 -0
  92. package/dist/utils/query-helpers.d.ts.map +1 -0
  93. package/dist/utils/query-helpers.js +34 -0
  94. package/dist/utils/query-helpers.js.map +1 -0
  95. package/dist/utils/user-helpers.d.ts +15 -0
  96. package/dist/utils/user-helpers.d.ts.map +1 -0
  97. package/dist/utils/user-helpers.js +32 -0
  98. package/dist/utils/user-helpers.js.map +1 -0
  99. package/dist/vectors/EmbeddingService.d.ts +58 -0
  100. package/dist/vectors/EmbeddingService.d.ts.map +1 -0
  101. package/dist/vectors/EmbeddingService.js +90 -0
  102. package/dist/vectors/EmbeddingService.js.map +1 -0
  103. package/dist/vectors/SimilaritySearch.d.ts +51 -0
  104. package/dist/vectors/SimilaritySearch.d.ts.map +1 -0
  105. package/dist/vectors/SimilaritySearch.js +85 -0
  106. package/dist/vectors/SimilaritySearch.js.map +1 -0
  107. package/docs/API.md +1040 -0
  108. package/docs/ARCHITECTURE.md +1120 -0
  109. package/examples/advanced-usage.ts +401 -0
  110. package/examples/basic-usage.ts +285 -0
  111. package/package.json +48 -6
  112. package/src/cli/commands/export.ts +173 -0
  113. package/src/cli/commands/generate.ts +330 -0
  114. package/src/cli/commands/validate.ts +185 -0
  115. package/src/cli/config.ts +203 -0
  116. package/src/cli/index.ts +63 -0
  117. package/src/core/EntityGrouper.ts +318 -0
  118. package/src/core/MetadataExporter.ts +148 -0
  119. package/src/core/QueryDatabaseWriter.ts +187 -0
  120. package/src/core/QueryFixer.ts +153 -0
  121. package/src/core/QueryRefiner.ts +382 -0
  122. package/src/core/QueryTester.ts +264 -0
  123. package/src/core/QueryWriter.ts +239 -0
  124. package/src/core/QuestionGenerator.ts +199 -0
  125. package/src/data/golden-queries.json +1371 -0
  126. package/src/data/schema.ts +252 -0
  127. package/src/index.ts +49 -0
  128. package/src/prompts/PromptNames.ts +36 -0
  129. package/src/utils/category-builder.ts +97 -0
  130. package/src/utils/entity-helpers.ts +203 -0
  131. package/src/utils/error-handlers.ts +41 -0
  132. package/src/utils/graph-helpers.ts +99 -0
  133. package/src/utils/prompt-helpers.ts +79 -0
  134. package/src/utils/query-helpers.ts +32 -0
  135. package/src/utils/user-helpers.ts +39 -0
  136. package/src/vectors/EmbeddingService.ts +109 -0
  137. package/src/vectors/SimilaritySearch.ts +108 -0
  138. package/tsconfig.json +39 -0
@@ -0,0 +1,252 @@
1
+ /**
2
+ * Type definitions for QueryGen package
3
+ */
4
+
5
+ import { EntityInfo, EntityRelationshipInfo } from '@memberjunction/core';
6
+
7
+ /**
8
+ * Represents a group of related entities for query generation
9
+ */
10
+ export interface EntityGroup {
11
+ entities: EntityInfo[];
12
+ relationships: EntityRelationshipInfo[];
13
+ primaryEntity: EntityInfo;
14
+ relationshipType: 'single' | 'parent-child' | 'many-to-many';
15
+
16
+ // LLM-generated semantic metadata
17
+ businessDomain: string; // e.g., "Sales Pipeline", "Inventory Management"
18
+ businessRationale: string; // Why this grouping makes business sense
19
+ expectedQuestionTypes: string[]; // e.g., ["trend_analysis", "aggregation", "comparison"]
20
+ }
21
+
22
+ /**
23
+ * Entity metadata formatted for AI prompts
24
+ */
25
+ export interface EntityMetadataForPrompt {
26
+ entityName: string;
27
+ description: string;
28
+ schemaName: string;
29
+ baseTable: string;
30
+ baseView: string;
31
+ fields: EntityFieldMetadata[];
32
+ relationships: EntityRelationshipMetadata[];
33
+ }
34
+
35
+ /**
36
+ * Entity field metadata for prompts
37
+ */
38
+ export interface EntityFieldMetadata {
39
+ name: string;
40
+ displayName: string;
41
+ type: string;
42
+ sqlFullType: string;
43
+ description: string;
44
+ isPrimaryKey: boolean;
45
+ isForeignKey: boolean;
46
+ isVirtual: boolean;
47
+ allowsNull: boolean;
48
+ relatedEntity?: string;
49
+ isRequired: boolean;
50
+ defaultValue?: string;
51
+ possibleValues?: string[];
52
+ }
53
+
54
+ /**
55
+ * Entity relationship metadata for prompts
56
+ */
57
+ export interface EntityRelationshipMetadata {
58
+ type: 'one-to-many' | 'many-to-one' | 'many-to-many';
59
+ relatedEntity: string;
60
+ relatedEntityView: string;
61
+ relatedEntitySchema: string;
62
+ foreignKeyField: string;
63
+ description: string;
64
+ }
65
+
66
+ /**
67
+ * Business question generated by AI
68
+ */
69
+ export interface BusinessQuestion {
70
+ userQuestion: string;
71
+ description: string;
72
+ technicalDescription: string;
73
+ complexity: 'simple' | 'medium' | 'complex';
74
+ requiresAggregation: boolean;
75
+ requiresJoins: boolean;
76
+ entities: string[];
77
+ }
78
+
79
+ /**
80
+ * SQL query generated by AI
81
+ * Note: selectClause removed - QueryEntity automatically creates QueryFieldEntity records from SQL
82
+ */
83
+ export interface GeneratedQuery {
84
+ sql: string;
85
+ parameters: QueryParameter[];
86
+ }
87
+
88
+ /**
89
+ * Query output field definition
90
+ */
91
+ export interface QueryOutputField {
92
+ name: string;
93
+ description: string;
94
+ type: string;
95
+ optional: boolean;
96
+ }
97
+
98
+ /**
99
+ * Query parameter definition
100
+ */
101
+ export interface QueryParameter {
102
+ name: string;
103
+ type: string;
104
+ isRequired: boolean;
105
+ description: string;
106
+ usage: string[];
107
+ defaultValue: string | null;
108
+ sampleValue: string;
109
+ }
110
+
111
+ /**
112
+ * Golden query for few-shot learning
113
+ */
114
+ export interface GoldenQuery {
115
+ name: string;
116
+ userQuestion: string;
117
+ description: string;
118
+ technicalDescription: string;
119
+ sql: string;
120
+ parameters: QueryParameter[];
121
+ selectClause: QueryOutputField[];
122
+ }
123
+
124
+ /**
125
+ * Similarity search result
126
+ * Note: nameSim excluded since queries don't have names until after generation
127
+ */
128
+ export interface SimilarQuery {
129
+ query: GoldenQuery;
130
+ similarity: number;
131
+ fieldScores: {
132
+ userQuestionSim: number;
133
+ descSim: number;
134
+ techDescSim: number;
135
+ };
136
+ }
137
+
138
+ /**
139
+ * Query test result
140
+ */
141
+ export interface QueryTestResult {
142
+ success: boolean;
143
+ renderedSQL?: string;
144
+ rowCount?: number;
145
+ sampleRows?: unknown[];
146
+ attempts?: number;
147
+ error?: string;
148
+ }
149
+
150
+ /**
151
+ * Query evaluation result
152
+ */
153
+ export interface QueryEvaluation {
154
+ answersQuestion: boolean;
155
+ confidence: number;
156
+ reasoning: string;
157
+ suggestions: string[];
158
+ needsRefinement: boolean;
159
+ }
160
+
161
+ /**
162
+ * Refined query result
163
+ */
164
+ export interface RefinedQuery {
165
+ query: GeneratedQuery;
166
+ testResult: QueryTestResult;
167
+ evaluation: QueryEvaluation;
168
+ refinementCount: number;
169
+ reachedMaxRefinements?: boolean;
170
+ }
171
+
172
+ /**
173
+ * Query category information for metadata export and database write
174
+ */
175
+ export interface QueryCategoryInfo {
176
+ /** Name of the category */
177
+ name: string;
178
+ /** Parent category name (null for root categories) */
179
+ parentName: string | null;
180
+ /** Category description */
181
+ description: string;
182
+ /** Full category path (e.g., "Golden-Queries/Members") */
183
+ path: string;
184
+ }
185
+
186
+ /**
187
+ * Validated query ready for export
188
+ */
189
+ export interface ValidatedQuery {
190
+ businessQuestion: BusinessQuestion;
191
+ query: GeneratedQuery;
192
+ testResult: QueryTestResult;
193
+ evaluation: QueryEvaluation;
194
+ entityGroup: EntityGroup;
195
+ category: QueryCategoryInfo;
196
+ }
197
+
198
+ /**
199
+ * Metadata export result
200
+ */
201
+ export interface ExportResult {
202
+ success: boolean;
203
+ outputPath: string;
204
+ queryCount: number;
205
+ }
206
+
207
+ /**
208
+ * Database write result
209
+ */
210
+ export interface WriteResult {
211
+ success: boolean;
212
+ results: string[];
213
+ }
214
+
215
+ /**
216
+ * Query metadata record for MJ metadata format
217
+ *
218
+ * Note: QueryFields and QueryParameters are not included here.
219
+ * They are automatically extracted by QueryEntity.server.ts using
220
+ * AI analysis of the SQL template during the Save() operation.
221
+ */
222
+ export interface QueryMetadataRecord {
223
+ fields: {
224
+ Name: string;
225
+ CategoryID: string;
226
+ UserQuestion: string;
227
+ Description: string;
228
+ TechnicalDescription: string;
229
+ SQL: string;
230
+ OriginalSQL: string;
231
+ UsesTemplate: boolean;
232
+ Status: string;
233
+ };
234
+ }
235
+
236
+ /**
237
+ * Embeddings for a query or golden query
238
+ * Note: name is excluded since queries don't have names until after generation
239
+ */
240
+ export interface QueryEmbeddings {
241
+ userQuestion: number[];
242
+ description: number[];
243
+ technicalDescription: number[];
244
+ }
245
+
246
+ /**
247
+ * Golden query with embeddings
248
+ */
249
+ export interface EmbeddedGoldenQuery {
250
+ query: GoldenQuery;
251
+ embeddings: QueryEmbeddings;
252
+ }
package/src/index.ts ADDED
@@ -0,0 +1,49 @@
1
+ /**
2
+ * QueryGen package main entry point
3
+ *
4
+ * @memberjunction/query-gen
5
+ *
6
+ * AI-powered generation of domain-specific SQL query templates with
7
+ * automatic testing, refinement, and metadata export.
8
+ */
9
+
10
+ // Export core classes
11
+ export { EntityGrouper } from './core/EntityGrouper';
12
+ export { QuestionGenerator } from './core/QuestionGenerator';
13
+ export { QueryWriter } from './core/QueryWriter';
14
+ export { QueryTester } from './core/QueryTester';
15
+ export { QueryFixer } from './core/QueryFixer';
16
+ export { QueryRefiner } from './core/QueryRefiner';
17
+ export { MetadataExporter } from './core/MetadataExporter';
18
+ export { QueryDatabaseWriter } from './core/QueryDatabaseWriter';
19
+
20
+ // Export utility classes
21
+ export { SimilaritySearch } from './vectors/SimilaritySearch';
22
+ export { EmbeddingService } from './vectors/EmbeddingService';
23
+
24
+ // Export types
25
+ export * from './data/schema';
26
+
27
+ // Export prompt names
28
+ export * from './prompts/PromptNames';
29
+
30
+ // Export configuration
31
+ export { QueryGenConfig, loadConfig } from './cli/config';
32
+
33
+ // Export CLI commands
34
+ export { generateCommand } from './cli/commands/generate';
35
+ export { validateCommand } from './cli/commands/validate';
36
+ export { exportCommand } from './cli/commands/export';
37
+
38
+ // Export utilities
39
+ export { extractErrorMessage, requireValue, getPropertyOrDefault } from './utils/error-handlers';
40
+ export {
41
+ formatEntityMetadataForPrompt,
42
+ formatEntityGroupForPrompt,
43
+ findEntityById,
44
+ getPrimaryKeyFields,
45
+ getForeignKeyFields,
46
+ hasRelationships,
47
+ getRelationshipCount
48
+ } from './utils/entity-helpers';
49
+ export { generateQueryName } from './utils/query-helpers';
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Static prompt name constants for QueryGen
3
+ *
4
+ * All AI prompts used by QueryGen system with their exact names
5
+ * as defined in the AI Prompt metadata.
6
+ */
7
+
8
+ /**
9
+ * Business Question Generator prompt
10
+ * Generates 1-2 business questions per entity group
11
+ */
12
+ export const PROMPT_BUSINESS_QUESTION_GENERATOR = 'Business Question Generator';
13
+
14
+ /**
15
+ * SQL Query Writer prompt
16
+ * Generates Nunjucks SQL templates from business questions
17
+ */
18
+ export const PROMPT_SQL_QUERY_WRITER = 'SQL Query Writer';
19
+
20
+ /**
21
+ * SQL Query Fixer prompt
22
+ * Fixes SQL syntax and logic errors in generated queries
23
+ */
24
+ export const PROMPT_SQL_QUERY_FIXER = 'SQL Query Fixer';
25
+
26
+ /**
27
+ * Query Result Evaluator prompt
28
+ * Evaluates if a query correctly answers the business question
29
+ */
30
+ export const PROMPT_QUERY_EVALUATOR = 'Query Result Evaluator';
31
+
32
+ /**
33
+ * Query Refiner prompt
34
+ * Refines queries based on evaluation feedback
35
+ */
36
+ export const PROMPT_QUERY_REFINER = 'Query Refiner';
@@ -0,0 +1,97 @@
1
+ /**
2
+ * CategoryBuilder - Builds QueryCategoryInfo from configuration and entity groups
3
+ *
4
+ * Centralizes the logic for determining what categories should be created
5
+ * for queries based on the QueryGen configuration.
6
+ */
7
+
8
+ import { QueryCategoryInfo, EntityGroup } from '../data/schema';
9
+ import { QueryGenConfig } from '../cli/config';
10
+
11
+ /**
12
+ * Build category information for a query based on configuration
13
+ *
14
+ * @param config - QueryGen configuration
15
+ * @param entityGroup - Entity group for the query
16
+ * @returns QueryCategoryInfo with full category details
17
+ */
18
+ export function buildQueryCategory(
19
+ config: QueryGenConfig,
20
+ entityGroup: EntityGroup
21
+ ): QueryCategoryInfo {
22
+ const rootCategoryName = config.rootQueryCategory || 'Auto-Generated';
23
+
24
+ if (config.autoCreateEntityQueryCategories) {
25
+ // Create entity-specific category under root
26
+ const entityName = entityGroup?.primaryEntity?.Name;
27
+
28
+ if (!entityName) {
29
+ console.warn('buildQueryCategory: primaryEntity.Name is undefined, falling back to root category');
30
+ return {
31
+ name: rootCategoryName,
32
+ parentName: null,
33
+ description: 'Automatically generated queries from query-gen tool',
34
+ path: rootCategoryName
35
+ };
36
+ }
37
+
38
+ return {
39
+ name: entityName,
40
+ parentName: rootCategoryName,
41
+ description: `Queries for the ${entityName} entity`,
42
+ path: `${rootCategoryName}/${entityName}`
43
+ };
44
+ } else {
45
+ // Use root category only
46
+ return {
47
+ name: rootCategoryName,
48
+ parentName: null,
49
+ description: 'Automatically generated queries from query-gen tool',
50
+ path: rootCategoryName
51
+ };
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Extract all unique categories from validated queries
57
+ * Returns categories in hierarchical order (root first, then children)
58
+ *
59
+ * IMPORTANT: Also creates parent categories for any child categories
60
+ * (e.g., if "Golden-Queries/Members" is provided, also creates "Golden-Queries")
61
+ *
62
+ * @param categories - Array of QueryCategoryInfo from validated queries
63
+ * @returns Unique categories sorted hierarchically (includes auto-generated parents)
64
+ */
65
+ export function extractUniqueCategories(categories: QueryCategoryInfo[]): QueryCategoryInfo[] {
66
+ const uniqueMap = new Map<string, QueryCategoryInfo>();
67
+
68
+ // Collect all unique categories (by path)
69
+ for (const cat of categories) {
70
+ if (!uniqueMap.has(cat.path)) {
71
+ uniqueMap.set(cat.path, cat);
72
+ }
73
+
74
+ // If this is a child category, ensure parent category exists
75
+ if (cat.parentName && !uniqueMap.has(cat.parentName)) {
76
+ uniqueMap.set(cat.parentName, {
77
+ name: cat.parentName,
78
+ parentName: null,
79
+ description: 'Automatically generated queries from query-gen tool',
80
+ path: cat.parentName
81
+ });
82
+ }
83
+ }
84
+
85
+ // Convert to array and sort: root categories first, then children
86
+ const result = Array.from(uniqueMap.values());
87
+ result.sort((a, b) => {
88
+ // Root categories (no parent) come first
89
+ if (a.parentName === null && b.parentName !== null) return -1;
90
+ if (a.parentName !== null && b.parentName === null) return 1;
91
+
92
+ // Then sort alphabetically by path
93
+ return a.path.localeCompare(b.path);
94
+ });
95
+
96
+ return result;
97
+ }
@@ -0,0 +1,203 @@
1
+ /**
2
+ * Entity helper utilities
3
+ *
4
+ * Helper functions for working with entity metadata and formatting for AI prompts
5
+ */
6
+
7
+ import { EntityInfo, EntityFieldInfo } from '@memberjunction/core';
8
+ import {
9
+ EntityMetadataForPrompt,
10
+ EntityFieldMetadata,
11
+ EntityRelationshipMetadata,
12
+ EntityGroup
13
+ } from '../data/schema';
14
+
15
+ /**
16
+ * Format entity metadata for AI prompt consumption
17
+ * Includes schema name, base view, fields, and relationships
18
+ *
19
+ * CRITICAL: Must include schemaName and baseView for functional SQL generation
20
+ *
21
+ * @param entity - Entity to format
22
+ * @param allEntities - All available entities for relationship lookups
23
+ * @returns Formatted entity metadata ready for AI prompts
24
+ */
25
+ export function formatEntityMetadataForPrompt(entity: EntityInfo, allEntities: EntityInfo[]): EntityMetadataForPrompt {
26
+ return {
27
+ entityName: entity.Name,
28
+ description: entity.Description || '',
29
+ schemaName: entity.SchemaName || 'dbo',
30
+ baseTable: entity.BaseTable || entity.Name,
31
+ baseView: entity.BaseView || `vw${entity.Name}`,
32
+ fields: formatEntityFields(entity),
33
+ relationships: formatEntityRelationships(entity, allEntities),
34
+ };
35
+ }
36
+
37
+ /**
38
+ * Format entity fields for AI prompt
39
+ * Includes field metadata with types, descriptions, and relationship info
40
+ * Labels internal (__mj_*) and virtual fields appropriately
41
+ */
42
+ function formatEntityFields(entity: EntityInfo): EntityFieldMetadata[] {
43
+ return entity.Fields.map((field) => {
44
+ const isInternalField = field.Name.startsWith('__mj_');
45
+ const isVirtualField = field.IsVirtual || false;
46
+
47
+ let description = field.Description || '';
48
+
49
+ // Add labels for special fields
50
+ if (isInternalField) {
51
+ description = `[INTERNAL] ${description}`;
52
+ } else if (isVirtualField) {
53
+ description = `[VIRTUAL] ${description}`;
54
+ }
55
+
56
+ // Extract possible values from EntityFieldValues if available
57
+ const possibleValues = field.EntityFieldValues && field.EntityFieldValues.length > 0
58
+ ? field.EntityFieldValues.map(efv => efv.Value)
59
+ : undefined;
60
+
61
+ return {
62
+ name: field.Name,
63
+ displayName: field.DisplayName || field.Name,
64
+ type: field.Type,
65
+ sqlFullType: field.SQLFullType,
66
+ description,
67
+ isPrimaryKey: field.IsPrimaryKey || false,
68
+ isForeignKey: field.RelatedEntityID != null && field.RelatedEntityID.trim().length > 0,
69
+ isVirtual: isVirtualField,
70
+ allowsNull: field.AllowsNull,
71
+ relatedEntity: field.RelatedEntity || undefined,
72
+ isRequired: !field.AllowsNull,
73
+ defaultValue: field.DefaultValue || undefined,
74
+ possibleValues,
75
+ };
76
+ });
77
+ }
78
+
79
+ /**
80
+ * Format entity relationships for AI prompt
81
+ * Includes schema and view names for proper JOIN generation
82
+ * Only includes relationships to entities in the current entity group
83
+ * Uses EntityInfo.RelatedEntities getter for pre-computed relationships
84
+ */
85
+ function formatEntityRelationships(entity: EntityInfo, allEntities: EntityInfo[]): EntityRelationshipMetadata[] {
86
+ // Create set of entity names in the group for filtering
87
+ const entityNamesInGroup = new Set(allEntities.map(e => e.Name));
88
+
89
+ // Use entity.RelatedEntities getter which returns EntityRelationshipInfo[]
90
+ return entity.RelatedEntities
91
+ .filter(rel => entityNamesInGroup.has(rel.RelatedEntity)) // Only include relationships within the group
92
+ .map(rel => {
93
+ const relatedEntity = findEntityById(rel.RelatedEntityID, allEntities);
94
+
95
+ // Determine the foreign key field based on relationship type and available fields
96
+ let foreignKeyField: string;
97
+ let joinDescription: string;
98
+
99
+ const currentSchema = entity.SchemaName || 'dbo';
100
+ const currentView = entity.BaseView || `vw${entity.Name}`;
101
+ const relatedSchema = relatedEntity?.SchemaName || 'dbo';
102
+ const relatedView = relatedEntity?.BaseView || `vw${rel.RelatedEntity}`;
103
+
104
+ if (rel.EntityKeyField && rel.EntityKeyField.trim() !== '') {
105
+ // Current entity has the foreign key
106
+ foreignKeyField = rel.EntityKeyField;
107
+ const relatedJoinField = rel.RelatedEntityJoinField || 'ID';
108
+ joinDescription = `${currentSchema}.${currentView}.${foreignKeyField} = ${relatedSchema}.${relatedView}.${relatedJoinField}`;
109
+ } else if (rel.RelatedEntityJoinField && rel.RelatedEntityJoinField.trim() !== '') {
110
+ // Related entity has the foreign key pointing back to this entity
111
+ foreignKeyField = rel.RelatedEntityJoinField;
112
+ joinDescription = `${relatedSchema}.${relatedView}.${foreignKeyField} = ${currentSchema}.${currentView}.ID`;
113
+ } else {
114
+ // No foreign key field specified (possibly many-to-many through join table)
115
+ foreignKeyField = '';
116
+ joinDescription = `Related via ${rel.JoinView || 'join table'}`;
117
+ }
118
+
119
+ return {
120
+ type: mapRelationshipType(rel.Type),
121
+ relatedEntity: rel.RelatedEntity,
122
+ relatedEntityView: relatedEntity?.BaseView || `vw${rel.RelatedEntity}`,
123
+ relatedEntitySchema: relatedEntity?.SchemaName || 'dbo',
124
+ foreignKeyField,
125
+ description: joinDescription,
126
+ };
127
+ });
128
+ }
129
+
130
+ /**
131
+ * Map MJ relationship types to QueryGen types
132
+ */
133
+ function mapRelationshipType(mjType: string): 'one-to-many' | 'many-to-one' | 'many-to-many' {
134
+ const normalized = mjType.toLowerCase().replace(/\s+/g, '-');
135
+ if (normalized === 'many-to-one') return 'many-to-one';
136
+ if (normalized === 'one-to-many') return 'one-to-many';
137
+ if (normalized === 'many-to-many') return 'many-to-many';
138
+ return 'many-to-one'; // Default fallback
139
+ }
140
+
141
+ /**
142
+ * Check if a field is a foreign key field
143
+ */
144
+ function isForeignKeyField(field: EntityFieldInfo): boolean {
145
+ return field.RelatedEntityID != null && field.RelatedEntityID.trim().length > 0;
146
+ }
147
+
148
+ /**
149
+ * Find entity by name in array
150
+ */
151
+ function findEntityByName(name: string, entities: EntityInfo[]): EntityInfo | undefined {
152
+ return entities.find((e) => e.Name === name);
153
+ }
154
+
155
+ /**
156
+ * Find entity by ID in array
157
+ */
158
+ export function findEntityById(id: string, entities: EntityInfo[]): EntityInfo | undefined {
159
+ return entities.find((e) => e.ID === id);
160
+ }
161
+
162
+ /**
163
+ * Get primary key field(s) for an entity
164
+ */
165
+ export function getPrimaryKeyFields(entity: EntityInfo): EntityFieldInfo[] {
166
+ return entity.Fields.filter((f) => f.IsPrimaryKey);
167
+ }
168
+
169
+ /**
170
+ * Get foreign key fields for an entity
171
+ */
172
+ export function getForeignKeyFields(entity: EntityInfo): EntityFieldInfo[] {
173
+ return entity.Fields.filter((f) => isForeignKeyField(f));
174
+ }
175
+
176
+ /**
177
+ * Check if an entity has any relationships
178
+ * Uses EntityInfo.RelatedEntities getter
179
+ */
180
+ export function hasRelationships(entity: EntityInfo, allEntities: EntityInfo[]): boolean {
181
+ return entity.RelatedEntities.length > 0;
182
+ }
183
+
184
+ /**
185
+ * Get count of relationships for an entity
186
+ * Uses EntityInfo.RelatedEntities getter
187
+ */
188
+ export function getRelationshipCount(entity: EntityInfo, allEntities: EntityInfo[]): number {
189
+ return entity.RelatedEntities.length;
190
+ }
191
+
192
+ /**
193
+ * Format an entire entity group for AI prompt consumption
194
+ * Converts all entities in the group to structured metadata
195
+ *
196
+ * @param entityGroup - Entity group to format
197
+ * @returns Array of formatted entity metadata for Nunjucks template
198
+ */
199
+ export function formatEntityGroupForPrompt(entityGroup: EntityGroup): EntityMetadataForPrompt[] {
200
+ return entityGroup.entities.map((entity) =>
201
+ formatEntityMetadataForPrompt(entity, entityGroup.entities)
202
+ );
203
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Error handling utilities
3
+ *
4
+ * Standardized error handling functions following MJ patterns
5
+ */
6
+
7
+ /**
8
+ * Extract error message from unknown error type
9
+ * Uses MJ's extractErrorMessage pattern
10
+ */
11
+ export function extractErrorMessage(error: unknown, context: string): string {
12
+ if (error instanceof Error) {
13
+ return `${context}: ${error.message}`;
14
+ }
15
+ if (typeof error === 'string') {
16
+ return `${context}: ${error}`;
17
+ }
18
+ return `${context}: Unknown error occurred`;
19
+ }
20
+
21
+ /**
22
+ * Validate required value is not null/undefined
23
+ */
24
+ export function requireValue<T>(value: T | null | undefined, fieldName: string): T {
25
+ if (value === null || value === undefined) {
26
+ throw new Error(`Required value '${fieldName}' is missing`);
27
+ }
28
+ return value;
29
+ }
30
+
31
+ /**
32
+ * Get property with fallback value
33
+ */
34
+ export function getPropertyOrDefault<T>(
35
+ obj: Record<string, unknown>,
36
+ key: string,
37
+ defaultValue: T
38
+ ): T {
39
+ const value = obj[key];
40
+ return value !== undefined ? (value as T) : defaultValue;
41
+ }