outlet-orm 6.5.0 → 7.0.0

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.
@@ -0,0 +1,318 @@
1
+ /**
2
+ * Outlet ORM — Prompt-based Model Generator
3
+ * Parses natural language descriptions and generates models + migrations.
4
+ *
5
+ * Used by `outlet-init --prompt "..."` to bootstrap projects from descriptions.
6
+ *
7
+ * @since 7.0.0
8
+ */
9
+
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+
13
+ // ─── Domain pattern recognition ──────────────────────────────────
14
+
15
+ const DOMAIN_PATTERNS = {
16
+ // E-commerce
17
+ 'e-?commerce|shop|store|product|cart|order|payment|checkout': {
18
+ tables: {
19
+ users: { columns: ['name:string', 'email:string:unique', 'password:string', 'role:string:default(customer)'] },
20
+ products: { columns: ['name:string', 'description:text:nullable', 'price:decimal(10,2)', 'stock:integer:default(0)', 'sku:string:unique', 'category_id:foreignId'] },
21
+ categories: { columns: ['name:string', 'slug:string:unique', 'parent_id:integer:nullable'] },
22
+ orders: { columns: ['user_id:foreignId', 'status:string:default(pending)', 'total:decimal(10,2)', 'shipping_address:text'] },
23
+ order_items:{ columns: ['order_id:foreignId', 'product_id:foreignId', 'quantity:integer', 'price:decimal(10,2)'] },
24
+ payments: { columns: ['order_id:foreignId', 'method:string', 'amount:decimal(10,2)', 'status:string:default(pending)', 'transaction_id:string:nullable'] }
25
+ }
26
+ },
27
+
28
+ // Blog / CMS
29
+ 'blog|article|post|cms|content|comment|tag': {
30
+ tables: {
31
+ users: { columns: ['name:string', 'email:string:unique', 'password:string', 'bio:text:nullable', 'avatar:string:nullable'] },
32
+ posts: { columns: ['user_id:foreignId', 'title:string', 'slug:string:unique', 'content:text', 'excerpt:text:nullable', 'status:string:default(draft)', 'published_at:timestamp:nullable'] },
33
+ categories: { columns: ['name:string', 'slug:string:unique', 'description:text:nullable'] },
34
+ tags: { columns: ['name:string', 'slug:string:unique'] },
35
+ post_tag: { columns: ['post_id:foreignId', 'tag_id:foreignId'], pivot: true },
36
+ comments: { columns: ['post_id:foreignId', 'user_id:foreignId:nullable', 'author_name:string:nullable', 'body:text', 'approved:boolean:default(false)'] }
37
+ }
38
+ },
39
+
40
+ // Task / Project management
41
+ 'task|project|todo|kanban|board|sprint|ticket': {
42
+ tables: {
43
+ users: { columns: ['name:string', 'email:string:unique', 'password:string', 'avatar:string:nullable'] },
44
+ projects: { columns: ['name:string', 'description:text:nullable', 'owner_id:foreignId', 'status:string:default(active)'] },
45
+ tasks: { columns: ['project_id:foreignId', 'assigned_to:integer:nullable', 'title:string', 'description:text:nullable', 'status:string:default(todo)', 'priority:string:default(medium)', 'due_date:date:nullable'] },
46
+ labels: { columns: ['name:string', 'color:string:default(#3498db)'] },
47
+ task_label: { columns: ['task_id:foreignId', 'label_id:foreignId'], pivot: true }
48
+ }
49
+ },
50
+
51
+ // Social network
52
+ 'social|friend|follow|like|feed|profile|message|chat': {
53
+ tables: {
54
+ users: { columns: ['name:string', 'email:string:unique', 'password:string', 'username:string:unique', 'bio:text:nullable', 'avatar:string:nullable'] },
55
+ posts: { columns: ['user_id:foreignId', 'content:text', 'media_url:string:nullable', 'visibility:string:default(public)'] },
56
+ comments: { columns: ['post_id:foreignId', 'user_id:foreignId', 'body:text'] },
57
+ likes: { columns: ['user_id:foreignId', 'likeable_id:integer', 'likeable_type:string'] },
58
+ follows: { columns: ['follower_id:foreignId', 'following_id:foreignId'] },
59
+ messages: { columns: ['sender_id:foreignId', 'receiver_id:foreignId', 'body:text', 'read_at:timestamp:nullable'] }
60
+ }
61
+ },
62
+
63
+ // SaaS / Multi-tenant
64
+ 'saas|tenant|subscription|plan|billing|organization': {
65
+ tables: {
66
+ organizations: { columns: ['name:string', 'slug:string:unique', 'plan_id:foreignId:nullable'] },
67
+ users: { columns: ['organization_id:foreignId', 'name:string', 'email:string:unique', 'password:string', 'role:string:default(member)'] },
68
+ plans: { columns: ['name:string', 'slug:string:unique', 'price:decimal(8,2)', 'features:json:nullable', 'max_users:integer:default(5)'] },
69
+ subscriptions: { columns: ['organization_id:foreignId', 'plan_id:foreignId', 'status:string:default(active)', 'starts_at:timestamp', 'ends_at:timestamp:nullable', 'trial_ends_at:timestamp:nullable'] },
70
+ invoices: { columns: ['subscription_id:foreignId', 'amount:decimal(10,2)', 'status:string:default(pending)', 'paid_at:timestamp:nullable'] }
71
+ }
72
+ },
73
+
74
+ // Habit tracker / Health
75
+ 'habit|tracker|health|fitness|goal|streak|log': {
76
+ tables: {
77
+ users: { columns: ['name:string', 'email:string:unique', 'password:string', 'timezone:string:default(UTC)'] },
78
+ habits: { columns: ['user_id:foreignId', 'name:string', 'description:text:nullable', 'frequency:string:default(daily)', 'color:string:default(#3498db)', 'target:integer:default(1)'] },
79
+ logs: { columns: ['habit_id:foreignId', 'date:date', 'completed:boolean:default(false)', 'value:integer:default(0)', 'notes:text:nullable'] },
80
+ goals: { columns: ['user_id:foreignId', 'title:string', 'target_value:integer', 'current_value:integer:default(0)', 'deadline:date:nullable'] }
81
+ }
82
+ },
83
+
84
+ // API / Auth / Generic
85
+ 'api|auth|rest|user': {
86
+ tables: {
87
+ users: { columns: ['name:string', 'email:string:unique', 'password:string', 'role:string:default(user)', 'email_verified_at:timestamp:nullable'] },
88
+ tokens: { columns: ['user_id:foreignId', 'token:string:unique', 'type:string:default(api)', 'expires_at:timestamp:nullable'] },
89
+ password_resets:{ columns: ['email:string', 'token:string', 'created_at:timestamp'] }
90
+ }
91
+ }
92
+ };
93
+
94
+ // ─── Generator ──────────────────────────────────────────────────
95
+
96
+ class PromptGenerator {
97
+ /**
98
+ * Parse a natural language prompt and return a project blueprint.
99
+ * @param {string} prompt - e.g. "Create a blog application with comments and tags"
100
+ * @returns {{ domain: string, tables: object }}
101
+ */
102
+ static parse(prompt) {
103
+ const lower = prompt.toLowerCase();
104
+ let bestMatch = null;
105
+ let bestScore = 0;
106
+
107
+ for (const [pattern, blueprint] of Object.entries(DOMAIN_PATTERNS)) {
108
+ const regex = new RegExp(pattern, 'i');
109
+ const keywords = pattern.split('|').map(k => k.replace(/[^a-z]/g, ''));
110
+ let score = 0;
111
+
112
+ for (const kw of keywords) {
113
+ if (lower.includes(kw)) score++;
114
+ }
115
+ if (regex.test(lower)) score += 2;
116
+
117
+ if (score > bestScore) {
118
+ bestScore = score;
119
+ bestMatch = { pattern, blueprint, score };
120
+ }
121
+ }
122
+
123
+ // Default to generic API/Auth if no match
124
+ if (!bestMatch || bestScore < 1) {
125
+ bestMatch = { pattern: 'api|auth', blueprint: DOMAIN_PATTERNS['api|auth|rest|user'], score: 0 };
126
+ }
127
+
128
+ return {
129
+ domain: bestMatch.pattern.split('|')[0],
130
+ tables: bestMatch.blueprint.tables,
131
+ score: bestMatch.score
132
+ };
133
+ }
134
+
135
+ /**
136
+ * Generate model files from a blueprint.
137
+ * @param {{ tables: object }} blueprint
138
+ * @param {string} outputDir - e.g. process.cwd() + '/models'
139
+ * @returns {string[]} created file paths
140
+ */
141
+ static generateModels(blueprint, outputDir) {
142
+ fs.mkdirSync(outputDir, { recursive: true });
143
+ const created = [];
144
+
145
+ for (const [tableName, config] of Object.entries(blueprint.tables)) {
146
+ if (config.pivot) continue; // Skip pivot tables for model files
147
+
148
+ const className = this._toClassName(tableName);
149
+ const fillable = config.columns
150
+ .map(c => c.split(':')[0])
151
+ .filter(c => !['id', 'created_at', 'updated_at'].includes(c));
152
+
153
+ const hidden = fillable.filter(c => c === 'password' || c.includes('token') || c.includes('secret'));
154
+
155
+ const content = `const { Model } = require('outlet-orm');
156
+
157
+ class ${className} extends Model {
158
+ static table = '${tableName}';
159
+ static fillable = ${JSON.stringify(fillable)};${hidden.length > 0 ? `\n static hidden = ${JSON.stringify(hidden)};` : ''}
160
+ }
161
+
162
+ module.exports = ${className};
163
+ `;
164
+
165
+ const filePath = path.join(outputDir, `${className}.js`);
166
+ fs.writeFileSync(filePath, content);
167
+ created.push(filePath);
168
+ }
169
+
170
+ return created;
171
+ }
172
+
173
+ /**
174
+ * Generate migration files from a blueprint.
175
+ * @param {{ tables: object }} blueprint
176
+ * @param {string} outputDir - e.g. process.cwd() + '/database/migrations'
177
+ * @returns {string[]} created file paths
178
+ */
179
+ static generateMigrations(blueprint, outputDir) {
180
+ fs.mkdirSync(outputDir, { recursive: true });
181
+ const created = [];
182
+ let index = 0;
183
+
184
+ for (const [tableName, config] of Object.entries(blueprint.tables)) {
185
+ index++;
186
+ const timestamp = new Date(Date.now() + index * 1000)
187
+ .toISOString()
188
+ .replace(/[-:]/g, '')
189
+ .replace(/T/, '_')
190
+ .replace(/\..+/, '');
191
+
192
+ const className = `Create${this._toClassName(tableName)}Table`;
193
+ const fileName = `${timestamp}_create_${tableName}_table.js`;
194
+ const filePath = path.join(outputDir, fileName);
195
+
196
+ const columnDefs = config.columns.map(c => this._columnToSchema(c)).join('\n ');
197
+
198
+ const content = `const { Migration } = require('outlet-orm');
199
+
200
+ class ${className} extends Migration {
201
+ async up() {
202
+ const schema = this.getSchema();
203
+ await schema.create('${tableName}', (table) => {
204
+ table.id();
205
+ ${columnDefs}
206
+ table.timestamps();
207
+ });
208
+ }
209
+
210
+ async down() {
211
+ const schema = this.getSchema();
212
+ await schema.dropIfExists('${tableName}');
213
+ }
214
+ }
215
+
216
+ module.exports = ${className};
217
+ `;
218
+
219
+ fs.writeFileSync(filePath, content);
220
+ created.push(filePath);
221
+ }
222
+
223
+ return created;
224
+ }
225
+
226
+ /**
227
+ * Generate a seed file from a blueprint.
228
+ * @param {{ tables: object }} blueprint
229
+ * @param {string} outputDir
230
+ * @returns {string}
231
+ */
232
+ static generateSeeder(blueprint, outputDir) {
233
+ fs.mkdirSync(outputDir, { recursive: true });
234
+ const tables = Object.keys(blueprint.tables);
235
+
236
+ const seedCalls = tables
237
+ .filter(t => !blueprint.tables[t].pivot)
238
+ .map(t => ` // await this.call('${this._toClassName(t)}Seeder');`)
239
+ .join('\n');
240
+
241
+ const content = `const { Seeder } = require('outlet-orm');
242
+
243
+ class DatabaseSeeder extends Seeder {
244
+ async run() {
245
+ ${seedCalls}
246
+ }
247
+ }
248
+
249
+ module.exports = DatabaseSeeder;
250
+ `;
251
+
252
+ const filePath = path.join(outputDir, 'DatabaseSeeder.js');
253
+ fs.writeFileSync(filePath, content);
254
+ return filePath;
255
+ }
256
+
257
+ // ─── Helpers ───────────────────────────────────────────────────
258
+
259
+ static _toClassName(tableName) {
260
+ // users -> User, order_items -> OrderItem
261
+ return tableName
262
+ .split('_')
263
+ .map(part => part.charAt(0).toUpperCase() + part.slice(1))
264
+ .join('')
265
+ .replace(/s$/, ''); // naive singularize (plural -> singular)
266
+ }
267
+
268
+ static _columnToSchema(columnDef) {
269
+ // Format: name:type:modifier1:modifier2
270
+ // e.g. "email:string:unique", "price:decimal(10,2)", "status:string:default(pending)"
271
+ const parts = columnDef.split(':');
272
+ const name = parts[0];
273
+ const type = parts[1] || 'string';
274
+ const modifiers = parts.slice(2);
275
+
276
+ // Parse type and arguments
277
+ const typeMatch = type.match(/^(\w+)(?:\((.+)\))?$/);
278
+ const typeName = typeMatch ? typeMatch[1] : type;
279
+ const typeArgs = typeMatch && typeMatch[2] ? `, ${typeMatch[2]}` : '';
280
+
281
+ // Map to schema builder methods
282
+ const typeMap = {
283
+ string: 'string',
284
+ text: 'text',
285
+ integer: 'integer',
286
+ int: 'integer',
287
+ boolean: 'boolean',
288
+ bool: 'boolean',
289
+ decimal: 'decimal',
290
+ float: 'float',
291
+ date: 'date',
292
+ timestamp: 'timestamp',
293
+ json: 'json',
294
+ foreignId: 'integer'
295
+ };
296
+
297
+ const schemaType = typeMap[typeName] || 'string';
298
+ let line = `table.${schemaType}('${name}'${typeArgs})`;
299
+
300
+ // Apply modifiers
301
+ for (const mod of modifiers) {
302
+ if (mod === 'unique') line += '.unique()';
303
+ else if (mod === 'nullable') line += '.nullable()';
304
+ else if (mod.startsWith('default(')) {
305
+ const val = mod.match(/default\((.+)\)/)?.[1] || '';
306
+ // Smart quoting: don't quote numbers or booleans
307
+ const isNum = !isNaN(val);
308
+ const isBool = val === 'true' || val === 'false';
309
+ const quoted = isNum || isBool ? val : `'${val}'`;
310
+ line += `.default(${quoted})`;
311
+ }
312
+ }
313
+
314
+ return line + ';';
315
+ }
316
+ }
317
+
318
+ module.exports = PromptGenerator;
package/src/index.js CHANGED
@@ -28,6 +28,11 @@ const BackupEncryption = require('./Backup/BackupEncryption');
28
28
  const BackupSocketServer = require('./Backup/BackupSocketServer');
29
29
  const BackupSocketClient = require('./Backup/BackupSocketClient');
30
30
 
31
+ // AI (v7.0.0)
32
+ const MCPServer = require('./AI/MCPServer');
33
+ const AISafetyGuardrails = require('./AI/AISafetyGuardrails');
34
+ const PromptGenerator = require('./AI/PromptGenerator');
35
+
31
36
  module.exports = {
32
37
  // Core
33
38
  Model,
@@ -65,5 +70,10 @@ module.exports = {
65
70
  BackupScheduler,
66
71
  BackupEncryption,
67
72
  BackupSocketServer,
68
- BackupSocketClient
73
+ BackupSocketClient,
74
+
75
+ // AI (v7.0.0)
76
+ MCPServer,
77
+ AISafetyGuardrails,
78
+ PromptGenerator
69
79
  };
package/types/index.d.ts CHANGED
@@ -937,4 +937,110 @@ declare module 'outlet-orm' {
937
937
  on(event: 'serverEvent', listener: (payload: Record<string, any>) => void): this;
938
938
  on(event: string, listener: (...args: any[]) => void): this;
939
939
  }
940
+
941
+ // ==================== AI Integration (v7.0.0) ====================
942
+
943
+ /** MCP tool definition */
944
+ export interface MCPToolDefinition {
945
+ name: string;
946
+ description: string;
947
+ inputSchema: {
948
+ type: 'object';
949
+ properties: Record<string, any>;
950
+ required: string[];
951
+ };
952
+ }
953
+
954
+ /** MCP server options */
955
+ export interface MCPServerOptions {
956
+ /** DatabaseConnection instance (optional, auto-loaded from project) */
957
+ connection?: DatabaseConnection;
958
+ /** Project root directory (default: process.cwd()) */
959
+ projectDir?: string;
960
+ /** Enable AI safety guardrails (default: true) */
961
+ safetyGuardrails?: boolean;
962
+ }
963
+
964
+ /** JSON-RPC 2.0 message */
965
+ export interface JSONRPCMessage {
966
+ jsonrpc: '2.0';
967
+ id?: number | string;
968
+ method?: string;
969
+ params?: Record<string, any>;
970
+ result?: any;
971
+ error?: { code: number; message: string };
972
+ }
973
+
974
+ /** MCP Server — exposes ORM capabilities to AI agents via JSON-RPC 2.0 */
975
+ export class MCPServer {
976
+ constructor(options?: MCPServerOptions);
977
+
978
+ /** Project root directory */
979
+ projectDir: string;
980
+ /** Whether safety guardrails are enabled */
981
+ safetyGuardrails: boolean;
982
+
983
+ /** Start the MCP server on stdio */
984
+ start(): void;
985
+ /** Get a programmatic handler function (for testing/embedding) */
986
+ handler(): (message: JSONRPCMessage) => Promise<JSONRPCMessage | null>;
987
+ /** Graceful shutdown */
988
+ close(): Promise<void>;
989
+
990
+ on(event: 'started', listener: () => void): this;
991
+ on(event: 'initialized', listener: () => void): this;
992
+ on(event: 'response', listener: (response: JSONRPCMessage) => void): this;
993
+ on(event: 'close', listener: () => void): this;
994
+ on(event: string, listener: (...args: any[]) => void): this;
995
+ }
996
+
997
+ /** Agent detection result */
998
+ export interface AgentDetectionResult {
999
+ /** Whether an AI agent was detected */
1000
+ detected: boolean;
1001
+ /** Name of the detected agent (null if not detected) */
1002
+ agentName: string | null;
1003
+ }
1004
+
1005
+ /** Destructive action validation result */
1006
+ export interface DestructiveActionResult {
1007
+ /** Whether the action is allowed */
1008
+ allowed: boolean;
1009
+ /** Message explaining why action was blocked (empty if allowed) */
1010
+ message: string;
1011
+ }
1012
+
1013
+ /** AI Safety Guardrails — detects AI agents and protects against destructive operations */
1014
+ export class AISafetyGuardrails {
1015
+ /** Detect if the current process is invoked by an AI agent */
1016
+ static detectAgent(): AgentDetectionResult;
1017
+ /** Check if a CLI command is destructive */
1018
+ static isDestructiveCommand(command: string): boolean;
1019
+ /** Validate whether user consent is present for a destructive operation */
1020
+ static validateDestructiveAction(command: string, flags?: { consent?: string; yes?: boolean; force?: boolean }): DestructiveActionResult;
1021
+ /** The environment variable name for AI consent */
1022
+ static readonly CONSENT_ENV_VAR: string;
1023
+ }
1024
+
1025
+ /** Prompt blueprint — parsed from a natural language description */
1026
+ export interface PromptBlueprint {
1027
+ /** Detected domain (e.g. 'blog', 'e-commerce', 'saas') */
1028
+ domain: string;
1029
+ /** Table definitions */
1030
+ tables: Record<string, { columns: string[]; pivot?: boolean }>;
1031
+ /** Match confidence score */
1032
+ score: number;
1033
+ }
1034
+
1035
+ /** Prompt Generator — generates projects from natural language descriptions */
1036
+ export class PromptGenerator {
1037
+ /** Parse a natural language prompt into a project blueprint */
1038
+ static parse(prompt: string): PromptBlueprint;
1039
+ /** Generate model files from a blueprint */
1040
+ static generateModels(blueprint: PromptBlueprint, outputDir: string): string[];
1041
+ /** Generate migration files from a blueprint */
1042
+ static generateMigrations(blueprint: PromptBlueprint, outputDir: string): string[];
1043
+ /** Generate a seeder file from a blueprint */
1044
+ static generateSeeder(blueprint: PromptBlueprint, outputDir: string): string;
1045
+ }
940
1046
  }