@objectql/core 1.7.1 → 1.7.3
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/CHANGELOG.md +16 -0
- package/LICENSE +118 -21
- package/dist/ai-agent.d.ts +175 -0
- package/dist/ai-agent.js +746 -0
- package/dist/ai-agent.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/repository.d.ts +7 -0
- package/dist/repository.js +64 -1
- package/dist/repository.js.map +1 -1
- package/dist/validator.d.ts +6 -2
- package/dist/validator.js +172 -13
- package/dist/validator.js.map +1 -1
- package/package.json +7 -3
- package/src/ai-agent.ts +888 -0
- package/src/index.ts +1 -2
- package/src/repository.ts +86 -3
- package/src/validator.ts +196 -13
- package/test/repository-validation.test.ts +343 -0
- package/tsconfig.tsbuildinfo +1 -1
package/dist/ai-agent.js
ADDED
|
@@ -0,0 +1,746 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* ObjectQL AI Agent - Programmatic API for AI-powered application generation
|
|
4
|
+
*
|
|
5
|
+
* This module provides a high-level API for using AI to generate and validate
|
|
6
|
+
* ObjectQL metadata programmatically.
|
|
7
|
+
*/
|
|
8
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
9
|
+
if (k2 === undefined) k2 = k;
|
|
10
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
11
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
12
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
13
|
+
}
|
|
14
|
+
Object.defineProperty(o, k2, desc);
|
|
15
|
+
}) : (function(o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
o[k2] = m[k];
|
|
18
|
+
}));
|
|
19
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
20
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
21
|
+
}) : function(o, v) {
|
|
22
|
+
o["default"] = v;
|
|
23
|
+
});
|
|
24
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
25
|
+
var ownKeys = function(o) {
|
|
26
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
27
|
+
var ar = [];
|
|
28
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
29
|
+
return ar;
|
|
30
|
+
};
|
|
31
|
+
return ownKeys(o);
|
|
32
|
+
};
|
|
33
|
+
return function (mod) {
|
|
34
|
+
if (mod && mod.__esModule) return mod;
|
|
35
|
+
var result = {};
|
|
36
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
37
|
+
__setModuleDefault(result, mod);
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
40
|
+
})();
|
|
41
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
42
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
43
|
+
};
|
|
44
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
|
+
exports.ObjectQLAgent = void 0;
|
|
46
|
+
exports.createAgent = createAgent;
|
|
47
|
+
const openai_1 = __importDefault(require("openai"));
|
|
48
|
+
const yaml = __importStar(require("js-yaml"));
|
|
49
|
+
const validator_1 = require("./validator");
|
|
50
|
+
/**
|
|
51
|
+
* Regular expression patterns for parsing AI responses
|
|
52
|
+
*/
|
|
53
|
+
const AI_RESPONSE_PATTERNS = {
|
|
54
|
+
/**
|
|
55
|
+
* Matches YAML file blocks with explicit headers in the format:
|
|
56
|
+
* # filename.object.yml or File: filename.object.yml
|
|
57
|
+
* followed by a YAML code block
|
|
58
|
+
*
|
|
59
|
+
* Groups:
|
|
60
|
+
* 1. filename (e.g., "user.object.yml")
|
|
61
|
+
* 2. YAML content
|
|
62
|
+
*/
|
|
63
|
+
FILE_BLOCK_YAML: /(?:^|\n)(?:#|File:)\s*([a-zA-Z0-9_-]+\.[a-z]+\.yml)\s*\n```(?:yaml|yml)?\n([\s\S]*?)```/gi,
|
|
64
|
+
/**
|
|
65
|
+
* Matches TypeScript file blocks with explicit headers in the format:
|
|
66
|
+
* // filename.action.ts or File: filename.hook.ts or filename.test.ts
|
|
67
|
+
* followed by a TypeScript code block
|
|
68
|
+
*
|
|
69
|
+
* Groups:
|
|
70
|
+
* 1. filename (e.g., "approve_order.action.ts", "user.hook.ts", "user.test.ts")
|
|
71
|
+
* 2. TypeScript content
|
|
72
|
+
*/
|
|
73
|
+
FILE_BLOCK_TS: /(?:^|\n)(?:\/\/|File:)\s*([a-zA-Z0-9_-]+\.(?:action|hook|test|spec)\.ts)\s*\n```(?:typescript|ts)?\n([\s\S]*?)```/gi,
|
|
74
|
+
/**
|
|
75
|
+
* Matches generic YAML/YML code blocks without explicit headers
|
|
76
|
+
*
|
|
77
|
+
* Groups:
|
|
78
|
+
* 1. YAML content
|
|
79
|
+
*/
|
|
80
|
+
CODE_BLOCK_YAML: /```(?:yaml|yml)\n([\s\S]*?)```/g,
|
|
81
|
+
/**
|
|
82
|
+
* Matches generic TypeScript code blocks without explicit headers
|
|
83
|
+
*
|
|
84
|
+
* Groups:
|
|
85
|
+
* 1. TypeScript content
|
|
86
|
+
*/
|
|
87
|
+
CODE_BLOCK_TS: /```(?:typescript|ts)\n([\s\S]*?)```/g,
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* ObjectQL AI Agent for programmatic application generation and validation
|
|
91
|
+
*/
|
|
92
|
+
class ObjectQLAgent {
|
|
93
|
+
constructor(config) {
|
|
94
|
+
var _a;
|
|
95
|
+
this.config = {
|
|
96
|
+
apiKey: config.apiKey,
|
|
97
|
+
model: config.model || 'gpt-4',
|
|
98
|
+
temperature: (_a = config.temperature) !== null && _a !== void 0 ? _a : 0.7,
|
|
99
|
+
language: config.language || 'en',
|
|
100
|
+
};
|
|
101
|
+
this.openai = new openai_1.default({ apiKey: this.config.apiKey });
|
|
102
|
+
this.validator = new validator_1.Validator({ language: this.config.language });
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Generate application metadata from natural language description
|
|
106
|
+
*/
|
|
107
|
+
async generateApp(options) {
|
|
108
|
+
var _a, _b;
|
|
109
|
+
const systemPrompt = this.getSystemPrompt();
|
|
110
|
+
const userPrompt = this.buildGenerationPrompt(options);
|
|
111
|
+
try {
|
|
112
|
+
const completion = await this.openai.chat.completions.create({
|
|
113
|
+
model: this.config.model,
|
|
114
|
+
messages: [
|
|
115
|
+
{ role: 'system', content: systemPrompt },
|
|
116
|
+
{ role: 'user', content: userPrompt }
|
|
117
|
+
],
|
|
118
|
+
temperature: this.config.temperature,
|
|
119
|
+
max_tokens: options.maxTokens || 4000,
|
|
120
|
+
});
|
|
121
|
+
const response = (_b = (_a = completion.choices[0]) === null || _a === void 0 ? void 0 : _a.message) === null || _b === void 0 ? void 0 : _b.content;
|
|
122
|
+
if (!response) {
|
|
123
|
+
return {
|
|
124
|
+
success: false,
|
|
125
|
+
files: [],
|
|
126
|
+
errors: ['No response from AI model'],
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
// Parse response and extract files
|
|
130
|
+
const files = this.parseGenerationResponse(response);
|
|
131
|
+
return {
|
|
132
|
+
success: files.length > 0,
|
|
133
|
+
files,
|
|
134
|
+
rawResponse: response,
|
|
135
|
+
errors: files.length === 0 ? ['Failed to extract metadata files from response'] : undefined,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
return {
|
|
140
|
+
success: false,
|
|
141
|
+
files: [],
|
|
142
|
+
errors: [error instanceof Error ? error.message : 'Unknown error'],
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Validate metadata using AI
|
|
148
|
+
*/
|
|
149
|
+
async validateMetadata(options) {
|
|
150
|
+
var _a, _b;
|
|
151
|
+
// Parse metadata if it's a string
|
|
152
|
+
let parsedMetadata;
|
|
153
|
+
if (typeof options.metadata === 'string') {
|
|
154
|
+
try {
|
|
155
|
+
parsedMetadata = yaml.load(options.metadata);
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
return {
|
|
159
|
+
valid: false,
|
|
160
|
+
errors: [{
|
|
161
|
+
message: `YAML parsing error: ${error instanceof Error ? error.message : 'Invalid YAML'}`,
|
|
162
|
+
location: 'root',
|
|
163
|
+
}],
|
|
164
|
+
warnings: [],
|
|
165
|
+
info: [],
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
parsedMetadata = options.metadata;
|
|
171
|
+
}
|
|
172
|
+
// Build validation prompt
|
|
173
|
+
const validationPrompt = this.buildValidationPrompt(options);
|
|
174
|
+
try {
|
|
175
|
+
const completion = await this.openai.chat.completions.create({
|
|
176
|
+
model: this.config.model,
|
|
177
|
+
messages: [
|
|
178
|
+
{ role: 'system', content: this.getValidationSystemPrompt() },
|
|
179
|
+
{ role: 'user', content: validationPrompt }
|
|
180
|
+
],
|
|
181
|
+
temperature: 0.3, // Lower temperature for more consistent validation
|
|
182
|
+
max_tokens: 2000,
|
|
183
|
+
});
|
|
184
|
+
const feedback = ((_b = (_a = completion.choices[0]) === null || _a === void 0 ? void 0 : _a.message) === null || _b === void 0 ? void 0 : _b.content) || '';
|
|
185
|
+
// Parse feedback into structured result
|
|
186
|
+
return this.parseFeedback(feedback);
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
return {
|
|
190
|
+
valid: false,
|
|
191
|
+
errors: [{
|
|
192
|
+
message: `Validation error: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
193
|
+
}],
|
|
194
|
+
warnings: [],
|
|
195
|
+
info: [],
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Refine existing metadata based on feedback
|
|
201
|
+
*/
|
|
202
|
+
async refineMetadata(metadata, feedback, iterations = 1) {
|
|
203
|
+
var _a, _b;
|
|
204
|
+
const systemPrompt = this.getSystemPrompt();
|
|
205
|
+
let currentMetadata = metadata;
|
|
206
|
+
let messages = [
|
|
207
|
+
{ role: 'system', content: systemPrompt },
|
|
208
|
+
{ role: 'user', content: `Here is the current metadata:\n\n${metadata}\n\nPlease refine it based on this feedback: ${feedback}` }
|
|
209
|
+
];
|
|
210
|
+
for (let i = 0; i < iterations; i++) {
|
|
211
|
+
try {
|
|
212
|
+
const completion = await this.openai.chat.completions.create({
|
|
213
|
+
model: this.config.model,
|
|
214
|
+
messages,
|
|
215
|
+
temperature: 0.5,
|
|
216
|
+
max_tokens: 4000,
|
|
217
|
+
});
|
|
218
|
+
const response = (_b = (_a = completion.choices[0]) === null || _a === void 0 ? void 0 : _a.message) === null || _b === void 0 ? void 0 : _b.content;
|
|
219
|
+
if (!response)
|
|
220
|
+
break;
|
|
221
|
+
currentMetadata = response;
|
|
222
|
+
messages.push({ role: 'assistant', content: response });
|
|
223
|
+
// If this isn't the last iteration, validate and continue
|
|
224
|
+
if (i < iterations - 1) {
|
|
225
|
+
const validation = await this.validateMetadata({
|
|
226
|
+
metadata: response,
|
|
227
|
+
checkBusinessLogic: true,
|
|
228
|
+
});
|
|
229
|
+
if (validation.valid) {
|
|
230
|
+
break; // Stop if validation passes
|
|
231
|
+
}
|
|
232
|
+
// Add validation feedback for next iteration
|
|
233
|
+
const validationFeedback = [
|
|
234
|
+
...validation.errors.map(e => `ERROR: ${e.message}`),
|
|
235
|
+
...validation.warnings.map(w => `WARNING: ${w.message}`)
|
|
236
|
+
].join('\n');
|
|
237
|
+
messages.push({
|
|
238
|
+
role: 'user',
|
|
239
|
+
content: `Please address these issues:\n${validationFeedback}`
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
catch (error) {
|
|
244
|
+
return {
|
|
245
|
+
success: false,
|
|
246
|
+
files: [],
|
|
247
|
+
errors: [error instanceof Error ? error.message : 'Unknown error'],
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
const files = this.parseGenerationResponse(currentMetadata);
|
|
252
|
+
return {
|
|
253
|
+
success: files.length > 0,
|
|
254
|
+
files,
|
|
255
|
+
rawResponse: currentMetadata,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Conversational generation with step-by-step refinement
|
|
260
|
+
* This allows users to iteratively improve the application through dialogue
|
|
261
|
+
*/
|
|
262
|
+
async generateConversational(options) {
|
|
263
|
+
var _a, _b;
|
|
264
|
+
const systemPrompt = this.getSystemPrompt();
|
|
265
|
+
// Initialize or continue conversation
|
|
266
|
+
const messages = [
|
|
267
|
+
{ role: 'system', content: systemPrompt }
|
|
268
|
+
];
|
|
269
|
+
// Add conversation history if provided
|
|
270
|
+
if (options.conversationHistory) {
|
|
271
|
+
messages.push(...options.conversationHistory.filter(m => m.role !== 'system'));
|
|
272
|
+
}
|
|
273
|
+
// Build the user message
|
|
274
|
+
let userMessage = options.message;
|
|
275
|
+
// If there's a current app state, include it in context
|
|
276
|
+
if (options.currentApp && options.currentApp.files.length > 0) {
|
|
277
|
+
const currentState = options.currentApp.files
|
|
278
|
+
.map(f => `# ${f.filename}\n${f.content}`)
|
|
279
|
+
.join('\n\n---\n\n');
|
|
280
|
+
userMessage = `Current application state:\n\n${currentState}\n\n---\n\nUser request: ${options.message}\n\nPlease update the application according to the user's request. Provide the complete updated files.`;
|
|
281
|
+
}
|
|
282
|
+
messages.push({ role: 'user', content: userMessage });
|
|
283
|
+
try {
|
|
284
|
+
const completion = await this.openai.chat.completions.create({
|
|
285
|
+
model: this.config.model,
|
|
286
|
+
messages,
|
|
287
|
+
temperature: this.config.temperature,
|
|
288
|
+
max_tokens: 4000,
|
|
289
|
+
});
|
|
290
|
+
const response = (_b = (_a = completion.choices[0]) === null || _a === void 0 ? void 0 : _a.message) === null || _b === void 0 ? void 0 : _b.content;
|
|
291
|
+
if (!response) {
|
|
292
|
+
return {
|
|
293
|
+
success: false,
|
|
294
|
+
files: [],
|
|
295
|
+
conversationHistory: [...(options.conversationHistory || []),
|
|
296
|
+
{ role: 'user', content: options.message }],
|
|
297
|
+
errors: ['No response from AI model'],
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
// Parse the response
|
|
301
|
+
const files = this.parseGenerationResponse(response);
|
|
302
|
+
// Update conversation history
|
|
303
|
+
const updatedHistory = [
|
|
304
|
+
...(options.conversationHistory || []),
|
|
305
|
+
{ role: 'user', content: options.message },
|
|
306
|
+
{ role: 'assistant', content: response }
|
|
307
|
+
];
|
|
308
|
+
// Generate suggestions for next steps
|
|
309
|
+
const suggestions = this.generateSuggestions(files, options.currentApp);
|
|
310
|
+
return {
|
|
311
|
+
success: files.length > 0,
|
|
312
|
+
files,
|
|
313
|
+
rawResponse: response,
|
|
314
|
+
conversationHistory: updatedHistory,
|
|
315
|
+
suggestions,
|
|
316
|
+
errors: files.length === 0 ? ['Failed to extract metadata files from response'] : undefined,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
catch (error) {
|
|
320
|
+
return {
|
|
321
|
+
success: false,
|
|
322
|
+
files: [],
|
|
323
|
+
conversationHistory: [...(options.conversationHistory || []),
|
|
324
|
+
{ role: 'user', content: options.message }],
|
|
325
|
+
errors: [error instanceof Error ? error.message : 'Unknown error'],
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Generate suggestions for next steps based on current application state
|
|
331
|
+
*/
|
|
332
|
+
generateSuggestions(currentFiles, previousApp) {
|
|
333
|
+
const suggestions = [];
|
|
334
|
+
// Check what metadata types are missing
|
|
335
|
+
const fileTypes = new Set(currentFiles.map(f => f.type));
|
|
336
|
+
const allTypes = [
|
|
337
|
+
'object', 'validation', 'form', 'view', 'page',
|
|
338
|
+
'menu', 'action', 'hook', 'permission', 'workflow', 'report', 'data'
|
|
339
|
+
];
|
|
340
|
+
const missingTypes = allTypes.filter(t => !fileTypes.has(t));
|
|
341
|
+
if (missingTypes.length > 0) {
|
|
342
|
+
suggestions.push(`Consider adding: ${missingTypes.join(', ')}`);
|
|
343
|
+
}
|
|
344
|
+
if (!fileTypes.has('permission')) {
|
|
345
|
+
suggestions.push('Add permissions to control access');
|
|
346
|
+
}
|
|
347
|
+
if (!fileTypes.has('menu')) {
|
|
348
|
+
suggestions.push('Create a menu for navigation');
|
|
349
|
+
}
|
|
350
|
+
if (!fileTypes.has('workflow') && fileTypes.has('object')) {
|
|
351
|
+
suggestions.push('Add workflows for approval processes');
|
|
352
|
+
}
|
|
353
|
+
if (!fileTypes.has('report') && fileTypes.has('object')) {
|
|
354
|
+
suggestions.push('Generate reports for analytics');
|
|
355
|
+
}
|
|
356
|
+
return suggestions;
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Get system prompt for metadata generation
|
|
360
|
+
*/
|
|
361
|
+
getSystemPrompt() {
|
|
362
|
+
return `You are an expert ObjectQL architect. Generate valid ObjectQL metadata in YAML format AND TypeScript implementation files for business logic.
|
|
363
|
+
|
|
364
|
+
Follow ObjectQL metadata standards for ALL metadata types:
|
|
365
|
+
|
|
366
|
+
**1. Core Data Layer:**
|
|
367
|
+
- Objects (*.object.yml): entities, fields, relationships, indexes
|
|
368
|
+
- Validations (*.validation.yml): validation rules, business constraints
|
|
369
|
+
- Data (*.data.yml): seed data and initial records
|
|
370
|
+
|
|
371
|
+
**2. Business Logic Layer (YAML + TypeScript):**
|
|
372
|
+
- Actions (*.action.yml + *.action.ts): custom RPC operations with TypeScript implementation
|
|
373
|
+
- Hooks (*.hook.yml + *.hook.ts): lifecycle triggers with TypeScript implementation
|
|
374
|
+
- beforeCreate, afterCreate, beforeUpdate, afterUpdate, beforeDelete, afterDelete
|
|
375
|
+
- Workflows (*.workflow.yml): approval processes, automation
|
|
376
|
+
|
|
377
|
+
**3. Presentation Layer:**
|
|
378
|
+
- Pages (*.page.yml): composable UI pages with layouts
|
|
379
|
+
- Views (*.view.yml): list views, kanban, calendar displays
|
|
380
|
+
- Forms (*.form.yml): data entry forms with field layouts
|
|
381
|
+
- Reports (*.report.yml): tabular, summary, matrix reports
|
|
382
|
+
- Menus (*.menu.yml): navigation structure
|
|
383
|
+
|
|
384
|
+
**4. Security Layer:**
|
|
385
|
+
- Permissions (*.permission.yml): access control rules
|
|
386
|
+
- Application (*.application.yml): app-level configuration
|
|
387
|
+
|
|
388
|
+
**Field Types:** text, number, boolean, select, date, datetime, lookup, currency, email, phone, url, textarea, formula, file, image
|
|
389
|
+
|
|
390
|
+
**For Actions - Generate BOTH files:**
|
|
391
|
+
Example:
|
|
392
|
+
# approve_order.action.yml
|
|
393
|
+
\`\`\`yaml
|
|
394
|
+
label: Approve Order
|
|
395
|
+
type: record
|
|
396
|
+
params:
|
|
397
|
+
comment:
|
|
398
|
+
type: textarea
|
|
399
|
+
label: Comment
|
|
400
|
+
\`\`\`
|
|
401
|
+
|
|
402
|
+
# approve_order.action.ts
|
|
403
|
+
\`\`\`typescript
|
|
404
|
+
import { ActionContext } from '@objectql/types';
|
|
405
|
+
|
|
406
|
+
export default async function approveOrder(context: ActionContext) {
|
|
407
|
+
const { recordId, params, user, app } = context;
|
|
408
|
+
|
|
409
|
+
// Business logic here
|
|
410
|
+
const record = await app.findOne('orders', { _id: recordId });
|
|
411
|
+
|
|
412
|
+
if (!record) {
|
|
413
|
+
throw new Error('Order not found');
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
await app.update('orders', recordId, {
|
|
417
|
+
status: 'approved',
|
|
418
|
+
approved_by: user.id,
|
|
419
|
+
approved_at: new Date(),
|
|
420
|
+
approval_comment: params.comment
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
return { success: true, message: 'Order approved successfully' };
|
|
424
|
+
}
|
|
425
|
+
\`\`\`
|
|
426
|
+
|
|
427
|
+
**For Hooks - Generate BOTH files:**
|
|
428
|
+
Example:
|
|
429
|
+
# user.hook.yml
|
|
430
|
+
\`\`\`yaml
|
|
431
|
+
triggers:
|
|
432
|
+
- beforeCreate
|
|
433
|
+
- beforeUpdate
|
|
434
|
+
\`\`\`
|
|
435
|
+
|
|
436
|
+
# user.hook.ts
|
|
437
|
+
\`\`\`typescript
|
|
438
|
+
import { HookContext } from '@objectql/types';
|
|
439
|
+
|
|
440
|
+
export async function beforeCreate(context: HookContext) {
|
|
441
|
+
const { data, user } = context;
|
|
442
|
+
|
|
443
|
+
// Auto-assign creator
|
|
444
|
+
data.created_by = user.id;
|
|
445
|
+
data.created_at = new Date();
|
|
446
|
+
|
|
447
|
+
// Validate email uniqueness (example)
|
|
448
|
+
const existing = await context.app.findOne('users', { email: data.email });
|
|
449
|
+
if (existing) {
|
|
450
|
+
throw new Error('Email already exists');
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
export async function beforeUpdate(context: HookContext) {
|
|
455
|
+
const { data, previousData, user } = context;
|
|
456
|
+
|
|
457
|
+
data.updated_by = user.id;
|
|
458
|
+
data.updated_at = new Date();
|
|
459
|
+
}
|
|
460
|
+
\`\`\`
|
|
461
|
+
|
|
462
|
+
**For Tests - Generate test files:**
|
|
463
|
+
Example:
|
|
464
|
+
// approve_order.test.ts
|
|
465
|
+
\`\`\`typescript
|
|
466
|
+
import { describe, it, expect, beforeEach } from '@jest/globals';
|
|
467
|
+
import { ObjectQL } from '@objectql/core';
|
|
468
|
+
import approveOrder from './approve_order.action';
|
|
469
|
+
|
|
470
|
+
describe('approve_order action', () => {
|
|
471
|
+
let app: ObjectQL;
|
|
472
|
+
let testUser: any;
|
|
473
|
+
|
|
474
|
+
beforeEach(async () => {
|
|
475
|
+
// Setup test environment
|
|
476
|
+
app = new ObjectQL(/* test config */);
|
|
477
|
+
await app.connect();
|
|
478
|
+
testUser = { id: 'user123', name: 'Test User' };
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
it('should approve an order successfully', async () => {
|
|
482
|
+
// Create test order
|
|
483
|
+
const order = await app.create('orders', {
|
|
484
|
+
status: 'pending',
|
|
485
|
+
total: 100
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
// Execute action
|
|
489
|
+
const result = await approveOrder({
|
|
490
|
+
recordId: order.id,
|
|
491
|
+
params: { comment: 'Approved' },
|
|
492
|
+
user: testUser,
|
|
493
|
+
app
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
// Verify
|
|
497
|
+
expect(result.success).toBe(true);
|
|
498
|
+
const updated = await app.findOne('orders', { _id: order.id });
|
|
499
|
+
expect(updated.status).toBe('approved');
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
it('should reject if order not found', async () => {
|
|
503
|
+
await expect(approveOrder({
|
|
504
|
+
recordId: 'invalid_id',
|
|
505
|
+
params: { comment: 'Test' },
|
|
506
|
+
user: testUser,
|
|
507
|
+
app
|
|
508
|
+
})).rejects.toThrow('Order not found');
|
|
509
|
+
});
|
|
510
|
+
});
|
|
511
|
+
\`\`\`
|
|
512
|
+
|
|
513
|
+
**Best Practices:**
|
|
514
|
+
- Use snake_case for names
|
|
515
|
+
- Clear, business-friendly labels
|
|
516
|
+
- Include validation rules
|
|
517
|
+
- Add help text for clarity
|
|
518
|
+
- Define proper relationships
|
|
519
|
+
- Consider security from the start
|
|
520
|
+
- Implement actual business logic in TypeScript files
|
|
521
|
+
- Include error handling in implementations
|
|
522
|
+
- Add comments explaining complex logic
|
|
523
|
+
- Write comprehensive tests for all business logic
|
|
524
|
+
- Test both success and failure cases
|
|
525
|
+
|
|
526
|
+
Output format: Provide each file in code blocks with filename headers (e.g., "# filename.object.yml" or "// filename.action.ts").`;
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Get system prompt for validation
|
|
530
|
+
*/
|
|
531
|
+
getValidationSystemPrompt() {
|
|
532
|
+
return `You are an expert ObjectQL metadata validator. Analyze metadata for:
|
|
533
|
+
1. YAML structure and syntax
|
|
534
|
+
2. ObjectQL specification compliance
|
|
535
|
+
3. Business logic consistency
|
|
536
|
+
4. Data modeling best practices
|
|
537
|
+
5. Security considerations
|
|
538
|
+
6. Performance implications
|
|
539
|
+
|
|
540
|
+
Provide feedback in this format:
|
|
541
|
+
- [ERROR] Location: Issue description
|
|
542
|
+
- [WARNING] Location: Issue description
|
|
543
|
+
- [INFO] Location: Suggestion`;
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* Build generation prompt based on options
|
|
547
|
+
*/
|
|
548
|
+
buildGenerationPrompt(options) {
|
|
549
|
+
const { description, type = 'custom' } = options;
|
|
550
|
+
switch (type) {
|
|
551
|
+
case 'basic':
|
|
552
|
+
return `Generate a minimal ObjectQL application for: ${description}
|
|
553
|
+
|
|
554
|
+
Include:
|
|
555
|
+
- 2-3 core objects with essential fields
|
|
556
|
+
- Basic relationships between objects
|
|
557
|
+
- Simple validation rules
|
|
558
|
+
- At least one form and view per object
|
|
559
|
+
- At least one action with TypeScript implementation
|
|
560
|
+
- At least one hook with TypeScript implementation
|
|
561
|
+
|
|
562
|
+
Output: Provide each file separately with clear filename headers (e.g., "# filename.object.yml" or "// filename.action.ts").`;
|
|
563
|
+
case 'complete':
|
|
564
|
+
return `Generate a complete ObjectQL enterprise application for: ${description}
|
|
565
|
+
|
|
566
|
+
Include ALL necessary metadata types WITH implementations:
|
|
567
|
+
1. **Objects**: All entities with comprehensive fields
|
|
568
|
+
2. **Validations**: Business rules and constraints
|
|
569
|
+
3. **Forms**: Create and edit forms for each object
|
|
570
|
+
4. **Views**: List views for browsing data
|
|
571
|
+
5. **Pages**: Dashboard and detail pages
|
|
572
|
+
6. **Menus**: Navigation structure
|
|
573
|
+
7. **Actions WITH TypeScript implementations**: Common operations (approve, export, etc.) - Generate BOTH .yml metadata AND .action.ts implementation files
|
|
574
|
+
8. **Hooks WITH TypeScript implementations**: Lifecycle triggers - Generate .hook.ts implementation files
|
|
575
|
+
9. **Permissions**: Basic access control
|
|
576
|
+
10. **Data**: Sample seed data (optional)
|
|
577
|
+
11. **Workflows**: Approval processes if applicable
|
|
578
|
+
12. **Reports**: Key reports for analytics
|
|
579
|
+
13. **Tests**: Generate test files (.test.ts) for actions and hooks to validate business logic
|
|
580
|
+
|
|
581
|
+
Consider:
|
|
582
|
+
- Security and permissions from the start
|
|
583
|
+
- User experience in form/view design
|
|
584
|
+
- Business processes and workflows
|
|
585
|
+
- Data integrity and validation
|
|
586
|
+
- Complete TypeScript implementations for all actions and hooks
|
|
587
|
+
- Test coverage for business logic
|
|
588
|
+
|
|
589
|
+
Output: Provide each file separately with clear filename headers (e.g., "# filename.object.yml" or "// filename.action.ts").`;
|
|
590
|
+
default:
|
|
591
|
+
return `Generate ObjectQL metadata for: ${description}
|
|
592
|
+
|
|
593
|
+
Analyze the requirements and create appropriate metadata across ALL relevant types:
|
|
594
|
+
- Objects, Validations, Forms, Views, Pages, Menus, Actions, Hooks, Permissions, Workflows, Reports, Data, Application
|
|
595
|
+
- For Actions and Hooks: Generate BOTH YAML metadata AND TypeScript implementation files
|
|
596
|
+
- Include test files to validate business logic
|
|
597
|
+
|
|
598
|
+
Output: Provide each file separately with clear filename headers (e.g., "# filename.object.yml" or "// filename.action.ts").`;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* Build validation prompt
|
|
603
|
+
*/
|
|
604
|
+
buildValidationPrompt(options) {
|
|
605
|
+
const metadataStr = typeof options.metadata === 'string'
|
|
606
|
+
? options.metadata
|
|
607
|
+
: yaml.dump(options.metadata);
|
|
608
|
+
const checks = [];
|
|
609
|
+
if (options.checkBusinessLogic !== false)
|
|
610
|
+
checks.push('Business logic consistency');
|
|
611
|
+
if (options.checkPerformance)
|
|
612
|
+
checks.push('Performance considerations');
|
|
613
|
+
if (options.checkSecurity)
|
|
614
|
+
checks.push('Security issues');
|
|
615
|
+
return `Validate this ObjectQL metadata file:
|
|
616
|
+
|
|
617
|
+
${options.filename ? `Filename: ${options.filename}\n` : ''}
|
|
618
|
+
Content:
|
|
619
|
+
\`\`\`yaml
|
|
620
|
+
${metadataStr}
|
|
621
|
+
\`\`\`
|
|
622
|
+
|
|
623
|
+
Check for:
|
|
624
|
+
- YAML syntax and structure
|
|
625
|
+
- ObjectQL specification compliance
|
|
626
|
+
${checks.length > 0 ? '- ' + checks.join('\n- ') : ''}
|
|
627
|
+
|
|
628
|
+
Provide feedback in the specified format.`;
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* Parse generation response and extract files
|
|
632
|
+
*/
|
|
633
|
+
parseGenerationResponse(response) {
|
|
634
|
+
const files = [];
|
|
635
|
+
let match;
|
|
636
|
+
// Extract YAML files with explicit headers
|
|
637
|
+
while ((match = AI_RESPONSE_PATTERNS.FILE_BLOCK_YAML.exec(response)) !== null) {
|
|
638
|
+
const filename = match[1];
|
|
639
|
+
const content = match[2].trim();
|
|
640
|
+
const type = this.inferFileType(filename);
|
|
641
|
+
files.push({ filename, content, type });
|
|
642
|
+
}
|
|
643
|
+
// Extract TypeScript files with explicit headers
|
|
644
|
+
while ((match = AI_RESPONSE_PATTERNS.FILE_BLOCK_TS.exec(response)) !== null) {
|
|
645
|
+
const filename = match[1];
|
|
646
|
+
const content = match[2].trim();
|
|
647
|
+
const type = this.inferFileType(filename);
|
|
648
|
+
files.push({ filename, content, type });
|
|
649
|
+
}
|
|
650
|
+
// Fallback: Generic code blocks if no explicit headers found
|
|
651
|
+
if (files.length === 0) {
|
|
652
|
+
let yamlIndex = 0;
|
|
653
|
+
let tsIndex = 0;
|
|
654
|
+
// Try to extract generic YAML blocks
|
|
655
|
+
while ((match = AI_RESPONSE_PATTERNS.CODE_BLOCK_YAML.exec(response)) !== null) {
|
|
656
|
+
const content = match[1].trim();
|
|
657
|
+
const filename = `generated_${yamlIndex}.object.yml`;
|
|
658
|
+
files.push({ filename, content, type: 'object' });
|
|
659
|
+
yamlIndex++;
|
|
660
|
+
}
|
|
661
|
+
// Try to extract generic TypeScript blocks
|
|
662
|
+
while ((match = AI_RESPONSE_PATTERNS.CODE_BLOCK_TS.exec(response)) !== null) {
|
|
663
|
+
const content = match[1].trim();
|
|
664
|
+
// Use generic .ts extension since we can't determine the specific type
|
|
665
|
+
const filename = `generated_${tsIndex}.ts`;
|
|
666
|
+
files.push({ filename, content, type: 'typescript' });
|
|
667
|
+
tsIndex++;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
return files;
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* Parse validation feedback into structured result
|
|
674
|
+
*/
|
|
675
|
+
parseFeedback(feedback) {
|
|
676
|
+
const errors = [];
|
|
677
|
+
const warnings = [];
|
|
678
|
+
const info = [];
|
|
679
|
+
const lines = feedback.split('\n');
|
|
680
|
+
for (let i = 0; i < lines.length; i++) {
|
|
681
|
+
const line = lines[i];
|
|
682
|
+
if (line.includes('[ERROR]')) {
|
|
683
|
+
const message = line.replace(/^\s*-?\s*\[ERROR\]\s*/, '');
|
|
684
|
+
errors.push({ message });
|
|
685
|
+
}
|
|
686
|
+
else if (line.includes('[WARNING]')) {
|
|
687
|
+
const message = line.replace(/^\s*-?\s*\[WARNING\]\s*/, '');
|
|
688
|
+
warnings.push({ message });
|
|
689
|
+
}
|
|
690
|
+
else if (line.includes('[INFO]')) {
|
|
691
|
+
const message = line.replace(/^\s*-?\s*\[INFO\]\s*/, '');
|
|
692
|
+
info.push({ message });
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
return {
|
|
696
|
+
valid: errors.length === 0,
|
|
697
|
+
errors,
|
|
698
|
+
warnings,
|
|
699
|
+
info,
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* Infer file type from filename
|
|
704
|
+
*/
|
|
705
|
+
inferFileType(filename) {
|
|
706
|
+
if (filename.includes('.object.yml'))
|
|
707
|
+
return 'object';
|
|
708
|
+
if (filename.includes('.validation.yml'))
|
|
709
|
+
return 'validation';
|
|
710
|
+
if (filename.includes('.form.yml'))
|
|
711
|
+
return 'form';
|
|
712
|
+
if (filename.includes('.view.yml'))
|
|
713
|
+
return 'view';
|
|
714
|
+
if (filename.includes('.page.yml'))
|
|
715
|
+
return 'page';
|
|
716
|
+
if (filename.includes('.menu.yml'))
|
|
717
|
+
return 'menu';
|
|
718
|
+
if (filename.includes('.action.yml'))
|
|
719
|
+
return 'action';
|
|
720
|
+
if (filename.includes('.hook.yml'))
|
|
721
|
+
return 'hook';
|
|
722
|
+
if (filename.includes('.permission.yml'))
|
|
723
|
+
return 'permission';
|
|
724
|
+
if (filename.includes('.workflow.yml'))
|
|
725
|
+
return 'workflow';
|
|
726
|
+
if (filename.includes('.report.yml'))
|
|
727
|
+
return 'report';
|
|
728
|
+
if (filename.includes('.data.yml'))
|
|
729
|
+
return 'data';
|
|
730
|
+
if (filename.includes('.application.yml') || filename.includes('.app.yml'))
|
|
731
|
+
return 'application';
|
|
732
|
+
if (filename.includes('.action.ts') || filename.includes('.hook.ts'))
|
|
733
|
+
return 'typescript';
|
|
734
|
+
if (filename.includes('.test.ts') || filename.includes('.spec.ts'))
|
|
735
|
+
return 'test';
|
|
736
|
+
return 'other';
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
exports.ObjectQLAgent = ObjectQLAgent;
|
|
740
|
+
/**
|
|
741
|
+
* Convenience function to create an agent instance
|
|
742
|
+
*/
|
|
743
|
+
function createAgent(apiKey, options) {
|
|
744
|
+
return new ObjectQLAgent({ apiKey, ...options });
|
|
745
|
+
}
|
|
746
|
+
//# sourceMappingURL=ai-agent.js.map
|