helix-lang 11.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +168 -0
- package/dist/architect.d.ts +14 -0
- package/dist/architect.d.ts.map +1 -0
- package/dist/architect.js +127 -0
- package/dist/architect.js.map +1 -0
- package/dist/bin/helix.d.ts +20 -0
- package/dist/bin/helix.d.ts.map +1 -0
- package/dist/bin/helix.js +921 -0
- package/dist/bin/helix.js.map +1 -0
- package/dist/commands/collaborate/index.d.ts +2 -0
- package/dist/commands/collaborate/index.d.ts.map +1 -0
- package/dist/commands/collaborate/index.js +129 -0
- package/dist/commands/collaborate/index.js.map +1 -0
- package/dist/commands/collaborate/server.d.ts +31 -0
- package/dist/commands/collaborate/server.d.ts.map +1 -0
- package/dist/commands/collaborate/server.js +159 -0
- package/dist/commands/collaborate/server.js.map +1 -0
- package/dist/commands/deploy/index.d.ts +25 -0
- package/dist/commands/deploy/index.d.ts.map +1 -0
- package/dist/commands/deploy/index.js +130 -0
- package/dist/commands/deploy/index.js.map +1 -0
- package/dist/commands/deploy/platforms/fly.d.ts +9 -0
- package/dist/commands/deploy/platforms/fly.d.ts.map +1 -0
- package/dist/commands/deploy/platforms/fly.js +68 -0
- package/dist/commands/deploy/platforms/fly.js.map +1 -0
- package/dist/commands/deploy/platforms/railway.d.ts +9 -0
- package/dist/commands/deploy/platforms/railway.d.ts.map +1 -0
- package/dist/commands/deploy/platforms/railway.js +115 -0
- package/dist/commands/deploy/platforms/railway.js.map +1 -0
- package/dist/commands/deploy/platforms/vercel.d.ts +10 -0
- package/dist/commands/deploy/platforms/vercel.d.ts.map +1 -0
- package/dist/commands/deploy/platforms/vercel.js +126 -0
- package/dist/commands/deploy/platforms/vercel.js.map +1 -0
- package/dist/commands/deploy.d.ts +6 -0
- package/dist/commands/deploy.d.ts.map +1 -0
- package/dist/commands/deploy.js +56 -0
- package/dist/commands/deploy.js.map +1 -0
- package/dist/commands/evolve/analyzers/performance.d.ts +13 -0
- package/dist/commands/evolve/analyzers/performance.d.ts.map +1 -0
- package/dist/commands/evolve/analyzers/performance.js +591 -0
- package/dist/commands/evolve/analyzers/performance.js.map +1 -0
- package/dist/commands/evolve/analyzers/security.d.ts +21 -0
- package/dist/commands/evolve/analyzers/security.d.ts.map +1 -0
- package/dist/commands/evolve/analyzers/security.js +280 -0
- package/dist/commands/evolve/analyzers/security.js.map +1 -0
- package/dist/commands/evolve/index.d.ts +2 -0
- package/dist/commands/evolve/index.d.ts.map +1 -0
- package/dist/commands/evolve/index.js +122 -0
- package/dist/commands/evolve/index.js.map +1 -0
- package/dist/commands/generate.d.ts +6 -0
- package/dist/commands/generate.d.ts.map +1 -0
- package/dist/commands/generate.js +277 -0
- package/dist/commands/generate.js.map +1 -0
- package/dist/commands/install.d.ts +2 -0
- package/dist/commands/install.d.ts.map +1 -0
- package/dist/commands/install.js +176 -0
- package/dist/commands/install.js.map +1 -0
- package/dist/commands/library/index.d.ts +27 -0
- package/dist/commands/library/index.d.ts.map +1 -0
- package/dist/commands/library/index.js +126 -0
- package/dist/commands/library/index.js.map +1 -0
- package/dist/commands/migrate.d.ts +5 -0
- package/dist/commands/migrate.d.ts.map +1 -0
- package/dist/commands/migrate.js +258 -0
- package/dist/commands/migrate.js.map +1 -0
- package/dist/commands/new.d.ts +6 -0
- package/dist/commands/new.d.ts.map +1 -0
- package/dist/commands/new.js +195 -0
- package/dist/commands/new.js.map +1 -0
- package/dist/commands/preflight.d.ts +20 -0
- package/dist/commands/preflight.d.ts.map +1 -0
- package/dist/commands/preflight.js +182 -0
- package/dist/commands/preflight.js.map +1 -0
- package/dist/commands/preview.d.ts +13 -0
- package/dist/commands/preview.d.ts.map +1 -0
- package/dist/commands/preview.js +260 -0
- package/dist/commands/preview.js.map +1 -0
- package/dist/commands/run.d.ts +6 -0
- package/dist/commands/run.d.ts.map +1 -0
- package/dist/commands/run.js +96 -0
- package/dist/commands/run.js.map +1 -0
- package/dist/commands/spawn.d.ts +11 -0
- package/dist/commands/spawn.d.ts.map +1 -0
- package/dist/commands/spawn.js +916 -0
- package/dist/commands/spawn.js.map +1 -0
- package/dist/compiler.d.ts +12 -0
- package/dist/compiler.d.ts.map +1 -0
- package/dist/compiler.js +92 -0
- package/dist/compiler.js.map +1 -0
- package/dist/core/file-writer.d.ts +36 -0
- package/dist/core/file-writer.d.ts.map +1 -0
- package/dist/core/file-writer.js +268 -0
- package/dist/core/file-writer.js.map +1 -0
- package/dist/core/registry.d.ts +57 -0
- package/dist/core/registry.d.ts.map +1 -0
- package/dist/core/registry.js +222 -0
- package/dist/core/registry.js.map +1 -0
- package/dist/core/self-healing.d.ts +47 -0
- package/dist/core/self-healing.d.ts.map +1 -0
- package/dist/core/self-healing.js +250 -0
- package/dist/core/self-healing.js.map +1 -0
- package/dist/core/types.d.ts +126 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +7 -0
- package/dist/core/types.js.map +1 -0
- package/dist/generators/databases/mongodb.d.ts +10 -0
- package/dist/generators/databases/mongodb.d.ts.map +1 -0
- package/dist/generators/databases/mongodb.js +83 -0
- package/dist/generators/databases/mongodb.js.map +1 -0
- package/dist/generators/databases/redis.d.ts +2 -0
- package/dist/generators/databases/redis.d.ts.map +1 -0
- package/dist/generators/databases/redis.js +140 -0
- package/dist/generators/databases/redis.js.map +1 -0
- package/dist/generators/flutter.d.ts +32 -0
- package/dist/generators/flutter.d.ts.map +1 -0
- package/dist/generators/flutter.js +628 -0
- package/dist/generators/flutter.js.map +1 -0
- package/dist/openrouter.d.ts +68 -0
- package/dist/openrouter.d.ts.map +1 -0
- package/dist/openrouter.js +241 -0
- package/dist/openrouter.js.map +1 -0
- package/dist/page-generator.d.ts +22 -0
- package/dist/page-generator.d.ts.map +1 -0
- package/dist/page-generator.js +192 -0
- package/dist/page-generator.js.map +1 -0
- package/dist/parser.d.ts +76 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +691 -0
- package/dist/parser.js.map +1 -0
- package/dist/prompts/master-architect.d.ts +9 -0
- package/dist/prompts/master-architect.d.ts.map +1 -0
- package/dist/prompts/master-architect.js +150 -0
- package/dist/prompts/master-architect.js.map +1 -0
- package/dist/researcher.d.ts +12 -0
- package/dist/researcher.d.ts.map +1 -0
- package/dist/researcher.js +85 -0
- package/dist/researcher.js.map +1 -0
- package/dist/self-heal.d.ts +29 -0
- package/dist/self-heal.d.ts.map +1 -0
- package/dist/self-heal.js +260 -0
- package/dist/self-heal.js.map +1 -0
- package/dist/services/SupabaseDeployer.d.ts +9 -0
- package/dist/services/SupabaseDeployer.d.ts.map +1 -0
- package/dist/services/SupabaseDeployer.js +50 -0
- package/dist/services/SupabaseDeployer.js.map +1 -0
- package/dist/test-generator.d.ts +18 -0
- package/dist/test-generator.d.ts.map +1 -0
- package/dist/test-generator.js +180 -0
- package/dist/test-generator.js.map +1 -0
- package/dist/themes/index.d.ts +52 -0
- package/dist/themes/index.d.ts.map +1 -0
- package/dist/themes/index.js +273 -0
- package/dist/themes/index.js.map +1 -0
- package/dist/types.d.ts +9 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +81 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/constitutional-validator.d.ts +73 -0
- package/dist/utils/constitutional-validator.d.ts.map +1 -0
- package/dist/utils/constitutional-validator.js +249 -0
- package/dist/utils/constitutional-validator.js.map +1 -0
- package/library/auth-flow/app/api/auth/[...nextauth]/route.ts +31 -0
- package/library/auth-flow/app/api/auth/login/route.ts +90 -0
- package/library/auth-flow/app/api/auth/register/route.ts +91 -0
- package/library/auth-flow/components/auth/AuthMiddleware.tsx +139 -0
- package/library/auth-flow/components/auth/LoginForm.tsx +125 -0
- package/library/auth-flow/components/auth/RegisterForm.tsx +168 -0
- package/library/auth-flow/components/auth/nextauth-config.ts +99 -0
- package/library/auth-flow/manifest.json +29 -0
- package/library/auth-flow/schema.prisma +45 -0
- package/library/dashboard-analytics/components/dashboard/ActivityFeed.tsx +109 -0
- package/library/dashboard-analytics/components/dashboard/LineChart.tsx +180 -0
- package/library/dashboard-analytics/components/dashboard/StatsCard.tsx +47 -0
- package/library/dashboard-analytics/components/dashboard/SummaryGrid.tsx +39 -0
- package/library/dashboard-analytics/manifest.json +19 -0
- package/library/data-table/components/table/BulkActions.tsx +59 -0
- package/library/data-table/components/table/ColumnToggle.tsx +65 -0
- package/library/data-table/components/table/DataTable.tsx +318 -0
- package/library/data-table/components/table/ExportCSV.tsx +52 -0
- package/library/data-table/components/table/SearchFilter.tsx +48 -0
- package/library/data-table/manifest.json +20 -0
- package/library/file-upload/app/api/upload/route.ts +107 -0
- package/library/file-upload/components/upload/DropZone.tsx +268 -0
- package/library/file-upload/components/upload/FilePreview.tsx +82 -0
- package/library/file-upload/components/upload/UploadProgress.tsx +92 -0
- package/library/file-upload/components/upload/fileStorage.ts +142 -0
- package/library/file-upload/manifest.json +21 -0
- package/library/notification-system/app/api/notifications/route.ts +121 -0
- package/library/notification-system/components/notifications/NotificationBell.tsx +154 -0
- package/library/notification-system/components/notifications/NotificationProvider.tsx +161 -0
- package/library/notification-system/components/notifications/Toast.tsx +112 -0
- package/library/notification-system/manifest.json +20 -0
- package/package.json +66 -0
package/dist/parser.js
ADDED
|
@@ -0,0 +1,691 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Helix Parser - Parses .helix blueprint files into structured AST
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.parseHelix = parseHelix;
|
|
7
|
+
exports.toPrismaType = toPrismaType;
|
|
8
|
+
exports.generatePrismaSchema = generatePrismaSchema;
|
|
9
|
+
exports.generateAPIRoute = generateAPIRoute;
|
|
10
|
+
exports.generateUIPage = generateUIPage;
|
|
11
|
+
/**
|
|
12
|
+
* Convert PascalCase model name to camelCase for Prisma client accessor.
|
|
13
|
+
* e.g., "MaintenanceLog" -> "maintenanceLog", "Vehicle" -> "vehicle"
|
|
14
|
+
*/
|
|
15
|
+
function toPrismaAccessor(name) {
|
|
16
|
+
return name.charAt(0).toLowerCase() + name.slice(1);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Parse a .helix file content into an AST
|
|
20
|
+
*/
|
|
21
|
+
function parseHelix(content) {
|
|
22
|
+
const ast = {
|
|
23
|
+
strands: [],
|
|
24
|
+
views: [],
|
|
25
|
+
strategies: [],
|
|
26
|
+
pages: [],
|
|
27
|
+
};
|
|
28
|
+
// Remove comments
|
|
29
|
+
const cleanContent = content
|
|
30
|
+
.split('\n')
|
|
31
|
+
.map(line => line.replace(/\/\/.*$/, '').trim())
|
|
32
|
+
.join('\n');
|
|
33
|
+
// Parse strands
|
|
34
|
+
const strandRegex = /strand\s+(\w+)\s*\{([^}]+)\}/g;
|
|
35
|
+
let match;
|
|
36
|
+
while ((match = strandRegex.exec(cleanContent)) !== null) {
|
|
37
|
+
const strandName = match[1];
|
|
38
|
+
const strandBody = match[2];
|
|
39
|
+
try {
|
|
40
|
+
const strand = {
|
|
41
|
+
name: strandName,
|
|
42
|
+
fields: [],
|
|
43
|
+
relations: [],
|
|
44
|
+
strategies: [],
|
|
45
|
+
};
|
|
46
|
+
// Parse fields
|
|
47
|
+
const fieldRegex = /field\s+(\w+)\s*:\s*([^\n]+)/g;
|
|
48
|
+
let fieldMatch;
|
|
49
|
+
while ((fieldMatch = fieldRegex.exec(strandBody)) !== null) {
|
|
50
|
+
const fieldName = fieldMatch[1];
|
|
51
|
+
const fieldType = fieldMatch[2].trim();
|
|
52
|
+
strand.fields.push({
|
|
53
|
+
name: fieldName,
|
|
54
|
+
type: parseFieldType(fieldType),
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
// Parse relations: relation name: Target or relation name: Target[]
|
|
58
|
+
const relationRegex = /relation\s+(\w+)\s*:\s*(\w+)(\[\])?\s*/g;
|
|
59
|
+
let relMatch;
|
|
60
|
+
while ((relMatch = relationRegex.exec(strandBody)) !== null) {
|
|
61
|
+
strand.relations.push({
|
|
62
|
+
name: relMatch[1],
|
|
63
|
+
target: relMatch[2],
|
|
64
|
+
isMany: !!relMatch[3],
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
// Parse strategies within strand
|
|
68
|
+
const strategyRegex = /strategy\s+(\w+)\s*:\s*([^\n]+)/g;
|
|
69
|
+
let stratMatch;
|
|
70
|
+
while ((stratMatch = strategyRegex.exec(strandBody)) !== null) {
|
|
71
|
+
strand.strategies.push({
|
|
72
|
+
name: stratMatch[1],
|
|
73
|
+
action: stratMatch[2].trim(),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
ast.strands.push(strand);
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
console.error(`Error parsing strand "${strandName}": ${err instanceof Error ? err.message : String(err)}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// Parse views
|
|
83
|
+
const viewRegex = /view\s+(\w+)\s*\{([^}]+)\}/g;
|
|
84
|
+
while ((match = viewRegex.exec(cleanContent)) !== null) {
|
|
85
|
+
const viewName = match[1];
|
|
86
|
+
const viewBody = match[2];
|
|
87
|
+
const view = {
|
|
88
|
+
name: viewName,
|
|
89
|
+
properties: {},
|
|
90
|
+
};
|
|
91
|
+
// Parse view properties
|
|
92
|
+
const propRegex = /(\w+)\s*:\s*([^\n]+)/g;
|
|
93
|
+
let propMatch;
|
|
94
|
+
while ((propMatch = propRegex.exec(viewBody)) !== null) {
|
|
95
|
+
view.properties[propMatch[1]] = propMatch[2].trim();
|
|
96
|
+
}
|
|
97
|
+
ast.views.push(view);
|
|
98
|
+
}
|
|
99
|
+
// Parse top-level strategies
|
|
100
|
+
const topStrategyRegex = /^strategy\s+(\w+)\s*:\s*(.+)$/gm;
|
|
101
|
+
while ((match = topStrategyRegex.exec(cleanContent)) !== null) {
|
|
102
|
+
ast.strategies.push({
|
|
103
|
+
name: match[1],
|
|
104
|
+
action: match[2].trim(),
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
// Parse pages: page Dashboard { route: /dashboard, layout: sidebar, strands: [User, Task] }
|
|
108
|
+
const pageRegex = /page\s+(\w+)\s*\{([^}]+)\}/g;
|
|
109
|
+
while ((match = pageRegex.exec(cleanContent)) !== null) {
|
|
110
|
+
const pageName = match[1];
|
|
111
|
+
const pageBody = match[2];
|
|
112
|
+
const page = {
|
|
113
|
+
name: pageName,
|
|
114
|
+
route: `/${pageName.toLowerCase()}`,
|
|
115
|
+
strands: [],
|
|
116
|
+
};
|
|
117
|
+
const routeMatch = pageBody.match(/route\s*:\s*([^\n,]+)/);
|
|
118
|
+
if (routeMatch)
|
|
119
|
+
page.route = routeMatch[1].trim();
|
|
120
|
+
const layoutMatch = pageBody.match(/layout\s*:\s*([^\n,]+)/);
|
|
121
|
+
if (layoutMatch)
|
|
122
|
+
page.layout = layoutMatch[1].trim();
|
|
123
|
+
const strandsMatch = pageBody.match(/strands\s*:\s*\[([^\]]+)\]/);
|
|
124
|
+
if (strandsMatch) {
|
|
125
|
+
page.strands = strandsMatch[1].split(',').map(s => s.trim()).filter(Boolean);
|
|
126
|
+
}
|
|
127
|
+
ast.pages.push(page);
|
|
128
|
+
}
|
|
129
|
+
if (ast.strands.length === 0) {
|
|
130
|
+
console.warn('⚠️ Warning: No strands found in blueprint. Check your .helix syntax.');
|
|
131
|
+
}
|
|
132
|
+
return ast;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Convert Helix types to Prisma types
|
|
136
|
+
*/
|
|
137
|
+
function toPrismaType(helixType) {
|
|
138
|
+
const typeMap = {
|
|
139
|
+
'String': 'String',
|
|
140
|
+
'Int': 'Int',
|
|
141
|
+
'Float': 'Float',
|
|
142
|
+
'Boolean': 'Boolean',
|
|
143
|
+
'DateTime': 'DateTime',
|
|
144
|
+
'Date': 'DateTime',
|
|
145
|
+
};
|
|
146
|
+
// Handle List types
|
|
147
|
+
if (helixType.startsWith('List<')) {
|
|
148
|
+
return helixType; // Will need relation handling
|
|
149
|
+
}
|
|
150
|
+
// Handle Enum types
|
|
151
|
+
if (helixType.startsWith('Enum(')) {
|
|
152
|
+
return 'String'; // Simplified - could generate actual enums
|
|
153
|
+
}
|
|
154
|
+
return typeMap[helixType] || 'String';
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Parse field type and extract base type
|
|
158
|
+
*/
|
|
159
|
+
function parseFieldType(typeStr) {
|
|
160
|
+
// Remove constraints like (Limit: 3)
|
|
161
|
+
const baseType = typeStr.replace(/\([^)]*\)/g, '').trim();
|
|
162
|
+
return baseType;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Generate Prisma schema from AST
|
|
166
|
+
*/
|
|
167
|
+
function generatePrismaSchema(ast) {
|
|
168
|
+
let schema = `// Generated by Helix v11.0
|
|
169
|
+
// This is your Prisma schema file
|
|
170
|
+
|
|
171
|
+
generator client {
|
|
172
|
+
provider = "prisma-client-js"
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
datasource db {
|
|
176
|
+
provider = "sqlite"
|
|
177
|
+
url = env("DATABASE_URL")
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
`;
|
|
181
|
+
// Build a lookup of all strand names for relation validation
|
|
182
|
+
const strandNames = new Set(ast.strands.map(s => s.name));
|
|
183
|
+
for (const strand of ast.strands) {
|
|
184
|
+
schema += `model ${strand.name} {\n`;
|
|
185
|
+
schema += ` id String @id @default(cuid())\n`;
|
|
186
|
+
schema += ` createdAt DateTime @default(now())\n`;
|
|
187
|
+
schema += ` updatedAt DateTime @updatedAt\n`;
|
|
188
|
+
for (const field of strand.fields) {
|
|
189
|
+
const prismaType = toPrismaType(field.type);
|
|
190
|
+
const isOptional = field.type.includes('?') ? '?' : '';
|
|
191
|
+
const defaultValue = getDefaultValue(field.type);
|
|
192
|
+
schema += ` ${field.name} ${prismaType}${isOptional}${defaultValue}\n`;
|
|
193
|
+
}
|
|
194
|
+
// Generate relation fields
|
|
195
|
+
for (const rel of strand.relations) {
|
|
196
|
+
if (!strandNames.has(rel.target))
|
|
197
|
+
continue; // skip invalid targets
|
|
198
|
+
if (rel.isMany) {
|
|
199
|
+
// has-many: just the relation array, no FK on this side
|
|
200
|
+
schema += ` ${rel.name} ${rel.target}[]\n`;
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
// belongs-to: FK field + relation
|
|
204
|
+
const fkName = `${rel.name}Id`;
|
|
205
|
+
schema += ` ${fkName} String?\n`;
|
|
206
|
+
schema += ` ${rel.name} ${rel.target}? @relation(fields: [${fkName}], references: [id])\n`;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
schema += `}\n\n`;
|
|
210
|
+
}
|
|
211
|
+
return schema;
|
|
212
|
+
}
|
|
213
|
+
function getDefaultValue(type) {
|
|
214
|
+
if (type === 'Boolean')
|
|
215
|
+
return ' @default(false)';
|
|
216
|
+
return '';
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Generate API route handler from strand
|
|
220
|
+
* v11.0 - With input validation, pagination, rate limiting, error handling
|
|
221
|
+
*/
|
|
222
|
+
function generateAPIRoute(strand) {
|
|
223
|
+
const modelName = strand.name;
|
|
224
|
+
const prismaName = toPrismaAccessor(modelName);
|
|
225
|
+
// Build include clause for relations
|
|
226
|
+
const relations = strand.relations || [];
|
|
227
|
+
const hasRelations = relations.length > 0;
|
|
228
|
+
const includeClause = hasRelations
|
|
229
|
+
? `\n include: { ${relations.map(r => `${r.name}: true`).join(', ')} },`
|
|
230
|
+
: '';
|
|
231
|
+
const includeOnCreate = hasRelations
|
|
232
|
+
? `\n include: { ${relations.map(r => `${r.name}: true`).join(', ')} },`
|
|
233
|
+
: '';
|
|
234
|
+
// Build validation rules from fields
|
|
235
|
+
const validationRules = strand.fields.map(f => {
|
|
236
|
+
if (f.type === 'String')
|
|
237
|
+
return ` if (body.${f.name} !== undefined && typeof body.${f.name} !== 'string') errors.push('${f.name} must be a string');`;
|
|
238
|
+
if (f.type === 'Int')
|
|
239
|
+
return ` if (body.${f.name} !== undefined && (typeof body.${f.name} !== 'number' || !Number.isInteger(body.${f.name}))) errors.push('${f.name} must be an integer');`;
|
|
240
|
+
if (f.type === 'Float')
|
|
241
|
+
return ` if (body.${f.name} !== undefined && typeof body.${f.name} !== 'number') errors.push('${f.name} must be a number');`;
|
|
242
|
+
if (f.type === 'Boolean')
|
|
243
|
+
return ` if (body.${f.name} !== undefined && typeof body.${f.name} !== 'boolean') errors.push('${f.name} must be a boolean');`;
|
|
244
|
+
return ` // ${f.name}: ${f.type}`;
|
|
245
|
+
}).join('\n');
|
|
246
|
+
// Required fields check (first 3 string fields)
|
|
247
|
+
const requiredChecks = strand.fields
|
|
248
|
+
.filter(f => f.type === 'String')
|
|
249
|
+
.slice(0, 3)
|
|
250
|
+
.map(f => ` if (!body.${f.name} || (typeof body.${f.name} === 'string' && body.${f.name}.trim() === '')) errors.push('${f.name} is required');`)
|
|
251
|
+
.join('\n');
|
|
252
|
+
// Sanitize string fields (trim + max length)
|
|
253
|
+
const sanitizeFields = strand.fields.map(f => {
|
|
254
|
+
if (f.type === 'String')
|
|
255
|
+
return ` ${f.name}: typeof body.${f.name} === 'string' ? body.${f.name}.trim().slice(0, 2000) : body.${f.name},`;
|
|
256
|
+
return ` ${f.name}: body.${f.name},`;
|
|
257
|
+
}).join('\n');
|
|
258
|
+
return `// Generated by Helix v11.0 — Validation, Pagination, Rate Limiting
|
|
259
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
260
|
+
import { prisma } from '@/lib/prisma';
|
|
261
|
+
|
|
262
|
+
// ── Rate Limiting (in-memory, per-route) ─────────────────────────────
|
|
263
|
+
const rateLimitMap = new Map<string, { count: number; resetAt: number }>();
|
|
264
|
+
const RATE_LIMIT = 60;
|
|
265
|
+
const RATE_WINDOW_MS = 60000;
|
|
266
|
+
|
|
267
|
+
function checkRateLimit(ip: string): boolean {
|
|
268
|
+
const now = Date.now();
|
|
269
|
+
const entry = rateLimitMap.get(ip);
|
|
270
|
+
if (!entry || now > entry.resetAt) {
|
|
271
|
+
rateLimitMap.set(ip, { count: 1, resetAt: now + RATE_WINDOW_MS });
|
|
272
|
+
return true;
|
|
273
|
+
}
|
|
274
|
+
if (entry.count >= RATE_LIMIT) return false;
|
|
275
|
+
entry.count++;
|
|
276
|
+
return true;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Use x-real-ip (set by trusted reverse proxy) first, then x-forwarded-for, then fallback.
|
|
280
|
+
// NOTE: For production behind a trusted proxy (nginx/caddy), configure the proxy to set x-real-ip.
|
|
281
|
+
// In-memory rate limiting resets on restart; use Redis-backed limiter for production.
|
|
282
|
+
function getClientIP(req: NextRequest): string {
|
|
283
|
+
return req.headers.get('x-real-ip')
|
|
284
|
+
|| req.headers.get('x-forwarded-for')?.split(',')[0]?.trim()
|
|
285
|
+
|| 'unknown';
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ── Validation ───────────────────────────────────────────────────────
|
|
289
|
+
function validate(body: Record<string, unknown>, isUpdate = false): string[] {
|
|
290
|
+
const errors: string[] = [];
|
|
291
|
+
if (!isUpdate) {
|
|
292
|
+
${requiredChecks}
|
|
293
|
+
}
|
|
294
|
+
${validationRules}
|
|
295
|
+
return errors;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function sanitize(body: Record<string, unknown>): Record<string, unknown> {
|
|
299
|
+
return {
|
|
300
|
+
${sanitizeFields}
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// ── GET (with pagination) ────────────────────────────────────────────
|
|
305
|
+
export async function GET(request: NextRequest) {
|
|
306
|
+
try {
|
|
307
|
+
const ip = getClientIP(request);
|
|
308
|
+
if (!checkRateLimit(ip)) {
|
|
309
|
+
return NextResponse.json({ error: 'Too many requests' }, { status: 429 });
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const { searchParams } = new URL(request.url);
|
|
313
|
+
const page = Math.max(1, parseInt(searchParams.get('page') || '1'));
|
|
314
|
+
const limit = Math.min(100, Math.max(1, parseInt(searchParams.get('limit') || '50')));
|
|
315
|
+
const skip = (page - 1) * limit;
|
|
316
|
+
|
|
317
|
+
const [items, total] = await Promise.all([
|
|
318
|
+
prisma.${prismaName}.findMany({
|
|
319
|
+
orderBy: { createdAt: 'desc' },${includeClause}
|
|
320
|
+
skip,
|
|
321
|
+
take: limit,
|
|
322
|
+
}),
|
|
323
|
+
prisma.${prismaName}.count(),
|
|
324
|
+
]);
|
|
325
|
+
|
|
326
|
+
return NextResponse.json({
|
|
327
|
+
data: items,
|
|
328
|
+
pagination: { page, limit, total, totalPages: Math.ceil(total / limit) },
|
|
329
|
+
});
|
|
330
|
+
} catch (error) {
|
|
331
|
+
console.error('[${modelName}] GET error:', error);
|
|
332
|
+
return NextResponse.json({ error: 'Failed to fetch ${modelName}s' }, { status: 500 });
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// ── POST ─────────────────────────────────────────────────────────────
|
|
337
|
+
export async function POST(request: NextRequest) {
|
|
338
|
+
try {
|
|
339
|
+
const ip = getClientIP(request);
|
|
340
|
+
if (!checkRateLimit(ip)) {
|
|
341
|
+
return NextResponse.json({ error: 'Too many requests' }, { status: 429 });
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
let body: Record<string, unknown>;
|
|
345
|
+
try { body = await request.json(); }
|
|
346
|
+
catch { return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }); }
|
|
347
|
+
|
|
348
|
+
const errors = validate(body);
|
|
349
|
+
if (errors.length > 0) {
|
|
350
|
+
return NextResponse.json({ error: 'Validation failed', details: errors }, { status: 400 });
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const item = await prisma.${prismaName}.create({
|
|
354
|
+
data: sanitize(body) as any,${includeOnCreate}
|
|
355
|
+
});
|
|
356
|
+
return NextResponse.json(item, { status: 201 });
|
|
357
|
+
} catch (error) {
|
|
358
|
+
console.error('[${modelName}] POST error:', error);
|
|
359
|
+
return NextResponse.json({ error: 'Failed to create ${modelName}' }, { status: 500 });
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// ── PUT ──────────────────────────────────────────────────────────────
|
|
364
|
+
export async function PUT(request: NextRequest) {
|
|
365
|
+
try {
|
|
366
|
+
const ip = getClientIP(request);
|
|
367
|
+
if (!checkRateLimit(ip)) {
|
|
368
|
+
return NextResponse.json({ error: 'Too many requests' }, { status: 429 });
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
let body: Record<string, unknown>;
|
|
372
|
+
try { body = await request.json(); }
|
|
373
|
+
catch { return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }); }
|
|
374
|
+
|
|
375
|
+
const { id, ...data } = body;
|
|
376
|
+
if (!id || typeof id !== 'string') {
|
|
377
|
+
return NextResponse.json({ error: 'Valid ID is required' }, { status: 400 });
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const errors = validate(data, true);
|
|
381
|
+
if (errors.length > 0) {
|
|
382
|
+
return NextResponse.json({ error: 'Validation failed', details: errors }, { status: 400 });
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const item = await prisma.${prismaName}.update({
|
|
386
|
+
where: { id: id as string },
|
|
387
|
+
data: sanitize(data) as any,${includeClause}
|
|
388
|
+
});
|
|
389
|
+
return NextResponse.json(item);
|
|
390
|
+
} catch (error: any) {
|
|
391
|
+
if (error?.code === 'P2025') return NextResponse.json({ error: '${modelName} not found' }, { status: 404 });
|
|
392
|
+
console.error('[${modelName}] PUT error:', error);
|
|
393
|
+
return NextResponse.json({ error: 'Failed to update ${modelName}' }, { status: 500 });
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// ── DELETE ───────────────────────────────────────────────────────────
|
|
398
|
+
export async function DELETE(request: NextRequest) {
|
|
399
|
+
try {
|
|
400
|
+
const ip = getClientIP(request);
|
|
401
|
+
if (!checkRateLimit(ip)) {
|
|
402
|
+
return NextResponse.json({ error: 'Too many requests' }, { status: 429 });
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const { searchParams } = new URL(request.url);
|
|
406
|
+
const id = searchParams.get('id');
|
|
407
|
+
if (!id) return NextResponse.json({ error: 'ID is required' }, { status: 400 });
|
|
408
|
+
|
|
409
|
+
await prisma.${prismaName}.delete({ where: { id } });
|
|
410
|
+
return NextResponse.json({ success: true });
|
|
411
|
+
} catch (error: any) {
|
|
412
|
+
if (error?.code === 'P2025') return NextResponse.json({ error: '${modelName} not found' }, { status: 404 });
|
|
413
|
+
console.error('[${modelName}] DELETE error:', error);
|
|
414
|
+
return NextResponse.json({ error: 'Failed to delete ${modelName}' }, { status: 500 });
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
`;
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Generate UI page component from view
|
|
421
|
+
* FIX: Uses camelCase for Prisma, adapts to model fields instead of hardcoding is_completed
|
|
422
|
+
*/
|
|
423
|
+
function generateUIPage(view, strand, allStrands, themeClasses) {
|
|
424
|
+
const viewName = view.name;
|
|
425
|
+
const modelName = strand.name;
|
|
426
|
+
const lowerName = modelName.toLowerCase();
|
|
427
|
+
// Theme-aware classes with fallbacks to glassmorphism defaults
|
|
428
|
+
const tc = themeClasses || {
|
|
429
|
+
primaryButton: 'bg-indigo-600 hover:bg-indigo-500 text-white',
|
|
430
|
+
secondaryButton: 'bg-white/5 hover:bg-white/10 border border-white/10 text-gray-300',
|
|
431
|
+
card: 'glass',
|
|
432
|
+
text: 'text-white',
|
|
433
|
+
textMuted: 'text-gray-400',
|
|
434
|
+
heading: 'text-white',
|
|
435
|
+
badge: 'bg-amber-500/20 text-amber-300 border border-amber-500/30',
|
|
436
|
+
statusColors: { success: '#10b981', warning: '#f59e0b', info: '#6366f1' },
|
|
437
|
+
};
|
|
438
|
+
const fields = strand.fields.map(f => f.name);
|
|
439
|
+
const relations = strand.relations || [];
|
|
440
|
+
const belongsToRelations = relations.filter(r => !r.isMany);
|
|
441
|
+
const hasManyRelations = relations.filter(r => r.isMany);
|
|
442
|
+
const booleanField = strand.fields.find(f => f.type === 'Boolean' && (f.name.includes('complet') || f.name.includes('done') || f.name.includes('active') || f.name.includes('finished')));
|
|
443
|
+
const hasBooleanToggle = !!booleanField;
|
|
444
|
+
const toggleFieldName = booleanField?.name || '';
|
|
445
|
+
const toggleFunction = hasBooleanToggle ? `
|
|
446
|
+
const toggleItem = async (item: ${modelName}) => {
|
|
447
|
+
try {
|
|
448
|
+
await fetch('/api/${lowerName}', {
|
|
449
|
+
method: 'PUT',
|
|
450
|
+
headers: { 'Content-Type': 'application/json' },
|
|
451
|
+
body: JSON.stringify({
|
|
452
|
+
id: item.id,
|
|
453
|
+
${toggleFieldName}: !item.${toggleFieldName}
|
|
454
|
+
}),
|
|
455
|
+
});
|
|
456
|
+
fetchItems();
|
|
457
|
+
} catch (error) {
|
|
458
|
+
console.error('Failed to update item:', error);
|
|
459
|
+
}
|
|
460
|
+
};` : '';
|
|
461
|
+
const toggleButton = hasBooleanToggle ? `
|
|
462
|
+
<button
|
|
463
|
+
onClick={() => toggleItem(item)}
|
|
464
|
+
className={\`w-6 h-6 rounded-full border-2 flex items-center justify-center transition-colors \${
|
|
465
|
+
item.${toggleFieldName}
|
|
466
|
+
? 'bg-green-500 border-green-500'
|
|
467
|
+
: 'border-gray-400 hover:border-indigo-500'
|
|
468
|
+
}\`}
|
|
469
|
+
>
|
|
470
|
+
{item.${toggleFieldName} && <Check size={14} className="text-white" />}
|
|
471
|
+
</button>` : '';
|
|
472
|
+
const itemTextClass = hasBooleanToggle
|
|
473
|
+
? `\`flex-1 ${tc.text} \${item.${toggleFieldName} ? 'line-through opacity-50' : ''}\``
|
|
474
|
+
: `"flex-1 ${tc.text}"`;
|
|
475
|
+
const lucideImports = hasBooleanToggle
|
|
476
|
+
? `import { Plus, Trash2, Check, X } from 'lucide-react';`
|
|
477
|
+
: `import { Plus, Trash2 } from 'lucide-react';`;
|
|
478
|
+
// Generate relation interfaces and state for belongs-to dropdowns
|
|
479
|
+
const relatedInterfaces = belongsToRelations.map(rel => {
|
|
480
|
+
const targetStrand = allStrands?.find(s => s.name === rel.target);
|
|
481
|
+
const targetFirstField = targetStrand?.fields[0]?.name || 'name';
|
|
482
|
+
return `interface ${rel.target}Option { id: string; ${targetFirstField}: string; }`;
|
|
483
|
+
}).join('\n');
|
|
484
|
+
const relatedState = belongsToRelations.map(rel => {
|
|
485
|
+
return ` const [${rel.name}Options, set${capitalize(rel.name)}Options] = useState<${rel.target}Option[]>([]);`;
|
|
486
|
+
}).join('\n');
|
|
487
|
+
const relatedFetches = belongsToRelations.map(rel => {
|
|
488
|
+
const targetLower = rel.target.toLowerCase();
|
|
489
|
+
return `
|
|
490
|
+
fetch('/api/${targetLower}').then(r => r.json()).then(d => set${capitalize(rel.name)}Options(d.data || d)).catch(() => {});`;
|
|
491
|
+
}).join('');
|
|
492
|
+
const relatedFormState = belongsToRelations.map(rel => {
|
|
493
|
+
return ` const [selected${capitalize(rel.name)}, setSelected${capitalize(rel.name)}] = useState('');`;
|
|
494
|
+
}).join('\n');
|
|
495
|
+
const relatedFormFields = belongsToRelations.map(rel => {
|
|
496
|
+
const targetStrand = allStrands?.find(s => s.name === rel.target);
|
|
497
|
+
const targetFirstField = targetStrand?.fields[0]?.name || 'name';
|
|
498
|
+
return `
|
|
499
|
+
<select
|
|
500
|
+
value={selected${capitalize(rel.name)}}
|
|
501
|
+
onChange={(e) => setSelected${capitalize(rel.name)}(e.target.value)}
|
|
502
|
+
className="rounded-lg px-4 py-3 focus:outline-none focus:ring-2"
|
|
503
|
+
>
|
|
504
|
+
<option value="">Select ${rel.name}...</option>
|
|
505
|
+
{${rel.name}Options.map((opt) => (
|
|
506
|
+
<option key={opt.id} value={opt.id}>{opt.${targetFirstField}}</option>
|
|
507
|
+
))}
|
|
508
|
+
</select>`;
|
|
509
|
+
}).join('');
|
|
510
|
+
const relatedCreateData = belongsToRelations.map(rel => {
|
|
511
|
+
return `${rel.name}Id: selected${capitalize(rel.name)} || undefined, `;
|
|
512
|
+
}).join('');
|
|
513
|
+
const relatedResetState = belongsToRelations.map(rel => {
|
|
514
|
+
return `\n setSelected${capitalize(rel.name)}('');`;
|
|
515
|
+
}).join('');
|
|
516
|
+
// Display related data in list items
|
|
517
|
+
const relatedDisplayBelongsTo = belongsToRelations.map(rel => {
|
|
518
|
+
const targetStrand = allStrands?.find(s => s.name === rel.target);
|
|
519
|
+
const targetFirstField = targetStrand?.fields[0]?.name || 'name';
|
|
520
|
+
return `
|
|
521
|
+
{item.${rel.name} && (
|
|
522
|
+
<span className="text-xs ${tc.badge} px-2 py-1 rounded">
|
|
523
|
+
{item.${rel.name}.${targetFirstField}}
|
|
524
|
+
</span>
|
|
525
|
+
)}`;
|
|
526
|
+
}).join('');
|
|
527
|
+
const relatedDisplayHasMany = hasManyRelations.map(rel => {
|
|
528
|
+
return `
|
|
529
|
+
{item.${rel.name} && item.${rel.name}.length > 0 && (
|
|
530
|
+
<span className="text-xs bg-emerald-500/20 text-emerald-300 px-2 py-1 rounded">
|
|
531
|
+
{item.${rel.name}.length} ${rel.name}
|
|
532
|
+
</span>
|
|
533
|
+
)}`;
|
|
534
|
+
}).join('');
|
|
535
|
+
// Build interface with relations
|
|
536
|
+
const relatedInterfaceFields = belongsToRelations.map(rel => {
|
|
537
|
+
const targetStrand = allStrands?.find(s => s.name === rel.target);
|
|
538
|
+
const targetFirstField = targetStrand?.fields[0]?.name || 'name';
|
|
539
|
+
return ` ${rel.name}Id?: string;\n ${rel.name}?: { id: string; ${targetFirstField}: string };`;
|
|
540
|
+
}).join('\n');
|
|
541
|
+
const hasManyInterfaceFields = hasManyRelations.map(rel => {
|
|
542
|
+
return ` ${rel.name}?: { id: string }[];`;
|
|
543
|
+
}).join('\n');
|
|
544
|
+
return `// Generated by Helix v11.0
|
|
545
|
+
'use client';
|
|
546
|
+
|
|
547
|
+
import { useState, useEffect } from 'react';
|
|
548
|
+
${lucideImports}
|
|
549
|
+
|
|
550
|
+
interface ${modelName} {
|
|
551
|
+
id: string;
|
|
552
|
+
${strand.fields.map(f => ` ${f.name}: ${tsType(f.type)};`).join('\n')}
|
|
553
|
+
${relatedInterfaceFields}
|
|
554
|
+
${hasManyInterfaceFields}
|
|
555
|
+
createdAt: string;
|
|
556
|
+
}
|
|
557
|
+
${relatedInterfaces}
|
|
558
|
+
|
|
559
|
+
export default function ${viewName}Page() {
|
|
560
|
+
const [items, setItems] = useState<${modelName}[]>([]);
|
|
561
|
+
const [loading, setLoading] = useState(true);
|
|
562
|
+
const [newItem, setNewItem] = useState('');
|
|
563
|
+
${relatedState}
|
|
564
|
+
${relatedFormState}
|
|
565
|
+
|
|
566
|
+
useEffect(() => {
|
|
567
|
+
fetchItems();${relatedFetches}
|
|
568
|
+
}, []);
|
|
569
|
+
|
|
570
|
+
const fetchItems = async () => {
|
|
571
|
+
try {
|
|
572
|
+
const res = await fetch('/api/${lowerName}');
|
|
573
|
+
const json = await res.json();
|
|
574
|
+
setItems(json.data || json);
|
|
575
|
+
} catch (error) {
|
|
576
|
+
console.error('Failed to fetch items:', error);
|
|
577
|
+
} finally {
|
|
578
|
+
setLoading(false);
|
|
579
|
+
}
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
const createItem = async () => {
|
|
583
|
+
if (!newItem.trim()) return;
|
|
584
|
+
|
|
585
|
+
try {
|
|
586
|
+
const res = await fetch('/api/${lowerName}', {
|
|
587
|
+
method: 'POST',
|
|
588
|
+
headers: { 'Content-Type': 'application/json' },
|
|
589
|
+
body: JSON.stringify({ ${fields[0]}: newItem, ${relatedCreateData}}),
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
if (res.ok) {
|
|
593
|
+
setNewItem('');${relatedResetState}
|
|
594
|
+
fetchItems();
|
|
595
|
+
}
|
|
596
|
+
} catch (error) {
|
|
597
|
+
console.error('Failed to create item:', error);
|
|
598
|
+
}
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
const deleteItem = async (id: string) => {
|
|
602
|
+
try {
|
|
603
|
+
await fetch(\`/api/${lowerName}?id=\${id}\`, { method: 'DELETE' });
|
|
604
|
+
fetchItems();
|
|
605
|
+
} catch (error) {
|
|
606
|
+
console.error('Failed to delete item:', error);
|
|
607
|
+
}
|
|
608
|
+
};
|
|
609
|
+
${toggleFunction}
|
|
610
|
+
|
|
611
|
+
if (loading) {
|
|
612
|
+
return (
|
|
613
|
+
<div className="min-h-screen flex items-center justify-center">
|
|
614
|
+
<div className="text-xl">Loading...</div>
|
|
615
|
+
</div>
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
return (
|
|
620
|
+
<div className="min-h-screen p-8">
|
|
621
|
+
<div className="max-w-2xl mx-auto">
|
|
622
|
+
<h1 className="text-4xl font-bold mb-8 ${tc.heading}">${viewName}</h1>
|
|
623
|
+
|
|
624
|
+
{/* Add New Item */}
|
|
625
|
+
<div className="flex gap-4 mb-8 flex-wrap">
|
|
626
|
+
<input
|
|
627
|
+
type="text"
|
|
628
|
+
value={newItem}
|
|
629
|
+
onChange={(e) => setNewItem(e.target.value)}
|
|
630
|
+
onKeyDown={(e) => e.key === 'Enter' && createItem()}
|
|
631
|
+
placeholder="Add new ${lowerName}..."
|
|
632
|
+
className="flex-1 min-w-[200px]"
|
|
633
|
+
/>${relatedFormFields}
|
|
634
|
+
<button
|
|
635
|
+
onClick={createItem}
|
|
636
|
+
className="${tc.primaryButton} px-6 py-3 rounded-lg flex items-center gap-2 transition-colors"
|
|
637
|
+
>
|
|
638
|
+
<Plus size={20} />
|
|
639
|
+
Add
|
|
640
|
+
</button>
|
|
641
|
+
</div>
|
|
642
|
+
|
|
643
|
+
{/* Items List */}
|
|
644
|
+
<div className="space-y-3">
|
|
645
|
+
{items.map((item) => (
|
|
646
|
+
<div
|
|
647
|
+
key={item.id}
|
|
648
|
+
className="glass p-4 flex items-center gap-4 group hover:opacity-90 transition-all"
|
|
649
|
+
>${toggleButton}
|
|
650
|
+
|
|
651
|
+
<span className={${itemTextClass}}>
|
|
652
|
+
{item.${fields[0]}}
|
|
653
|
+
</span>
|
|
654
|
+
${relatedDisplayBelongsTo}${relatedDisplayHasMany}
|
|
655
|
+
|
|
656
|
+
<button
|
|
657
|
+
onClick={() => deleteItem(item.id)}
|
|
658
|
+
className="opacity-0 group-hover:opacity-100 text-red-400 hover:text-red-300 transition-opacity"
|
|
659
|
+
>
|
|
660
|
+
<Trash2 size={18} />
|
|
661
|
+
</button>
|
|
662
|
+
</div>
|
|
663
|
+
))}
|
|
664
|
+
|
|
665
|
+
{items.length === 0 && (
|
|
666
|
+
<div className="text-center opacity-50 py-12 ${tc.textMuted}">
|
|
667
|
+
No items yet. Add your first ${lowerName}!
|
|
668
|
+
</div>
|
|
669
|
+
)}
|
|
670
|
+
</div>
|
|
671
|
+
</div>
|
|
672
|
+
</div>
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
`;
|
|
676
|
+
}
|
|
677
|
+
function capitalize(str) {
|
|
678
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
679
|
+
}
|
|
680
|
+
function tsType(helixType) {
|
|
681
|
+
const typeMap = {
|
|
682
|
+
'String': 'string',
|
|
683
|
+
'Int': 'number',
|
|
684
|
+
'Float': 'number',
|
|
685
|
+
'Boolean': 'boolean',
|
|
686
|
+
'DateTime': 'string',
|
|
687
|
+
'Date': 'string',
|
|
688
|
+
};
|
|
689
|
+
return typeMap[helixType] || 'string';
|
|
690
|
+
}
|
|
691
|
+
//# sourceMappingURL=parser.js.map
|