naider 1.21.1 → 1.23.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.
- package/bin/naide.js +8 -4
- package/mcp/server.js +1 -1
- package/package.json +1 -1
- package/src/gen.js +639 -103
package/bin/naide.js
CHANGED
|
@@ -8,7 +8,7 @@ import { spawn } from 'child_process';
|
|
|
8
8
|
import { createRequire } from 'module';
|
|
9
9
|
const _require = createRequire(import.meta.url);
|
|
10
10
|
|
|
11
|
-
const NAIDE_VERSION = '1.
|
|
11
|
+
const NAIDE_VERSION = '1.23.0';
|
|
12
12
|
|
|
13
13
|
function crashReport(err, context = {}) {
|
|
14
14
|
const info = [
|
|
@@ -279,22 +279,26 @@ if (files[0] === 'gen' || files[0] === '~~') {
|
|
|
279
279
|
writeFileSync(fp, f.content, 'utf-8');
|
|
280
280
|
}
|
|
281
281
|
const a = result.analysis;
|
|
282
|
-
const mode = isUpdate ? 'Updated' : 'Created';
|
|
282
|
+
const mode = result.action === 'removed' ? 'Removed' : isUpdate ? 'Updated' : 'Created';
|
|
283
283
|
process.stderr.write(`\n ── NAIDE Agent (${mode}) ────────────────────────────────\n`);
|
|
284
|
-
process.stderr.write(` Input: "${a.instruction}"\n`);
|
|
284
|
+
process.stderr.write(` Input: "${a.instruction || instruction}"\n`);
|
|
285
285
|
if (a.entities.length > 0) {
|
|
286
286
|
process.stderr.write(` Entities: ${a.entities.map(e => `${e.name} (${e.fieldCount} fields)`).join(', ')}\n`);
|
|
287
287
|
}
|
|
288
288
|
if (a.relationships && a.relationships.length > 0) {
|
|
289
289
|
for (const r of a.relationships) process.stderr.write(` Relation: ${r.parent} 1→N ${r.child} (${r.fk})\n`);
|
|
290
290
|
}
|
|
291
|
+
if (result.removed && result.removed.length > 0) {
|
|
292
|
+
process.stderr.write(` Removed: ${result.removed.join(', ')}\n`);
|
|
293
|
+
}
|
|
291
294
|
if (a.added && a.added.length > 0) {
|
|
292
295
|
process.stderr.write(` Added: ${a.added.join(', ')}\n`);
|
|
293
296
|
}
|
|
294
297
|
process.stderr.write(` Plan:\n`);
|
|
295
298
|
for (const step of a.plan) process.stderr.write(` + ${step}\n`);
|
|
299
|
+
process.stderr.write(` Files: ${result.files.length} generated\n`);
|
|
296
300
|
process.stderr.write(' ────────────────────────────────────────────────────────\n\n');
|
|
297
|
-
console.log(` Project ${
|
|
301
|
+
console.log(` Project ${mode.toLowerCase()}: ${dir}/`);
|
|
298
302
|
for (const f of result.files) console.log(` ${f.path}`);
|
|
299
303
|
if (!isUpdate) console.log(`\n cd ${dir} && npm install && npm run dev\n`);
|
|
300
304
|
else console.log(`\n Files regenerated. Run: npm run dev\n`);
|
package/mcp/server.js
CHANGED
|
@@ -192,7 +192,7 @@ async function handleMessage(msg) {
|
|
|
192
192
|
respond(msg.id, {
|
|
193
193
|
protocolVersion: '2024-11-05',
|
|
194
194
|
capabilities: { tools: {} },
|
|
195
|
-
serverInfo: { name: 'naide-mcp', version: '1.
|
|
195
|
+
serverInfo: { name: 'naide-mcp', version: '1.23.0' }
|
|
196
196
|
});
|
|
197
197
|
break;
|
|
198
198
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "naider",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.23.0",
|
|
4
4
|
"description": "NAIDE - Simpler than Python, compiles to 15 targets. AI-specialized language with 35+ built-in functions, syntax sugar (unless/until/repeat/swap/is/isnt), and 47 features. Targets: Node.js, Python, TypeScript, C, C++, Java, Go, Rust, PHP, Ruby, Kotlin, Swift, Dart, C#, Bun.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"exports": {
|
package/src/gen.js
CHANGED
|
@@ -93,7 +93,7 @@ const COMPOSITE_WORDS = {
|
|
|
93
93
|
const NOISE = new Set([
|
|
94
94
|
'a', 'an', 'the', 'for', 'with', 'and', 'or', 'on', 'at', 'to', 'of', 'in', 'by',
|
|
95
95
|
'that', 'which', 'this', 'it', 'its', 'my', 'is', 'are', 'has', 'have', 'do', 'does',
|
|
96
|
-
'make', 'create', 'build', 'generate', 'add', 'implement', 'simple', 'basic', 'new',
|
|
96
|
+
'make', 'create', 'build', 'generate', 'add', 'implement', 'simple', 'basic', 'new', 'remove', 'delete', 'drop',
|
|
97
97
|
'please', 'want', 'need', 'like', 'just', 'some', 'using', 'use', 'based',
|
|
98
98
|
'setup', 'set', 'up', 'get', 'should', 'can', 'could', 'would',
|
|
99
99
|
'の', 'を', 'に', 'で', 'は', 'が', 'と', 'も', 'から', 'まで', 'より', 'って',
|
|
@@ -266,6 +266,65 @@ function applyRelationships(entities, relationships) {
|
|
|
266
266
|
}
|
|
267
267
|
}
|
|
268
268
|
|
|
269
|
+
// ── Many-to-Many Relationships ────────────────────────────
|
|
270
|
+
|
|
271
|
+
const MANY_TO_MANY = {
|
|
272
|
+
'User-Product': { junction: 'Favorite', fkLeft: 'user_id', fkRight: 'product_id' },
|
|
273
|
+
'User-Team': { junction: 'TeamMember', fkLeft: 'user_id', fkRight: 'team_id' },
|
|
274
|
+
'Post-Category':{ junction: 'PostTag', fkLeft: 'post_id', fkRight: 'category_id' },
|
|
275
|
+
'Product-Category':{ junction: 'ProductCategory', fkLeft: 'product_id', fkRight: 'category_id' },
|
|
276
|
+
'User-Group': { junction: 'GroupMember', fkLeft: 'user_id', fkRight: 'group_id' },
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
function detectManyToMany(entityNames) {
|
|
280
|
+
const rels = [];
|
|
281
|
+
for (let i = 0; i < entityNames.length; i++) {
|
|
282
|
+
for (let j = i + 1; j < entityNames.length; j++) {
|
|
283
|
+
const key1 = entityNames[i] + '-' + entityNames[j];
|
|
284
|
+
const key2 = entityNames[j] + '-' + entityNames[i];
|
|
285
|
+
const match = MANY_TO_MANY[key1] || MANY_TO_MANY[key2];
|
|
286
|
+
if (match) rels.push({ left: entityNames[i], right: entityNames[j], ...match });
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return rels;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function junctionSchemaBlock(rel) {
|
|
293
|
+
return `schema ${rel.junction}:\n id auto\n ${rel.fkLeft.padEnd(10)}int required\n ${rel.fkRight.padEnd(10)}int required\n created auto`;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function manyToManyRoutes(rel) {
|
|
297
|
+
const leftPlural = pluralize(rel.left.toLowerCase());
|
|
298
|
+
const rightPlural = pluralize(rel.right.toLowerCase());
|
|
299
|
+
const store = rel.junction + 'Store';
|
|
300
|
+
return [
|
|
301
|
+
`get "/api/${leftPlural}/:id/${rightPlural}" (req, res):`,
|
|
302
|
+
` list links = ${store}.findAll("${rel.fkLeft}", req.params.id)`,
|
|
303
|
+
` ret links`,
|
|
304
|
+
``,
|
|
305
|
+
`post "/api/${leftPlural}/:id/${rightPlural}" (req, res):`,
|
|
306
|
+
` any link = ${store}.create({${rel.fkLeft}: req.params.id, ${rel.fkRight}: req.body.${rel.fkRight}})`,
|
|
307
|
+
` ret ok(link, null)`,
|
|
308
|
+
``,
|
|
309
|
+
`del "/api/${leftPlural}/:id/${rightPlural}/:linkId" (req, res):`,
|
|
310
|
+
` ${store}.remove(req.params.linkId)`,
|
|
311
|
+
` ret ok({removed: true}, null)`,
|
|
312
|
+
``,
|
|
313
|
+
`get "/api/${rightPlural}/:id/${leftPlural}" (req, res):`,
|
|
314
|
+
` list links = ${store}.findAll("${rel.fkRight}", req.params.id)`,
|
|
315
|
+
` ret links`,
|
|
316
|
+
];
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// ── Enum Fields ───────────────────────────────────────────
|
|
320
|
+
|
|
321
|
+
const ENUM_VALUES = {
|
|
322
|
+
status: ['pending', 'active', 'completed', 'cancelled'],
|
|
323
|
+
priority: ['low', 'medium', 'high', 'critical'],
|
|
324
|
+
role: ['user', 'admin', 'moderator'],
|
|
325
|
+
visibility: ['public', 'private', 'unlisted'],
|
|
326
|
+
};
|
|
327
|
+
|
|
269
328
|
// ── Utilities ───────────────────────────────────────────────
|
|
270
329
|
|
|
271
330
|
function singularize(w) {
|
|
@@ -382,6 +441,16 @@ function classify(words, raw) {
|
|
|
382
441
|
return !NOISE.has(wl) && !allKnown.has(wl) && !/^\d+$/.test(wl) && wl.length > 2 && /^[a-z]+s?$/i.test(wl);
|
|
383
442
|
}) || Object.keys(JA_ENTITIES).some(ja => raw.includes(ja));
|
|
384
443
|
|
|
444
|
+
// Entity removal detection (B8)
|
|
445
|
+
if (/(?:remove|delete|drop|取り除|削除)\s+/i.test(raw)) {
|
|
446
|
+
mods.removal = true;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// API versioning detection (B13)
|
|
450
|
+
if (/(?:version|v\d|versioning|バージョン)/i.test(raw)) {
|
|
451
|
+
mods.apiVersion = 'v1';
|
|
452
|
+
}
|
|
453
|
+
|
|
385
454
|
// Complex NL phrase detection
|
|
386
455
|
if (/(?:users?\s+can\s+(?:comment|post|review|write)|コメントできる|投稿できる|レビューできる)/i.test(raw)) {
|
|
387
456
|
intents.add('auth'); intents.add('crud');
|
|
@@ -474,7 +543,6 @@ function extractParams(words, raw, intents, mods) {
|
|
|
474
543
|
if (!wl || NOISE.has(wl) || allKnown.has(wl) || /^\d+$/.test(wl) || wl.length <= 2) continue;
|
|
475
544
|
const name = capitalize(singularize(wl));
|
|
476
545
|
if (!entityNames.includes(name)) entityNames.push(name);
|
|
477
|
-
break;
|
|
478
546
|
}
|
|
479
547
|
}
|
|
480
548
|
}
|
|
@@ -540,6 +608,7 @@ function extractParams(words, raw, intents, mods) {
|
|
|
540
608
|
if (params.entities.length > 1) {
|
|
541
609
|
params.relationships = detectRelationships(params.entities);
|
|
542
610
|
applyRelationships(params.entities, params.relationships);
|
|
611
|
+
params.m2mRelationships = detectManyToMany(params.entities.map(e => e.name));
|
|
543
612
|
}
|
|
544
613
|
|
|
545
614
|
// Extract bot commands
|
|
@@ -649,16 +718,33 @@ function paginatedListBlock(entity) {
|
|
|
649
718
|
const store = entity.name + 'Store';
|
|
650
719
|
return [
|
|
651
720
|
`get "/api/${plural}" (req, res):`,
|
|
652
|
-
` int page = req.query.page
|
|
653
|
-
` int limit = req.query.limit
|
|
654
|
-
` str search = req.query.search
|
|
655
|
-
`
|
|
721
|
+
` int page = req.query.page or 1`,
|
|
722
|
+
` int limit = req.query.limit or 20`,
|
|
723
|
+
` str search = req.query.search or ""`,
|
|
724
|
+
` str sortBy = req.query.sort or "id"`,
|
|
725
|
+
` str order = req.query.order or "desc"`,
|
|
726
|
+
` mut list rows = ${store}.all()`,
|
|
656
727
|
` if search:`,
|
|
657
|
-
`
|
|
658
|
-
`
|
|
659
|
-
`
|
|
660
|
-
`
|
|
661
|
-
`
|
|
728
|
+
` rows = rows.filter((item) => JSON.stringify(item).toLowerCase().includes(search.toLowerCase()))`,
|
|
729
|
+
` each key in Object.keys(req.query):`,
|
|
730
|
+
` if key.endsWith("_gte"):`,
|
|
731
|
+
` str field = key.replace("_gte", "")`,
|
|
732
|
+
` rows = rows.filter((item) => item[field] >= Number(req.query[key]))`,
|
|
733
|
+
` if key.endsWith("_lte"):`,
|
|
734
|
+
` str field = key.replace("_lte", "")`,
|
|
735
|
+
` rows = rows.filter((item) => item[field] <= Number(req.query[key]))`,
|
|
736
|
+
` if key.endsWith("_eq"):`,
|
|
737
|
+
` str field = key.replace("_eq", "")`,
|
|
738
|
+
` rows = rows.filter((item) => String(item[field]) == req.query[key])`,
|
|
739
|
+
` if order == "asc":`,
|
|
740
|
+
` rows = rows.sort((a, b) => String(a[sortBy]).localeCompare(String(b[sortBy])))`,
|
|
741
|
+
` else:`,
|
|
742
|
+
` rows = rows.sort((a, b) => String(b[sortBy]).localeCompare(String(a[sortBy])))`,
|
|
743
|
+
` int total = rows.length`,
|
|
744
|
+
` int offset = page - 1`,
|
|
745
|
+
` int start = offset * limit`,
|
|
746
|
+
` list items = rows.slice(start, start + limit)`,
|
|
747
|
+
` ret {items, total, page, limit, sort: sortBy, order}`,
|
|
662
748
|
];
|
|
663
749
|
}
|
|
664
750
|
|
|
@@ -817,7 +903,9 @@ function openApiSpec(entities, intents, params, relationships) {
|
|
|
817
903
|
{ name: 'page', in: 'query', schema: { type: 'integer', default: 1 } },
|
|
818
904
|
{ name: 'limit', in: 'query', schema: { type: 'integer', default: 20 } },
|
|
819
905
|
{ name: 'search', in: 'query', schema: { type: 'string' } },
|
|
820
|
-
|
|
906
|
+
{ name: 'sort', in: 'query', schema: { type: 'string' }, description: 'Field to sort by' },
|
|
907
|
+
{ name: 'order', in: 'query', schema: { type: 'string', enum: ['asc','desc'], default: 'asc' } },
|
|
908
|
+
], responses: { '200': { description: `List of ${plural} with pagination` } } },
|
|
821
909
|
post: { summary: `Create ${entity.name.toLowerCase()}`, requestBody: { content: { 'application/json': { schema: {
|
|
822
910
|
type: 'object', required: requiredFields.map(f => f.name),
|
|
823
911
|
properties: Object.fromEntries(requiredFields.map(f => [f.name, properties[f.name]])),
|
|
@@ -847,12 +935,24 @@ function openApiSpec(entities, intents, params, relationships) {
|
|
|
847
935
|
};
|
|
848
936
|
}
|
|
849
937
|
if (intents.has('auth')) {
|
|
850
|
-
paths['/api/auth/register'] = { post: { summary: 'Register', responses: { '200': { description: 'Token + user' } } } };
|
|
851
|
-
paths['/api/auth/login'] = { post: { summary: 'Login', responses: { '200': { description: 'Token + user' } } } };
|
|
938
|
+
paths['/api/auth/register'] = { post: { summary: 'Register', requestBody: { content: { 'application/json': { schema: { type: 'object', required: ['email','password'], properties: { email:{type:'string'}, password:{type:'string'}, name:{type:'string'} } } } } }, responses: { '200': { description: 'Token + user' }, '400': { description: 'Validation error' } } } };
|
|
939
|
+
paths['/api/auth/login'] = { post: { summary: 'Login', requestBody: { content: { 'application/json': { schema: { type: 'object', required: ['email','password'], properties: { email:{type:'string'}, password:{type:'string'} } } } } }, responses: { '200': { description: 'Token + user' }, '401': { description: 'Invalid credentials' } } } };
|
|
852
940
|
paths['/api/auth/me'] = { get: { summary: 'Current user', security: [{ bearerAuth: [] }], responses: { '200': { description: 'User' } } } };
|
|
941
|
+
paths['/api/auth/forgot-password'] = { post: { summary: 'Request password reset', requestBody: { content: { 'application/json': { schema: { type: 'object', required: ['email'], properties: { email:{type:'string'} } } } } }, responses: { '200': { description: 'Reset token sent' } } } };
|
|
942
|
+
paths['/api/auth/reset-password'] = { post: { summary: 'Reset password with token', requestBody: { content: { 'application/json': { schema: { type: 'object', required: ['token','password'], properties: { token:{type:'string'}, password:{type:'string'} } } } } }, responses: { '200': { description: 'Password reset' }, '400': { description: 'Invalid token' } } } };
|
|
943
|
+
paths['/api/auth/change-password'] = { post: { summary: 'Change password (authenticated)', security: [{ bearerAuth: [] }], requestBody: { content: { 'application/json': { schema: { type: 'object', required: ['oldPassword','newPassword'], properties: { oldPassword:{type:'string'}, newPassword:{type:'string'} } } } } }, responses: { '200': { description: 'Password changed' }, '401': { description: 'Wrong password' } } } };
|
|
944
|
+
}
|
|
945
|
+
const m2m = detectManyToMany(entities.map(e => e.name));
|
|
946
|
+
for (const rel of m2m) {
|
|
947
|
+
const lp = pluralize(rel.left.toLowerCase());
|
|
948
|
+
const rp = pluralize(rel.right.toLowerCase());
|
|
949
|
+
paths[`/api/${lp}/{id}/${rp}`] = { get: { summary: `${rel.right}s for ${rel.left}`, responses: { '200': { description: `List of ${rp}` } } } };
|
|
950
|
+
paths[`/api/${rp}/{id}/${lp}`] = { get: { summary: `${rel.left}s for ${rel.right}`, responses: { '200': { description: `List of ${lp}` } } } };
|
|
951
|
+
paths[`/api/${rel.junction.toLowerCase()}`] = { post: { summary: `Link ${rel.left}↔${rel.right}`, responses: { '201': { description: 'Linked' } } }, delete: { summary: `Unlink ${rel.left}↔${rel.right}`, responses: { '200': { description: 'Unlinked' } } } };
|
|
853
952
|
}
|
|
854
953
|
paths['/api/health'] = { get: { summary: 'Health check', responses: { '200': { description: 'Status' } } } };
|
|
855
954
|
paths['/api/upload'] = { post: { summary: 'Upload file', responses: { '200': { description: 'File info' } } } };
|
|
955
|
+
paths['/docs'] = { get: { summary: 'Swagger UI documentation', responses: { '200': { description: 'HTML page' } } } };
|
|
856
956
|
|
|
857
957
|
const spec = {
|
|
858
958
|
openapi: '3.0.3',
|
|
@@ -991,6 +1091,373 @@ function apiClientCode(entities, intents, port) {
|
|
|
991
1091
|
return lines.join('\n') + '\n';
|
|
992
1092
|
}
|
|
993
1093
|
|
|
1094
|
+
// ── Input Validation (A4) ──────────────────────────────────
|
|
1095
|
+
|
|
1096
|
+
function validationBlock(entity) {
|
|
1097
|
+
const lines = [];
|
|
1098
|
+
lines.push(`fn validate${entity.name}(any body):`);
|
|
1099
|
+
lines.push(` list errors = []`);
|
|
1100
|
+
for (const f of entity.fields) {
|
|
1101
|
+
if (f.type === 'auto') continue;
|
|
1102
|
+
if (f.modifiers.includes('required')) {
|
|
1103
|
+
lines.push(` if not body.${f.name}:`);
|
|
1104
|
+
lines.push(` errors.push("${f.name} is required")`);
|
|
1105
|
+
}
|
|
1106
|
+
if (f.modifiers.includes('email')) {
|
|
1107
|
+
lines.push(` if body.${f.name} and not body.${f.name}.includes("@"):`);
|
|
1108
|
+
lines.push(` errors.push("${f.name} must be a valid email")`);
|
|
1109
|
+
}
|
|
1110
|
+
const minMod = f.modifiers.find(m => m.startsWith('min('));
|
|
1111
|
+
if (minMod) {
|
|
1112
|
+
const minVal = minMod.match(/\d+/)[0];
|
|
1113
|
+
if (f.type === 'str') {
|
|
1114
|
+
lines.push(` if body.${f.name} and body.${f.name}.length < ${minVal}:`);
|
|
1115
|
+
lines.push(` errors.push("${f.name} must be at least ${minVal} characters")`);
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
const maxMod = f.modifiers.find(m => m.startsWith('max('));
|
|
1119
|
+
if (maxMod) {
|
|
1120
|
+
const maxVal = maxMod.match(/\d+/)[0];
|
|
1121
|
+
if (f.type === 'str') {
|
|
1122
|
+
lines.push(` if body.${f.name} and body.${f.name}.length > ${maxVal}:`);
|
|
1123
|
+
lines.push(` errors.push("${f.name} must be at most ${maxVal} characters")`);
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
if (ENUM_VALUES[f.name]) {
|
|
1127
|
+
const vals = ENUM_VALUES[f.name].map(v => `"${v}"`).join(', ');
|
|
1128
|
+
lines.push(` if body.${f.name} and not [${vals}].includes(body.${f.name}):`);
|
|
1129
|
+
lines.push(` errors.push("${f.name} must be one of: ${ENUM_VALUES[f.name].join(', ')}")`);
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
lines.push(` ret errors`);
|
|
1133
|
+
return lines.join('\n');
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
function validatedCreateRoute(entity) {
|
|
1137
|
+
const lower = entity.name.toLowerCase();
|
|
1138
|
+
const plural = pluralize(lower);
|
|
1139
|
+
const store = entity.name + 'Store';
|
|
1140
|
+
return [
|
|
1141
|
+
`post "/api/${plural}" (req, res):`,
|
|
1142
|
+
` list errors = validate${entity.name}(req.body)`,
|
|
1143
|
+
` if errors.length > 0:`,
|
|
1144
|
+
` ret.status(400) {ok: false, errors}`,
|
|
1145
|
+
` any created = ${store}.create(req.body)`,
|
|
1146
|
+
` ret.status(201) ok(created, null)`,
|
|
1147
|
+
``,
|
|
1148
|
+
`put "/api/${plural}/:id" (req, res):`,
|
|
1149
|
+
` list errors = validate${entity.name}(req.body)`,
|
|
1150
|
+
` if errors.length > 0:`,
|
|
1151
|
+
` ret.status(400) {ok: false, errors}`,
|
|
1152
|
+
` any updated = ${store}.update(req.params.id, req.body)`,
|
|
1153
|
+
` ret ok(updated, null)`,
|
|
1154
|
+
];
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
// ── Swagger UI Route (A5) ──────────────────────────────────
|
|
1158
|
+
|
|
1159
|
+
function swaggerUIRoute() {
|
|
1160
|
+
return [
|
|
1161
|
+
`get "/docs" (req, res):`,
|
|
1162
|
+
` str html = "<!DOCTYPE html><html><head><title>API Docs</title><link rel=stylesheet href=https://unpkg.com/swagger-ui-dist/swagger-ui.css></head><body><div id=swagger-ui></div><script src=https://unpkg.com/swagger-ui-dist/swagger-ui-bundle.js></script><script>SwaggerUIBundle({url:'/api/openapi.json',dom_id:'#swagger-ui'})</script></body></html>"`,
|
|
1163
|
+
` res.type("html")`,
|
|
1164
|
+
` ret html`,
|
|
1165
|
+
];
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
// ── Password Reset Routes (A6) ─────────────────────────────
|
|
1169
|
+
|
|
1170
|
+
function passwordResetRoutes() {
|
|
1171
|
+
return [
|
|
1172
|
+
`post "/api/auth/forgot-password" (req, res):`,
|
|
1173
|
+
` any user = UserStore.findBy("email", req.body.email)`,
|
|
1174
|
+
` if not user:`,
|
|
1175
|
+
` ret ok({message: "If the email exists, a reset link has been sent"}, null)`,
|
|
1176
|
+
` str resetToken = crypto.randomUUID()`,
|
|
1177
|
+
` UserStore.update(user.id, {resetToken})`,
|
|
1178
|
+
` log("Password reset requested for: " + req.body.email)`,
|
|
1179
|
+
` ret ok({message: "If the email exists, a reset link has been sent"}, null)`,
|
|
1180
|
+
``,
|
|
1181
|
+
`post "/api/auth/reset-password" (req, res):`,
|
|
1182
|
+
` if not req.body.token or not req.body.password:`,
|
|
1183
|
+
` ret.status(400) err("Token and password are required", 400)`,
|
|
1184
|
+
` any user = UserStore.findBy("resetToken", req.body.token)`,
|
|
1185
|
+
` if not user:`,
|
|
1186
|
+
` ret.status(400) err("Invalid or expired token", 400)`,
|
|
1187
|
+
` str hashed = await hash(req.body.password)`,
|
|
1188
|
+
` UserStore.update(user.id, {password: hashed, resetToken: null})`,
|
|
1189
|
+
` ret ok({message: "Password has been reset"}, null)`,
|
|
1190
|
+
``,
|
|
1191
|
+
`post "/api/auth/change-password" (req, res):`,
|
|
1192
|
+
` if not req.body.oldPassword or not req.body.newPassword:`,
|
|
1193
|
+
` ret.status(400) err("Old and new passwords are required", 400)`,
|
|
1194
|
+
` bool valid = await verify(req.body.oldPassword, req.user.password)`,
|
|
1195
|
+
` if not valid:`,
|
|
1196
|
+
` ret.status(401) err("Invalid old password", 401)`,
|
|
1197
|
+
` str hashed = await hash(req.body.newPassword)`,
|
|
1198
|
+
` UserStore.update(req.user.id, {password: hashed})`,
|
|
1199
|
+
` ret ok({message: "Password changed"}, null)`,
|
|
1200
|
+
];
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
// ── WebSocket Rooms (B11) ──────────────────────────────────
|
|
1204
|
+
|
|
1205
|
+
function wsRoomsBlock() {
|
|
1206
|
+
return [
|
|
1207
|
+
`ws "/ws":`,
|
|
1208
|
+
` on "connection" (socket):`,
|
|
1209
|
+
` socket.room = "general"`,
|
|
1210
|
+
` log("Client connected to room: general")`,
|
|
1211
|
+
` on "message" (data, socket):`,
|
|
1212
|
+
` any msg = JSON.parse(data)`,
|
|
1213
|
+
` if msg.type == "join":`,
|
|
1214
|
+
` socket.room = msg.room`,
|
|
1215
|
+
` socket.send(JSON.stringify({type: "joined", room: msg.room}))`,
|
|
1216
|
+
` if msg.type == "leave":`,
|
|
1217
|
+
` socket.room = "general"`,
|
|
1218
|
+
` if msg.type == "message":`,
|
|
1219
|
+
` broadcast({type: "message", room: socket.room, data: msg.data, sender: msg.sender}, socket.room)`,
|
|
1220
|
+
` if msg.type == "broadcast":`,
|
|
1221
|
+
` broadcast(msg.data)`,
|
|
1222
|
+
` on "close" (socket):`,
|
|
1223
|
+
` log("Client disconnected")`,
|
|
1224
|
+
];
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
// ── Rate Limiting (B12) ────────────────────────────────────
|
|
1228
|
+
|
|
1229
|
+
function rateLimitBlock() {
|
|
1230
|
+
return `fn rateLimit(int windowMs, int maxReqs):
|
|
1231
|
+
map hits = {}
|
|
1232
|
+
ret fn(req, res, next):
|
|
1233
|
+
str key = req.ip
|
|
1234
|
+
int now = Date.now()
|
|
1235
|
+
if not hits[key] or now - hits[key].start > windowMs:
|
|
1236
|
+
hits[key] = {count: 1, start: now}
|
|
1237
|
+
else:
|
|
1238
|
+
hits[key].count = hits[key].count + 1
|
|
1239
|
+
if hits[key].count > maxReqs:
|
|
1240
|
+
ret.status(429) {error: "Too many requests"}
|
|
1241
|
+
next()`;
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
// ── Logging Middleware (C17) ───────────────────────────────
|
|
1245
|
+
|
|
1246
|
+
function loggingBlock() {
|
|
1247
|
+
return `fn requestLogger(req, res, next):
|
|
1248
|
+
int startTime = Date.now()
|
|
1249
|
+
log("[" + req.method + "] " + req.path)
|
|
1250
|
+
next()`;
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
// ── Caching Headers (C18) ─────────────────────────────────
|
|
1254
|
+
|
|
1255
|
+
function cachingRoute() {
|
|
1256
|
+
return [
|
|
1257
|
+
`get "/api/cache-headers" (req, res):`,
|
|
1258
|
+
` res.set("Cache-Control", "public, max-age=60")`,
|
|
1259
|
+
` ret {cached: true, ttl: 60}`,
|
|
1260
|
+
];
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
// ── Frontend SPA (S2) ─────────────────────────────────────
|
|
1264
|
+
|
|
1265
|
+
function frontendSPA(entities, intents, port) {
|
|
1266
|
+
const L = [];
|
|
1267
|
+
L.push('<!DOCTYPE html>');
|
|
1268
|
+
L.push('<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">');
|
|
1269
|
+
L.push('<title>App</title>');
|
|
1270
|
+
L.push(`<style>
|
|
1271
|
+
:root{--bg:#f8fafc;--card:#fff;--primary:#2563eb;--danger:#dc2626;--text:#1e293b;--muted:#64748b;--border:#e2e8f0;--radius:8px}
|
|
1272
|
+
*{margin:0;padding:0;box-sizing:border-box}body{font-family:system-ui,-apple-system,sans-serif;background:var(--bg);color:var(--text);line-height:1.6}
|
|
1273
|
+
nav{background:var(--primary);color:#fff;padding:1rem 2rem;display:flex;gap:1rem;align-items:center;box-shadow:0 2px 4px rgba(0,0,0,.1)}
|
|
1274
|
+
nav a{color:#fff;text-decoration:none;padding:.4rem 1rem;border-radius:var(--radius);opacity:.8;cursor:pointer}nav a:hover,nav a.active{opacity:1;background:rgba(255,255,255,.15)}
|
|
1275
|
+
.container{max-width:1200px;margin:2rem auto;padding:0 1rem}
|
|
1276
|
+
.card{background:var(--card);border:1px solid var(--border);border-radius:var(--radius);padding:1.5rem;margin-bottom:1rem;box-shadow:0 1px 3px rgba(0,0,0,.05)}
|
|
1277
|
+
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:1rem}
|
|
1278
|
+
.form-group{margin-bottom:1rem}
|
|
1279
|
+
.form-group label{display:block;font-weight:500;margin-bottom:.25rem;font-size:.875rem}
|
|
1280
|
+
.form-group input,.form-group textarea,.form-group select{width:100%;padding:.5rem .75rem;border:1px solid var(--border);border-radius:var(--radius);font-size:.875rem}
|
|
1281
|
+
.form-group textarea{min-height:80px;resize:vertical}
|
|
1282
|
+
.btn{padding:.5rem 1rem;border:none;border-radius:var(--radius);cursor:pointer;font-size:.875rem;font-weight:500;transition:opacity .2s}
|
|
1283
|
+
.btn-primary{background:var(--primary);color:#fff}.btn-danger{background:var(--danger);color:#fff}
|
|
1284
|
+
.btn:hover{opacity:.85}.btn-sm{padding:.25rem .5rem;font-size:.75rem}
|
|
1285
|
+
table{width:100%;border-collapse:collapse}th,td{padding:.75rem;text-align:left;border-bottom:1px solid var(--border)}
|
|
1286
|
+
th{background:var(--bg);font-weight:600;font-size:.875rem;color:var(--muted)}
|
|
1287
|
+
.search{display:flex;gap:.5rem;margin-bottom:1rem}
|
|
1288
|
+
.search input{flex:1;padding:.5rem .75rem;border:1px solid var(--border);border-radius:var(--radius)}
|
|
1289
|
+
.pagination{display:flex;gap:.5rem;justify-content:center;margin-top:1rem;align-items:center}
|
|
1290
|
+
.toast{position:fixed;top:1rem;right:1rem;background:#22c55e;color:#fff;padding:.75rem 1.5rem;border-radius:var(--radius);opacity:0;transition:opacity .3s;z-index:999}
|
|
1291
|
+
.toast.show{opacity:1}.modal-bg{position:fixed;inset:0;background:rgba(0,0,0,.4);display:flex;align-items:center;justify-content:center;z-index:100}
|
|
1292
|
+
.modal{background:var(--card);border-radius:var(--radius);padding:2rem;width:90%;max-width:500px;box-shadow:0 20px 60px rgba(0,0,0,.2)}
|
|
1293
|
+
.modal h3{margin-bottom:1rem}.modal-actions{display:flex;gap:.5rem;justify-content:flex-end;margin-top:1.5rem}
|
|
1294
|
+
.badge{display:inline-block;padding:.15rem .5rem;border-radius:99px;font-size:.75rem;font-weight:500;background:var(--border)}
|
|
1295
|
+
.hidden{display:none}
|
|
1296
|
+
</style></head><body>`);
|
|
1297
|
+
L.push('<div id="toast" class="toast"></div>');
|
|
1298
|
+
const hasAuth = intents.has('auth');
|
|
1299
|
+
const navLinks = entities.map(e => `<a onclick="showSection('${e.name.toLowerCase()}')" id="nav-${e.name.toLowerCase()}">${pluralize(e.name)}</a>`).join('');
|
|
1300
|
+
const authNav = hasAuth ? `<a onclick="showSection('auth')" id="nav-auth">Login</a><span id="user-info" class="hidden" style="margin-left:auto;font-size:.875rem"></span><a id="logout-btn" class="hidden" onclick="doLogout()">Logout</a>` : '';
|
|
1301
|
+
L.push(`<nav><strong style="font-size:1.25rem">App</strong>${navLinks}${authNav}</nav>`);
|
|
1302
|
+
L.push('<div class="container">');
|
|
1303
|
+
if (hasAuth) {
|
|
1304
|
+
L.push(`<div id="section-auth"><div class="card" style="max-width:400px;margin:2rem auto"><h2 id="auth-title">Login</h2>`);
|
|
1305
|
+
L.push(`<div class="form-group"><label>Email</label><input type="email" id="auth-email" placeholder="email@example.com"></div>`);
|
|
1306
|
+
L.push(`<div class="form-group"><label>Password</label><input type="password" id="auth-password" placeholder="password"></div>`);
|
|
1307
|
+
L.push(`<div id="auth-name-group" class="form-group hidden"><label>Name</label><input type="text" id="auth-name" placeholder="Your name"></div>`);
|
|
1308
|
+
L.push(`<button class="btn btn-primary" style="width:100%" onclick="doAuth()">Login</button>`);
|
|
1309
|
+
L.push(`<p style="text-align:center;margin-top:1rem;font-size:.875rem">Don't have an account? <a onclick="toggleAuthMode()" style="color:var(--primary);cursor:pointer">Register</a></p>`);
|
|
1310
|
+
L.push(`</div></div>`);
|
|
1311
|
+
}
|
|
1312
|
+
for (const entity of entities) {
|
|
1313
|
+
const lower = entity.name.toLowerCase();
|
|
1314
|
+
const plural = pluralize(lower);
|
|
1315
|
+
const fields = entity.fields.filter(f => f.type !== 'auto');
|
|
1316
|
+
L.push(`<div id="section-${lower}" class="hidden">`);
|
|
1317
|
+
L.push(`<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem"><h2>${pluralize(entity.name)}</h2><button class="btn btn-primary" onclick="${lower}ShowModal()">+ New ${entity.name}</button></div>`);
|
|
1318
|
+
L.push(`<div class="search"><input id="${lower}-search" placeholder="Search ${plural}..." oninput="${lower}Load()"><select id="${lower}-sort" onchange="${lower}Load()">`);
|
|
1319
|
+
for (const f of entity.fields) L.push(`<option value="${f.name}">${f.name}</option>`);
|
|
1320
|
+
L.push(`</select><select id="${lower}-order" onchange="${lower}Load()"><option value="desc">Newest</option><option value="asc">Oldest</option></select></div>`);
|
|
1321
|
+
L.push(`<div class="card"><table><thead><tr>${entity.fields.map(f => `<th>${f.name}</th>`).join('')}<th>Actions</th></tr></thead><tbody id="${lower}-tbody"></tbody></table>`);
|
|
1322
|
+
L.push(`<div class="pagination" id="${lower}-pager"></div></div>`);
|
|
1323
|
+
L.push(`<div id="${lower}-modal" class="modal-bg hidden" onclick="if(event.target===this)this.classList.add('hidden')"><div class="modal"><h3 id="${lower}-modal-title">New ${entity.name}</h3>`);
|
|
1324
|
+
L.push(`<input type="hidden" id="${lower}-edit-id">`);
|
|
1325
|
+
for (const f of fields) {
|
|
1326
|
+
if (f.type === 'bool') {
|
|
1327
|
+
L.push(`<div class="form-group"><label><input type="checkbox" id="${lower}-f-${f.name}"> ${f.name}</label></div>`);
|
|
1328
|
+
} else {
|
|
1329
|
+
const inputType = f.type === 'int' ? 'number' : f.name === 'password' ? 'password' : f.name === 'email' ? 'email' : 'text';
|
|
1330
|
+
const tag = f.name === 'body' || f.name === 'content' || f.name === 'description' ? 'textarea' : 'input';
|
|
1331
|
+
L.push(`<div class="form-group"><label>${f.name}${f.modifiers.includes('required') ? ' *' : ''}</label><${tag} ${tag === 'input' ? `type="${inputType}" ` : ''}id="${lower}-f-${f.name}" placeholder="${f.name}"></${tag}></div>`);
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
L.push(`<div class="modal-actions"><button class="btn" onclick="document.getElementById('${lower}-modal').classList.add('hidden')">Cancel</button><button class="btn btn-primary" onclick="${lower}Save()">Save</button></div></div></div>`);
|
|
1335
|
+
L.push('</div>');
|
|
1336
|
+
}
|
|
1337
|
+
L.push('</div>');
|
|
1338
|
+
L.push('<script>');
|
|
1339
|
+
L.push(`const API="";let currentPage={};let _token=null;let _isRegister=false;`);
|
|
1340
|
+
L.push(`function authHeaders(){const h={"Content-Type":"application/json"};if(_token)h.Authorization="Bearer "+_token;return h}`);
|
|
1341
|
+
L.push(`function toast(m){const t=document.getElementById("toast");t.textContent=m;t.classList.add("show");setTimeout(()=>t.classList.remove("show"),2500)}`);
|
|
1342
|
+
L.push(`function showSection(name){document.querySelectorAll('[id^="section-"]').forEach(s=>s.classList.add("hidden"));const el=document.getElementById("section-"+name);if(el)el.classList.remove("hidden");document.querySelectorAll("nav a").forEach(a=>a.classList.remove("active"));const n=document.getElementById("nav-"+name);if(n)n.classList.add("active");if(window[name+"Load"])window[name+"Load"]()}`);
|
|
1343
|
+
if (hasAuth) {
|
|
1344
|
+
L.push(`function toggleAuthMode(){_isRegister=!_isRegister;document.getElementById("auth-title").textContent=_isRegister?"Register":"Login";document.getElementById("auth-name-group").classList.toggle("hidden",!_isRegister)}`);
|
|
1345
|
+
L.push(`async function doAuth(){const email=document.getElementById("auth-email").value;const password=document.getElementById("auth-password").value;const url=_isRegister?"/api/auth/register":"/api/auth/login";const body=_isRegister?{name:document.getElementById("auth-name").value,email,password}:{email,password};try{const r=await fetch(API+url,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(body)});const d=await r.json();if(d.token){_token=d.token;document.getElementById("user-info").textContent="Hi, "+(d.user?.name||email);document.getElementById("user-info").classList.remove("hidden");document.getElementById("logout-btn").classList.remove("hidden");document.getElementById("nav-auth").classList.add("hidden");toast(_isRegister?"Registered!":"Logged in!");showSection("${entities[0].name.toLowerCase()}")}else{toast(d.error||"Auth failed")}}catch(e){toast("Error: "+e.message)}}`);
|
|
1346
|
+
L.push(`function doLogout(){_token=null;document.getElementById("user-info").classList.add("hidden");document.getElementById("logout-btn").classList.add("hidden");document.getElementById("nav-auth").classList.remove("hidden");toast("Logged out");showSection("auth")}`);
|
|
1347
|
+
}
|
|
1348
|
+
for (const entity of entities) {
|
|
1349
|
+
const lower = entity.name.toLowerCase();
|
|
1350
|
+
const plural = pluralize(lower);
|
|
1351
|
+
const fields = entity.fields.filter(f => f.type !== 'auto');
|
|
1352
|
+
L.push(`async function ${lower}Load(page){page=page||1;currentPage["${lower}"]=page;const s=document.getElementById("${lower}-search").value;const sort=document.getElementById("${lower}-sort").value;const ord=document.getElementById("${lower}-order").value;`);
|
|
1353
|
+
L.push(`const r=await fetch(API+"/api/${plural}?page="+page+"&limit=10&search="+encodeURIComponent(s)+"&sort="+sort+"&order="+ord,{headers:authHeaders()});const d=await r.json();const items=d.items||d;`);
|
|
1354
|
+
L.push(`document.getElementById("${lower}-tbody").innerHTML=items.map(i=>"<tr>${entity.fields.map(f => `<td>"+(i.${f.name}!=null?i.${f.name}:"")+"</td>`).join('')}<td><button class='btn btn-sm btn-primary' onclick='${lower}Edit("+i.id+")'>Edit</button> <button class='btn btn-sm btn-danger' onclick='${lower}Del("+i.id+")'>Del</button></td></tr>").join("");`);
|
|
1355
|
+
L.push(`const tp=Math.ceil((d.total||items.length)/10);let pg="";for(let p=1;p<=tp;p++)pg+="<button class='btn btn-sm "+(p===page?"btn-primary":"")+"' onclick='${lower}Load("+p+")'>"+p+"</button>";document.getElementById("${lower}-pager").innerHTML=pg}`);
|
|
1356
|
+
L.push(`function ${lower}ShowModal(id){document.getElementById("${lower}-edit-id").value=id||"";document.getElementById("${lower}-modal-title").textContent=id?"Edit ${entity.name}":"New ${entity.name}";${fields.map(f => f.type === 'bool' ? `document.getElementById("${lower}-f-${f.name}").checked=false` : `document.getElementById("${lower}-f-${f.name}").value=""`).join(';')};document.getElementById("${lower}-modal").classList.remove("hidden")}`);
|
|
1357
|
+
L.push(`async function ${lower}Edit(id){const r=await fetch(API+"/api/${plural}/"+id,{headers:authHeaders()});const d=await r.json();const i=d.data||d;${lower}ShowModal(id);${fields.map(f => f.type === 'bool' ? `document.getElementById("${lower}-f-${f.name}").checked=!!i.${f.name}` : `document.getElementById("${lower}-f-${f.name}").value=i.${f.name}||""`).join(';')}}`);
|
|
1358
|
+
L.push(`async function ${lower}Save(){const id=document.getElementById("${lower}-edit-id").value;const body={${fields.map(f => {
|
|
1359
|
+
if (f.type === 'bool') return `${f.name}:document.getElementById("${lower}-f-${f.name}").checked`;
|
|
1360
|
+
if (f.type === 'int') return `${f.name}:+document.getElementById("${lower}-f-${f.name}").value`;
|
|
1361
|
+
return `${f.name}:document.getElementById("${lower}-f-${f.name}").value`;
|
|
1362
|
+
}).join(',')}};`);
|
|
1363
|
+
L.push(`const method=id?"PUT":"POST";const url=API+"/api/${plural}"+(id?"/"+id:"");await fetch(url,{method,headers:authHeaders(),body:JSON.stringify(body)});document.getElementById("${lower}-modal").classList.add("hidden");toast(id?"Updated!":"Created!");${lower}Load()}`);
|
|
1364
|
+
L.push(`async function ${lower}Del(id){if(!confirm("Delete this ${lower}?"))return;await fetch(API+"/api/${plural}/"+id,{method:"DELETE",headers:authHeaders()});toast("Deleted!");${lower}Load()}`);
|
|
1365
|
+
}
|
|
1366
|
+
const initSection = hasAuth ? 'auth' : entities[0].name.toLowerCase();
|
|
1367
|
+
L.push(`showSection("${initSection}")`);
|
|
1368
|
+
L.push('</script></body></html>');
|
|
1369
|
+
return L.join('\n');
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
// ── Docker Compose (B14) ──────────────────────────────────
|
|
1373
|
+
|
|
1374
|
+
function dockerCompose(projectName, port, intents) {
|
|
1375
|
+
const lines = ['version: "3.8"', '', 'services:'];
|
|
1376
|
+
lines.push(` app:`);
|
|
1377
|
+
lines.push(` build: .`);
|
|
1378
|
+
lines.push(` ports:`);
|
|
1379
|
+
lines.push(` - "${port}:${port}"`);
|
|
1380
|
+
lines.push(` env_file: .env`);
|
|
1381
|
+
lines.push(` volumes:`);
|
|
1382
|
+
lines.push(` - app-data:/app/data`);
|
|
1383
|
+
if (intents.has('database')) {
|
|
1384
|
+
lines.push(` depends_on:`);
|
|
1385
|
+
lines.push(` - db`);
|
|
1386
|
+
lines.push('');
|
|
1387
|
+
lines.push(` db:`);
|
|
1388
|
+
lines.push(` image: postgres:16-alpine`);
|
|
1389
|
+
lines.push(` environment:`);
|
|
1390
|
+
lines.push(` POSTGRES_DB: ${projectName.replace(/-/g, '_')}`);
|
|
1391
|
+
lines.push(` POSTGRES_USER: app`);
|
|
1392
|
+
lines.push(` POSTGRES_PASSWORD: changeme`);
|
|
1393
|
+
lines.push(` ports:`);
|
|
1394
|
+
lines.push(` - "5432:5432"`);
|
|
1395
|
+
lines.push(` volumes:`);
|
|
1396
|
+
lines.push(` - db-data:/var/lib/postgresql/data`);
|
|
1397
|
+
}
|
|
1398
|
+
lines.push('');
|
|
1399
|
+
lines.push('volumes:');
|
|
1400
|
+
lines.push(' app-data:');
|
|
1401
|
+
if (intents.has('database')) lines.push(' db-data:');
|
|
1402
|
+
return lines.join('\n') + '\n';
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
// ── GitHub Actions CI (C16) ────────────────────────────────
|
|
1406
|
+
|
|
1407
|
+
function githubActionsCI(projectName) {
|
|
1408
|
+
return `name: CI
|
|
1409
|
+
|
|
1410
|
+
on:
|
|
1411
|
+
push:
|
|
1412
|
+
branches: [main]
|
|
1413
|
+
pull_request:
|
|
1414
|
+
branches: [main]
|
|
1415
|
+
|
|
1416
|
+
jobs:
|
|
1417
|
+
test:
|
|
1418
|
+
runs-on: ubuntu-latest
|
|
1419
|
+
strategy:
|
|
1420
|
+
matrix:
|
|
1421
|
+
node-version: [20, 22]
|
|
1422
|
+
steps:
|
|
1423
|
+
- uses: actions/checkout@v4
|
|
1424
|
+
- uses: actions/setup-node@v4
|
|
1425
|
+
with:
|
|
1426
|
+
node-version: \${{ matrix.node-version }}
|
|
1427
|
+
cache: npm
|
|
1428
|
+
- run: npm ci
|
|
1429
|
+
- run: npm run build
|
|
1430
|
+
- run: npm test
|
|
1431
|
+
|
|
1432
|
+
lint:
|
|
1433
|
+
runs-on: ubuntu-latest
|
|
1434
|
+
steps:
|
|
1435
|
+
- uses: actions/checkout@v4
|
|
1436
|
+
- uses: actions/setup-node@v4
|
|
1437
|
+
with:
|
|
1438
|
+
node-version: 22
|
|
1439
|
+
cache: npm
|
|
1440
|
+
- run: npm ci
|
|
1441
|
+
- run: npx naide app.naide --check
|
|
1442
|
+
`;
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
// ── Environment Files (C15) ───────────────────────────────
|
|
1446
|
+
|
|
1447
|
+
function envDevelopment(port, intents) {
|
|
1448
|
+
const lines = ['NODE_ENV=development', `PORT=${port}`, 'LOG_LEVEL=debug'];
|
|
1449
|
+
if (intents.has('auth')) lines.push('JWT_SECRET=dev-secret-do-not-use-in-prod');
|
|
1450
|
+
if (intents.has('database')) lines.push('DATABASE_URL=postgres://app:changeme@localhost:5432/app_dev');
|
|
1451
|
+
return lines.join('\n') + '\n';
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
function envProduction(port, intents) {
|
|
1455
|
+
const lines = ['NODE_ENV=production', `PORT=${port}`, 'LOG_LEVEL=warn'];
|
|
1456
|
+
if (intents.has('auth')) lines.push('JWT_SECRET=CHANGE_ME_TO_RANDOM_SECRET');
|
|
1457
|
+
if (intents.has('database')) lines.push('DATABASE_URL=postgres://user:pass@db-host:5432/app_prod');
|
|
1458
|
+
return lines.join('\n') + '\n';
|
|
1459
|
+
}
|
|
1460
|
+
|
|
994
1461
|
// ── Auth Route Templates ────────────────────────────────────
|
|
995
1462
|
|
|
996
1463
|
function authRoutesBlock(userEntity) {
|
|
@@ -1058,6 +1525,19 @@ function compose(intents, params) {
|
|
|
1058
1525
|
const utils = utilityFunctions(intents);
|
|
1059
1526
|
if (utils.length > 0) sections.push(...utils);
|
|
1060
1527
|
|
|
1528
|
+
// Rate limiting & logging middleware (B12, C17)
|
|
1529
|
+
if (intents.has('server')) {
|
|
1530
|
+
sections.push(rateLimitBlock());
|
|
1531
|
+
sections.push(loggingBlock());
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
// Input validation functions (A4)
|
|
1535
|
+
if (intents.has('schema') && intents.has('crud')) {
|
|
1536
|
+
for (const entity of entities) {
|
|
1537
|
+
sections.push(validationBlock(entity));
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1061
1541
|
// Schema blocks for all entities (add role + deleted fields)
|
|
1062
1542
|
if (intents.has('schema')) {
|
|
1063
1543
|
for (const entity of entities) {
|
|
@@ -1071,6 +1551,12 @@ function compose(intents, params) {
|
|
|
1071
1551
|
sections.push(schemaBlock(entity.name, entity.fields));
|
|
1072
1552
|
}
|
|
1073
1553
|
}
|
|
1554
|
+
// M:N junction table schemas (S3)
|
|
1555
|
+
const m2mRels = detectManyToMany(entities.map(e => e.name));
|
|
1556
|
+
for (const rel of m2mRels) {
|
|
1557
|
+
sections.push(junctionSchemaBlock(rel));
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1074
1560
|
// Audit log schema when auth is present
|
|
1075
1561
|
if (hasAuth) {
|
|
1076
1562
|
sections.push(auditSchema());
|
|
@@ -1099,6 +1585,8 @@ function compose(intents, params) {
|
|
|
1099
1585
|
body.push(' protect "/api/*"');
|
|
1100
1586
|
body.push(' public "/api/auth/*"');
|
|
1101
1587
|
body.push(' public "/api/health"');
|
|
1588
|
+
body.push(' public "/docs"');
|
|
1589
|
+
body.push(' public "/api/openapi.json"');
|
|
1102
1590
|
if (userEntity) {
|
|
1103
1591
|
body.push('');
|
|
1104
1592
|
body.push(...authRoutesBlock(userEntity.name));
|
|
@@ -1114,6 +1602,12 @@ function compose(intents, params) {
|
|
|
1114
1602
|
body.push(' ret {status: "ok"}');
|
|
1115
1603
|
}
|
|
1116
1604
|
}
|
|
1605
|
+
|
|
1606
|
+
// Password reset routes (A6)
|
|
1607
|
+
if (userEntity) {
|
|
1608
|
+
body.push('');
|
|
1609
|
+
body.push(...passwordResetRoutes());
|
|
1610
|
+
}
|
|
1117
1611
|
}
|
|
1118
1612
|
|
|
1119
1613
|
// Paginated list endpoints (override default CRUD list)
|
|
@@ -1125,21 +1619,30 @@ function compose(intents, params) {
|
|
|
1125
1619
|
}
|
|
1126
1620
|
}
|
|
1127
1621
|
|
|
1622
|
+
// Validated create/update routes (A4)
|
|
1623
|
+
if (intents.has('crud') && intents.has('schema')) {
|
|
1624
|
+
for (const entity of entities) {
|
|
1625
|
+
body.push(...validatedCreateRoute(entity));
|
|
1626
|
+
body.push('');
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1128
1630
|
// Nested routes for relationships
|
|
1129
1631
|
for (const rel of relationships) {
|
|
1130
1632
|
body.push(...nestedRouteBlock(rel.parent, rel.child, rel.fk));
|
|
1131
1633
|
body.push('');
|
|
1132
1634
|
}
|
|
1133
1635
|
|
|
1134
|
-
//
|
|
1636
|
+
// M:N routes (S3)
|
|
1637
|
+
const m2mRels = detectManyToMany(entities.map(e => e.name));
|
|
1638
|
+
for (const rel of m2mRels) {
|
|
1639
|
+
body.push(...manyToManyRoutes(rel));
|
|
1640
|
+
body.push('');
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
// WebSocket with rooms (B11)
|
|
1135
1644
|
if (intents.has('websocket')) {
|
|
1136
|
-
body.push(
|
|
1137
|
-
body.push(' on "connection" (socket):');
|
|
1138
|
-
body.push(' log("Client connected")');
|
|
1139
|
-
body.push(' on "message" (data, socket):');
|
|
1140
|
-
body.push(' socket.send(data)');
|
|
1141
|
-
body.push(' on "close" (socket):');
|
|
1142
|
-
body.push(' log("Client disconnected")');
|
|
1645
|
+
body.push(...wsRoomsBlock());
|
|
1143
1646
|
body.push('');
|
|
1144
1647
|
}
|
|
1145
1648
|
|
|
@@ -1175,6 +1678,14 @@ function compose(intents, params) {
|
|
|
1175
1678
|
body.push('');
|
|
1176
1679
|
}
|
|
1177
1680
|
|
|
1681
|
+
// Swagger UI (A5)
|
|
1682
|
+
body.push(...swaggerUIRoute());
|
|
1683
|
+
body.push('');
|
|
1684
|
+
|
|
1685
|
+
// Cache control (C18)
|
|
1686
|
+
body.push(...cachingRoute());
|
|
1687
|
+
body.push('');
|
|
1688
|
+
|
|
1178
1689
|
// Health check
|
|
1179
1690
|
body.push('get "/api/health" (req, res):');
|
|
1180
1691
|
if (intents.has('schema') && intents.has('crud') && entities.length > 0) {
|
|
@@ -1416,40 +1927,33 @@ CMD ["node", "dist/app.mjs"]
|
|
|
1416
1927
|
if (intents.has('server')) {
|
|
1417
1928
|
readmeLines.push(`## API Endpoints\n`);
|
|
1418
1929
|
readmeLines.push(`- \`GET /api/health\` — Health check`);
|
|
1930
|
+
readmeLines.push(`- \`GET /docs\` — Swagger UI`);
|
|
1419
1931
|
for (const e of entities) {
|
|
1420
1932
|
const ep = pluralize(e.toLowerCase());
|
|
1421
|
-
readmeLines.push(`- \`GET /api/${ep}?page=1&limit=20&search
|
|
1422
|
-
readmeLines.push(`- \`POST /api/${ep}\` — Create
|
|
1423
|
-
readmeLines.push(`- \`GET /api/${ep}/:id\` — Get
|
|
1424
|
-
readmeLines.push(`- \`
|
|
1425
|
-
readmeLines.push(`- \`
|
|
1426
|
-
readmeLines.push(`- \`DELETE /api/${ep}/:id/soft\` — Soft delete ${e.toLowerCase()}`);
|
|
1427
|
-
readmeLines.push(`- \`PUT /api/${ep}/:id/restore\` — Restore ${e.toLowerCase()}`);
|
|
1428
|
-
readmeLines.push(`- \`POST /api/${ep}/batch\` — Batch create ${ep}`);
|
|
1429
|
-
readmeLines.push(`- \`DELETE /api/${ep}/batch\` — Batch delete ${ep}`);
|
|
1933
|
+
readmeLines.push(`- \`GET /api/${ep}?page=1&limit=20&search=&sort=id&order=desc\` — List (paginated, sortable, filterable)`);
|
|
1934
|
+
readmeLines.push(`- \`POST /api/${ep}\` — Create (validated)`, `- \`PUT /api/${ep}/:id\` — Update (validated)`);
|
|
1935
|
+
readmeLines.push(`- \`GET /api/${ep}/:id\` — Get`, `- \`DELETE /api/${ep}/:id\` — Delete`);
|
|
1936
|
+
readmeLines.push(`- \`DELETE /api/${ep}/:id/soft\` — Soft delete`, `- \`PUT /api/${ep}/:id/restore\` — Restore`);
|
|
1937
|
+
readmeLines.push(`- \`POST /api/${ep}/batch\` — Batch create`, `- \`DELETE /api/${ep}/batch\` — Batch delete`);
|
|
1430
1938
|
}
|
|
1431
1939
|
for (const r of result.analysis.relationships) {
|
|
1432
|
-
|
|
1433
|
-
const childPlural = pluralize(r.child.toLowerCase());
|
|
1434
|
-
readmeLines.push(`- \`GET /api/${parentPlural}/:id/${childPlural}\` — ${r.child}s by ${r.parent}`);
|
|
1940
|
+
readmeLines.push(`- \`GET /api/${pluralize(r.parent.toLowerCase())}/:id/${pluralize(r.child.toLowerCase())}\` — ${r.child}s by ${r.parent}`);
|
|
1435
1941
|
}
|
|
1436
|
-
readmeLines.push(`- \`POST /api/upload\` — File upload`);
|
|
1437
|
-
readmeLines.push(`- \`POST /api/webhooks\` — Register webhook`);
|
|
1438
|
-
readmeLines.push(`- \`GET /api/webhooks/health\` — Webhook health`);
|
|
1942
|
+
readmeLines.push(`- \`POST /api/upload\` — File upload`, `- \`POST /api/webhooks\` — Webhook`);
|
|
1439
1943
|
if (intents.has('auth')) {
|
|
1440
|
-
readmeLines.push(`- \`POST /api/auth/register\` — Register`);
|
|
1441
|
-
readmeLines.push(`- \`POST /api/auth/
|
|
1442
|
-
readmeLines.push(`- \`
|
|
1443
|
-
readmeLines.push(`- \`GET /api/admin/stats\` — Admin stats (role: admin)`);
|
|
1944
|
+
readmeLines.push(`- \`POST /api/auth/register\` — Register`, `- \`POST /api/auth/login\` — Login`, `- \`GET /api/auth/me\` — Current user`);
|
|
1945
|
+
readmeLines.push(`- \`POST /api/auth/forgot-password\` — Forgot password`, `- \`POST /api/auth/reset-password\` — Reset password`);
|
|
1946
|
+
readmeLines.push(`- \`POST /api/auth/change-password\` — Change password`, `- \`GET /api/admin/stats\` — Admin stats`);
|
|
1444
1947
|
}
|
|
1445
1948
|
}
|
|
1446
1949
|
if (intents.has('server')) {
|
|
1447
|
-
readmeLines.push(`\n## Docker\n`);
|
|
1448
|
-
readmeLines.push('```bash');
|
|
1449
|
-
readmeLines.push(`docker build -t ${projectName} .`);
|
|
1450
|
-
readmeLines.push(`docker run -p ${result.port}:${result.port} --env-file .env ${projectName}`);
|
|
1451
|
-
readmeLines.push('```');
|
|
1950
|
+
readmeLines.push(`\n## Docker\n`, '```bash', `docker-compose up -d`, '```');
|
|
1452
1951
|
}
|
|
1952
|
+
readmeLines.push(`\n## Generated Files\n`);
|
|
1953
|
+
readmeLines.push('| File | Purpose |', '|------|---------|');
|
|
1954
|
+
readmeLines.push('| app.naide | Main application |', '| index.html | Frontend SPA |', '| admin.html | Admin dashboard |');
|
|
1955
|
+
readmeLines.push('| client.mjs | API client library |', '| openapi.json | OpenAPI 3.0 spec |');
|
|
1956
|
+
readmeLines.push('| docker-compose.yml | Docker orchestration |', '| .github/workflows/ci.yml | CI/CD pipeline |');
|
|
1453
1957
|
files.push({ path: 'README.md', content: readmeLines.join('\n') + '\n' });
|
|
1454
1958
|
|
|
1455
1959
|
// Build full entity objects for generators
|
|
@@ -1476,6 +1980,23 @@ CMD ["node", "dist/app.mjs"]
|
|
|
1476
1980
|
files.push({ path: 'client.mjs', content: apiClientCode(entityObjs, intents, result.port) });
|
|
1477
1981
|
}
|
|
1478
1982
|
|
|
1983
|
+
// Frontend SPA (S2)
|
|
1984
|
+
if (intents.has('server') && entityObjs.length > 0) {
|
|
1985
|
+
files.push({ path: 'index.html', content: frontendSPA(entityObjs, intents, result.port) });
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
// Docker Compose (B14)
|
|
1989
|
+
if (intents.has('server')) {
|
|
1990
|
+
files.push({ path: 'docker-compose.yml', content: dockerCompose(projectName, result.port, intents) });
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
// Environment files (C15)
|
|
1994
|
+
files.push({ path: '.env.development', content: envDevelopment(result.port, intents) });
|
|
1995
|
+
files.push({ path: '.env.production', content: envProduction(result.port, intents) });
|
|
1996
|
+
|
|
1997
|
+
// GitHub Actions CI (C16)
|
|
1998
|
+
files.push({ path: '.github/workflows/ci.yml', content: githubActionsCI(projectName) });
|
|
1999
|
+
|
|
1479
2000
|
return { ...result, files };
|
|
1480
2001
|
}
|
|
1481
2002
|
|
|
@@ -1487,8 +2008,34 @@ export function updateProject(projectDir, instruction, options = {}) {
|
|
|
1487
2008
|
|
|
1488
2009
|
const existingState = existing ? parseExistingProject(existing) : { entities: [], intents: [], port: 3000 };
|
|
1489
2010
|
const newResult = generate(instruction, options);
|
|
1490
|
-
const newEntities = (newResult.entities || []).filter(e => !existingState.entities.includes(e));
|
|
1491
2011
|
|
|
2012
|
+
// Entity removal detection (B8)
|
|
2013
|
+
const isRemoval = /(?:remove|delete|drop|取り除|削除)\s+/i.test(instruction);
|
|
2014
|
+
let removedEntities = [];
|
|
2015
|
+
if (isRemoval) {
|
|
2016
|
+
const removeTargets = (newResult.entities || []);
|
|
2017
|
+
removedEntities = existingState.entities.filter(e => removeTargets.includes(e));
|
|
2018
|
+
const allEntities = existingState.entities.filter(e => !removeTargets.includes(e));
|
|
2019
|
+
if (allEntities.length === 0) {
|
|
2020
|
+
return { code: '', valid: true, fixed: false, repairs: [], intents: [], entities: [], relationships: [], port: existingState.port,
|
|
2021
|
+
files: [{ path: 'app.naide', content: '' }], analysis: { instruction, entities: [], relationships: [], plan: [`Removed: ${removedEntities.join(', ')}`], added: [] },
|
|
2022
|
+
action: 'removed', removed: removedEntities };
|
|
2023
|
+
}
|
|
2024
|
+
const combinedIntents = existingState.intents.filter(i => i !== 'schema' || allEntities.length > 0);
|
|
2025
|
+
const extras = [];
|
|
2026
|
+
if (combinedIntents.includes('auth')) extras.push('auth');
|
|
2027
|
+
if (combinedIntents.includes('websocket')) extras.push('websocket');
|
|
2028
|
+
if (combinedIntents.includes('database')) extras.push('database');
|
|
2029
|
+
const fullInstruction = 'REST API for ' + allEntities.join(' and ') + (extras.length ? ' with ' + extras.join(' and ') : '');
|
|
2030
|
+
const combined = generate(fullInstruction, options);
|
|
2031
|
+
const result = buildUpdateFiles(projectDir, combined, allEntities, existingState, options);
|
|
2032
|
+
result.action = 'removed';
|
|
2033
|
+
result.removed = removedEntities;
|
|
2034
|
+
result.analysis.plan.unshift(`Removed: ${removedEntities.join(', ')}`);
|
|
2035
|
+
return result;
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
const newEntities = (newResult.entities || []).filter(e => !existingState.entities.includes(e));
|
|
1492
2039
|
const allEntities = [...existingState.entities, ...newEntities];
|
|
1493
2040
|
const combinedIntents = [...new Set([...existingState.intents, ...newResult.intents])];
|
|
1494
2041
|
const extras = [];
|
|
@@ -1498,8 +2045,12 @@ export function updateProject(projectDir, instruction, options = {}) {
|
|
|
1498
2045
|
const fullInstruction = 'REST API for ' + allEntities.join(' and ') + (extras.length ? ' with ' + extras.join(' and ') : '');
|
|
1499
2046
|
|
|
1500
2047
|
const combined = generate(fullInstruction, options);
|
|
1501
|
-
|
|
2048
|
+
return buildUpdateFiles(projectDir, combined, allEntities, existingState, options);
|
|
2049
|
+
}
|
|
1502
2050
|
|
|
2051
|
+
function buildUpdateFiles(projectDir, combined, allEntities, existingState, options) {
|
|
2052
|
+
const existing = _existsFS(projectDir + '/app.naide') ? _readFS(projectDir + '/app.naide', 'utf-8') : '';
|
|
2053
|
+
const finalCode = combined.code;
|
|
1503
2054
|
const files = [];
|
|
1504
2055
|
files.push({ path: 'app.naide', content: finalCode });
|
|
1505
2056
|
|
|
@@ -1514,10 +2065,7 @@ export function updateProject(projectDir, instruction, options = {}) {
|
|
|
1514
2065
|
const deps = {};
|
|
1515
2066
|
if (intents.has('server')) deps.express = '^4.21.0';
|
|
1516
2067
|
if (intents.has('websocket')) deps.ws = '^8.18.0';
|
|
1517
|
-
if (intents.has('auth')) {
|
|
1518
|
-
deps.jsonwebtoken = '^9.0.2';
|
|
1519
|
-
deps.bcryptjs = '^2.4.3';
|
|
1520
|
-
}
|
|
2068
|
+
if (intents.has('auth')) { deps.jsonwebtoken = '^9.0.2'; deps.bcryptjs = '^2.4.3'; }
|
|
1521
2069
|
if (intents.has('mail')) deps.nodemailer = '^6.9.0';
|
|
1522
2070
|
if (entities.length > 0) deps.naider = '^1.21.0';
|
|
1523
2071
|
|
|
@@ -1536,8 +2084,7 @@ export function updateProject(projectDir, instruction, options = {}) {
|
|
|
1536
2084
|
}
|
|
1537
2085
|
files.push({ path: 'package.json', content: JSON.stringify(pkg, null, 2) + '\n' });
|
|
1538
2086
|
|
|
1539
|
-
const envL = [];
|
|
1540
|
-
envL.push(`PORT=${port}`);
|
|
2087
|
+
const envL = [`PORT=${port}`];
|
|
1541
2088
|
if (intents.has('auth')) envL.push('JWT_SECRET=change-me-in-production');
|
|
1542
2089
|
if (intents.has('bot')) envL.push(`${(options.platform || 'DISCORD').toUpperCase()}_TOKEN=your-bot-token`);
|
|
1543
2090
|
if (intents.has('mail')) { envL.push('SMTP_HOST=smtp.example.com'); envL.push('SMTP_USER=user@example.com'); envL.push('SMTP_PASS=password'); }
|
|
@@ -1547,9 +2094,7 @@ export function updateProject(projectDir, instruction, options = {}) {
|
|
|
1547
2094
|
const existingEnv = _readFS(existingEnvPath, 'utf-8');
|
|
1548
2095
|
const existingKeys = new Set(existingEnv.split('\n').map(l => l.split('=')[0]).filter(Boolean));
|
|
1549
2096
|
const newEnvLines = envL.filter(l => !existingKeys.has(l.split('=')[0]));
|
|
1550
|
-
if (newEnvLines.length > 0) {
|
|
1551
|
-
files.push({ path: '.env', content: existingEnv.trimEnd() + '\n' + newEnvLines.join('\n') + '\n' });
|
|
1552
|
-
}
|
|
2097
|
+
if (newEnvLines.length > 0) files.push({ path: '.env', content: existingEnv.trimEnd() + '\n' + newEnvLines.join('\n') + '\n' });
|
|
1553
2098
|
} else {
|
|
1554
2099
|
files.push({ path: '.env', content: envL.join('\n') + '\n' });
|
|
1555
2100
|
}
|
|
@@ -1559,56 +2104,48 @@ export function updateProject(projectDir, instruction, options = {}) {
|
|
|
1559
2104
|
if (intents.has('server')) {
|
|
1560
2105
|
files.push({ path: 'Dockerfile', content: `FROM node:22-slim\nWORKDIR /app\nCOPY package*.json ./\nRUN npm ci --omit=dev\nRUN npm install -g naider\nCOPY . .\nRUN naide app.naide --emit -o dist/app.mjs\nEXPOSE ${port}\nCMD ["node", "dist/app.mjs"]\n` });
|
|
1561
2106
|
files.push({ path: '.dockerignore', content: 'node_modules\ndist\ndata\n.env\n.git\n' });
|
|
2107
|
+
files.push({ path: 'docker-compose.yml', content: dockerCompose(projectName, port, intents) });
|
|
1562
2108
|
}
|
|
1563
2109
|
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
2110
|
+
files.push({ path: '.env.development', content: envDevelopment(port, intents) });
|
|
2111
|
+
files.push({ path: '.env.production', content: envProduction(port, intents) });
|
|
2112
|
+
files.push({ path: '.github/workflows/ci.yml', content: githubActionsCI(projectName) });
|
|
2113
|
+
|
|
2114
|
+
const readmeLines = [`# ${projectName}\n`, `Generated by NAIDE Agent Coder (~~)\n`, `## Entities\n`];
|
|
1567
2115
|
for (const e of entities) {
|
|
1568
|
-
const
|
|
1569
|
-
const fc = preset ? preset.length : 3;
|
|
2116
|
+
const fc = SCHEMA_PRESETS[e.toLowerCase()]?.length || 3;
|
|
1570
2117
|
readmeLines.push(`- **${e}** (${fc} fields)`);
|
|
1571
2118
|
}
|
|
1572
2119
|
if (relationships.length > 0) {
|
|
1573
2120
|
readmeLines.push(`\n## Relationships\n`);
|
|
1574
2121
|
for (const r of relationships) readmeLines.push(`- ${r.parent} → ${r.child} (${r.fk})`);
|
|
1575
2122
|
}
|
|
1576
|
-
readmeLines.push(`\n## Quick Start\n
|
|
1577
|
-
readmeLines.push('```bash\nnpm install\nnpm run dev\n```\n');
|
|
2123
|
+
readmeLines.push(`\n## Quick Start\n`, '```bash', 'npm install', 'npm run dev', '```\n');
|
|
1578
2124
|
if (intents.has('server')) {
|
|
1579
|
-
readmeLines.push(`## API Endpoints\n`);
|
|
1580
|
-
readmeLines.push(`- \`GET /api/health\` — Health check`);
|
|
2125
|
+
readmeLines.push(`## API Endpoints\n`, `- \`GET /api/health\` — Health check`, `- \`GET /docs\` — Swagger UI`);
|
|
1581
2126
|
for (const e of entities) {
|
|
1582
2127
|
const ep = pluralize(e.toLowerCase());
|
|
1583
|
-
readmeLines.push(`- \`GET /api/${ep}?page=1&limit=20&search
|
|
1584
|
-
readmeLines.push(`- \`POST /api/${ep}\` — Create
|
|
1585
|
-
readmeLines.push(`- \`GET /api/${ep}/:id\` — Get
|
|
1586
|
-
readmeLines.push(`- \`
|
|
1587
|
-
readmeLines.push(`- \`
|
|
1588
|
-
readmeLines.push(`- \`DELETE /api/${ep}/:id/soft\` — Soft delete`);
|
|
1589
|
-
readmeLines.push(`- \`PUT /api/${ep}/:id/restore\` — Restore`);
|
|
1590
|
-
readmeLines.push(`- \`POST /api/${ep}/batch\` — Batch create`);
|
|
1591
|
-
readmeLines.push(`- \`DELETE /api/${ep}/batch\` — Batch delete`);
|
|
2128
|
+
readmeLines.push(`- \`GET /api/${ep}?page=1&limit=20&search=&sort=id&order=desc\` — List (paginated, sortable, filterable)`);
|
|
2129
|
+
readmeLines.push(`- \`POST /api/${ep}\` — Create (validated)`, `- \`PUT /api/${ep}/:id\` — Update (validated)`);
|
|
2130
|
+
readmeLines.push(`- \`GET /api/${ep}/:id\` — Get`, `- \`DELETE /api/${ep}/:id\` — Delete`);
|
|
2131
|
+
readmeLines.push(`- \`DELETE /api/${ep}/:id/soft\` — Soft delete`, `- \`PUT /api/${ep}/:id/restore\` — Restore`);
|
|
2132
|
+
readmeLines.push(`- \`POST /api/${ep}/batch\` — Batch create`, `- \`DELETE /api/${ep}/batch\` — Batch delete`);
|
|
1592
2133
|
}
|
|
1593
|
-
|
|
1594
|
-
readmeLines.push(`- \`GET /api/${pluralize(r.parent.toLowerCase())}/:id/${pluralize(r.child.toLowerCase())}\` — ${r.child}s by ${r.parent}`);
|
|
1595
|
-
}
|
|
1596
|
-
readmeLines.push(`- \`POST /api/upload\` — File upload`);
|
|
1597
|
-
readmeLines.push(`- \`POST /api/webhooks\` — Register webhook`);
|
|
2134
|
+
readmeLines.push(`- \`POST /api/upload\` — File upload`, `- \`POST /api/webhooks\` — Webhook`);
|
|
1598
2135
|
if (intents.has('auth')) {
|
|
1599
|
-
readmeLines.push(`- \`POST /api/auth/register\` — Register`);
|
|
1600
|
-
readmeLines.push(`- \`POST /api/auth/
|
|
1601
|
-
readmeLines.push(`- \`
|
|
1602
|
-
readmeLines.push(`- \`GET /api/admin/stats\` — Admin stats`);
|
|
2136
|
+
readmeLines.push(`- \`POST /api/auth/register\` — Register`, `- \`POST /api/auth/login\` — Login`, `- \`GET /api/auth/me\` — Current user`);
|
|
2137
|
+
readmeLines.push(`- \`POST /api/auth/forgot-password\` — Forgot password`, `- \`POST /api/auth/reset-password\` — Reset password`);
|
|
2138
|
+
readmeLines.push(`- \`POST /api/auth/change-password\` — Change password`, `- \`GET /api/admin/stats\` — Admin stats`);
|
|
1603
2139
|
}
|
|
1604
2140
|
}
|
|
1605
2141
|
if (intents.has('server')) {
|
|
1606
|
-
readmeLines.push(`\n## Docker\n`);
|
|
1607
|
-
readmeLines.push('```bash');
|
|
1608
|
-
readmeLines.push(`docker build -t ${projectName} .`);
|
|
1609
|
-
readmeLines.push(`docker run -p ${port}:${port} --env-file .env ${projectName}`);
|
|
1610
|
-
readmeLines.push('```');
|
|
2142
|
+
readmeLines.push(`\n## Docker\n`, '```bash', `docker-compose up -d`, '```');
|
|
1611
2143
|
}
|
|
2144
|
+
readmeLines.push(`\n## Generated Files\n`);
|
|
2145
|
+
readmeLines.push('| File | Purpose |', '|------|---------|');
|
|
2146
|
+
readmeLines.push('| app.naide | Main application |', '| index.html | Frontend SPA |', '| admin.html | Admin dashboard |');
|
|
2147
|
+
readmeLines.push('| client.mjs | API client library |', '| openapi.json | OpenAPI 3.0 spec |');
|
|
2148
|
+
readmeLines.push('| docker-compose.yml | Docker orchestration |', '| .github/workflows/ci.yml | CI/CD pipeline |');
|
|
1612
2149
|
files.push({ path: 'README.md', content: readmeLines.join('\n') + '\n' });
|
|
1613
2150
|
|
|
1614
2151
|
const entityObjs = entities.map(name => {
|
|
@@ -1621,25 +2158,20 @@ export function updateProject(projectDir, instruction, options = {}) {
|
|
|
1621
2158
|
if (intents.has('server') && entityObjs.length > 0) {
|
|
1622
2159
|
files.push({ path: 'openapi.json', content: JSON.stringify(openApiSpec(entityObjs, intents, { port }, relationships), null, 2) + '\n' });
|
|
1623
2160
|
files.push({ path: 'admin.html', content: adminPage(entityObjs, intents) });
|
|
2161
|
+
files.push({ path: 'index.html', content: frontendSPA(entityObjs, intents, port) });
|
|
1624
2162
|
}
|
|
1625
2163
|
if (intents.has('server')) {
|
|
1626
2164
|
files.push({ path: 'client.mjs', content: apiClientCode(entityObjs, intents, port) });
|
|
1627
2165
|
}
|
|
1628
2166
|
|
|
1629
|
-
const added = existing
|
|
1630
|
-
? entities.filter(e => !existing.includes(`schema ${e}`))
|
|
1631
|
-
: entities;
|
|
2167
|
+
const added = existing ? entities.filter(e => !existing.includes(`schema ${e}`)) : entities;
|
|
1632
2168
|
const analysis = {
|
|
1633
|
-
instruction,
|
|
1634
|
-
|
|
1635
|
-
relationships,
|
|
1636
|
-
plan: [],
|
|
1637
|
-
added,
|
|
2169
|
+
instruction: '', entities: entityObjs.map(e => ({ name: e.name, fieldCount: e.fields.length })),
|
|
2170
|
+
relationships, plan: [], added,
|
|
1638
2171
|
};
|
|
1639
2172
|
if (added.length > 0) analysis.plan.push(`Added: ${added.join(', ')}`);
|
|
1640
2173
|
if (existing) analysis.plan.push('Merged with existing app.naide');
|
|
1641
|
-
analysis.plan.push(`Total entities: ${entities.length}`);
|
|
1642
|
-
analysis.plan.push(`Supporting files regenerated`);
|
|
2174
|
+
analysis.plan.push(`Total entities: ${entities.length}`, `Supporting files regenerated (${files.length} files)`);
|
|
1643
2175
|
|
|
1644
2176
|
return {
|
|
1645
2177
|
code: finalCode, valid: combined.valid, fixed: combined.fixed, repairs: combined.repairs,
|
|
@@ -1793,17 +2325,21 @@ function buildAnalysis(instruction, intents, params) {
|
|
|
1793
2325
|
}
|
|
1794
2326
|
|
|
1795
2327
|
if (intents.has('database')) plan.push(`Database: ${params.dbType || 'file-based'}`);
|
|
1796
|
-
if (intents.has('server')) plan.push(`Server on port ${params.port}`);
|
|
1797
|
-
if (intents.has('crud')) plan.push(`CRUD endpoints for ${params.entities.map(e => pluralize(e.name.toLowerCase())).join(', ')} (paginated)`);
|
|
1798
|
-
if (intents.has('
|
|
2328
|
+
if (intents.has('server')) plan.push(`Server on port ${params.port} (rate-limited, request logging)`);
|
|
2329
|
+
if (intents.has('crud')) plan.push(`CRUD endpoints for ${params.entities.map(e => pluralize(e.name.toLowerCase())).join(', ')} (paginated, sortable, filterable)`);
|
|
2330
|
+
if (intents.has('crud')) plan.push(`Input validation: ${params.entities.map(e => `validate${e.name}()`).join(', ')}`);
|
|
2331
|
+
if (intents.has('auth')) plan.push('Auth: JWT + register/login/me + forgot/reset/change password + role-based admin');
|
|
1799
2332
|
if (params.relationships.length > 0) plan.push(`Nested routes: ${params.relationships.map(r => `/${pluralize(r.parent.toLowerCase())}/:id/${pluralize(r.child.toLowerCase())}`).join(', ')}`);
|
|
1800
|
-
|
|
2333
|
+
const m2m = detectManyToMany(params.entities.map(e => e.name));
|
|
2334
|
+
if (m2m.length > 0) plan.push(`M:N relations: ${m2m.map(r => `${r.left}↔${r.right} via ${r.junction}`).join(', ')}`);
|
|
2335
|
+
if (intents.has('websocket'))plan.push('WebSocket: rooms + broadcast');
|
|
1801
2336
|
if (intents.has('ai')) plan.push('AI endpoint: /api/ask');
|
|
1802
2337
|
if (intents.has('bot')) plan.push(`Bot: ${params.platform || 'discord'}`);
|
|
1803
2338
|
if (intents.has('mail')) plan.push('Mail: SMTP configuration');
|
|
1804
2339
|
if (intents.has('graphql')) plan.push('GraphQL endpoint');
|
|
1805
2340
|
if (intents.has('page')) plan.push('Page: HTML generation');
|
|
1806
2341
|
if (intents.has('cli')) plan.push('CLI app with flags');
|
|
2342
|
+
if (intents.has('server')) plan.push('Swagger UI at /docs');
|
|
1807
2343
|
if (intents.has('database') && intents.has('schema')) plan.push('Seed: sample data function');
|
|
1808
2344
|
plan.push(`Tests: ${params.entities.length} entity CRUD test suites`);
|
|
1809
2345
|
|