@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.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +29 -0
- package/package.json +14 -17
- package/src/__tests__/QueryGen.test.ts +274 -0
- package/tsconfig.json +10 -2
- package/vitest.config.ts +8 -0
package/.turbo/turbo-build.log
CHANGED
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.
|
|
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": "
|
|
18
|
-
"test:
|
|
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.
|
|
33
|
-
"@memberjunction/ai-core-plus": "4.
|
|
34
|
-
"@memberjunction/aiengine": "4.
|
|
35
|
-
"@memberjunction/ai-prompts": "4.
|
|
36
|
-
"@memberjunction/ai-vectors-memory": "4.
|
|
37
|
-
"@memberjunction/core": "4.
|
|
38
|
-
"@memberjunction/core-entities": "4.
|
|
39
|
-
"@memberjunction/global": "4.
|
|
40
|
-
"@memberjunction/sqlserver-dataprovider": "4.
|
|
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": [
|
|
8
|
-
|
|
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
|
}
|