webspresso 0.0.97 → 0.0.98

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/.agents/ADMIN.md +153 -5
  2. package/.agents/ARCHITECTURE.md +10 -0
  3. package/.agents/CLI.md +195 -1
  4. package/.agents/PLUGINS.md +153 -9
  5. package/.agents/QUEUE.md +43 -0
  6. package/bin/commands/add-esbuild.js +80 -0
  7. package/bin/commands/add-tailwind.js +76 -31
  8. package/bin/commands/analytics-prune.js +57 -0
  9. package/bin/commands/api.js +207 -15
  10. package/bin/commands/build.js +99 -0
  11. package/bin/commands/dev.js +64 -35
  12. package/bin/commands/doctor.js +26 -0
  13. package/bin/commands/email-prune.js +47 -0
  14. package/bin/commands/make-job.js +86 -0
  15. package/bin/commands/make-model.js +250 -0
  16. package/bin/commands/new.js +45 -28
  17. package/bin/commands/openapi.js +102 -0
  18. package/bin/commands/page.js +6 -3
  19. package/bin/commands/queue-prune.js +59 -0
  20. package/bin/commands/routes.js +52 -0
  21. package/bin/commands/service.js +6 -3
  22. package/bin/commands/typegen.js +106 -0
  23. package/bin/commands/upload-prune.js +79 -0
  24. package/bin/webspresso.js +22 -0
  25. package/core/applySchema.js +13 -6
  26. package/core/auth/hash.js +21 -12
  27. package/core/compression/middleware.js +23 -17
  28. package/core/openapi/build-from-api-routes.js +160 -29
  29. package/core/orm/cache/fingerprint.js +15 -1
  30. package/core/orm/cache/memory-provider.js +27 -2
  31. package/core/orm/index.js +12 -7
  32. package/core/orm/model.js +49 -7
  33. package/core/queue/adapters/database.js +94 -0
  34. package/core/queue/adapters/memory.js +62 -0
  35. package/core/queue/adapters/redis.js +80 -0
  36. package/core/queue/manager.js +123 -0
  37. package/core/ssr/stream.js +11 -0
  38. package/index.d.ts +660 -10
  39. package/index.js +44 -96
  40. package/package.json +1 -1
  41. package/plugins/admin-panel/api.js +279 -17
  42. package/plugins/admin-panel/app.js +114 -22
  43. package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +314 -9
  44. package/plugins/admin-panel/client/parts/04-field-renderers.js +264 -67
  45. package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +10 -5
  46. package/plugins/admin-panel/client/parts/06-login-setup-forms.js +119 -28
  47. package/plugins/admin-panel/client/parts/07-model-list.js +49 -8
  48. package/plugins/admin-panel/client/parts/08-record-list.js +243 -23
  49. package/plugins/admin-panel/client/parts/09-record-form.js +59 -12
  50. package/plugins/admin-panel/client/parts/10-export-registry.js +23 -0
  51. package/plugins/admin-panel/core/admin-module.js +13 -7
  52. package/plugins/admin-panel/core/api-extensions.js +30 -2
  53. package/plugins/admin-panel/core/registry.js +96 -16
  54. package/plugins/admin-panel/index.js +96 -0
  55. package/plugins/admin-panel/lib/login-rate-limiter.js +119 -0
  56. package/plugins/admin-panel/modules/admin-users.js +16 -1
  57. package/plugins/admin-panel/modules/bulk-actions.js +13 -5
  58. package/plugins/admin-panel/modules/custom-pages.js +492 -3
  59. package/plugins/admin-panel/modules/dashboard.js +283 -0
  60. package/plugins/admin-panel/modules/menu.js +56 -18
  61. package/plugins/admin-panel/modules/user-management.js +24 -6
  62. package/plugins/audit-log/admin-component.js +11 -0
  63. package/plugins/audit-log/api-handlers.js +34 -0
  64. package/plugins/audit-log/index.js +129 -3
  65. package/plugins/audit-log/purge.js +60 -10
  66. package/plugins/dashboard/app.js +152 -27
  67. package/plugins/dashboard/index.js +143 -39
  68. package/plugins/dashboard/styles.js +78 -0
  69. package/plugins/data-exchange/export-xlsx.js +9 -2
  70. package/plugins/data-exchange/import.js +9 -2
  71. package/plugins/data-exchange/index.js +6 -3
  72. package/plugins/email/index.js +132 -2
  73. package/plugins/email/purge.js +60 -10
  74. package/plugins/index.js +4 -0
  75. package/plugins/orm-cache-admin/index.js +4 -2
  76. package/plugins/queue/index.js +11 -0
  77. package/plugins/schema-explorer/admin-component.js +714 -0
  78. package/plugins/schema-explorer/relations-graph.js +205 -0
  79. package/plugins/schema-explorer.js +248 -69
  80. package/plugins/site-analytics/admin-component.js +23 -0
  81. package/plugins/site-analytics/api-handlers.js +42 -0
  82. package/plugins/site-analytics/index.js +147 -2
  83. package/plugins/site-analytics/purge.js +168 -0
  84. package/plugins/swagger.js +261 -13
  85. package/plugins/upload/index.js +26 -0
  86. package/plugins/upload/orphan-pruner.js +292 -0
  87. package/src/api/api-loader.js +3 -1
  88. package/src/bundle/index.js +517 -0
  89. package/src/bundle/manifest.js +165 -0
  90. package/src/dev/banner.js +296 -0
  91. package/src/dev/colors.js +70 -0
  92. package/src/dev/index.js +62 -0
  93. package/src/dev/profiler.js +354 -0
  94. package/src/dev/typegen.js +497 -0
  95. package/src/dev-reload/index.js +611 -0
  96. package/src/file-router.js +210 -62
  97. package/src/helpers.js +958 -98
  98. package/src/njk-frontmatter.js +12 -0
  99. package/src/pages/page-loader.js +61 -11
  100. package/src/server.js +629 -63
@@ -0,0 +1,714 @@
1
+ /**
2
+ * Schema Explorer Admin SPA Component (Mithril.js)
3
+ * Clean, modern, zero-CDN interface for exploring database tables, columns, constraints, and relationships.
4
+ */
5
+
6
+ 'use strict';
7
+
8
+ /**
9
+ * Generates the Mithril client-side component code as a string
10
+ * @param {Object} [options]
11
+ * @param {string} [options.apiPrefix='/schema']
12
+ * @returns {string} JavaScript client code
13
+ */
14
+ function generateSchemaExplorerAdminComponent(options = {}) {
15
+ const { apiPrefix = '/schema' } = options;
16
+
17
+ return `
18
+ (function() {
19
+
20
+ function getSchemaApi(method, path) {
21
+ var adminPath = window.__ADMIN_PATH__ || '/_admin';
22
+ var url = adminPath + '/api${apiPrefix}' + (path || '');
23
+ return fetch(url, {
24
+ method: method || 'GET',
25
+ credentials: 'include',
26
+ headers: { 'Accept': 'application/json' }
27
+ }).then(function(res) {
28
+ if (!res.ok) {
29
+ return res.json().catch(function() { return { error: res.statusText }; }).then(function(err) {
30
+ throw new Error(err.error || 'Failed to load schema data (' + res.status + ')');
31
+ });
32
+ }
33
+ return res.json();
34
+ });
35
+ }
36
+
37
+ function copyTextToClipboard(text, onSuccess) {
38
+ if (navigator.clipboard && navigator.clipboard.writeText) {
39
+ navigator.clipboard.writeText(text).then(function() {
40
+ if (typeof onSuccess === 'function') onSuccess();
41
+ }).catch(function() {
42
+ fallbackCopy(text, onSuccess);
43
+ });
44
+ } else {
45
+ fallbackCopy(text, onSuccess);
46
+ }
47
+ }
48
+
49
+ function fallbackCopy(text, onSuccess) {
50
+ var ta = document.createElement('textarea');
51
+ ta.value = text;
52
+ ta.style.position = 'fixed';
53
+ ta.style.opacity = '0';
54
+ document.body.appendChild(ta);
55
+ ta.select();
56
+ try {
57
+ document.execCommand('copy');
58
+ if (typeof onSuccess === 'function') onSuccess();
59
+ } catch (e) {
60
+ console.error('Copy failed:', e);
61
+ }
62
+ document.body.removeChild(ta);
63
+ }
64
+
65
+ function downloadFile(content, fileName, contentType) {
66
+ var blob = new Blob([content], { type: contentType || 'text/plain;charset=utf-8' });
67
+ var url = URL.createObjectURL(blob);
68
+ var a = document.createElement('a');
69
+ a.href = url;
70
+ a.download = fileName;
71
+ document.body.appendChild(a);
72
+ a.click();
73
+ setTimeout(function() {
74
+ document.body.removeChild(a);
75
+ URL.revokeObjectURL(url);
76
+ }, 100);
77
+ }
78
+
79
+ var SchemaExplorerPage = {
80
+ oninit: function(vnode) {
81
+ vnode.state.loading = true;
82
+ vnode.state.error = null;
83
+ vnode.state.models = [];
84
+ vnode.state.relationships = [];
85
+ vnode.state.mermaid = '';
86
+ vnode.state.activeTab = 'details'; // 'details' | 'graph' | 'mermaid'
87
+ vnode.state.selectedModelName = null;
88
+ vnode.state.searchQuery = '';
89
+ vnode.state.selectedGraphModel = null;
90
+ vnode.state.copiedMermaid = false;
91
+ vnode.state.copiedJson = false;
92
+
93
+ this.loadData(vnode);
94
+ },
95
+
96
+ loadData: function(vnode) {
97
+ vnode.state.loading = true;
98
+ vnode.state.error = null;
99
+
100
+ getSchemaApi('GET', '')
101
+ .then(function(data) {
102
+ vnode.state.models = data.models || [];
103
+ vnode.state.relationships = data.relationships || [];
104
+ vnode.state.mermaid = data.mermaid || '';
105
+
106
+ // Auto-select first model if none selected
107
+ if (!vnode.state.selectedModelName && vnode.state.models.length > 0) {
108
+ vnode.state.selectedModelName = vnode.state.models[0].name;
109
+ }
110
+
111
+ vnode.state.loading = false;
112
+ m.redraw();
113
+ })
114
+ .catch(function(err) {
115
+ vnode.state.error = err.message || String(err);
116
+ vnode.state.loading = false;
117
+ m.redraw();
118
+ });
119
+ },
120
+
121
+ selectModel: function(vnode, name) {
122
+ vnode.state.selectedModelName = name;
123
+ vnode.state.activeTab = 'details';
124
+ m.redraw();
125
+ },
126
+
127
+ copyMermaid: function(vnode) {
128
+ var self = this;
129
+ copyTextToClipboard(vnode.state.mermaid, function() {
130
+ vnode.state.copiedMermaid = true;
131
+ m.redraw();
132
+ setTimeout(function() {
133
+ vnode.state.copiedMermaid = false;
134
+ m.redraw();
135
+ }, 2000);
136
+ });
137
+ },
138
+
139
+ downloadJson: function(vnode) {
140
+ var payload = JSON.stringify({
141
+ models: vnode.state.models,
142
+ relationships: vnode.state.relationships,
143
+ generatedAt: new Date().toISOString()
144
+ }, null, 2);
145
+ downloadFile(payload, 'database-schema.json', 'application/json');
146
+ },
147
+
148
+ view: function(vnode) {
149
+ var s = vnode.state;
150
+ var self = this;
151
+
152
+ // Filter models by search query
153
+ var q = (s.searchQuery || '').toLowerCase().trim();
154
+ var filteredModels = s.models.filter(function(m) {
155
+ if (!q) return true;
156
+ if (m.name.toLowerCase().indexOf(q) !== -1) return true;
157
+ if (m.table.toLowerCase().indexOf(q) !== -1) return true;
158
+ if (m.columns && m.columns.some(function(c) { return c.name.toLowerCase().indexOf(q) !== -1; })) return true;
159
+ return false;
160
+ });
161
+
162
+ // Active selected model
163
+ var selectedModel = s.models.find(function(m) { return m.name === s.selectedModelName; }) || filteredModels[0] || null;
164
+
165
+ // Compute stats
166
+ var totalColumns = s.models.reduce(function(acc, m) { return acc + (m.columns ? m.columns.length : 0); }, 0);
167
+ var totalRelations = s.relationships.length;
168
+
169
+ return m('div.space-y-6', [
170
+ // 1. Top Header Banner
171
+ m('div.bg-gradient-to-r.from-slate-900.via-indigo-950.to-slate-900.text-white.p-6.rounded-2xl.shadow-lg.border.border-slate-800', [
172
+ m('div.flex.flex-col.md:flex-row.md:items-center.justify-between.gap-4', [
173
+ m('div.flex.items-center.space-x-4', [
174
+ m('div.w-12.h-12.rounded-xl.bg-indigo-600.bg-opacity-80.flex.items-center.justify-center.text-2xl.shadow-inner.flex-shrink-0', '🗄️'),
175
+ m('div', [
176
+ m('h1.text-2xl.font-extrabold.tracking-tight', 'Database Schema & Relations Explorer'),
177
+ m('p.text-sm.text-slate-300.mt-1', 'Explore database tables, column constraints, and relationships interactively.'),
178
+ ]),
179
+ ]),
180
+ m('div.flex.flex-wrap.items-center.gap-2', [
181
+ m('button.inline-flex.items-center.px-3.py-2.rounded-lg.text-xs.font-semibold.bg-slate-800.hover:bg-slate-700.text-slate-200.border.border-slate-700.transition-colors.shadow-sm', {
182
+ onclick: function() { self.loadData(vnode); },
183
+ title: 'Refresh Schema'
184
+ }, [
185
+ m('span.mr-1.5', '🔄'),
186
+ 'Refresh'
187
+ ]),
188
+ m('button.inline-flex.items-center.px-3.py-2.rounded-lg.text-xs.font-semibold.bg-indigo-600.hover:bg-indigo-500.text-white.transition-colors.shadow-sm', {
189
+ onclick: function() { self.copyMermaid(vnode); },
190
+ title: 'Copy Mermaid ER Code'
191
+ }, [
192
+ m('span.mr-1.5', s.copiedMermaid ? '✓' : '📋'),
193
+ s.copiedMermaid ? 'Copied!' : 'Copy Mermaid'
194
+ ]),
195
+ m('button.inline-flex.items-center.px-3.py-2.rounded-lg.text-xs.font-semibold.bg-slate-800.hover:bg-slate-700.text-slate-200.border.border-slate-700.transition-colors.shadow-sm', {
196
+ onclick: function() { self.downloadJson(vnode); },
197
+ title: 'Download Schema as JSON'
198
+ }, [
199
+ m('span.mr-1.5', '💾'),
200
+ 'Download JSON'
201
+ ]),
202
+ ]),
203
+ ]),
204
+
205
+ // Stat Counter Badges
206
+ m('div.grid.grid-cols-2.sm:grid-cols-4.gap-3.mt-6.pt-4.border-t.border-slate-800/80', [
207
+ renderStatPill('📦 Models', s.models.length + ' Tables'),
208
+ renderStatPill('📑 Total Columns', totalColumns + ' Columns'),
209
+ renderStatPill('🔗 Relationships', totalRelations + ' Connections'),
210
+ renderStatPill('⚡ Status', s.loading ? 'Loading…' : 'Active & Synced'),
211
+ ]),
212
+ ]),
213
+
214
+ // Loading & Error States
215
+ s.loading && m('div.flex.items-center.justify-center.py-16.bg-white.dark:bg-slate-800.rounded-xl.shadow-sm.border.border-gray-200.dark:border-slate-700', [
216
+ m('div.text-center', [
217
+ m('div.inline-block.animate-spin.rounded-full.h-8.w-8.border-4.border-indigo-500.border-t-transparent.mb-3'),
218
+ m('p.text-sm.text-gray-500.dark:text-slate-400', 'Scanning schemas and relationships…'),
219
+ ])
220
+ ]),
221
+
222
+ s.error && m('div.p-4.rounded-xl.bg-red-50.dark:bg-red-950/40.border.border-red-200.dark:border-red-800.text-red-700.dark:text-red-300.flex.items-start.space-x-3', [
223
+ m('span.text-xl', '⚠️'),
224
+ m('div', [
225
+ m('h4.font-bold.text-sm', 'Schema Loading Error'),
226
+ m('p.text-xs.mt-1', s.error),
227
+ m('button.mt-2.text-xs.underline.font-semibold', { onclick: function() { self.loadData(vnode); } }, 'Retry'),
228
+ ])
229
+ ]),
230
+
231
+ !s.loading && !s.error && m('div.space-y-6', [
232
+ // 2. Tab Navigation
233
+ m('div.border-b.border-gray-200.dark:border-slate-700.flex.space-x-4', [
234
+ renderTabButton('details', '📋 Table & Column Details', s.activeTab, function() { s.activeTab = 'details'; }),
235
+ renderTabButton('graph', '🌐 Relationship Map (ER)', s.activeTab, function() { s.activeTab = 'graph'; }),
236
+ renderTabButton('mermaid', '🧜‍♂️ Mermaid ER Code', s.activeTab, function() { s.activeTab = 'mermaid'; }),
237
+ ]),
238
+
239
+ // 3. Tab Contents
240
+ s.activeTab === 'details' && renderDetailsTab(vnode, filteredModels, selectedModel, self),
241
+ s.activeTab === 'graph' && renderGraphTab(vnode, self),
242
+ s.activeTab === 'mermaid' && renderMermaidTab(vnode, self),
243
+ ])
244
+ ]);
245
+ }
246
+ };
247
+
248
+ function renderStatPill(label, val) {
249
+ return m('div.bg-slate-800/60.rounded-xl.p-3.border.border-slate-700/50', [
250
+ m('div.text-xs.text-slate-400.font-medium', label),
251
+ m('div.text-base.font-bold.text-white.mt-0.5', val),
252
+ ]);
253
+ }
254
+
255
+ function renderTabButton(id, title, activeTab, onClick) {
256
+ var isActive = activeTab === id;
257
+ return m('button.py-3.px-4.border-b-2.text-sm.font-medium.transition-colors.flex.items-center.space-x-2', {
258
+ class: isActive
259
+ ? 'border-indigo-600 text-indigo-600 dark:text-indigo-400 font-semibold'
260
+ : 'border-transparent text-gray-500 hover:text-gray-700 dark:text-slate-400 dark:hover:text-slate-200',
261
+ onclick: onClick
262
+ }, title);
263
+ }
264
+
265
+ /** TAB 1: Table & Column Details */
266
+ function renderDetailsTab(vnode, filteredModels, selectedModel, self) {
267
+ var s = vnode.state;
268
+
269
+ return m('div.grid.grid-cols-1.lg:grid-cols-12.gap-6', [
270
+ // Left: Table List
271
+ m('div.lg:col-span-4.space-y-4', [
272
+ m('div.bg-white.dark:bg-slate-800.rounded-xl.shadow-sm.border.border-gray-200.dark:border-slate-700.p-4', [
273
+ m('div.relative.mb-3', [
274
+ m('input.w-full.bg-gray-50.dark:bg-slate-900.border.border-gray-200.dark:border-slate-700.rounded-lg.px-3.py-2.text-sm.text-gray-900.dark:text-slate-100.placeholder-gray-400.focus:outline-none.focus:ring-2.focus:ring-indigo-500', {
275
+ placeholder: '🔍 Search tables or columns…',
276
+ value: s.searchQuery,
277
+ oninput: function(e) { s.searchQuery = e.target.value; }
278
+ }),
279
+ s.searchQuery && m('button.absolute.right-2.top-2.text-gray-400.hover:text-gray-600.text-xs', {
280
+ onclick: function() { s.searchQuery = ''; }
281
+ }, '✕')
282
+ ]),
283
+
284
+ m('div.text-xs.font-semibold.uppercase.tracking-wider.text-gray-400.dark:text-slate-500.mb-2',
285
+ 'Tables (' + filteredModels.length + ')'
286
+ ),
287
+
288
+ m('div.space-y-1.5.max-h-[600px].overflow-y-auto.pr-1', filteredModels.map(function(model) {
289
+ var isSelected = selectedModel && selectedModel.name === model.name;
290
+ var colCount = (model.columns || []).length;
291
+ var outCount = (model.outgoingRelations || []).length;
292
+ var inCount = (model.incomingRelations || []).length;
293
+
294
+ return m('div.p-3.rounded-xl.cursor-pointer.transition-all.border', {
295
+ class: isSelected
296
+ ? 'bg-indigo-50.dark:bg-indigo-950/40.border-indigo-400.dark:border-indigo-600.shadow-sm'
297
+ : 'bg-white.dark:bg-slate-800.border-gray-200.dark:border-slate-700/60.hover:border-indigo-300.hover:bg-gray-50.dark:hover:bg-slate-700/50',
298
+ onclick: function() { self.selectModel(vnode, model.name); }
299
+ }, [
300
+ m('div.flex.items-center.justify-between', [
301
+ m('span.font-bold.text-sm', {
302
+ class: isSelected ? 'text-indigo-900 dark:text-indigo-200' : 'text-gray-900 dark:text-slate-100'
303
+ }, model.name),
304
+ m('span.text-xs.font-mono.px-2.py-0.5.rounded-md.bg-gray-100.dark:bg-slate-700.text-gray-600.dark:text-slate-300',
305
+ model.table
306
+ ),
307
+ ]),
308
+ m('div.flex.items-center.space-x-3.mt-2.text-xs.text-gray-500.dark:text-slate-400', [
309
+ m('span', colCount + ' cols'),
310
+ (outCount + inCount > 0) && m('span.text-indigo-600.dark:text-indigo-400.font-medium',
311
+ '🔗 ' + (outCount + inCount) + ' relations'
312
+ ),
313
+ model.scopes && model.scopes.softDelete && m('span.text-amber-600.dark:text-amber-400', '🗑️ SoftDel'),
314
+ ])
315
+ ]);
316
+ }))
317
+ ])
318
+ ]),
319
+
320
+ // Right: Selected Model Schema Details
321
+ m('div.lg:col-span-8.space-y-6', [
322
+ !selectedModel ? m('div.p-12.text-center.bg-white.dark:bg-slate-800.rounded-xl.border.border-gray-200.dark:border-slate-700', [
323
+ m('p.text-gray-500.dark:text-slate-400', 'Select a table on the left to view details.')
324
+ ]) : [
325
+ // Model Header Card
326
+ m('div.bg-white.dark:bg-slate-800.rounded-xl.shadow-sm.border.border-gray-200.dark:border-slate-700.p-6', [
327
+ m('div.flex.flex-col.sm:flex-row.sm:items-center.justify-between.gap-4', [
328
+ m('div', [
329
+ m('div.flex.items-center.space-x-3', [
330
+ m('h2.text-xl.font-extrabold.text-gray-900.dark:text-slate-100', selectedModel.name),
331
+ m('span.text-xs.font-mono.px-2.5.py-1.rounded-lg.bg-slate-100.dark:bg-slate-700.text-slate-700.dark:text-slate-200.border.border-slate-200.dark:border-slate-600',
332
+ 'table: ' + selectedModel.table
333
+ ),
334
+ ]),
335
+ m('p.text-xs.text-gray-500.dark:text-slate-400.mt-1',
336
+ 'Primary Key: ' + (selectedModel.primaryKey || 'id') + ' • Total ' + (selectedModel.columns || []).length + ' Columns'
337
+ ),
338
+ ]),
339
+
340
+ // Scopes Badges
341
+ m('div.flex.flex-wrap.gap-2', [
342
+ selectedModel.scopes && selectedModel.scopes.timestamps
343
+ ? m('span.text-xs.font-medium.px-2.5.py-1.rounded-md.bg-emerald-50.dark:bg-emerald-950/40.text-emerald-700.dark:text-emerald-300.border.border-emerald-200.dark:border-emerald-800', '⏱️ Timestamps')
344
+ : null,
345
+ selectedModel.scopes && selectedModel.scopes.softDelete
346
+ ? m('span.text-xs.font-medium.px-2.5.py-1.rounded-md.bg-amber-50.dark:bg-amber-950/40.text-amber-700.dark:text-amber-300.border.border-amber-200.dark:border-amber-800', '🗑️ Soft Delete')
347
+ : null,
348
+ selectedModel.scopes && selectedModel.scopes.tenant
349
+ ? m('span.text-xs.font-medium.px-2.5.py-1.rounded-md.bg-purple-50.dark:bg-purple-950/40.text-purple-700.dark:text-purple-300.border.border-purple-200.dark:border-purple-800', '🏢 Multi-Tenant')
350
+ : null,
351
+ ])
352
+ ]),
353
+ ]),
354
+
355
+ // Columns Breakdown
356
+ m('div.bg-white.dark:bg-slate-800.rounded-xl.shadow-sm.border.border-gray-200.dark:border-slate-700.overflow-hidden', [
357
+ m('div.px-6.py-4.border-b.border-gray-200.dark:border-slate-700.flex.items-center.justify-between', [
358
+ m('h3.text-base.font-bold.text-gray-900.dark:text-slate-100', 'Column Definitions'),
359
+ m('span.text-xs.text-gray-500.dark:text-slate-400', (selectedModel.columns || []).length + ' fields')
360
+ ]),
361
+
362
+ m('div.overflow-x-auto', [
363
+ m('table.min-w-full.divide-y.divide-gray-200.dark:divide-slate-700.text-left.text-sm', [
364
+ m('thead.bg-gray-50.dark:bg-slate-900/60', [
365
+ m('tr', [
366
+ m('th.px-4.py-3.text-xs.font-semibold.text-gray-500.dark:text-slate-400.uppercase', 'Column'),
367
+ m('th.px-4.py-3.text-xs.font-semibold.text-gray-500.dark:text-slate-400.uppercase', 'Data Type'),
368
+ m('th.px-4.py-3.text-xs.font-semibold.text-gray-500.dark:text-slate-400.uppercase', 'Nullable'),
369
+ m('th.px-4.py-3.text-xs.font-semibold.text-gray-500.dark:text-slate-400.uppercase', 'Constraints'),
370
+ m('th.px-4.py-3.text-xs.font-semibold.text-gray-500.dark:text-slate-400.uppercase', 'Default'),
371
+ ])
372
+ ]),
373
+ m('tbody.divide-y.divide-gray-100.dark:divide-slate-700/60', (selectedModel.columns || []).map(function(col) {
374
+ var isPk = col.primary || col.name === selectedModel.primaryKey;
375
+ var isFk = col.references || (selectedModel.outgoingRelations || []).some(function(r) { return r.foreignKey === col.name; });
376
+
377
+ return m('tr.hover:bg-gray-50.dark:hover:bg-slate-700/30.transition-colors', [
378
+ // Name + Icon
379
+ m('td.px-4.py-3.font-mono.text-sm.font-semibold.text-gray-900.dark:text-slate-100.flex.items-center.space-x-2', [
380
+ isPk && m('span.text-amber-500', { title: 'Primary Key' }, '🔑'),
381
+ !isPk && isFk && m('span.text-indigo-500', { title: 'Foreign Key' }, '🔗'),
382
+ m('span', col.name)
383
+ ]),
384
+
385
+ // Type pill
386
+ m('td.px-4.py-3', [
387
+ renderTypeBadge(col)
388
+ ]),
389
+
390
+ // Nullable
391
+ m('td.px-4.py-3.text-xs', [
392
+ col.nullable
393
+ ? m('span.px-2.py-0.5.rounded.bg-amber-50.dark:bg-amber-950/40.text-amber-700.dark:text-amber-300.font-medium', 'NULL')
394
+ : m('span.px-2.py-0.5.rounded.bg-gray-100.dark:bg-slate-700.text-gray-700.dark:text-slate-300.font-medium', 'NOT NULL')
395
+ ]),
396
+
397
+ // Constraints
398
+ m('td.px-4.py-3', [
399
+ m('div.flex.flex-wrap.gap-1.5', [
400
+ isPk && m('span.text-2xs.font-bold.px-2.py-0.5.rounded.bg-amber-100.dark:bg-amber-900/60.text-amber-800.dark:text-amber-200', 'PK'),
401
+ col.unique && m('span.text-2xs.font-bold.px-2.py-0.5.rounded.bg-purple-100.dark:bg-purple-900/60.text-purple-800.dark:text-purple-200', 'UNIQUE'),
402
+ col.index && m('span.text-2xs.font-bold.px-2.py-0.5.rounded.bg-slate-100.dark:bg-slate-700.text-slate-700.dark:text-slate-300', 'INDEX'),
403
+ col.autoIncrement && m('span.text-2xs.font-bold.px-2.py-0.5.rounded.bg-blue-100.dark:bg-blue-900/60.text-blue-800.dark:text-blue-200', 'AUTO_INC'),
404
+ col.references && m('span.text-2xs.font-bold.px-2.py-0.5.rounded.bg-cyan-100.dark:bg-cyan-900/60.text-cyan-800.dark:text-cyan-200', 'FK ➔ ' + col.references),
405
+ !isPk && !col.unique && !col.index && !col.references && !col.autoIncrement && m('span.text-xs.text-gray-400', '—')
406
+ ])
407
+ ]),
408
+
409
+ // Default
410
+ m('td.px-4.py-3.font-mono.text-xs.text-gray-600.dark:text-slate-400',
411
+ col.default !== undefined && col.default !== null ? String(col.default) : '—'
412
+ )
413
+ ]);
414
+ }))
415
+ ])
416
+ ])
417
+ ]),
418
+
419
+ // Relationships Card
420
+ renderModelRelationsSection(vnode, selectedModel, self)
421
+ ]
422
+ ])
423
+ ]);
424
+ }
425
+
426
+ function renderTypeBadge(col) {
427
+ var type = col.type || 'string';
428
+ var label = type;
429
+ if (col.maxLength) label += '(' + col.maxLength + ')';
430
+ else if (col.precision) label += '(' + col.precision + (col.scale ? ',' + col.scale : '') + ')';
431
+ else if (col.enumValues && col.enumValues.length) label += '[' + col.enumValues.slice(0, 3).join(',') + '…]';
432
+
433
+ var bg = 'bg-slate-100 text-slate-800 dark:bg-slate-700 dark:text-slate-200';
434
+ if (type === 'integer' || type === 'bigint' || type === 'float' || type === 'decimal') {
435
+ bg = 'bg-blue-50 text-blue-700 dark:bg-blue-950/60 dark:text-blue-300 border border-blue-200 dark:border-blue-800';
436
+ } else if (type === 'string' || type === 'text') {
437
+ bg = 'bg-emerald-50 text-emerald-700 dark:bg-emerald-950/60 dark:text-emerald-300 border border-emerald-200 dark:border-emerald-800';
438
+ } else if (type === 'boolean') {
439
+ bg = 'bg-amber-50 text-amber-700 dark:bg-amber-950/60 dark:text-amber-300 border border-amber-200 dark:border-amber-800';
440
+ } else if (type === 'timestamp' || type === 'date') {
441
+ bg = 'bg-purple-50 text-purple-700 dark:bg-purple-950/60 dark:text-purple-300 border border-purple-200 dark:border-purple-800';
442
+ } else if (type === 'json' || type === 'jsonb') {
443
+ bg = 'bg-pink-50 text-pink-700 dark:bg-pink-950/60 dark:text-pink-300 border border-pink-200 dark:border-pink-800';
444
+ }
445
+
446
+ return m('span.px-2.py-0.5.rounded-md.font-mono.text-xs.font-medium', { class: bg }, label);
447
+ }
448
+
449
+ function renderModelRelationsSection(vnode, selectedModel, self) {
450
+ var outgoing = selectedModel.outgoingRelations || [];
451
+ var incoming = selectedModel.incomingRelations || [];
452
+
453
+ return m('div.bg-white.dark:bg-slate-800.rounded-xl.shadow-sm.border.border-gray-200.dark:border-slate-700.p-6.space-y-6', [
454
+ m('div.flex.items-center.justify-between.border-b.border-gray-200.dark:border-slate-700.pb-4', [
455
+ m('h3.text-base.font-bold.text-gray-900.dark:text-slate-100', 'Model Relationships'),
456
+ m('span.text-xs.text-gray-500.dark:text-slate-400', (outgoing.length + incoming.length) + ' total connections')
457
+ ]),
458
+
459
+ outgoing.length === 0 && incoming.length === 0 && m('p.text-sm.text-gray-500.dark:text-slate-400.py-4',
460
+ 'No relationships defined or detected for this model.'
461
+ ),
462
+
463
+ // Outgoing Relations
464
+ outgoing.length > 0 && m('div.space-y-3', [
465
+ m('h4.text-xs.font-bold.uppercase.tracking-wider.text-indigo-600.dark:text-indigo-400',
466
+ '↗ Outgoing Relations (' + selectedModel.name + ' references)'
467
+ ),
468
+ m('div.grid.grid-cols-1.md:grid-cols-2.gap-3', outgoing.map(function(rel) {
469
+ return m('div.p-4.rounded-xl.border.border-gray-200.dark:border-slate-700.bg-gray-50.dark:bg-slate-900/40.space-y-2', [
470
+ m('div.flex.items-center.justify-between', [
471
+ m('span.font-bold.text-sm.text-gray-900.dark:text-slate-100', rel.name),
472
+ renderRelationTypeBadge(rel.type),
473
+ ]),
474
+ m('div.text-xs.text-gray-600.dark:text-slate-300.font-mono', [
475
+ m('span.text-gray-500', rel.foreignKey || 'id'),
476
+ m('span.mx-1.5.text-indigo-500', '➔'),
477
+ m('span.font-semibold', (rel.targetModel || rel.targetTable) + '.' + (rel.localKey || 'id')),
478
+ ]),
479
+ rel.targetModel && m('button.text-xs.text-indigo-600.dark:text-indigo-400.font-semibold.hover:underline.pt-1', {
480
+ onclick: function() { self.selectModel(vnode, rel.targetModel); }
481
+ }, 'Go to Table: ' + rel.targetModel + ' ➔')
482
+ ]);
483
+ }))
484
+ ]),
485
+
486
+ // Incoming Relations
487
+ incoming.length > 0 && m('div.space-y-3.pt-2', [
488
+ m('h4.text-xs.font-bold.uppercase.tracking-wider.text-emerald-600.dark:text-emerald-400',
489
+ '↙ Incoming Relations (Other models referencing ' + selectedModel.name + ')'
490
+ ),
491
+ m('div.grid.grid-cols-1.md:grid-cols-2.gap-3', incoming.map(function(rel) {
492
+ return m('div.p-4.rounded-xl.border.border-gray-200.dark:border-slate-700.bg-gray-50.dark:bg-slate-900/40.space-y-2', [
493
+ m('div.flex.items-center.justify-between', [
494
+ m('span.font-bold.text-sm.text-gray-900.dark:text-slate-100', rel.sourceModel),
495
+ renderRelationTypeBadge(rel.type),
496
+ ]),
497
+ m('div.text-xs.text-gray-600.dark:text-slate-300.font-mono', [
498
+ m('span.font-semibold', rel.sourceTable + '.' + (rel.foreignKey || 'id')),
499
+ m('span.mx-1.5.text-emerald-500', '➔'),
500
+ m('span.text-gray-500', selectedModel.table + '.' + (rel.localKey || 'id')),
501
+ ]),
502
+ m('button.text-xs.text-emerald-600.dark:text-emerald-400.font-semibold.hover:underline.pt-1', {
503
+ onclick: function() { self.selectModel(vnode, rel.sourceModel); }
504
+ }, 'Go to Table: ' + rel.sourceModel + ' ➔')
505
+ ]);
506
+ }))
507
+ ])
508
+ ]);
509
+ }
510
+
511
+ function renderRelationTypeBadge(type) {
512
+ var t = type || 'belongsTo';
513
+ var bg = 'bg-indigo-100 text-indigo-800 dark:bg-indigo-950/60 dark:text-indigo-300';
514
+ if (t === 'hasMany') bg = 'bg-emerald-100 text-emerald-800 dark:bg-emerald-950/60 dark:text-emerald-300';
515
+ else if (t === 'hasOne') bg = 'bg-purple-100 text-purple-800 dark:bg-purple-950/60 dark:text-purple-300';
516
+ else if (t === 'manyToMany') bg = 'bg-amber-100 text-amber-800 dark:bg-amber-950/60 dark:text-amber-300';
517
+
518
+ return m('span.px-2.py-0.5.rounded-md.text-2xs.font-bold.uppercase.tracking-wide', { class: bg }, t);
519
+ }
520
+
521
+ /** TAB 2: Visual Relationship Map (ER) */
522
+ function renderGraphTab(vnode, self) {
523
+ var s = vnode.state;
524
+ var sel = s.selectedGraphModel;
525
+
526
+ return m('div.space-y-6', [
527
+ // Info Banner
528
+ m('div.p-4.rounded-xl.bg-indigo-50.dark:bg-indigo-950/30.border.border-indigo-100.dark:border-indigo-900.flex.items-center.justify-between', [
529
+ m('div.text-xs.text-indigo-900.dark:text-indigo-300', [
530
+ m('span.font-bold', '💡 Tip: '),
531
+ 'Click on any table card to highlight all its direct relationships.'
532
+ ]),
533
+ sel && m('button.text-xs.font-bold.text-indigo-600.dark:text-indigo-400.hover:underline', {
534
+ onclick: function() { s.selectedGraphModel = null; }
535
+ }, 'Clear Selection ✕')
536
+ ]),
537
+
538
+ // Visual Cards Grid
539
+ m('div.grid.grid-cols-1.md:grid-cols-2.xl:grid-cols-3.gap-5', s.models.map(function(model) {
540
+ var isDirectlySelected = sel === model.name;
541
+ var isConnected = false;
542
+ if (sel) {
543
+ var activeModel = s.models.find(function(m) { return m.name === sel; });
544
+ if (activeModel && activeModel.connectedModels && activeModel.connectedModels.indexOf(model.name) !== -1) {
545
+ isConnected = true;
546
+ }
547
+ }
548
+
549
+ var cardBorder = 'border-gray-200 dark:border-slate-700';
550
+ var cardOpacity = 'opacity-100';
551
+ if (sel) {
552
+ if (isDirectlySelected) {
553
+ cardBorder = 'border-indigo-500 ring-2 ring-indigo-500 shadow-md';
554
+ } else if (isConnected) {
555
+ cardBorder = 'border-emerald-500 ring-2 ring-emerald-400/60 shadow-sm';
556
+ } else {
557
+ cardOpacity = 'opacity-40 hover:opacity-100';
558
+ }
559
+ }
560
+
561
+ var outRels = model.outgoingRelations || [];
562
+ var inRels = model.incomingRelations || [];
563
+
564
+ return m('div.rounded-2xl.border.bg-white.dark:bg-slate-800.shadow-sm.p-5.transition-all.cursor-pointer', {
565
+ class: cardBorder + ' ' + cardOpacity,
566
+ onclick: function() {
567
+ s.selectedGraphModel = (s.selectedGraphModel === model.name) ? null : model.name;
568
+ }
569
+ }, [
570
+ // Card Header
571
+ m('div.flex.items-center.justify-between.border-b.border-gray-100.dark:border-slate-700/80.pb-3.mb-3', [
572
+ m('div', [
573
+ m('div.font-extrabold.text-base.text-gray-900.dark:text-slate-100', model.name),
574
+ m('div.text-xs.font-mono.text-gray-500.dark:text-slate-400', model.table),
575
+ ]),
576
+ m('span.text-xs.font-bold.px-2.py-0.5.rounded.bg-amber-100.dark:bg-amber-950/60.text-amber-800.dark:text-amber-200',
577
+ 'PK: ' + (model.primaryKey || 'id')
578
+ )
579
+ ]),
580
+
581
+ // Column preview (top 6 columns)
582
+ m('div.space-y-1.text-xs.font-mono.py-1', (model.columns || []).slice(0, 6).map(function(c) {
583
+ var isFk = c.references || outRels.some(function(r) { return r.foreignKey === c.name; });
584
+ var isPk = c.primary || c.name === model.primaryKey;
585
+ return m('div.flex.items-center.justify-between.py-0.5.px-1.5.rounded', {
586
+ class: isPk ? 'bg-amber-50/50 dark:bg-amber-950/20' : (isFk ? 'bg-indigo-50/50 dark:bg-indigo-950/20' : '')
587
+ }, [
588
+ m('span.truncate', [
589
+ isPk ? '🔑 ' : (isFk ? '🔗 ' : '• '),
590
+ c.name
591
+ ]),
592
+ m('span.text-gray-400.text-2xs', c.type)
593
+ ]);
594
+ })),
595
+
596
+ (model.columns || []).length > 6 && m('div.text-2xs.text-gray-400.text-center.pt-1',
597
+ '+ ' + ((model.columns || []).length - 6) + ' more columns…'
598
+ ),
599
+
600
+ // Relations footer
601
+ m('div.mt-4.pt-3.border-t.border-gray-100.dark:border-slate-700/80.flex.flex-col.gap-2', [
602
+ m('div.flex.items-center.justify-between.text-xs', [
603
+ m('span.text-gray-500.dark:text-slate-400', 'Relations:'),
604
+ m('span.font-semibold.text-indigo-600.dark:text-indigo-400', (outRels.length + inRels.length) + ' connections')
605
+ ]),
606
+
607
+ m('div.flex.flex-wrap.gap-1', [
608
+ outRels.map(function(r) {
609
+ return m('span.text-2xs.font-semibold.px-2.py-0.5.rounded.bg-indigo-50.dark:bg-indigo-950/60.text-indigo-700.dark:text-indigo-300',
610
+ '➔ ' + (r.targetModel || r.targetTable)
611
+ );
612
+ }),
613
+ inRels.map(function(r) {
614
+ return m('span.text-2xs.font-semibold.px-2.py-0.5.rounded.bg-emerald-50.dark:bg-emerald-950/60.text-emerald-700.dark:text-emerald-300',
615
+ '⬅ ' + r.sourceModel
616
+ );
617
+ }),
618
+ ]),
619
+
620
+ m('button.mt-1.w-full.py-1.5.rounded-lg.bg-gray-100.dark:bg-slate-700.hover:bg-indigo-50.dark:hover:bg-indigo-950/40.text-gray-700.dark:text-slate-200.hover:text-indigo-600.text-xs.font-semibold.transition-colors', {
621
+ onclick: function(e) {
622
+ e.stopPropagation();
623
+ self.selectModel(vnode, model.name);
624
+ }
625
+ }, 'View Details ➔')
626
+ ])
627
+ ]);
628
+ })),
629
+
630
+ // Full Relationships Matrix / Table
631
+ m('div.bg-white.dark:bg-slate-800.rounded-xl.shadow-sm.border.border-gray-200.dark:border-slate-700.overflow-hidden', [
632
+ m('div.px-6.py-4.border-b.border-gray-200.dark:border-slate-700', [
633
+ m('h3.text-base.font-bold.text-gray-900.dark:text-slate-100', 'All Relationships Matrix'),
634
+ m('p.text-xs.text-gray-500.dark:text-slate-400.mt-0.5', 'All model connections and foreign key mappings detected in database.')
635
+ ]),
636
+
637
+ m('div.overflow-x-auto', [
638
+ m('table.min-w-full.divide-y.divide-gray-200.dark:divide-slate-700.text-left.text-sm', [
639
+ m('thead.bg-gray-50.dark:bg-slate-900/60', [
640
+ m('tr', [
641
+ m('th.px-4.py-3.text-xs.font-semibold.text-gray-500.dark:text-slate-400.uppercase', 'Source Model / Table'),
642
+ m('th.px-4.py-3.text-xs.font-semibold.text-gray-500.dark:text-slate-400.uppercase', 'Relation'),
643
+ m('th.px-4.py-3.text-xs.font-semibold.text-gray-500.dark:text-slate-400.uppercase', 'Type'),
644
+ m('th.px-4.py-3.text-xs.font-semibold.text-gray-500.dark:text-slate-400.uppercase', 'Target Model / Table'),
645
+ m('th.px-4.py-3.text-xs.font-semibold.text-gray-500.dark:text-slate-400.uppercase', 'Key Mapping'),
646
+ ])
647
+ ]),
648
+ m('tbody.divide-y.divide-gray-100.dark:divide-slate-700/60', s.relationships.map(function(r) {
649
+ return m('tr.hover:bg-gray-50.dark:hover:bg-slate-700/30.transition-colors', [
650
+ m('td.px-4.py-3', [
651
+ m('button.font-bold.text-indigo-600.dark:text-indigo-400.hover:underline', {
652
+ onclick: function() { self.selectModel(vnode, r.sourceModel); }
653
+ }, r.sourceModel),
654
+ m('span.text-xs.font-mono.text-gray-400.ml-1.5', '(' + r.sourceTable + ')')
655
+ ]),
656
+ m('td.px-4.py-3.font-mono.text-xs.font-semibold', r.name),
657
+ m('td.px-4.py-3', renderRelationTypeBadge(r.type)),
658
+ m('td.px-4.py-3', [
659
+ r.targetModel ? m('button.font-bold.text-emerald-600.dark:text-emerald-400.hover:underline', {
660
+ onclick: function() { self.selectModel(vnode, r.targetModel); }
661
+ }, r.targetModel) : m('span.text-gray-500', r.targetTable || '—'),
662
+ r.targetTable && m('span.text-xs.font-mono.text-gray-400.ml-1.5', '(' + r.targetTable + ')')
663
+ ]),
664
+ m('td.px-4.py-3.font-mono.text-xs.text-gray-600.dark:text-slate-300', [
665
+ m('span', r.sourceTable + '.' + (r.foreignKey || 'id')),
666
+ m('span.text-indigo-500.mx-1', '➔'),
667
+ m('span', (r.targetTable || r.targetModel) + '.' + (r.localKey || 'id')),
668
+ ])
669
+ ]);
670
+ }))
671
+ ])
672
+ ])
673
+ ])
674
+ ]);
675
+ }
676
+
677
+ /** TAB 3: Mermaid ER Diagram Code */
678
+ function renderMermaidTab(vnode, self) {
679
+ var s = vnode.state;
680
+
681
+ return m('div.space-y-4', [
682
+ m('div.bg-slate-900.text-white.p-6.rounded-2xl.border.border-slate-800.shadow-md.space-y-4', [
683
+ m('div.flex.items-center.justify-between', [
684
+ m('div', [
685
+ m('h3.text-lg.font-bold', 'Mermaid.js Entity-Relationship Diagram'),
686
+ m('p.text-xs.text-slate-400.mt-1',
687
+ 'Copy and paste this code block into GitHub READMEs, Markdown documentation, or the mermaid.live editor.'
688
+ ),
689
+ ]),
690
+ m('button.px-4.py-2.rounded-lg.bg-indigo-600.hover:bg-indigo-500.text-white.text-xs.font-bold.transition-colors.shadow-sm', {
691
+ onclick: function() { self.copyMermaid(vnode); }
692
+ }, [
693
+ m('span.mr-1.5', s.copiedMermaid ? '✓' : '📋'),
694
+ s.copiedMermaid ? 'Copied!' : 'Copy Code'
695
+ ])
696
+ ]),
697
+
698
+ m('div.bg-slate-950.rounded-xl.p-4.border.border-slate-800.overflow-x-auto', [
699
+ m('pre.font-mono.text-xs.text-emerald-400.leading-relaxed', s.mermaid || '// No schema data available yet')
700
+ ])
701
+ ])
702
+ ]);
703
+ }
704
+
705
+ window.__customPages = window.__customPages || {};
706
+ window.__customPages['schema-explorer'] = SchemaExplorerPage;
707
+
708
+ })();
709
+ `;
710
+ }
711
+
712
+ module.exports = {
713
+ generateSchemaExplorerAdminComponent,
714
+ };