@sonicjs-cms/core 2.0.4 → 2.0.5

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 (44) hide show
  1. package/dist/{chunk-LEG4KNFP.cjs → chunk-3JMOWGUU.cjs} +20 -2
  2. package/dist/chunk-3JMOWGUU.cjs.map +1 -0
  3. package/dist/{chunk-LH4Z7QID.js → chunk-6FR25MPC.js} +111 -3
  4. package/dist/chunk-6FR25MPC.js.map +1 -0
  5. package/dist/{chunk-3NVJ6W27.cjs → chunk-DOR2IU73.cjs} +111 -2
  6. package/dist/chunk-DOR2IU73.cjs.map +1 -0
  7. package/dist/{chunk-M6FPVS7E.js → chunk-G5KY3WJV.js} +16 -29
  8. package/dist/chunk-G5KY3WJV.js.map +1 -0
  9. package/dist/{chunk-CDBVZEWR.js → chunk-HSRPDEQQ.js} +20 -2
  10. package/dist/chunk-HSRPDEQQ.js.map +1 -0
  11. package/dist/{chunk-4BJGEGX5.cjs → chunk-IM5SDXOE.cjs} +19 -32
  12. package/dist/chunk-IM5SDXOE.cjs.map +1 -0
  13. package/dist/{chunk-CQ2VMJQO.js → chunk-LGC3TNCY.js} +252 -84
  14. package/dist/chunk-LGC3TNCY.js.map +1 -0
  15. package/dist/{chunk-RZW752PE.cjs → chunk-NPWWR6RI.cjs} +359 -191
  16. package/dist/chunk-NPWWR6RI.cjs.map +1 -0
  17. package/dist/{chunk-BRPONFW6.cjs → chunk-TRSHFTF6.cjs} +3 -3
  18. package/dist/{chunk-BRPONFW6.cjs.map → chunk-TRSHFTF6.cjs.map} +1 -1
  19. package/dist/{chunk-WKGONLHK.js → chunk-VSLEA22M.js} +3 -3
  20. package/dist/{chunk-WKGONLHK.js.map → chunk-VSLEA22M.js.map} +1 -1
  21. package/dist/index.cjs +876 -127
  22. package/dist/index.cjs.map +1 -1
  23. package/dist/index.js +759 -9
  24. package/dist/index.js.map +1 -1
  25. package/dist/middleware.cjs +23 -23
  26. package/dist/middleware.js +2 -2
  27. package/dist/routes.cjs +25 -25
  28. package/dist/routes.js +5 -5
  29. package/dist/services.cjs +25 -21
  30. package/dist/services.js +2 -2
  31. package/dist/utils.cjs +11 -11
  32. package/dist/utils.js +1 -1
  33. package/migrations/006_plugin_system.sql +2 -2
  34. package/migrations/011_config_managed_collections.sql +1 -0
  35. package/migrations/018_settings_table.sql +23 -0
  36. package/package.json +1 -1
  37. package/dist/chunk-3NVJ6W27.cjs.map +0 -1
  38. package/dist/chunk-4BJGEGX5.cjs.map +0 -1
  39. package/dist/chunk-CDBVZEWR.js.map +0 -1
  40. package/dist/chunk-CQ2VMJQO.js.map +0 -1
  41. package/dist/chunk-LEG4KNFP.cjs.map +0 -1
  42. package/dist/chunk-LH4Z7QID.js.map +0 -1
  43. package/dist/chunk-M6FPVS7E.js.map +0 -1
  44. package/dist/chunk-RZW752PE.cjs.map +0 -1
package/dist/index.js CHANGED
@@ -1,21 +1,743 @@
1
- import { api_default, api_media_default, api_system_default, admin_api_default, router, adminCollectionsRoutes, adminSettingsRoutes, admin_content_default, adminMediaRoutes, adminPluginRoutes, adminLogsRoutes, userRoutes, auth_default } from './chunk-CQ2VMJQO.js';
2
- export { ROUTES_INFO, admin_api_default as adminApiRoutes, adminCheckboxRoutes, admin_code_examples_default as adminCodeExamplesRoutes, adminCollectionsRoutes, admin_content_default as adminContentRoutes, router as adminDashboardRoutes, adminDesignRoutes, admin_faq_default as adminFAQRoutes, adminLogsRoutes, adminMediaRoutes, adminPluginRoutes, adminSettingsRoutes, admin_testimonials_default as adminTestimonialsRoutes, userRoutes as adminUsersRoutes, api_content_crud_default as apiContentCrudRoutes, api_media_default as apiMediaRoutes, api_default as apiRoutes, api_system_default as apiSystemRoutes, auth_default as authRoutes } from './chunk-CQ2VMJQO.js';
3
- import { schema_exports } from './chunk-LH4Z7QID.js';
4
- export { Logger, apiTokens, collections, content, contentVersions, getLogger, initLogger, insertCollectionSchema, insertContentSchema, insertLogConfigSchema, insertMediaSchema, insertPluginActivityLogSchema, insertPluginAssetSchema, insertPluginHookSchema, insertPluginRouteSchema, insertPluginSchema, insertSystemLogSchema, insertUserSchema, insertWorkflowHistorySchema, logConfig, media, pluginActivityLog, pluginAssets, pluginHooks, pluginRoutes, plugins, selectCollectionSchema, selectContentSchema, selectLogConfigSchema, selectMediaSchema, selectPluginActivityLogSchema, selectPluginAssetSchema, selectPluginHookSchema, selectPluginRouteSchema, selectPluginSchema, selectSystemLogSchema, selectUserSchema, selectWorkflowHistorySchema, systemLogs, users, workflowHistory } from './chunk-LH4Z7QID.js';
5
- import { metricsMiddleware, bootstrapMiddleware } from './chunk-M6FPVS7E.js';
6
- export { AuthManager, PermissionManager, bootstrapMiddleware, cacheHeaders, compressionMiddleware, detailedLoggingMiddleware, getActivePlugins, isPluginActive, logActivity, loggingMiddleware, optionalAuth, performanceLoggingMiddleware, requireActivePlugin, requireActivePlugins, requireAnyPermission, requireAuth, requirePermission, requireRole, securityHeaders, securityLoggingMiddleware } from './chunk-M6FPVS7E.js';
7
- export { MigrationService, PluginBootstrapService, PluginService as PluginServiceClass, cleanupRemovedCollections, fullCollectionSync, getAvailableCollectionNames, getManagedCollections, isCollectionManaged, loadCollectionConfig, loadCollectionConfigs, syncCollection, syncCollections, validateCollectionConfig } from './chunk-CDBVZEWR.js';
1
+ import { api_default, api_media_default, api_system_default, admin_api_default, router, adminCollectionsRoutes, adminSettingsRoutes, admin_content_default, adminMediaRoutes, adminPluginRoutes, adminLogsRoutes, userRoutes, auth_default } from './chunk-LGC3TNCY.js';
2
+ export { ROUTES_INFO, admin_api_default as adminApiRoutes, adminCheckboxRoutes, admin_code_examples_default as adminCodeExamplesRoutes, adminCollectionsRoutes, admin_content_default as adminContentRoutes, router as adminDashboardRoutes, adminDesignRoutes, admin_faq_default as adminFAQRoutes, adminLogsRoutes, adminMediaRoutes, adminPluginRoutes, adminSettingsRoutes, admin_testimonials_default as adminTestimonialsRoutes, userRoutes as adminUsersRoutes, api_content_crud_default as apiContentCrudRoutes, api_media_default as apiMediaRoutes, api_default as apiRoutes, api_system_default as apiSystemRoutes, auth_default as authRoutes } from './chunk-LGC3TNCY.js';
3
+ import { schema_exports } from './chunk-6FR25MPC.js';
4
+ export { Logger, apiTokens, collections, content, contentVersions, getLogger, initLogger, insertCollectionSchema, insertContentSchema, insertLogConfigSchema, insertMediaSchema, insertPluginActivityLogSchema, insertPluginAssetSchema, insertPluginHookSchema, insertPluginRouteSchema, insertPluginSchema, insertSystemLogSchema, insertUserSchema, insertWorkflowHistorySchema, logConfig, media, pluginActivityLog, pluginAssets, pluginHooks, pluginRoutes, plugins, selectCollectionSchema, selectContentSchema, selectLogConfigSchema, selectMediaSchema, selectPluginActivityLogSchema, selectPluginAssetSchema, selectPluginHookSchema, selectPluginRouteSchema, selectPluginSchema, selectSystemLogSchema, selectUserSchema, selectWorkflowHistorySchema, systemLogs, users, workflowHistory } from './chunk-6FR25MPC.js';
5
+ import { metricsMiddleware, bootstrapMiddleware, requireAuth } from './chunk-G5KY3WJV.js';
6
+ export { AuthManager, PermissionManager, bootstrapMiddleware, cacheHeaders, compressionMiddleware, detailedLoggingMiddleware, getActivePlugins, isPluginActive, logActivity, loggingMiddleware, optionalAuth, performanceLoggingMiddleware, requireActivePlugin, requireActivePlugins, requireAnyPermission, requireAuth, requirePermission, requireRole, securityHeaders, securityLoggingMiddleware } from './chunk-G5KY3WJV.js';
7
+ export { MigrationService, PluginBootstrapService, PluginService as PluginServiceClass, cleanupRemovedCollections, fullCollectionSync, getAvailableCollectionNames, getManagedCollections, isCollectionManaged, loadCollectionConfig, loadCollectionConfigs, syncCollection, syncCollections, validateCollectionConfig } from './chunk-HSRPDEQQ.js';
8
8
  export { renderFilterBar } from './chunk-RYQCT2IV.js';
9
+ import { init_admin_layout_catalyst_template, renderAdminLayoutCatalyst } from './chunk-3LZ6TLPC.js';
9
10
  export { getConfirmationDialogScript, renderAlert, renderConfirmationDialog, renderForm, renderFormField, renderPagination, renderTable } from './chunk-3LZ6TLPC.js';
10
11
  export { HookSystemImpl, HookUtils, PluginManager as PluginManagerClass, PluginRegistryImpl, PluginValidator as PluginValidatorClass, ScopedHookSystem as ScopedHookSystemClass } from './chunk-EAELJXRV.js';
11
- import { package_default, getCoreVersion } from './chunk-WKGONLHK.js';
12
- export { QueryFilterBuilder, SONICJS_VERSION, TemplateRenderer, buildQuery, escapeHtml, getCoreVersion, renderTemplate, sanitizeInput, sanitizeObject, templateRenderer } from './chunk-WKGONLHK.js';
12
+ import { package_default, getCoreVersion } from './chunk-VSLEA22M.js';
13
+ export { QueryFilterBuilder, SONICJS_VERSION, TemplateRenderer, buildQuery, escapeHtml, getCoreVersion, renderTemplate, sanitizeInput, sanitizeObject, templateRenderer } from './chunk-VSLEA22M.js';
13
14
  export { metricsTracker } from './chunk-FICTAGD4.js';
14
15
  export { HOOKS } from './chunk-LOUJRBXV.js';
15
16
  import './chunk-V4OQ3NZ2.js';
16
17
  import { Hono } from 'hono';
17
18
  import { drizzle } from 'drizzle-orm/d1';
18
19
 
20
+ // src/plugins/core-plugins/database-tools-plugin/services/database-service.ts
21
+ var DatabaseToolsService = class {
22
+ constructor(db) {
23
+ this.db = db;
24
+ }
25
+ /**
26
+ * Get database statistics
27
+ */
28
+ async getDatabaseStats() {
29
+ const tables = await this.getTables();
30
+ const stats = {
31
+ tables: [],
32
+ totalRows: 0
33
+ };
34
+ for (const tableName of tables) {
35
+ try {
36
+ const result = await this.db.prepare(`SELECT COUNT(*) as count FROM ${tableName}`).first();
37
+ const rowCount = result?.count || 0;
38
+ stats.tables.push({
39
+ name: tableName,
40
+ rowCount
41
+ });
42
+ stats.totalRows += rowCount;
43
+ } catch (error) {
44
+ console.warn(`Could not count rows in table ${tableName}:`, error);
45
+ }
46
+ }
47
+ return stats;
48
+ }
49
+ /**
50
+ * Get all tables in the database
51
+ */
52
+ async getTables() {
53
+ const result = await this.db.prepare(`
54
+ SELECT name FROM sqlite_master
55
+ WHERE type='table'
56
+ AND name NOT LIKE 'sqlite_%'
57
+ ORDER BY name
58
+ `).all();
59
+ return result.results?.map((row) => row.name) || [];
60
+ }
61
+ /**
62
+ * Truncate all data except admin user
63
+ */
64
+ async truncateAllData(adminEmail) {
65
+ const errors = [];
66
+ const tablesCleared = [];
67
+ let adminUserPreserved = false;
68
+ try {
69
+ const adminUser = await this.db.prepare(
70
+ "SELECT * FROM users WHERE email = ? AND role = ?"
71
+ ).bind(adminEmail, "admin").first();
72
+ if (!adminUser) {
73
+ return {
74
+ success: false,
75
+ message: "Admin user not found. Operation cancelled for safety.",
76
+ tablesCleared: [],
77
+ adminUserPreserved: false,
78
+ errors: ["Admin user not found"]
79
+ };
80
+ }
81
+ const tablesToTruncate = [
82
+ "content",
83
+ "content_versions",
84
+ "content_workflow_status",
85
+ "collections",
86
+ "media",
87
+ "sessions",
88
+ "notifications",
89
+ "api_tokens",
90
+ "workflow_history",
91
+ "scheduled_content",
92
+ "faqs",
93
+ "faq_categories",
94
+ "plugins",
95
+ "plugin_settings",
96
+ "email_templates",
97
+ "email_themes"
98
+ ];
99
+ const existingTables = await this.getTables();
100
+ const tablesToClear = tablesToTruncate.filter(
101
+ (table) => existingTables.includes(table)
102
+ );
103
+ for (const tableName of tablesToClear) {
104
+ try {
105
+ await this.db.prepare(`DELETE FROM ${tableName}`).run();
106
+ tablesCleared.push(tableName);
107
+ } catch (error) {
108
+ errors.push(`Failed to clear table ${tableName}: ${error}`);
109
+ console.error(`Error clearing table ${tableName}:`, error);
110
+ }
111
+ }
112
+ try {
113
+ await this.db.prepare("DELETE FROM users WHERE email != ? OR role != ?").bind(adminEmail, "admin").run();
114
+ const verifyAdmin = await this.db.prepare(
115
+ "SELECT id FROM users WHERE email = ? AND role = ?"
116
+ ).bind(adminEmail, "admin").first();
117
+ adminUserPreserved = !!verifyAdmin;
118
+ tablesCleared.push("users (non-admin)");
119
+ } catch (error) {
120
+ errors.push(`Failed to clear non-admin users: ${error}`);
121
+ console.error("Error clearing non-admin users:", error);
122
+ }
123
+ try {
124
+ await this.db.prepare("DELETE FROM sqlite_sequence").run();
125
+ } catch (error) {
126
+ }
127
+ const message = errors.length > 0 ? `Truncation completed with ${errors.length} errors. ${tablesCleared.length} tables cleared.` : `Successfully truncated database. ${tablesCleared.length} tables cleared.`;
128
+ return {
129
+ success: errors.length === 0,
130
+ message,
131
+ tablesCleared,
132
+ adminUserPreserved,
133
+ errors: errors.length > 0 ? errors : void 0
134
+ };
135
+ } catch (error) {
136
+ return {
137
+ success: false,
138
+ message: `Database truncation failed: ${error}`,
139
+ tablesCleared,
140
+ adminUserPreserved,
141
+ errors: [String(error)]
142
+ };
143
+ }
144
+ }
145
+ /**
146
+ * Create a backup of current data (simplified version)
147
+ */
148
+ async createBackup() {
149
+ try {
150
+ const backupId = `backup_${Date.now()}`;
151
+ const stats = await this.getDatabaseStats();
152
+ console.log(`Backup ${backupId} created with ${stats.totalRows} total rows`);
153
+ return {
154
+ success: true,
155
+ message: `Backup created successfully (${stats.totalRows} rows)`,
156
+ backupId
157
+ };
158
+ } catch (error) {
159
+ return {
160
+ success: false,
161
+ message: `Backup failed: ${error}`
162
+ };
163
+ }
164
+ }
165
+ /**
166
+ * Get table data with optional pagination and sorting
167
+ */
168
+ async getTableData(tableName, limit = 100, offset = 0, sortColumn, sortDirection = "asc") {
169
+ try {
170
+ const tables = await this.getTables();
171
+ if (!tables.includes(tableName)) {
172
+ throw new Error(`Table ${tableName} not found`);
173
+ }
174
+ const pragmaResult = await this.db.prepare(`PRAGMA table_info(${tableName})`).all();
175
+ const columns = pragmaResult.results?.map((col) => col.name) || [];
176
+ if (sortColumn && !columns.includes(sortColumn)) {
177
+ sortColumn = void 0;
178
+ }
179
+ const countResult = await this.db.prepare(`SELECT COUNT(*) as count FROM ${tableName}`).first();
180
+ const totalRows = countResult?.count || 0;
181
+ let query = `SELECT * FROM ${tableName}`;
182
+ if (sortColumn) {
183
+ query += ` ORDER BY ${sortColumn} ${sortDirection.toUpperCase()}`;
184
+ }
185
+ query += ` LIMIT ${limit} OFFSET ${offset}`;
186
+ const dataResult = await this.db.prepare(query).all();
187
+ return {
188
+ tableName,
189
+ columns,
190
+ rows: dataResult.results || [],
191
+ totalRows
192
+ };
193
+ } catch (error) {
194
+ throw new Error(`Failed to fetch table data: ${error}`);
195
+ }
196
+ }
197
+ /**
198
+ * Validate database integrity
199
+ */
200
+ async validateDatabase() {
201
+ const issues = [];
202
+ try {
203
+ const requiredTables = ["users", "content", "collections"];
204
+ const existingTables = await this.getTables();
205
+ for (const table of requiredTables) {
206
+ if (!existingTables.includes(table)) {
207
+ issues.push(`Critical table missing: ${table}`);
208
+ }
209
+ }
210
+ const adminCount = await this.db.prepare(
211
+ "SELECT COUNT(*) as count FROM users WHERE role = ?"
212
+ ).bind("admin").first();
213
+ if (adminCount?.count === 0) {
214
+ issues.push("No admin users found");
215
+ }
216
+ try {
217
+ const integrityResult = await this.db.prepare("PRAGMA integrity_check").first();
218
+ if (integrityResult && integrityResult.integrity_check !== "ok") {
219
+ issues.push(`Database integrity check failed: ${integrityResult.integrity_check}`);
220
+ }
221
+ } catch (error) {
222
+ issues.push(`Could not run integrity check: ${error}`);
223
+ }
224
+ } catch (error) {
225
+ issues.push(`Validation error: ${error}`);
226
+ }
227
+ return {
228
+ valid: issues.length === 0,
229
+ issues
230
+ };
231
+ }
232
+ };
233
+
234
+ // src/templates/pages/admin-database-table.template.ts
235
+ init_admin_layout_catalyst_template();
236
+ function renderDatabaseTablePage(data) {
237
+ const totalPages = Math.ceil(data.totalRows / data.pageSize);
238
+ const startRow = (data.currentPage - 1) * data.pageSize + 1;
239
+ const endRow = Math.min(data.currentPage * data.pageSize, data.totalRows);
240
+ const pageContent = `
241
+ <div class="space-y-6">
242
+ <!-- Header -->
243
+ <div class="flex flex-col sm:flex-row sm:items-center sm:justify-between">
244
+ <div>
245
+ <div class="flex items-center space-x-3">
246
+ <a
247
+ href="/admin/settings/database-tools"
248
+ class="inline-flex items-center text-sm/6 text-zinc-500 dark:text-zinc-400 hover:text-zinc-700 dark:hover:text-zinc-300"
249
+ >
250
+ <svg class="w-5 h-5 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
251
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
252
+ </svg>
253
+ Back to Database Tools
254
+ </a>
255
+ </div>
256
+ <h1 class="mt-2 text-2xl/8 font-semibold text-zinc-950 dark:text-white sm:text-xl/8">Table: ${data.tableName}</h1>
257
+ <p class="mt-2 text-sm/6 text-zinc-500 dark:text-zinc-400">
258
+ Showing ${startRow.toLocaleString()} - ${endRow.toLocaleString()} of ${data.totalRows.toLocaleString()} rows
259
+ </p>
260
+ </div>
261
+ <div class="mt-4 sm:mt-0 flex items-center space-x-3">
262
+ <div class="flex items-center space-x-2">
263
+ <label for="pageSize" class="text-sm font-medium text-zinc-700 dark:text-zinc-300">
264
+ Rows per page:
265
+ </label>
266
+ <select
267
+ id="pageSize"
268
+ onchange="changePageSize(this.value)"
269
+ class="rounded-lg bg-white dark:bg-zinc-800 px-3 py-2 text-sm font-semibold text-zinc-950 dark:text-white ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 hover:bg-zinc-50 dark:hover:bg-zinc-700 transition-colors shadow-sm cursor-pointer"
270
+ >
271
+ <option value="10" ${data.pageSize === 10 ? "selected" : ""}>10</option>
272
+ <option value="20" ${data.pageSize === 20 ? "selected" : ""}>20</option>
273
+ <option value="50" ${data.pageSize === 50 ? "selected" : ""}>50</option>
274
+ <option value="100" ${data.pageSize === 100 ? "selected" : ""}>100</option>
275
+ <option value="200" ${data.pageSize === 200 ? "selected" : ""}>200</option>
276
+ </select>
277
+ </div>
278
+ <button
279
+ onclick="refreshTableData()"
280
+ class="inline-flex items-center justify-center rounded-lg bg-white dark:bg-zinc-800 px-3.5 py-2.5 text-sm font-semibold text-zinc-950 dark:text-white ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 hover:bg-zinc-50 dark:hover:bg-zinc-700 transition-colors shadow-sm"
281
+ >
282
+ <svg class="-ml-0.5 mr-1.5 h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
283
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
284
+ </svg>
285
+ Refresh
286
+ </button>
287
+ </div>
288
+ </div>
289
+
290
+ <!-- Table Card -->
291
+ <div class="rounded-xl bg-white dark:bg-zinc-900 shadow-sm ring-1 ring-zinc-950/5 dark:ring-white/10 overflow-hidden">
292
+ <!-- Table -->
293
+ <div class="overflow-x-auto">
294
+ <table class="min-w-full divide-y divide-zinc-950/10 dark:divide-white/10">
295
+ <thead class="bg-zinc-50 dark:bg-white/5">
296
+ <tr>
297
+ ${data.columns.map((col) => `
298
+ <th
299
+ scope="col"
300
+ class="px-4 py-3.5 text-left text-xs font-semibold text-zinc-950 dark:text-white uppercase tracking-wider cursor-pointer hover:bg-zinc-100 dark:hover:bg-white/10 transition-colors"
301
+ onclick="sortTable('${col}')"
302
+ >
303
+ <div class="flex items-center space-x-1">
304
+ <span>${col}</span>
305
+ ${data.sortColumn === col ? `
306
+ <svg class="w-4 h-4 ${data.sortDirection === "asc" ? "" : "rotate-180"}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
307
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7"/>
308
+ </svg>
309
+ ` : `
310
+ <svg class="w-4 h-4 text-zinc-400 dark:text-zinc-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
311
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16V4m0 0L3 8m4-4l4 4m6 0v12m0 0l4-4m-4 4l-4-4"/>
312
+ </svg>
313
+ `}
314
+ </div>
315
+ </th>
316
+ `).join("")}
317
+ </tr>
318
+ </thead>
319
+ <tbody class="divide-y divide-zinc-950/5 dark:divide-white/5">
320
+ ${data.rows.length > 0 ? data.rows.map((row, idx) => `
321
+ <tr class="${idx % 2 === 0 ? "bg-white dark:bg-zinc-900" : "bg-zinc-50 dark:bg-zinc-900/50"}">
322
+ ${data.columns.map((col) => `
323
+ <td class="px-4 py-3 text-sm text-zinc-700 dark:text-zinc-300 whitespace-nowrap max-w-xs overflow-hidden text-ellipsis" title="${escapeHtml2(String(row[col] ?? ""))}">
324
+ ${formatCellValue(row[col])}
325
+ </td>
326
+ `).join("")}
327
+ </tr>
328
+ `).join("") : `
329
+ <tr>
330
+ <td colspan="${data.columns.length}" class="px-4 py-12 text-center text-sm text-zinc-500 dark:text-zinc-400">
331
+ <svg class="w-12 h-12 mx-auto mb-4 text-zinc-400 dark:text-zinc-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
332
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"/>
333
+ </svg>
334
+ <p>No data in this table</p>
335
+ </td>
336
+ </tr>
337
+ `}
338
+ </tbody>
339
+ </table>
340
+ </div>
341
+
342
+ <!-- Pagination -->
343
+ ${totalPages > 1 ? `
344
+ <div class="flex items-center justify-between border-t border-zinc-950/10 dark:border-white/10 bg-zinc-50 dark:bg-zinc-900/50 px-4 py-3 sm:px-6">
345
+ <div class="flex flex-1 justify-between sm:hidden">
346
+ <button
347
+ onclick="goToPage(${data.currentPage - 1})"
348
+ ${data.currentPage === 1 ? "disabled" : ""}
349
+ class="relative inline-flex items-center rounded-lg px-4 py-2 text-sm font-semibold text-zinc-950 dark:text-white ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 hover:bg-zinc-100 dark:hover:bg-zinc-800 disabled:opacity-50 disabled:cursor-not-allowed"
350
+ >
351
+ Previous
352
+ </button>
353
+ <button
354
+ onclick="goToPage(${data.currentPage + 1})"
355
+ ${data.currentPage === totalPages ? "disabled" : ""}
356
+ class="relative ml-3 inline-flex items-center rounded-lg px-4 py-2 text-sm font-semibold text-zinc-950 dark:text-white ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 hover:bg-zinc-100 dark:hover:bg-zinc-800 disabled:opacity-50 disabled:cursor-not-allowed"
357
+ >
358
+ Next
359
+ </button>
360
+ </div>
361
+ <div class="hidden sm:flex sm:flex-1 sm:items-center sm:justify-between">
362
+ <div>
363
+ <p class="text-sm text-zinc-700 dark:text-zinc-300">
364
+ Page <span class="font-medium">${data.currentPage}</span> of <span class="font-medium">${totalPages}</span>
365
+ </p>
366
+ </div>
367
+ <div>
368
+ <nav class="isolate inline-flex -space-x-px rounded-lg shadow-sm" aria-label="Pagination">
369
+ <button
370
+ onclick="goToPage(${data.currentPage - 1})"
371
+ ${data.currentPage === 1 ? "disabled" : ""}
372
+ class="relative inline-flex items-center rounded-l-lg px-2 py-2 text-zinc-400 ring-1 ring-inset ring-zinc-300 dark:ring-zinc-700 hover:bg-zinc-50 dark:hover:bg-zinc-800 focus:z-20 disabled:opacity-50 disabled:cursor-not-allowed"
373
+ >
374
+ <span class="sr-only">Previous</span>
375
+ <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
376
+ <path fill-rule="evenodd" d="M12.79 5.23a.75.75 0 01-.02 1.06L8.832 10l3.938 3.71a.75.75 0 11-1.04 1.08l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 011.06.02z" clip-rule="evenodd" />
377
+ </svg>
378
+ </button>
379
+
380
+ ${generatePageNumbers(data.currentPage, totalPages)}
381
+
382
+ <button
383
+ onclick="goToPage(${data.currentPage + 1})"
384
+ ${data.currentPage === totalPages ? "disabled" : ""}
385
+ class="relative inline-flex items-center rounded-r-lg px-2 py-2 text-zinc-400 ring-1 ring-inset ring-zinc-300 dark:ring-zinc-700 hover:bg-zinc-50 dark:hover:bg-zinc-800 focus:z-20 disabled:opacity-50 disabled:cursor-not-allowed"
386
+ >
387
+ <span class="sr-only">Next</span>
388
+ <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
389
+ <path fill-rule="evenodd" d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z" clip-rule="evenodd" />
390
+ </svg>
391
+ </button>
392
+ </nav>
393
+ </div>
394
+ </div>
395
+ </div>
396
+ ` : ""}
397
+ </div>
398
+ </div>
399
+
400
+ <script>
401
+ const currentTableName = '${data.tableName}';
402
+ let currentPage = ${data.currentPage};
403
+ let currentPageSize = ${data.pageSize};
404
+ let currentSort = '${data.sortColumn || ""}';
405
+ let currentSortDir = '${data.sortDirection || "asc"}';
406
+
407
+ function goToPage(page) {
408
+ if (page < 1 || page > ${totalPages}) return;
409
+ const params = new URLSearchParams();
410
+ params.set('page', page);
411
+ params.set('pageSize', currentPageSize);
412
+ if (currentSort) {
413
+ params.set('sort', currentSort);
414
+ params.set('dir', currentSortDir);
415
+ }
416
+ window.location.href = \`/admin/database-tools/tables/\${currentTableName}?\${params}\`;
417
+ }
418
+
419
+ function sortTable(column) {
420
+ let newDir = 'asc';
421
+ if (currentSort === column && currentSortDir === 'asc') {
422
+ newDir = 'desc';
423
+ }
424
+
425
+ const params = new URLSearchParams();
426
+ params.set('page', '1');
427
+ params.set('pageSize', currentPageSize);
428
+ params.set('sort', column);
429
+ params.set('dir', newDir);
430
+ window.location.href = \`/admin/database-tools/tables/\${currentTableName}?\${params}\`;
431
+ }
432
+
433
+ function changePageSize(newSize) {
434
+ const params = new URLSearchParams();
435
+ params.set('page', '1');
436
+ params.set('pageSize', newSize);
437
+ if (currentSort) {
438
+ params.set('sort', currentSort);
439
+ params.set('dir', currentSortDir);
440
+ }
441
+ window.location.href = \`/admin/database-tools/tables/\${currentTableName}?\${params}\`;
442
+ }
443
+
444
+ function refreshTableData() {
445
+ window.location.reload();
446
+ }
447
+
448
+ function formatCellValue(value) {
449
+ if (value === null || value === undefined) {
450
+ return '<span class="text-zinc-400 dark:text-zinc-500 italic">null</span>';
451
+ }
452
+ if (typeof value === 'boolean') {
453
+ return \`<span class="px-2 py-0.5 rounded text-xs font-medium \${value ? 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400' : 'bg-zinc-100 text-zinc-800 dark:bg-zinc-800 dark:text-zinc-400'}">\${value}</span>\`;
454
+ }
455
+ if (typeof value === 'object') {
456
+ return '<span class="text-xs font-mono text-zinc-600 dark:text-zinc-400">' + JSON.stringify(value).substring(0, 50) + (JSON.stringify(value).length > 50 ? '...' : '') + '</span>';
457
+ }
458
+ const str = String(value);
459
+ if (str.length > 100) {
460
+ return escapeHtml(str.substring(0, 100)) + '...';
461
+ }
462
+ return escapeHtml(str);
463
+ }
464
+
465
+ function escapeHtml(text) {
466
+ const map = {
467
+ '&': '&amp;',
468
+ '<': '&lt;',
469
+ '>': '&gt;',
470
+ '"': '&quot;',
471
+ "'": '&#039;'
472
+ };
473
+ return String(text).replace(/[&<>"']/g, m => map[m]);
474
+ }
475
+ </script>
476
+ `;
477
+ const layoutData = {
478
+ title: `Table: ${data.tableName}`,
479
+ pageTitle: `Database: ${data.tableName}`,
480
+ currentPath: `/admin/database-tools/tables/${data.tableName}`,
481
+ user: data.user,
482
+ content: pageContent
483
+ };
484
+ return renderAdminLayoutCatalyst(layoutData);
485
+ }
486
+ function generatePageNumbers(currentPage, totalPages) {
487
+ const pages = [];
488
+ const maxVisible = 7;
489
+ if (totalPages <= maxVisible) {
490
+ for (let i = 1; i <= totalPages; i++) {
491
+ pages.push(i);
492
+ }
493
+ } else {
494
+ if (currentPage <= 4) {
495
+ for (let i = 1; i <= 5; i++) pages.push(i);
496
+ pages.push(-1);
497
+ pages.push(totalPages);
498
+ } else if (currentPage >= totalPages - 3) {
499
+ pages.push(1);
500
+ pages.push(-1);
501
+ for (let i = totalPages - 4; i <= totalPages; i++) pages.push(i);
502
+ } else {
503
+ pages.push(1);
504
+ pages.push(-1);
505
+ for (let i = currentPage - 1; i <= currentPage + 1; i++) pages.push(i);
506
+ pages.push(-1);
507
+ pages.push(totalPages);
508
+ }
509
+ }
510
+ return pages.map((page) => {
511
+ if (page === -1) {
512
+ return `
513
+ <span class="relative inline-flex items-center px-4 py-2 text-sm font-semibold text-zinc-700 dark:text-zinc-300 ring-1 ring-inset ring-zinc-300 dark:ring-zinc-700">
514
+ ...
515
+ </span>
516
+ `;
517
+ }
518
+ const isActive = page === currentPage;
519
+ return `
520
+ <button
521
+ onclick="goToPage(${page})"
522
+ class="relative inline-flex items-center px-4 py-2 text-sm font-semibold ${isActive ? "z-10 bg-indigo-600 text-white focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" : "text-zinc-900 dark:text-zinc-100 ring-1 ring-inset ring-zinc-300 dark:ring-zinc-700 hover:bg-zinc-50 dark:hover:bg-zinc-800"}"
523
+ >
524
+ ${page}
525
+ </button>
526
+ `;
527
+ }).join("");
528
+ }
529
+ function escapeHtml2(text) {
530
+ const map = {
531
+ "&": "&amp;",
532
+ "<": "&lt;",
533
+ ">": "&gt;",
534
+ '"': "&quot;",
535
+ "'": "&#039;"
536
+ };
537
+ return String(text).replace(/[&<>"']/g, (m) => map[m] || m);
538
+ }
539
+ function formatCellValue(value) {
540
+ if (value === null || value === void 0) {
541
+ return '<span class="text-zinc-400 dark:text-zinc-500 italic">null</span>';
542
+ }
543
+ if (typeof value === "boolean") {
544
+ return `<span class="px-2 py-0.5 rounded text-xs font-medium ${value ? "bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400" : "bg-zinc-100 text-zinc-800 dark:bg-zinc-800 dark:text-zinc-400"}">${value}</span>`;
545
+ }
546
+ if (typeof value === "object") {
547
+ return '<span class="text-xs font-mono text-zinc-600 dark:text-zinc-400">' + JSON.stringify(value).substring(0, 50) + (JSON.stringify(value).length > 50 ? "..." : "") + "</span>";
548
+ }
549
+ const str = String(value);
550
+ if (str.length > 100) {
551
+ return escapeHtml2(str.substring(0, 100)) + "...";
552
+ }
553
+ return escapeHtml2(str);
554
+ }
555
+
556
+ // src/plugins/core-plugins/database-tools-plugin/admin-routes.ts
557
+ function createDatabaseToolsAdminRoutes() {
558
+ const router2 = new Hono();
559
+ router2.use("*", requireAuth());
560
+ router2.get("/api/stats", async (c) => {
561
+ try {
562
+ const user = c.get("user");
563
+ if (!user || user.role !== "admin") {
564
+ return c.json({
565
+ success: false,
566
+ error: "Unauthorized. Admin access required."
567
+ }, 403);
568
+ }
569
+ const db = c.env.DB;
570
+ const service = new DatabaseToolsService(db);
571
+ const stats = await service.getDatabaseStats();
572
+ return c.json({
573
+ success: true,
574
+ data: stats
575
+ });
576
+ } catch (error) {
577
+ console.error("Error fetching database stats:", error);
578
+ return c.json({
579
+ success: false,
580
+ error: "Failed to fetch database statistics"
581
+ }, 500);
582
+ }
583
+ });
584
+ router2.post("/api/truncate", async (c) => {
585
+ try {
586
+ const user = c.get("user");
587
+ if (!user || user.role !== "admin") {
588
+ return c.json({
589
+ success: false,
590
+ error: "Unauthorized. Admin access required."
591
+ }, 403);
592
+ }
593
+ const body = await c.req.json();
594
+ const { confirmText } = body;
595
+ if (confirmText !== "TRUNCATE ALL DATA") {
596
+ return c.json({
597
+ success: false,
598
+ error: "Invalid confirmation text. Operation cancelled."
599
+ }, 400);
600
+ }
601
+ const db = c.env.DB;
602
+ const service = new DatabaseToolsService(db);
603
+ const result = await service.truncateAllData(user.email);
604
+ return c.json({
605
+ success: result.success,
606
+ message: result.message,
607
+ data: {
608
+ tablesCleared: result.tablesCleared,
609
+ adminUserPreserved: result.adminUserPreserved,
610
+ errors: result.errors
611
+ }
612
+ });
613
+ } catch (error) {
614
+ console.error("Error truncating database:", error);
615
+ return c.json({
616
+ success: false,
617
+ error: "Failed to truncate database"
618
+ }, 500);
619
+ }
620
+ });
621
+ router2.post("/api/backup", async (c) => {
622
+ try {
623
+ const user = c.get("user");
624
+ if (!user || user.role !== "admin") {
625
+ return c.json({
626
+ success: false,
627
+ error: "Unauthorized. Admin access required."
628
+ }, 403);
629
+ }
630
+ const db = c.env.DB;
631
+ const service = new DatabaseToolsService(db);
632
+ const result = await service.createBackup();
633
+ return c.json({
634
+ success: result.success,
635
+ message: result.message,
636
+ data: {
637
+ backupId: result.backupId
638
+ }
639
+ });
640
+ } catch (error) {
641
+ console.error("Error creating backup:", error);
642
+ return c.json({
643
+ success: false,
644
+ error: "Failed to create backup"
645
+ }, 500);
646
+ }
647
+ });
648
+ router2.get("/api/validate", async (c) => {
649
+ try {
650
+ const user = c.get("user");
651
+ if (!user || user.role !== "admin") {
652
+ return c.json({
653
+ success: false,
654
+ error: "Unauthorized. Admin access required."
655
+ }, 403);
656
+ }
657
+ const db = c.env.DB;
658
+ const service = new DatabaseToolsService(db);
659
+ const validation = await service.validateDatabase();
660
+ return c.json({
661
+ success: true,
662
+ data: validation
663
+ });
664
+ } catch (error) {
665
+ console.error("Error validating database:", error);
666
+ return c.json({
667
+ success: false,
668
+ error: "Failed to validate database"
669
+ }, 500);
670
+ }
671
+ });
672
+ router2.get("/api/tables/:tableName", async (c) => {
673
+ try {
674
+ const user = c.get("user");
675
+ if (!user || user.role !== "admin") {
676
+ return c.json({
677
+ success: false,
678
+ error: "Unauthorized. Admin access required."
679
+ }, 403);
680
+ }
681
+ const tableName = c.req.param("tableName");
682
+ const limit = parseInt(c.req.query("limit") || "100");
683
+ const offset = parseInt(c.req.query("offset") || "0");
684
+ const sortColumn = c.req.query("sort");
685
+ const sortDirection = c.req.query("dir") || "asc";
686
+ const db = c.env.DB;
687
+ const service = new DatabaseToolsService(db);
688
+ const tableData = await service.getTableData(tableName, limit, offset, sortColumn, sortDirection);
689
+ return c.json({
690
+ success: true,
691
+ data: tableData
692
+ });
693
+ } catch (error) {
694
+ console.error("Error fetching table data:", error);
695
+ return c.json({
696
+ success: false,
697
+ error: `Failed to fetch table data: ${error}`
698
+ }, 500);
699
+ }
700
+ });
701
+ router2.get("/tables/:tableName", async (c) => {
702
+ try {
703
+ const user = c.get("user");
704
+ if (!user || user.role !== "admin") {
705
+ return c.redirect("/admin/login");
706
+ }
707
+ const tableName = c.req.param("tableName");
708
+ const page = parseInt(c.req.query("page") || "1");
709
+ const pageSize = parseInt(c.req.query("pageSize") || "20");
710
+ const sortColumn = c.req.query("sort");
711
+ const sortDirection = c.req.query("dir") || "asc";
712
+ const offset = (page - 1) * pageSize;
713
+ const db = c.env.DB;
714
+ const service = new DatabaseToolsService(db);
715
+ const tableData = await service.getTableData(tableName, pageSize, offset, sortColumn, sortDirection);
716
+ const pageData = {
717
+ user: {
718
+ name: user.email.split("@")[0] || "Unknown",
719
+ email: user.email,
720
+ role: user.role
721
+ },
722
+ tableName: tableData.tableName,
723
+ columns: tableData.columns,
724
+ rows: tableData.rows,
725
+ totalRows: tableData.totalRows,
726
+ currentPage: page,
727
+ pageSize,
728
+ sortColumn,
729
+ sortDirection
730
+ };
731
+ return c.html(renderDatabaseTablePage(pageData));
732
+ } catch (error) {
733
+ console.error("Error rendering table page:", error);
734
+ return c.text(`Error: ${error}`, 500);
735
+ }
736
+ });
737
+ return router2;
738
+ }
739
+
740
+ // src/app.ts
19
741
  function createSonicJSApp(config = {}) {
20
742
  const app = new Hono();
21
743
  const appVersion = config.version || getCoreVersion();
@@ -49,12 +771,40 @@ function createSonicJSApp(config = {}) {
49
771
  app.route("/admin/dashboard", router);
50
772
  app.route("/admin/collections", adminCollectionsRoutes);
51
773
  app.route("/admin/settings", adminSettingsRoutes);
774
+ app.route("/admin/database-tools", createDatabaseToolsAdminRoutes());
52
775
  app.route("/admin/content", admin_content_default);
53
776
  app.route("/admin/media", adminMediaRoutes);
54
777
  app.route("/admin/plugins", adminPluginRoutes);
55
778
  app.route("/admin/logs", adminLogsRoutes);
56
779
  app.route("/admin", userRoutes);
57
780
  app.route("/auth", auth_default);
781
+ app.get("/files/*", async (c) => {
782
+ try {
783
+ const url = new URL(c.req.url);
784
+ const pathname = url.pathname;
785
+ const objectKey = pathname.replace(/^\/files\//, "");
786
+ if (!objectKey) {
787
+ return c.notFound();
788
+ }
789
+ const object = await c.env.MEDIA_BUCKET.get(objectKey);
790
+ if (!object) {
791
+ return c.notFound();
792
+ }
793
+ const headers = new Headers();
794
+ object.httpMetadata?.contentType && headers.set("Content-Type", object.httpMetadata.contentType);
795
+ object.httpMetadata?.contentDisposition && headers.set("Content-Disposition", object.httpMetadata.contentDisposition);
796
+ headers.set("Cache-Control", "public, max-age=31536000");
797
+ headers.set("Access-Control-Allow-Origin", "*");
798
+ headers.set("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS");
799
+ headers.set("Access-Control-Allow-Headers", "Content-Type");
800
+ return new Response(object.body, {
801
+ headers
802
+ });
803
+ } catch (error) {
804
+ console.error("Error serving file:", error);
805
+ return c.notFound();
806
+ }
807
+ });
58
808
  if (config.routes) {
59
809
  for (const route of config.routes) {
60
810
  app.route(route.path, route.handler);