@claudetools/tools 0.9.0 → 0.9.2
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/dist/cli.js +9 -1
- package/dist/codedna/__tests__/examples/mongoose-example.d.ts +6 -0
- package/dist/codedna/__tests__/examples/mongoose-example.js +163 -0
- package/dist/codedna/__tests__/fixtures/typeorm-production-test.d.ts +1 -0
- package/dist/codedna/__tests__/fixtures/typeorm-production-test.js +231 -0
- package/dist/codedna/__tests__/fixtures/typeorm-test.d.ts +1 -0
- package/dist/codedna/__tests__/fixtures/typeorm-test.js +124 -0
- package/dist/codedna/__tests__/laravel-output-review.d.ts +1 -0
- package/dist/codedna/__tests__/laravel-output-review.js +249 -0
- package/dist/codedna/__tests__/mongoose-output-test.d.ts +1 -0
- package/dist/codedna/__tests__/mongoose-output-test.js +178 -0
- package/dist/codedna/examples/radix-example.d.ts +2 -0
- package/dist/codedna/examples/radix-example.js +259 -0
- package/dist/codedna/index.d.ts +5 -3
- package/dist/codedna/index.js +6 -3
- package/dist/codedna/kappa-ast.d.ts +143 -5
- package/dist/codedna/kappa-drizzle-generator.js +8 -5
- package/dist/codedna/kappa-gofiber-generator.d.ts +65 -0
- package/dist/codedna/kappa-gofiber-generator.js +587 -0
- package/dist/codedna/kappa-laravel-generator.d.ts +68 -0
- package/dist/codedna/kappa-laravel-generator.js +741 -0
- package/dist/codedna/kappa-lexer.d.ts +44 -0
- package/dist/codedna/kappa-lexer.js +124 -0
- package/dist/codedna/kappa-mantine-generator.d.ts +65 -0
- package/dist/codedna/kappa-mantine-generator.js +518 -0
- package/dist/codedna/kappa-mongoose-generator.d.ts +44 -0
- package/dist/codedna/kappa-mongoose-generator.js +442 -0
- package/dist/codedna/kappa-parser.d.ts +43 -1
- package/dist/codedna/kappa-parser.js +601 -0
- package/dist/codedna/kappa-radix-generator.d.ts +61 -0
- package/dist/codedna/kappa-radix-generator.js +566 -0
- package/dist/codedna/kappa-typeorm-generator.d.ts +59 -0
- package/dist/codedna/kappa-typeorm-generator.js +723 -0
- package/dist/codedna/kappa-vitest-generator.d.ts +85 -0
- package/dist/codedna/kappa-vitest-generator.js +739 -0
- package/dist/codedna/parser.js +26 -1
- package/dist/codegen/cloud-client.d.ts +160 -0
- package/dist/codegen/cloud-client.js +195 -0
- package/dist/codegen/codegen-tool.d.ts +35 -0
- package/dist/codegen/codegen-tool.js +312 -0
- package/dist/codegen/field-inference.d.ts +24 -0
- package/dist/codegen/field-inference.js +101 -0
- package/dist/codegen/form-parser.d.ts +13 -0
- package/dist/codegen/form-parser.js +186 -0
- package/dist/codegen/index.d.ts +2 -0
- package/dist/codegen/index.js +4 -0
- package/dist/codegen/natural-parser.d.ts +50 -0
- package/dist/codegen/natural-parser.js +769 -0
- package/dist/handlers/codedna-handlers.d.ts +1 -1
- package/dist/handlers/codegen-handlers.d.ts +20 -0
- package/dist/handlers/codegen-handlers.js +60 -0
- package/dist/handlers/kappa-handlers.d.ts +97 -0
- package/dist/handlers/kappa-handlers.js +408 -0
- package/dist/handlers/tool-handlers.js +124 -221
- package/dist/helpers/api-client.js +48 -3
- package/dist/helpers/compact-formatter.d.ts +9 -2
- package/dist/helpers/compact-formatter.js +26 -2
- package/dist/helpers/config.d.ts +7 -2
- package/dist/helpers/config.js +25 -10
- package/dist/helpers/session-validation.d.ts +1 -1
- package/dist/helpers/session-validation.js +2 -4
- package/dist/helpers/tasks.d.ts +21 -0
- package/dist/helpers/tasks.js +52 -0
- package/dist/helpers/workers.d.ts +1 -1
- package/dist/helpers/workers.js +19 -19
- package/dist/setup.d.ts +1 -0
- package/dist/setup.js +228 -3
- package/dist/templates/claude-md.d.ts +1 -1
- package/dist/templates/claude-md.js +37 -152
- package/dist/templates/orchestrator-prompt.d.ts +2 -2
- package/dist/templates/orchestrator-prompt.js +31 -38
- package/dist/templates/self-critique.d.ts +50 -0
- package/dist/templates/self-critique.js +209 -0
- package/dist/templates/worker-prompt.d.ts +3 -3
- package/dist/templates/worker-prompt.js +18 -18
- package/dist/tools.js +77 -413
- package/docs/codedna/generator-testing-summary.md +205 -0
- package/docs/codedna/radix-ui-generator.md +478 -0
- package/docs/kappa-gofiber-generator.md +274 -0
- package/docs/kappa-laravel-fixes.md +172 -0
- package/docs/kappa-mongoose-generator.md +322 -0
- package/docs/kappa-vitest-generator.md +337 -0
- package/package.json +1 -1
- package/dist/context/deduplication.test.d.ts +0 -6
- package/dist/context/deduplication.test.js +0 -84
|
@@ -0,0 +1,741 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Kappa v2.5 PHP/Laravel Generator
|
|
3
|
+
// =============================================================================
|
|
4
|
+
//
|
|
5
|
+
// Generates Laravel code from Kappa specifications.
|
|
6
|
+
// Supports: Controllers, Models, Migrations, Form Requests.
|
|
7
|
+
//
|
|
8
|
+
// =============================================================================
|
|
9
|
+
// Type Mappings
|
|
10
|
+
// =============================================================================
|
|
11
|
+
const PRIMITIVE_PHP_MAP = {
|
|
12
|
+
string: 'string',
|
|
13
|
+
int: 'int',
|
|
14
|
+
float: 'float',
|
|
15
|
+
bool: 'bool',
|
|
16
|
+
timestamp: '\\Carbon\\Carbon',
|
|
17
|
+
date: '\\Carbon\\Carbon',
|
|
18
|
+
time: '\\Carbon\\Carbon',
|
|
19
|
+
duration: 'int',
|
|
20
|
+
uuid: 'string',
|
|
21
|
+
email: 'string',
|
|
22
|
+
url: 'string',
|
|
23
|
+
phone: 'string',
|
|
24
|
+
slug: 'string',
|
|
25
|
+
markdown: 'string',
|
|
26
|
+
json: 'array',
|
|
27
|
+
};
|
|
28
|
+
const PRIMITIVE_MIGRATION_MAP = {
|
|
29
|
+
string: 'string',
|
|
30
|
+
int: 'integer',
|
|
31
|
+
float: 'decimal',
|
|
32
|
+
bool: 'boolean',
|
|
33
|
+
timestamp: 'timestamp',
|
|
34
|
+
date: 'date',
|
|
35
|
+
time: 'time',
|
|
36
|
+
duration: 'integer',
|
|
37
|
+
uuid: 'uuid',
|
|
38
|
+
email: 'string',
|
|
39
|
+
url: 'string',
|
|
40
|
+
phone: 'string',
|
|
41
|
+
slug: 'string',
|
|
42
|
+
markdown: 'text',
|
|
43
|
+
json: 'json',
|
|
44
|
+
};
|
|
45
|
+
const PRIMITIVE_VALIDATION_MAP = {
|
|
46
|
+
string: ['string'],
|
|
47
|
+
int: ['integer'],
|
|
48
|
+
float: ['numeric'],
|
|
49
|
+
bool: ['boolean'],
|
|
50
|
+
timestamp: ['date'],
|
|
51
|
+
date: ['date'],
|
|
52
|
+
time: ['date_format:H:i:s'],
|
|
53
|
+
duration: ['integer'],
|
|
54
|
+
uuid: ['uuid'],
|
|
55
|
+
email: ['email'],
|
|
56
|
+
url: ['url'],
|
|
57
|
+
phone: ['string'],
|
|
58
|
+
slug: ['string', 'regex:/^[a-z0-9-]+$/'],
|
|
59
|
+
markdown: ['string'],
|
|
60
|
+
json: ['array'],
|
|
61
|
+
};
|
|
62
|
+
const CRUD_METHOD_MAP = {
|
|
63
|
+
create: 'post',
|
|
64
|
+
read: 'get',
|
|
65
|
+
update: 'patch',
|
|
66
|
+
delete: 'delete',
|
|
67
|
+
list: 'get',
|
|
68
|
+
};
|
|
69
|
+
const CRUD_ACTION_MAP = {
|
|
70
|
+
create: 'store',
|
|
71
|
+
read: 'show',
|
|
72
|
+
update: 'update',
|
|
73
|
+
delete: 'destroy',
|
|
74
|
+
list: 'index',
|
|
75
|
+
};
|
|
76
|
+
// =============================================================================
|
|
77
|
+
// Generator Class
|
|
78
|
+
// =============================================================================
|
|
79
|
+
export class KappaLaravelGenerator {
|
|
80
|
+
modelNamespace;
|
|
81
|
+
controllerNamespace;
|
|
82
|
+
provenance;
|
|
83
|
+
migrations;
|
|
84
|
+
formRequests;
|
|
85
|
+
apiResources;
|
|
86
|
+
basePath;
|
|
87
|
+
entities;
|
|
88
|
+
constructor(options = {}) {
|
|
89
|
+
this.modelNamespace = options.modelNamespace ?? 'App\\Models';
|
|
90
|
+
this.controllerNamespace = options.controllerNamespace ?? 'App\\Http\\Controllers';
|
|
91
|
+
this.provenance = options.provenance ?? true;
|
|
92
|
+
this.migrations = options.migrations ?? true;
|
|
93
|
+
this.formRequests = options.formRequests ?? true;
|
|
94
|
+
this.apiResources = options.apiResources ?? true;
|
|
95
|
+
this.basePath = options.basePath ?? '/api';
|
|
96
|
+
this.entities = new Map();
|
|
97
|
+
}
|
|
98
|
+
// ===========================================================================
|
|
99
|
+
// Main Generation Methods
|
|
100
|
+
// ===========================================================================
|
|
101
|
+
generateFromAPIs(apis) {
|
|
102
|
+
const routes = this.extractRoutes(apis);
|
|
103
|
+
const controllers = {};
|
|
104
|
+
const formRequests = {};
|
|
105
|
+
// Group routes by controller
|
|
106
|
+
const controllerGroups = this.groupByController(routes);
|
|
107
|
+
for (const [controllerName, controllerRoutes] of Object.entries(controllerGroups)) {
|
|
108
|
+
controllers[controllerName] = this.generateController(controllerName, controllerRoutes);
|
|
109
|
+
if (this.formRequests) {
|
|
110
|
+
for (const route of controllerRoutes) {
|
|
111
|
+
if (route.method === 'post' || route.method === 'put' || route.method === 'patch') {
|
|
112
|
+
const requestName = `${this.toPascalCase(route.operationName)}Request`;
|
|
113
|
+
formRequests[requestName] = this.generateFormRequest(requestName, route);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
routes: this.generateRoutes(apis),
|
|
120
|
+
controllers,
|
|
121
|
+
models: {},
|
|
122
|
+
formRequests: this.formRequests ? formRequests : undefined,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
generateFromEntities(entities) {
|
|
126
|
+
const models = {};
|
|
127
|
+
const migrations = {};
|
|
128
|
+
const resources = {};
|
|
129
|
+
for (const entity of entities) {
|
|
130
|
+
models[entity.name] = this.generateModel(entity);
|
|
131
|
+
if (this.migrations) {
|
|
132
|
+
const migrationName = `create_${this.toSnakeCase(entity.name)}s_table`;
|
|
133
|
+
migrations[migrationName] = this.generateMigration(entity);
|
|
134
|
+
}
|
|
135
|
+
if (this.apiResources) {
|
|
136
|
+
resources[`${entity.name}Resource`] = this.generateResource(entity);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
routes: '',
|
|
141
|
+
controllers: {},
|
|
142
|
+
models,
|
|
143
|
+
migrations: this.migrations ? migrations : undefined,
|
|
144
|
+
resources: this.apiResources ? resources : undefined,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
generateAll(apis, entities) {
|
|
148
|
+
// Store entities for validation rule generation
|
|
149
|
+
for (const entity of entities) {
|
|
150
|
+
this.entities.set(entity.name, entity);
|
|
151
|
+
}
|
|
152
|
+
const apiResult = this.generateFromAPIs(apis);
|
|
153
|
+
const entityResult = this.generateFromEntities(entities);
|
|
154
|
+
return {
|
|
155
|
+
routes: apiResult.routes,
|
|
156
|
+
controllers: apiResult.controllers,
|
|
157
|
+
models: entityResult.models,
|
|
158
|
+
migrations: entityResult.migrations,
|
|
159
|
+
formRequests: apiResult.formRequests,
|
|
160
|
+
resources: entityResult.resources,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
// ===========================================================================
|
|
164
|
+
// Route Generation
|
|
165
|
+
// ===========================================================================
|
|
166
|
+
generateRoutes(apis) {
|
|
167
|
+
const lines = [];
|
|
168
|
+
const routes = this.extractRoutes(apis);
|
|
169
|
+
// Header
|
|
170
|
+
lines.push('<?php');
|
|
171
|
+
lines.push('');
|
|
172
|
+
if (this.provenance) {
|
|
173
|
+
lines.push('// Generated by Kappa v2.5 CodeDNA');
|
|
174
|
+
lines.push('// Framework: Laravel');
|
|
175
|
+
lines.push('');
|
|
176
|
+
}
|
|
177
|
+
lines.push("use Illuminate\\Support\\Facades\\Route;");
|
|
178
|
+
// Collect controller imports
|
|
179
|
+
const controllers = new Set();
|
|
180
|
+
for (const route of routes) {
|
|
181
|
+
controllers.add(`use ${this.controllerNamespace}\\${route.controller};`);
|
|
182
|
+
}
|
|
183
|
+
for (const controller of controllers) {
|
|
184
|
+
lines.push(controller);
|
|
185
|
+
}
|
|
186
|
+
lines.push('');
|
|
187
|
+
// API routes
|
|
188
|
+
lines.push("Route::prefix('api')->group(function () {");
|
|
189
|
+
// Group by auth requirement
|
|
190
|
+
const authRoutes = routes.filter((r) => r.requiresAuth);
|
|
191
|
+
const publicRoutes = routes.filter((r) => !r.requiresAuth);
|
|
192
|
+
if (publicRoutes.length > 0) {
|
|
193
|
+
lines.push(' // Public routes');
|
|
194
|
+
for (const route of publicRoutes) {
|
|
195
|
+
lines.push(this.generateRouteDefinition(route));
|
|
196
|
+
}
|
|
197
|
+
lines.push('');
|
|
198
|
+
}
|
|
199
|
+
if (authRoutes.length > 0) {
|
|
200
|
+
lines.push(" Route::middleware(['auth:sanctum'])->group(function () {");
|
|
201
|
+
for (const route of authRoutes) {
|
|
202
|
+
lines.push(' ' + this.generateRouteDefinition(route));
|
|
203
|
+
}
|
|
204
|
+
lines.push(' });');
|
|
205
|
+
}
|
|
206
|
+
lines.push('});');
|
|
207
|
+
lines.push('');
|
|
208
|
+
return lines.join('\n');
|
|
209
|
+
}
|
|
210
|
+
generateRouteDefinition(route) {
|
|
211
|
+
const middleware = route.middleware.length > 0
|
|
212
|
+
? `->middleware([${route.middleware.map((m) => `'${m}'`).join(', ')}])`
|
|
213
|
+
: '';
|
|
214
|
+
return ` Route::${route.method}('${route.path}', [${route.controller}::class, '${route.action}'])${middleware};`;
|
|
215
|
+
}
|
|
216
|
+
extractRoutes(apis) {
|
|
217
|
+
const routes = [];
|
|
218
|
+
for (const api of apis) {
|
|
219
|
+
const apiPath = `/${this.toKebabCase(api.name)}`;
|
|
220
|
+
const controllerName = `${api.name}Controller`;
|
|
221
|
+
for (const crud of api.crud || []) {
|
|
222
|
+
routes.push(...this.crudToRoutes(crud, apiPath, controllerName));
|
|
223
|
+
}
|
|
224
|
+
if (api.operations) {
|
|
225
|
+
for (const op of api.operations) {
|
|
226
|
+
routes.push(this.operationToRoute(op, apiPath, controllerName));
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return routes;
|
|
231
|
+
}
|
|
232
|
+
operationToRoute(op, apiPath, controller) {
|
|
233
|
+
const method = this.inferMethod(op.name);
|
|
234
|
+
const path = `${apiPath}/${this.toKebabCase(op.name)}`;
|
|
235
|
+
const middleware = [];
|
|
236
|
+
if (op.rateLimit) {
|
|
237
|
+
middleware.push(`throttle:${op.rateLimit}`);
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
method,
|
|
241
|
+
path,
|
|
242
|
+
controller,
|
|
243
|
+
action: this.toCamelCase(op.name),
|
|
244
|
+
operationName: op.name,
|
|
245
|
+
effects: op.effects,
|
|
246
|
+
requiresAuth: op.effects.includes('Auth'),
|
|
247
|
+
middleware,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
crudToRoutes(crud, apiPath, controller) {
|
|
251
|
+
const routes = [];
|
|
252
|
+
const entityPath = `${apiPath}/${this.toKebabCase(crud.entity)}s`;
|
|
253
|
+
for (const action of crud.actions) {
|
|
254
|
+
const method = CRUD_METHOD_MAP[action.action] ?? 'get';
|
|
255
|
+
const laravelAction = CRUD_ACTION_MAP[action.action] ?? action.action;
|
|
256
|
+
const pathSuffix = action.action === 'read' || action.action === 'update' || action.action === 'delete'
|
|
257
|
+
? '/{id}'
|
|
258
|
+
: '';
|
|
259
|
+
routes.push({
|
|
260
|
+
method,
|
|
261
|
+
path: `${entityPath}${pathSuffix}`,
|
|
262
|
+
controller,
|
|
263
|
+
action: laravelAction,
|
|
264
|
+
operationName: `${action.action}${crud.entity}`,
|
|
265
|
+
effects: action.effects,
|
|
266
|
+
requiresAuth: action.effects.includes('Auth'),
|
|
267
|
+
middleware: [],
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
return routes;
|
|
271
|
+
}
|
|
272
|
+
inferMethod(name) {
|
|
273
|
+
const lower = name.toLowerCase();
|
|
274
|
+
if (lower.startsWith('get') ||
|
|
275
|
+
lower.startsWith('list') ||
|
|
276
|
+
lower.startsWith('find') ||
|
|
277
|
+
lower.startsWith('fetch')) {
|
|
278
|
+
return 'get';
|
|
279
|
+
}
|
|
280
|
+
if (lower.startsWith('create') || lower.startsWith('add') || lower.startsWith('register')) {
|
|
281
|
+
return 'post';
|
|
282
|
+
}
|
|
283
|
+
if (lower.startsWith('update') || lower.startsWith('edit')) {
|
|
284
|
+
return 'patch';
|
|
285
|
+
}
|
|
286
|
+
if (lower.startsWith('delete') || lower.startsWith('remove')) {
|
|
287
|
+
return 'delete';
|
|
288
|
+
}
|
|
289
|
+
return 'post';
|
|
290
|
+
}
|
|
291
|
+
groupByController(routes) {
|
|
292
|
+
const groups = {};
|
|
293
|
+
for (const route of routes) {
|
|
294
|
+
if (!groups[route.controller]) {
|
|
295
|
+
groups[route.controller] = [];
|
|
296
|
+
}
|
|
297
|
+
groups[route.controller].push(route);
|
|
298
|
+
}
|
|
299
|
+
return groups;
|
|
300
|
+
}
|
|
301
|
+
// ===========================================================================
|
|
302
|
+
// Controller Generation
|
|
303
|
+
// ===========================================================================
|
|
304
|
+
generateController(name, routes) {
|
|
305
|
+
const lines = [];
|
|
306
|
+
lines.push('<?php');
|
|
307
|
+
lines.push('');
|
|
308
|
+
lines.push(`namespace ${this.controllerNamespace};`);
|
|
309
|
+
lines.push('');
|
|
310
|
+
if (this.provenance) {
|
|
311
|
+
lines.push('// Generated by Kappa v2.5 CodeDNA');
|
|
312
|
+
lines.push('');
|
|
313
|
+
}
|
|
314
|
+
lines.push('use Illuminate\\Http\\JsonResponse;');
|
|
315
|
+
lines.push('use Illuminate\\Http\\Request;');
|
|
316
|
+
lines.push('');
|
|
317
|
+
lines.push(`class ${name} extends Controller`);
|
|
318
|
+
lines.push('{');
|
|
319
|
+
for (const route of routes) {
|
|
320
|
+
lines.push(this.generateControllerMethod(route));
|
|
321
|
+
lines.push('');
|
|
322
|
+
}
|
|
323
|
+
lines.push('}');
|
|
324
|
+
lines.push('');
|
|
325
|
+
return lines.join('\n');
|
|
326
|
+
}
|
|
327
|
+
generateControllerMethod(route) {
|
|
328
|
+
const lines = [];
|
|
329
|
+
const paramType = route.method === 'post' || route.method === 'put' || route.method === 'patch'
|
|
330
|
+
? `${this.toPascalCase(route.operationName)}Request`
|
|
331
|
+
: 'Request';
|
|
332
|
+
lines.push(' /**');
|
|
333
|
+
lines.push(` * ${route.operationName}`);
|
|
334
|
+
if (route.effects.length > 0) {
|
|
335
|
+
lines.push(` * Effects: ${route.effects.join(', ')}`);
|
|
336
|
+
}
|
|
337
|
+
lines.push(' */');
|
|
338
|
+
lines.push(` public function ${route.action}(${paramType} $request): JsonResponse`);
|
|
339
|
+
lines.push(' {');
|
|
340
|
+
lines.push(` // TODO: Implement ${route.operationName}`);
|
|
341
|
+
lines.push(' return response()->json([');
|
|
342
|
+
lines.push(" 'message' => 'Not implemented',");
|
|
343
|
+
lines.push(' ], 501);');
|
|
344
|
+
lines.push(' }');
|
|
345
|
+
return lines.join('\n');
|
|
346
|
+
}
|
|
347
|
+
// ===========================================================================
|
|
348
|
+
// Model Generation
|
|
349
|
+
// ===========================================================================
|
|
350
|
+
generateModel(entity) {
|
|
351
|
+
const lines = [];
|
|
352
|
+
lines.push('<?php');
|
|
353
|
+
lines.push('');
|
|
354
|
+
lines.push(`namespace ${this.modelNamespace};`);
|
|
355
|
+
lines.push('');
|
|
356
|
+
if (this.provenance) {
|
|
357
|
+
lines.push('// Generated by Kappa v2.5 CodeDNA');
|
|
358
|
+
lines.push('');
|
|
359
|
+
}
|
|
360
|
+
lines.push('use Illuminate\\Database\\Eloquent\\Factories\\HasFactory;');
|
|
361
|
+
lines.push('use Illuminate\\Database\\Eloquent\\Model;');
|
|
362
|
+
// Check for UUID
|
|
363
|
+
const hasUUID = entity.fields.some((f) => f.type.kind === 'primitive' && f.type.type === 'uuid');
|
|
364
|
+
if (hasUUID) {
|
|
365
|
+
lines.push('use Illuminate\\Database\\Eloquent\\Concerns\\HasUuids;');
|
|
366
|
+
}
|
|
367
|
+
lines.push('');
|
|
368
|
+
lines.push(`class ${entity.name} extends Model`);
|
|
369
|
+
lines.push('{');
|
|
370
|
+
lines.push(' use HasFactory;');
|
|
371
|
+
if (hasUUID) {
|
|
372
|
+
lines.push(' use HasUuids;');
|
|
373
|
+
}
|
|
374
|
+
lines.push('');
|
|
375
|
+
// Table name
|
|
376
|
+
lines.push(` protected $table = '${this.toSnakeCase(entity.name)}s';`);
|
|
377
|
+
lines.push('');
|
|
378
|
+
// Fillable
|
|
379
|
+
const fillable = entity.fields
|
|
380
|
+
.filter((f) => !f.modifiers.includes('primary') && !f.modifiers.includes('auto'))
|
|
381
|
+
.map((f) => `'${this.toSnakeCase(f.name)}'`);
|
|
382
|
+
lines.push(' protected $fillable = [');
|
|
383
|
+
lines.push(` ${fillable.join(',\n ')},`);
|
|
384
|
+
lines.push(' ];');
|
|
385
|
+
lines.push('');
|
|
386
|
+
// Hidden
|
|
387
|
+
const hidden = entity.fields
|
|
388
|
+
.filter((f) => f.modifiers.includes('hashed') || f.name.toLowerCase().includes('password'))
|
|
389
|
+
.map((f) => `'${this.toSnakeCase(f.name)}'`);
|
|
390
|
+
if (hidden.length > 0) {
|
|
391
|
+
lines.push(' protected $hidden = [');
|
|
392
|
+
lines.push(` ${hidden.join(',\n ')},`);
|
|
393
|
+
lines.push(' ];');
|
|
394
|
+
lines.push('');
|
|
395
|
+
}
|
|
396
|
+
// Casts
|
|
397
|
+
const casts = this.generateCasts(entity.fields);
|
|
398
|
+
if (Object.keys(casts).length > 0) {
|
|
399
|
+
lines.push(' protected $casts = [');
|
|
400
|
+
for (const [field, type] of Object.entries(casts)) {
|
|
401
|
+
lines.push(` '${field}' => '${type}',`);
|
|
402
|
+
}
|
|
403
|
+
lines.push(' ];');
|
|
404
|
+
}
|
|
405
|
+
lines.push('}');
|
|
406
|
+
lines.push('');
|
|
407
|
+
return lines.join('\n');
|
|
408
|
+
}
|
|
409
|
+
generateCasts(fields) {
|
|
410
|
+
const casts = {};
|
|
411
|
+
for (const field of fields) {
|
|
412
|
+
const fieldName = this.toSnakeCase(field.name);
|
|
413
|
+
if (field.type.kind === 'primitive') {
|
|
414
|
+
switch (field.type.type) {
|
|
415
|
+
case 'timestamp':
|
|
416
|
+
case 'date':
|
|
417
|
+
casts[fieldName] = 'datetime';
|
|
418
|
+
break;
|
|
419
|
+
case 'bool':
|
|
420
|
+
casts[fieldName] = 'boolean';
|
|
421
|
+
break;
|
|
422
|
+
case 'int':
|
|
423
|
+
casts[fieldName] = 'integer';
|
|
424
|
+
break;
|
|
425
|
+
case 'float':
|
|
426
|
+
casts[fieldName] = 'float';
|
|
427
|
+
break;
|
|
428
|
+
case 'json':
|
|
429
|
+
casts[fieldName] = 'array';
|
|
430
|
+
break;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
else if (field.type.kind === 'array') {
|
|
434
|
+
casts[fieldName] = 'array';
|
|
435
|
+
}
|
|
436
|
+
// Handle hashed modifier for password-like fields
|
|
437
|
+
if (field.modifiers.includes('hashed')) {
|
|
438
|
+
casts[fieldName] = 'hashed';
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
return casts;
|
|
442
|
+
}
|
|
443
|
+
// ===========================================================================
|
|
444
|
+
// Migration Generation
|
|
445
|
+
// ===========================================================================
|
|
446
|
+
generateMigration(entity) {
|
|
447
|
+
const lines = [];
|
|
448
|
+
const tableName = `${this.toSnakeCase(entity.name)}s`;
|
|
449
|
+
lines.push('<?php');
|
|
450
|
+
lines.push('');
|
|
451
|
+
if (this.provenance) {
|
|
452
|
+
lines.push('// Generated by Kappa v2.5 CodeDNA');
|
|
453
|
+
lines.push('');
|
|
454
|
+
}
|
|
455
|
+
lines.push('use Illuminate\\Database\\Migrations\\Migration;');
|
|
456
|
+
lines.push('use Illuminate\\Database\\Schema\\Blueprint;');
|
|
457
|
+
lines.push('use Illuminate\\Support\\Facades\\Schema;');
|
|
458
|
+
lines.push('');
|
|
459
|
+
lines.push('return new class extends Migration');
|
|
460
|
+
lines.push('{');
|
|
461
|
+
lines.push(' public function up(): void');
|
|
462
|
+
lines.push(' {');
|
|
463
|
+
lines.push(` Schema::create('${tableName}', function (Blueprint $table) {`);
|
|
464
|
+
// Check for custom primary key
|
|
465
|
+
const hasPrimary = entity.fields.some((f) => f.modifiers.includes('primary'));
|
|
466
|
+
const hasUUID = entity.fields.some((f) => f.type.kind === 'primitive' && f.type.type === 'uuid' && f.modifiers.includes('primary'));
|
|
467
|
+
if (!hasPrimary) {
|
|
468
|
+
lines.push(' $table->id();');
|
|
469
|
+
}
|
|
470
|
+
for (const field of entity.fields) {
|
|
471
|
+
const migration = this.generateMigrationColumn(field);
|
|
472
|
+
if (migration) {
|
|
473
|
+
lines.push(` ${migration}`);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
lines.push(' $table->timestamps();');
|
|
477
|
+
lines.push(' });');
|
|
478
|
+
lines.push(' }');
|
|
479
|
+
lines.push('');
|
|
480
|
+
lines.push(' public function down(): void');
|
|
481
|
+
lines.push(' {');
|
|
482
|
+
lines.push(` Schema::dropIfExists('${tableName}');`);
|
|
483
|
+
lines.push(' }');
|
|
484
|
+
lines.push('};');
|
|
485
|
+
lines.push('');
|
|
486
|
+
return lines.join('\n');
|
|
487
|
+
}
|
|
488
|
+
generateMigrationColumn(field) {
|
|
489
|
+
const columnName = this.toSnakeCase(field.name);
|
|
490
|
+
let type;
|
|
491
|
+
let modifiers = [];
|
|
492
|
+
if (field.type.kind === 'primitive') {
|
|
493
|
+
type = PRIMITIVE_MIGRATION_MAP[field.type.type] ?? 'string';
|
|
494
|
+
if (field.type.type === 'uuid' && field.modifiers.includes('primary')) {
|
|
495
|
+
return `$table->uuid('${columnName}')->primary();`;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
else if (field.type.kind === 'enum') {
|
|
499
|
+
const values = field.type.values.map((v) => `'${v}'`).join(', ');
|
|
500
|
+
return `$table->enum('${columnName}', [${values}]);`;
|
|
501
|
+
}
|
|
502
|
+
else if (field.type.kind === 'array') {
|
|
503
|
+
type = 'json';
|
|
504
|
+
}
|
|
505
|
+
else if (field.type.kind === 'reference') {
|
|
506
|
+
// Foreign key - use foreignUuid if the referenced entity uses UUID
|
|
507
|
+
const refTable = `${this.toSnakeCase(field.type.entity)}s`;
|
|
508
|
+
// For now, assume UUID foreign keys (most common in modern Laravel)
|
|
509
|
+
// TODO: Could enhance to detect if referenced entity uses UUID or int
|
|
510
|
+
return `$table->foreignUuid('${columnName}')->constrained('${refTable}');`;
|
|
511
|
+
}
|
|
512
|
+
else {
|
|
513
|
+
return null;
|
|
514
|
+
}
|
|
515
|
+
// Apply modifiers
|
|
516
|
+
if (field.modifiers.includes('unique')) {
|
|
517
|
+
modifiers.push('unique()');
|
|
518
|
+
}
|
|
519
|
+
if (field.modifiers.includes('optional')) {
|
|
520
|
+
modifiers.push('nullable()');
|
|
521
|
+
}
|
|
522
|
+
if (field.defaultValue !== undefined) {
|
|
523
|
+
// Handle boolean literals correctly
|
|
524
|
+
if (field.type.kind === 'primitive' && field.type.type === 'bool') {
|
|
525
|
+
modifiers.push(`default(${field.defaultValue})`);
|
|
526
|
+
}
|
|
527
|
+
else if (typeof field.defaultValue === 'number') {
|
|
528
|
+
modifiers.push(`default(${field.defaultValue})`);
|
|
529
|
+
}
|
|
530
|
+
else {
|
|
531
|
+
modifiers.push(`default('${field.defaultValue}')`);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
const modifierStr = modifiers.length > 0 ? '->' + modifiers.join('->') : '';
|
|
535
|
+
return `$table->${type}('${columnName}')${modifierStr};`;
|
|
536
|
+
}
|
|
537
|
+
// ===========================================================================
|
|
538
|
+
// Form Request Generation
|
|
539
|
+
// ===========================================================================
|
|
540
|
+
generateFormRequest(name, route) {
|
|
541
|
+
const lines = [];
|
|
542
|
+
lines.push('<?php');
|
|
543
|
+
lines.push('');
|
|
544
|
+
lines.push('namespace App\\Http\\Requests;');
|
|
545
|
+
lines.push('');
|
|
546
|
+
if (this.provenance) {
|
|
547
|
+
lines.push('// Generated by Kappa v2.5 CodeDNA');
|
|
548
|
+
lines.push('');
|
|
549
|
+
}
|
|
550
|
+
lines.push('use Illuminate\\Foundation\\Http\\FormRequest;');
|
|
551
|
+
lines.push('');
|
|
552
|
+
lines.push(`class ${name} extends FormRequest`);
|
|
553
|
+
lines.push('{');
|
|
554
|
+
lines.push(' public function authorize(): bool');
|
|
555
|
+
lines.push(' {');
|
|
556
|
+
lines.push(` return ${route.requiresAuth ? 'auth()->check()' : 'true'};`);
|
|
557
|
+
lines.push(' }');
|
|
558
|
+
lines.push('');
|
|
559
|
+
lines.push(' public function rules(): array');
|
|
560
|
+
lines.push(' {');
|
|
561
|
+
lines.push(' return [');
|
|
562
|
+
// Try to extract entity name from route to generate validation rules
|
|
563
|
+
const validationRules = this.generateValidationRules(route);
|
|
564
|
+
if (validationRules.length > 0) {
|
|
565
|
+
for (const rule of validationRules) {
|
|
566
|
+
lines.push(` ${rule}`);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
else {
|
|
570
|
+
lines.push(' // TODO: Add validation rules');
|
|
571
|
+
}
|
|
572
|
+
lines.push(' ];');
|
|
573
|
+
lines.push(' }');
|
|
574
|
+
lines.push('}');
|
|
575
|
+
lines.push('');
|
|
576
|
+
return lines.join('\n');
|
|
577
|
+
}
|
|
578
|
+
generateValidationRules(route) {
|
|
579
|
+
const rules = [];
|
|
580
|
+
// Try to infer entity from route operation name
|
|
581
|
+
// e.g., "createPost" -> "Post", "updateUser" -> "User"
|
|
582
|
+
let entityName = null;
|
|
583
|
+
for (const [name] of this.entities) {
|
|
584
|
+
if (route.operationName.toLowerCase().includes(name.toLowerCase())) {
|
|
585
|
+
entityName = name;
|
|
586
|
+
break;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
if (!entityName) {
|
|
590
|
+
return rules;
|
|
591
|
+
}
|
|
592
|
+
const entity = this.entities.get(entityName);
|
|
593
|
+
if (!entity) {
|
|
594
|
+
return rules;
|
|
595
|
+
}
|
|
596
|
+
const isUpdate = route.action === 'update' || route.operationName.toLowerCase().includes('update');
|
|
597
|
+
for (const field of entity.fields) {
|
|
598
|
+
// Skip primary keys and auto-generated fields
|
|
599
|
+
if (field.modifiers.includes('primary') || field.modifiers.includes('auto')) {
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
const fieldName = this.toSnakeCase(field.name);
|
|
603
|
+
const fieldRules = [];
|
|
604
|
+
// Required/optional
|
|
605
|
+
if (isUpdate) {
|
|
606
|
+
fieldRules.push('sometimes');
|
|
607
|
+
}
|
|
608
|
+
else if (!field.modifiers.includes('optional')) {
|
|
609
|
+
fieldRules.push('required');
|
|
610
|
+
}
|
|
611
|
+
else {
|
|
612
|
+
fieldRules.push('nullable');
|
|
613
|
+
}
|
|
614
|
+
// Type-specific validation
|
|
615
|
+
if (field.type.kind === 'primitive') {
|
|
616
|
+
const primitiveRules = PRIMITIVE_VALIDATION_MAP[field.type.type] || [];
|
|
617
|
+
fieldRules.push(...primitiveRules);
|
|
618
|
+
}
|
|
619
|
+
else if (field.type.kind === 'enum') {
|
|
620
|
+
fieldRules.push(`in:${field.type.values.join(',')}`);
|
|
621
|
+
}
|
|
622
|
+
else if (field.type.kind === 'reference') {
|
|
623
|
+
const refTable = `${this.toSnakeCase(field.type.entity)}s`;
|
|
624
|
+
fieldRules.push(`exists:${refTable},id`);
|
|
625
|
+
}
|
|
626
|
+
// Modifiers
|
|
627
|
+
if (field.modifiers.includes('unique')) {
|
|
628
|
+
const tableName = `${this.toSnakeCase(entityName)}s`;
|
|
629
|
+
if (isUpdate) {
|
|
630
|
+
// For updates, we need to exclude the current record from unique check
|
|
631
|
+
// This will be concatenated in the rules array, not interpolated here
|
|
632
|
+
rules.push(`'${fieldName}' => ['${fieldRules.join('\', \'')}', 'unique:${tableName},${fieldName},' . $this->route('id')],`);
|
|
633
|
+
continue; // Skip the normal rule addition at the end
|
|
634
|
+
}
|
|
635
|
+
else {
|
|
636
|
+
fieldRules.push(`unique:${tableName},${fieldName}`);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
// Validators from field type range (if primitive with range constraint)
|
|
640
|
+
if (field.type.kind === 'primitive' && 'range' in field.type && field.type.range) {
|
|
641
|
+
const range = field.type.range;
|
|
642
|
+
if (range.min !== undefined) {
|
|
643
|
+
fieldRules.push(`min:${range.min}`);
|
|
644
|
+
}
|
|
645
|
+
if (range.max !== undefined) {
|
|
646
|
+
fieldRules.push(`max:${range.max}`);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
rules.push(`'${fieldName}' => '${fieldRules.join('|')}',`);
|
|
650
|
+
}
|
|
651
|
+
return rules;
|
|
652
|
+
}
|
|
653
|
+
// ===========================================================================
|
|
654
|
+
// Resource Generation
|
|
655
|
+
// ===========================================================================
|
|
656
|
+
generateResource(entity) {
|
|
657
|
+
const lines = [];
|
|
658
|
+
lines.push('<?php');
|
|
659
|
+
lines.push('');
|
|
660
|
+
lines.push('namespace App\\Http\\Resources;');
|
|
661
|
+
lines.push('');
|
|
662
|
+
if (this.provenance) {
|
|
663
|
+
lines.push('// Generated by Kappa v2.5 CodeDNA');
|
|
664
|
+
lines.push('');
|
|
665
|
+
}
|
|
666
|
+
lines.push('use Illuminate\\Http\\Request;');
|
|
667
|
+
lines.push('use Illuminate\\Http\\Resources\\Json\\JsonResource;');
|
|
668
|
+
lines.push('');
|
|
669
|
+
lines.push(`class ${entity.name}Resource extends JsonResource`);
|
|
670
|
+
lines.push('{');
|
|
671
|
+
lines.push(' public function toArray(Request $request): array');
|
|
672
|
+
lines.push(' {');
|
|
673
|
+
lines.push(' return [');
|
|
674
|
+
const addedFields = new Set();
|
|
675
|
+
for (const field of entity.fields) {
|
|
676
|
+
const fieldName = this.toSnakeCase(field.name);
|
|
677
|
+
// Skip password fields in resources
|
|
678
|
+
if (field.modifiers.includes('hashed') || field.name.toLowerCase().includes('password')) {
|
|
679
|
+
continue;
|
|
680
|
+
}
|
|
681
|
+
// Skip auto timestamp fields (created_at, updated_at) - we'll add them at the end
|
|
682
|
+
if (field.modifiers.includes('auto') && (fieldName === 'created_at' || fieldName === 'updated_at')) {
|
|
683
|
+
continue;
|
|
684
|
+
}
|
|
685
|
+
lines.push(` '${fieldName}' => $this->${fieldName},`);
|
|
686
|
+
addedFields.add(fieldName);
|
|
687
|
+
}
|
|
688
|
+
// Add timestamps if not already added
|
|
689
|
+
if (!addedFields.has('created_at')) {
|
|
690
|
+
lines.push(" 'created_at' => $this->created_at,");
|
|
691
|
+
}
|
|
692
|
+
if (!addedFields.has('updated_at')) {
|
|
693
|
+
lines.push(" 'updated_at' => $this->updated_at,");
|
|
694
|
+
}
|
|
695
|
+
lines.push(' ];');
|
|
696
|
+
lines.push(' }');
|
|
697
|
+
lines.push('}');
|
|
698
|
+
lines.push('');
|
|
699
|
+
return lines.join('\n');
|
|
700
|
+
}
|
|
701
|
+
// ===========================================================================
|
|
702
|
+
// Utility Methods
|
|
703
|
+
// ===========================================================================
|
|
704
|
+
toPascalCase(str) {
|
|
705
|
+
return str
|
|
706
|
+
.replace(/[-_](.)/g, (_, c) => c.toUpperCase())
|
|
707
|
+
.replace(/^(.)/, (_, c) => c.toUpperCase());
|
|
708
|
+
}
|
|
709
|
+
toCamelCase(str) {
|
|
710
|
+
return str
|
|
711
|
+
.replace(/[-_](.)/g, (_, c) => c.toUpperCase())
|
|
712
|
+
.replace(/^(.)/, (_, c) => c.toLowerCase());
|
|
713
|
+
}
|
|
714
|
+
toKebabCase(str) {
|
|
715
|
+
return str
|
|
716
|
+
.replace(/([a-z])([A-Z])/g, '$1-$2')
|
|
717
|
+
.replace(/[_\s]+/g, '-')
|
|
718
|
+
.toLowerCase();
|
|
719
|
+
}
|
|
720
|
+
toSnakeCase(str) {
|
|
721
|
+
return str
|
|
722
|
+
.replace(/([a-z])([A-Z])/g, '$1_$2')
|
|
723
|
+
.replace(/[-\s]+/g, '_')
|
|
724
|
+
.toLowerCase();
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
// =============================================================================
|
|
728
|
+
// Convenience Functions
|
|
729
|
+
// =============================================================================
|
|
730
|
+
export function generateLaravelAPI(apis, options) {
|
|
731
|
+
const generator = new KappaLaravelGenerator(options);
|
|
732
|
+
return generator.generateFromAPIs(apis);
|
|
733
|
+
}
|
|
734
|
+
export function generateLaravelModels(entities, options) {
|
|
735
|
+
const generator = new KappaLaravelGenerator(options);
|
|
736
|
+
return generator.generateFromEntities(entities);
|
|
737
|
+
}
|
|
738
|
+
export function generateLaravel(apis, entities, options) {
|
|
739
|
+
const generator = new KappaLaravelGenerator(options);
|
|
740
|
+
return generator.generateAll(apis, entities);
|
|
741
|
+
}
|