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