@memberjunction/query-gen 4.2.0 → 4.3.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.
@@ -1,4 +1,4 @@
1
1
 
2
- > @memberjunction/query-gen@4.2.0 build
2
+ > @memberjunction/query-gen@4.3.1 build
3
3
  > tsc && tsc-alias -f
4
4
 
package/CHANGELOG.md CHANGED
@@ -1,5 +1,34 @@
1
1
  # @memberjunction/query-gen
2
2
 
3
+ ## 4.3.1
4
+
5
+ ### Patch Changes
6
+
7
+ - @memberjunction/ai@4.3.1
8
+ - @memberjunction/ai-core-plus@4.3.1
9
+ - @memberjunction/aiengine@4.3.1
10
+ - @memberjunction/ai-prompts@4.3.1
11
+ - @memberjunction/ai-vectors-memory@4.3.1
12
+ - @memberjunction/core@4.3.1
13
+ - @memberjunction/core-entities@4.3.1
14
+ - @memberjunction/global@4.3.1
15
+ - @memberjunction/sqlserver-dataprovider@4.3.1
16
+
17
+ ## 4.3.0
18
+
19
+ ### Patch Changes
20
+
21
+ - Updated dependencies [564e1af]
22
+ - @memberjunction/core@4.3.0
23
+ - @memberjunction/core-entities@4.3.0
24
+ - @memberjunction/ai-core-plus@4.3.0
25
+ - @memberjunction/aiengine@4.3.0
26
+ - @memberjunction/ai-prompts@4.3.0
27
+ - @memberjunction/ai-vectors-memory@4.3.0
28
+ - @memberjunction/sqlserver-dataprovider@4.3.0
29
+ - @memberjunction/ai@4.3.0
30
+ - @memberjunction/global@4.3.0
31
+
3
32
  ## 4.2.0
4
33
 
5
34
  ### Patch Changes
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@memberjunction/query-gen",
3
3
  "type": "module",
4
- "version": "4.2.0",
4
+ "version": "4.3.1",
5
5
  "description": "AI-powered generation of domain-specific SQL query templates with automatic testing, refinement, and metadata export",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
@@ -14,9 +14,8 @@
14
14
  "clean": "rimraf dist",
15
15
  "lint": "eslint src --ext .ts",
16
16
  "format": "prettier --write \"src/**/*.ts\"",
17
- "test": "node --test src/__tests__/*.test.js",
18
- "test:jest": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
19
- "test:watch": "node --test --watch src/__tests__/*.test.js"
17
+ "test": "vitest run",
18
+ "test:watch": "vitest"
20
19
  },
21
20
  "keywords": [
22
21
  "memberjunction",
@@ -29,28 +28,26 @@
29
28
  "author": "MemberJunction",
30
29
  "license": "MIT",
31
30
  "dependencies": {
32
- "@memberjunction/ai": "4.2.0",
33
- "@memberjunction/ai-core-plus": "4.2.0",
34
- "@memberjunction/aiengine": "4.2.0",
35
- "@memberjunction/ai-prompts": "4.2.0",
36
- "@memberjunction/ai-vectors-memory": "4.2.0",
37
- "@memberjunction/core": "4.2.0",
38
- "@memberjunction/core-entities": "4.2.0",
39
- "@memberjunction/global": "4.2.0",
40
- "@memberjunction/sqlserver-dataprovider": "4.2.0",
31
+ "@memberjunction/ai": "4.3.1",
32
+ "@memberjunction/ai-core-plus": "4.3.1",
33
+ "@memberjunction/aiengine": "4.3.1",
34
+ "@memberjunction/ai-prompts": "4.3.1",
35
+ "@memberjunction/ai-vectors-memory": "4.3.1",
36
+ "@memberjunction/core": "4.3.1",
37
+ "@memberjunction/core-entities": "4.3.1",
38
+ "@memberjunction/global": "4.3.1",
39
+ "@memberjunction/sqlserver-dataprovider": "4.3.1",
41
40
  "chalk": "^5.6.2",
42
41
  "commander": "^14.0.3",
43
42
  "nunjucks": "^3.2.4",
44
43
  "ora": "^9.3.0"
45
44
  },
46
45
  "devDependencies": {
47
- "@types/jest": "^29.5.12",
48
46
  "@types/node": "24.10.11",
49
47
  "@types/nunjucks": "^3.2.6",
50
- "jest": "^29.7.0",
51
- "ts-jest": "^29.1.2",
52
48
  "ts-node-dev": "^2.0.0",
53
- "typescript": "^5.9.3"
49
+ "typescript": "^5.9.3",
50
+ "vitest": "^3.1.1"
54
51
  },
55
52
  "repository": {
56
53
  "type": "git",
@@ -0,0 +1,274 @@
1
+ /**
2
+ * Unit tests for the QueryGen package.
3
+ * Tests: graph-helpers, entity-helpers, category-builder, error-handlers.
4
+ */
5
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Mocks
9
+ // ---------------------------------------------------------------------------
10
+
11
+ vi.mock('@memberjunction/core', () => ({
12
+ EntityInfo: class {},
13
+ EntityFieldInfo: class {},
14
+ EntityRelationshipInfo: class {},
15
+ UserInfo: class {},
16
+ LogStatus: vi.fn(),
17
+ }));
18
+
19
+ vi.mock('@memberjunction/aiengine', () => ({
20
+ AIEngine: { Instance: { Prompts: [] } },
21
+ }));
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Import after mocks
25
+ // ---------------------------------------------------------------------------
26
+
27
+ import { generateRelationshipGraph, formatEntitiesForPrompt, generateMermaidDiagram } from '../utils/graph-helpers';
28
+ import { extractErrorMessage, requireValue, getPropertyOrDefault } from '../utils/error-handlers';
29
+ import { buildQueryCategory, extractUniqueCategories } from '../utils/category-builder';
30
+ import type { EntityInfo, EntityRelationshipInfo } from '@memberjunction/core';
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // Helper to create mock EntityInfo objects
34
+ // ---------------------------------------------------------------------------
35
+
36
+ function createMockEntity(
37
+ name: string,
38
+ schemaName = 'dbo',
39
+ relatedEntities: Partial<EntityRelationshipInfo>[] = [],
40
+ fieldCount = 5
41
+ ): EntityInfo {
42
+ return {
43
+ ID: `id-${name}`,
44
+ Name: name,
45
+ SchemaName: schemaName,
46
+ Description: `Description of ${name}`,
47
+ Fields: Array.from({ length: fieldCount }, (_, i) => ({
48
+ Name: `Field${i}`,
49
+ DisplayName: `Field ${i}`,
50
+ Type: 'nvarchar',
51
+ SQLFullType: 'nvarchar(100)',
52
+ Description: '',
53
+ IsPrimaryKey: i === 0,
54
+ IsVirtual: false,
55
+ AllowsNull: i > 0,
56
+ RelatedEntityID: null,
57
+ RelatedEntity: null,
58
+ DefaultValue: null,
59
+ EntityFieldValues: [],
60
+ RelatedEntityFieldName: null,
61
+ })),
62
+ RelatedEntities: relatedEntities as EntityRelationshipInfo[],
63
+ BaseView: `vw${name.replace(/\s/g, '')}`,
64
+ BaseTable: name.replace(/\s/g, ''),
65
+ PrimaryKeys: [{ Name: 'ID' }],
66
+ FirstPrimaryKey: { Name: 'ID' },
67
+ TrackRecordChanges: true,
68
+ } as unknown as EntityInfo;
69
+ }
70
+
71
+ // ---------------------------------------------------------------------------
72
+ // 1. graph-helpers
73
+ // ---------------------------------------------------------------------------
74
+
75
+ describe('generateRelationshipGraph', () => {
76
+ it('should produce text lines for entities with relationships', () => {
77
+ const entities = [
78
+ createMockEntity('Customers', 'dbo', [
79
+ { RelatedEntity: 'Orders', Type: 'One To Many' } as Partial<EntityRelationshipInfo>,
80
+ ]),
81
+ createMockEntity('Orders', 'dbo', []),
82
+ ];
83
+
84
+ const graph = generateRelationshipGraph(entities as EntityInfo[]);
85
+ expect(graph).toContain('Customers: \u2192 Orders');
86
+ expect(graph).toContain('Orders: (no relationships)');
87
+ });
88
+
89
+ it('should handle entities with no relationships', () => {
90
+ const entities = [createMockEntity('Standalone')];
91
+ const graph = generateRelationshipGraph(entities as EntityInfo[]);
92
+ expect(graph).toContain('Standalone: (no relationships)');
93
+ });
94
+ });
95
+
96
+ describe('formatEntitiesForPrompt', () => {
97
+ it('should format entity metadata correctly', () => {
98
+ const entities = [createMockEntity('Users', 'auth')];
99
+ const result = formatEntitiesForPrompt(entities as EntityInfo[]);
100
+ expect(result).toHaveLength(1);
101
+ expect(result[0].Name).toBe('Users');
102
+ expect(result[0].SchemaName).toBe('auth');
103
+ expect(result[0].FieldCount).toBe(5);
104
+ });
105
+
106
+ it('should default description for entities without one', () => {
107
+ const entity = createMockEntity('NoDesc');
108
+ (entity as Record<string, unknown>).Description = '';
109
+ const result = formatEntitiesForPrompt([entity as EntityInfo]);
110
+ expect(result[0].Description).toBe('No description available');
111
+ });
112
+ });
113
+
114
+ describe('generateMermaidDiagram', () => {
115
+ it('should produce mermaid graph syntax', () => {
116
+ const entities = [
117
+ createMockEntity('Customers', 'dbo', [
118
+ { RelatedEntity: 'Orders', Type: 'One To Many' } as Partial<EntityRelationshipInfo>,
119
+ ]),
120
+ createMockEntity('Orders'),
121
+ ];
122
+
123
+ const diagram = generateMermaidDiagram(entities as EntityInfo[]);
124
+ expect(diagram).toContain('graph LR');
125
+ expect(diagram).toContain('Customers[Customers] --> Orders[Orders]');
126
+ });
127
+
128
+ it('should deduplicate bidirectional relationships', () => {
129
+ const entities = [
130
+ createMockEntity('A', 'dbo', [{ RelatedEntity: 'B', Type: 'One To Many' } as Partial<EntityRelationshipInfo>]),
131
+ createMockEntity('B', 'dbo', [{ RelatedEntity: 'A', Type: 'Many To One' } as Partial<EntityRelationshipInfo>]),
132
+ ];
133
+ const diagram = generateMermaidDiagram(entities as EntityInfo[]);
134
+ // Only one edge between A and B
135
+ const edgeCount = (diagram.match(/-->/g) || []).length;
136
+ expect(edgeCount).toBe(1);
137
+ });
138
+ });
139
+
140
+ // ---------------------------------------------------------------------------
141
+ // 2. error-handlers
142
+ // ---------------------------------------------------------------------------
143
+
144
+ describe('extractErrorMessage', () => {
145
+ it('should extract from Error instance', () => {
146
+ const msg = extractErrorMessage(new Error('boom'), 'TestContext');
147
+ expect(msg).toBe('TestContext: boom');
148
+ });
149
+
150
+ it('should extract from string', () => {
151
+ const msg = extractErrorMessage('string error', 'Ctx');
152
+ expect(msg).toBe('Ctx: string error');
153
+ });
154
+
155
+ it('should handle unknown error type', () => {
156
+ const msg = extractErrorMessage(42, 'Ctx');
157
+ expect(msg).toBe('Ctx: Unknown error occurred');
158
+ });
159
+ });
160
+
161
+ describe('requireValue', () => {
162
+ it('should return value when present', () => {
163
+ expect(requireValue('hello', 'field')).toBe('hello');
164
+ });
165
+
166
+ it('should throw for null', () => {
167
+ expect(() => requireValue(null, 'myField')).toThrow("Required value 'myField' is missing");
168
+ });
169
+
170
+ it('should throw for undefined', () => {
171
+ expect(() => requireValue(undefined, 'myField')).toThrow("Required value 'myField' is missing");
172
+ });
173
+ });
174
+
175
+ describe('getPropertyOrDefault', () => {
176
+ it('should return property value when present', () => {
177
+ expect(getPropertyOrDefault({ x: 10 }, 'x', 0)).toBe(10);
178
+ });
179
+
180
+ it('should return default when property missing', () => {
181
+ expect(getPropertyOrDefault({}, 'x', 42)).toBe(42);
182
+ });
183
+ });
184
+
185
+ // ---------------------------------------------------------------------------
186
+ // 3. category-builder
187
+ // ---------------------------------------------------------------------------
188
+
189
+ describe('buildQueryCategory', () => {
190
+ it('should create entity-specific category when autoCreate is true', () => {
191
+ const config = {
192
+ autoCreateEntityQueryCategories: true,
193
+ rootQueryCategory: 'Generated',
194
+ minGroupSize: 1,
195
+ maxGroupSize: 5,
196
+ verbose: false,
197
+ };
198
+
199
+ const entityGroup = {
200
+ primaryEntity: createMockEntity('Users'),
201
+ entities: [createMockEntity('Users')],
202
+ relationships: [],
203
+ relationshipType: 'single' as const,
204
+ businessDomain: 'Auth',
205
+ businessRationale: 'User management',
206
+ expectedQuestionTypes: [],
207
+ };
208
+
209
+ const cat = buildQueryCategory(config as Parameters<typeof buildQueryCategory>[0], entityGroup);
210
+ expect(cat.name).toBe('Users');
211
+ expect(cat.parentName).toBe('Generated');
212
+ expect(cat.path).toBe('Generated/Users');
213
+ });
214
+
215
+ it('should use root-only category when autoCreate is false', () => {
216
+ const config = {
217
+ autoCreateEntityQueryCategories: false,
218
+ rootQueryCategory: 'Auto-Generated',
219
+ minGroupSize: 1,
220
+ maxGroupSize: 5,
221
+ verbose: false,
222
+ };
223
+ const entityGroup = {
224
+ primaryEntity: createMockEntity('Users'),
225
+ entities: [],
226
+ relationships: [],
227
+ relationshipType: 'single' as const,
228
+ businessDomain: '',
229
+ businessRationale: '',
230
+ expectedQuestionTypes: [],
231
+ };
232
+
233
+ const cat = buildQueryCategory(config as Parameters<typeof buildQueryCategory>[0], entityGroup);
234
+ expect(cat.name).toBe('Auto-Generated');
235
+ expect(cat.parentName).toBeNull();
236
+ });
237
+ });
238
+
239
+ describe('extractUniqueCategories', () => {
240
+ it('should deduplicate categories by path', () => {
241
+ const cats = [
242
+ { name: 'Root', parentName: null, description: 'Root', path: 'Root' },
243
+ { name: 'Root', parentName: null, description: 'Root', path: 'Root' },
244
+ { name: 'Users', parentName: 'Root', description: 'Users', path: 'Root/Users' },
245
+ ];
246
+
247
+ const unique = extractUniqueCategories(cats);
248
+ expect(unique).toHaveLength(2);
249
+ });
250
+
251
+ it('should auto-create parent categories', () => {
252
+ const cats = [
253
+ { name: 'Orders', parentName: 'Generated', description: 'Orders', path: 'Generated/Orders' },
254
+ ];
255
+
256
+ const unique = extractUniqueCategories(cats);
257
+ // Should have both the parent 'Generated' and child 'Orders'
258
+ expect(unique).toHaveLength(2);
259
+ const parentCat = unique.find(c => c.name === 'Generated');
260
+ expect(parentCat).toBeDefined();
261
+ expect(parentCat!.parentName).toBeNull();
262
+ });
263
+
264
+ it('should sort root categories first', () => {
265
+ const cats = [
266
+ { name: 'Zeta', parentName: 'Alpha', description: '', path: 'Alpha/Zeta' },
267
+ { name: 'Alpha', parentName: null, description: '', path: 'Alpha' },
268
+ ];
269
+
270
+ const sorted = extractUniqueCategories(cats);
271
+ expect(sorted[0].name).toBe('Alpha');
272
+ expect(sorted[1].name).toBe('Zeta');
273
+ });
274
+ });
package/tsconfig.json CHANGED
@@ -4,6 +4,14 @@
4
4
  "outDir": "dist",
5
5
  "rootDir": "src"
6
6
  },
7
- "include": ["src/**/*"],
8
- "exclude": ["node_modules"]
7
+ "include": [
8
+ "src/**/*"
9
+ ],
10
+ "exclude": [
11
+ "node_modules",
12
+ "src/__tests__/**",
13
+ "src/**/*.test.ts",
14
+ "src/**/*.spec.ts",
15
+ "vitest.config.ts"
16
+ ]
9
17
  }
@@ -0,0 +1,8 @@
1
+ import { defineProject, mergeConfig } from 'vitest/config';
2
+ import sharedConfig from '../../vitest.shared';
3
+
4
+ export default mergeConfig(sharedConfig, defineProject({
5
+ test: {
6
+ environment: 'node',
7
+ },
8
+ }));