@kyro-cms/admin 0.1.6 → 0.1.7

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 (163) hide show
  1. package/README.md +149 -51
  2. package/package.json +53 -6
  3. package/src/collections/auth/index.ts +2 -2
  4. package/src/collections/portfolio/index.ts +343 -0
  5. package/src/components/ActionBar.tsx +153 -16
  6. package/src/components/Admin.tsx +136 -27
  7. package/src/components/ApiExplorer.tsx +325 -0
  8. package/src/components/ApiKeysManager.tsx +563 -0
  9. package/src/components/AuditLogsPage.tsx +664 -0
  10. package/src/components/AutoForm.tsx +1417 -661
  11. package/src/components/BrandingHub.tsx +267 -0
  12. package/src/components/BulkActionsBar.tsx +3 -3
  13. package/src/components/CreateView.tsx +3 -3
  14. package/src/components/Dashboard.tsx +393 -0
  15. package/src/components/DetailView.tsx +199 -57
  16. package/src/components/DeveloperCenter.tsx +403 -0
  17. package/src/components/EnhancedListView.tsx +786 -0
  18. package/src/components/GraphQLExplorer.tsx +675 -0
  19. package/src/components/GraphQLPlayground.tsx +627 -0
  20. package/src/components/ListView.tsx +191 -53
  21. package/src/components/MediaGallery.tsx +1569 -0
  22. package/src/components/Modal.tsx +149 -0
  23. package/src/components/RestPlayground.tsx +951 -0
  24. package/src/components/Sidebar.astro +237 -0
  25. package/src/components/UserManagement.tsx +204 -0
  26. package/src/components/VersionHistoryPanel.tsx +3 -3
  27. package/src/components/WebhookManager.tsx +608 -0
  28. package/src/components/blocks/AccordionBlock.tsx +97 -0
  29. package/src/components/blocks/ArrayBlock.tsx +75 -0
  30. package/src/components/blocks/BlockEditModal.MARKER +12 -0
  31. package/src/components/blocks/BlockEditModal.tsx +774 -0
  32. package/src/components/blocks/ButtonBlock.tsx +165 -0
  33. package/src/components/blocks/ChildBlocksTree.tsx +551 -0
  34. package/src/components/blocks/CodeBlock.tsx +66 -0
  35. package/src/components/blocks/ColumnsBlock.tsx +151 -0
  36. package/src/components/blocks/DividerBlock.tsx +43 -0
  37. package/src/components/blocks/FileBlock.tsx +64 -0
  38. package/src/components/blocks/HeadingBlock.tsx +81 -0
  39. package/src/components/blocks/HeroBlock.tsx +157 -0
  40. package/src/components/blocks/ImageBlock.tsx +83 -0
  41. package/src/components/blocks/LinkBlock.tsx +71 -0
  42. package/src/components/blocks/ListBlock.tsx +39 -0
  43. package/src/components/blocks/ParagraphBlock.tsx +61 -0
  44. package/src/components/blocks/RelationshipBlock.tsx +279 -0
  45. package/src/components/blocks/VStackBlock.tsx +75 -0
  46. package/src/components/blocks/VideoBlock.tsx +45 -0
  47. package/src/components/blocks/index.ts +10 -0
  48. package/src/components/fields/BlocksField.tsx +323 -0
  49. package/src/components/fields/CheckboxField.tsx +15 -9
  50. package/src/components/fields/CodeField.tsx +234 -0
  51. package/src/components/fields/DateField.tsx +38 -11
  52. package/src/components/fields/EditorClient.tsx +271 -0
  53. package/src/components/fields/FileField.tsx +390 -0
  54. package/src/components/fields/HybridContentField.tsx +109 -0
  55. package/src/components/fields/ImageField.tsx +429 -0
  56. package/src/components/fields/JSONField.tsx +361 -0
  57. package/src/components/fields/MarkdownField.tsx +282 -0
  58. package/src/components/fields/NumberField.tsx +42 -12
  59. package/src/components/fields/PortableTextField.tsx +143 -0
  60. package/src/components/fields/PortableTextRenderer.tsx +68 -0
  61. package/src/components/fields/RelationshipField.tsx +231 -59
  62. package/src/components/fields/SelectField.tsx +25 -15
  63. package/src/components/fields/TextField.tsx +45 -14
  64. package/src/components/fields/extensions/blockComponents.tsx +237 -0
  65. package/src/components/fields/extensions/blocksStore.ts +273 -0
  66. package/src/components/fields/index.ts +13 -0
  67. package/src/components/index.ts +1 -2
  68. package/src/components/layout/Header.tsx +2 -2
  69. package/src/components/layout/Layout.tsx +2 -2
  70. package/src/components/ui/Badge.tsx +9 -4
  71. package/src/components/ui/BlockDrawer.tsx +79 -0
  72. package/src/components/ui/Button.tsx +1 -1
  73. package/src/components/ui/CommandPalette.tsx +362 -0
  74. package/src/components/ui/CommandPaletteWrapper.tsx +97 -0
  75. package/src/components/ui/Dropdown.tsx +1 -1
  76. package/src/components/ui/Modal.tsx +37 -12
  77. package/src/components/ui/PromptModal.tsx +94 -0
  78. package/src/components/ui/SlidePanel.tsx +43 -16
  79. package/src/components/ui/Toast.tsx +80 -14
  80. package/src/env.d.ts +16 -0
  81. package/src/env.ts +20 -0
  82. package/src/index.ts +0 -1
  83. package/src/layouts/AdminLayout.astro +164 -170
  84. package/src/layouts/AuthLayout.astro +23 -6
  85. package/src/lib/MediaService.ts +541 -0
  86. package/src/lib/auth/sqlite-adapter.ts +319 -0
  87. package/src/lib/config.ts +22 -6
  88. package/src/lib/dataStore.ts +132 -74
  89. package/src/lib/db/adapter.ts +54 -0
  90. package/src/lib/db/drizzle-mysql-adapter.ts +194 -0
  91. package/src/lib/db/drizzle-mysql-auth-adapter.ts +327 -0
  92. package/src/lib/db/drizzle-postgres-adapter.ts +202 -0
  93. package/src/lib/db/drizzle-postgres-auth-adapter.ts +304 -0
  94. package/src/lib/db/drizzle-sqlite-adapter.ts +227 -0
  95. package/src/lib/db/drizzle-sqlite-auth-adapter.ts +548 -0
  96. package/src/lib/db/index.ts +449 -0
  97. package/src/lib/db/mongodb-adapter.ts +207 -0
  98. package/src/lib/db/mongodb-auth-adapter.ts +305 -0
  99. package/src/lib/db/schema/mysql-auth.ts +113 -0
  100. package/src/lib/db/schema/mysql-content.ts +20 -0
  101. package/src/lib/db/schema/postgres-auth.ts +116 -0
  102. package/src/lib/db/schema/postgres-content.ts +35 -0
  103. package/src/lib/db/schema/postgres-media.ts +52 -0
  104. package/src/lib/db/schema/postgres-settings.ts +11 -0
  105. package/src/lib/db/schema/sqlite-auth.ts +112 -0
  106. package/src/lib/db/schema/sqlite-content.ts +20 -0
  107. package/src/lib/graphql/index.ts +1 -0
  108. package/src/lib/graphql/schema.ts +443 -0
  109. package/src/lib/rate-limit.ts +267 -0
  110. package/src/lib/storage.ts +374 -0
  111. package/src/lib/store.ts +85 -0
  112. package/src/middleware.ts +70 -11
  113. package/src/pages/[collection]/[id].astro +178 -122
  114. package/src/pages/[collection]/index.astro +24 -156
  115. package/src/pages/admin/api-explorer.astro +98 -0
  116. package/src/pages/admin/graphql-explorer.astro +40 -0
  117. package/src/pages/admin/graphql.astro +97 -0
  118. package/src/pages/admin/index.astro +200 -139
  119. package/src/pages/admin/keys.astro +8 -0
  120. package/src/pages/admin/rest-playground.astro +44 -0
  121. package/src/pages/admin/webhooks.astro +8 -0
  122. package/src/pages/api/[collection]/[id]/publish.ts +44 -0
  123. package/src/pages/api/[collection]/[id]/unpublish.ts +42 -0
  124. package/src/pages/api/[collection]/[id]/versions.ts +36 -0
  125. package/src/pages/api/[collection]/[id].ts +102 -159
  126. package/src/pages/api/[collection]/index.ts +151 -230
  127. package/src/pages/api/auth/[id].ts +48 -69
  128. package/src/pages/api/auth/audit-logs.ts +20 -43
  129. package/src/pages/api/auth/login.ts +159 -45
  130. package/src/pages/api/auth/logout.ts +42 -24
  131. package/src/pages/api/auth/refresh.ts +119 -0
  132. package/src/pages/api/auth/register.ts +110 -40
  133. package/src/pages/api/auth/users.ts +22 -97
  134. package/src/pages/api/collections.ts +59 -0
  135. package/src/pages/api/globals/[slug]/test.ts +172 -0
  136. package/src/pages/api/globals/[slug].ts +42 -0
  137. package/src/pages/api/graphql.ts +90 -0
  138. package/src/pages/api/health.ts +417 -40
  139. package/src/pages/api/keys/[id].ts +26 -0
  140. package/src/pages/api/keys/index.ts +75 -0
  141. package/src/pages/api/media/[id].ts +309 -0
  142. package/src/pages/api/media/folders.ts +609 -0
  143. package/src/pages/api/media/index.ts +146 -0
  144. package/src/pages/api/media/resize.ts +267 -0
  145. package/src/pages/api/search.ts +82 -0
  146. package/src/pages/api/slug-availability.ts +70 -0
  147. package/src/pages/api/storage-config.ts +20 -0
  148. package/src/pages/api/storage-status.ts +206 -0
  149. package/src/pages/api/upload.ts +334 -0
  150. package/src/pages/api/webhooks/index.ts +71 -0
  151. package/src/pages/audit/index.astro +2 -104
  152. package/src/pages/login.astro +11 -11
  153. package/src/pages/media.astro +10 -0
  154. package/src/pages/preview/[collection]/[id].astro +178 -0
  155. package/src/pages/register.astro +13 -13
  156. package/src/pages/roles/index.astro +21 -21
  157. package/src/pages/settings/[slug].astro +162 -0
  158. package/src/pages/settings/index.astro +9 -0
  159. package/src/pages/users/[id].astro +29 -21
  160. package/src/pages/users/index.astro +22 -17
  161. package/src/pages/users/new.astro +18 -17
  162. package/src/styles/main.css +553 -128
  163. package/src/components/layout/Sidebar.tsx +0 -497
@@ -0,0 +1,541 @@
1
+ import { getDatabaseAdapter, getDatabaseConfig, runMigrations } from "./db";
2
+
3
+ interface MediaItem {
4
+ id: string;
5
+ filename: string;
6
+ title: string | null;
7
+ originalName: string;
8
+ mimeType: string;
9
+ fileSize: number;
10
+ width: number | null;
11
+ height: number | null;
12
+ url: string;
13
+ thumbnailUrl: string | null;
14
+ folder: string | null;
15
+ provider: string;
16
+ alt: string | null;
17
+ caption: string | null;
18
+ metadata: Record<string, unknown> | null;
19
+ createdAt: Date;
20
+ updatedAt: Date;
21
+ }
22
+
23
+ interface FindOptions {
24
+ page?: number;
25
+ limit?: number;
26
+ search?: string;
27
+ type?: string;
28
+ folder?: string;
29
+ sortBy?: string;
30
+ sortDir?: "asc" | "desc";
31
+ }
32
+
33
+ interface FindResult {
34
+ docs: MediaItem[];
35
+ totalDocs: number;
36
+ page: number;
37
+ limit: number;
38
+ totalPages: number;
39
+ }
40
+
41
+ class AdminMediaService {
42
+ private db: any = null;
43
+ private dbType: string = "sqlite";
44
+
45
+ async init(): Promise<void> {
46
+ await runMigrations();
47
+ const config = getDatabaseConfig();
48
+ this.dbType = config.type;
49
+ console.log("[MediaService] Initializing with dbType:", this.dbType);
50
+ console.log("[MediaService] Config:", config);
51
+
52
+ if (config.type === "postgres") {
53
+ const { Pool } = await import("pg");
54
+ this.db = new Pool({
55
+ connectionString:
56
+ config.connectionString ||
57
+ `postgresql://${config.username}:${config.password}@${config.host}:${config.port}/${config.database}`,
58
+ });
59
+ } else if (config.type === "sqlite") {
60
+ const Database = (await import("better-sqlite3")).default;
61
+ this.db = new Database(config.contentDbPath || "./data/content.db");
62
+ } else if (config.type === "mysql") {
63
+ const mysql = await import("mysql2/promise");
64
+ const connString =
65
+ config.connectionString ||
66
+ `mysql://${config.username}:${config.password}@${config.host}:${config.port}/${config.database}`;
67
+ this.db = await mysql.createPool({
68
+ uri: connString,
69
+ });
70
+ } else if (config.type === "mongodb") {
71
+ const { MongoClient } = await import("mongodb");
72
+ const connString =
73
+ config.connectionString || `mongodb://${config.host}:${config.port}`;
74
+ const client = new MongoClient(connString);
75
+ await client.connect();
76
+ this.db = client.db(config.database);
77
+ }
78
+ }
79
+
80
+ private mapRow(row: any): MediaItem {
81
+ return {
82
+ id: row.id,
83
+ filename: row.filename,
84
+ title: row.title,
85
+ originalName: row.original_name,
86
+ mimeType: row.mime_type,
87
+ fileSize: row.file_size,
88
+ width: row.width,
89
+ height: row.height,
90
+ url: row.url,
91
+ thumbnailUrl: row.thumbnail_url,
92
+ folder: row.folder,
93
+ provider: row.provider,
94
+ alt: row.alt,
95
+ caption: row.caption,
96
+ metadata: row.metadata,
97
+ createdAt: row.created_at,
98
+ updatedAt: row.updated_at,
99
+ };
100
+ }
101
+
102
+ async find(options: FindOptions = {}): Promise<FindResult> {
103
+ const page = options.page || 1;
104
+ const limit = options.limit || 30;
105
+ const offset = (page - 1) * limit;
106
+ let sortBy = options.sortBy || "created_at";
107
+ if (sortBy === "createdAt") sortBy = "created_at";
108
+ if (sortBy === "updatedAt") sortBy = "updated_at";
109
+ if (sortBy === "fileSize") sortBy = "file_size";
110
+ const sortDir = options.sortDir === "asc" ? "ASC" : "DESC";
111
+
112
+ console.log("[MediaService.find] dbType:", this.dbType, "sortBy:", sortBy);
113
+
114
+ if (this.dbType === "mongodb") {
115
+ return this.findMongoDB(options, page, limit, sortBy, sortDir);
116
+ }
117
+
118
+ try {
119
+ return await this.findSql(options, page, limit, offset, sortBy, sortDir);
120
+ } catch (err: any) {
121
+ console.error("[MediaService.find] Error:", err.message, err.stack);
122
+ throw err;
123
+ }
124
+ }
125
+
126
+ async findById(id: string): Promise<MediaItem | null> {
127
+ if (this.dbType === "mongodb") {
128
+ const collection = this.db.collection("media");
129
+ const doc = await collection.findOne({ _id: id });
130
+ return doc ? this.mapRow(doc) : null;
131
+ }
132
+ if (this.dbType === "sqlite") {
133
+ const stmt = this.db.prepare("SELECT * FROM media WHERE id = ?");
134
+ const row = stmt.get(id);
135
+ return row ? this.mapRow(row) : null;
136
+ }
137
+ if (this.dbType === "postgres") {
138
+ const result = await this.db.query("SELECT * FROM media WHERE id = $1", [id]);
139
+ return result.rows[0] ? this.mapRow(result.rows[0]) : null;
140
+ }
141
+ if (this.dbType === "mysql") {
142
+ const [rows] = await this.db.execute("SELECT * FROM media WHERE id = ?", [id]);
143
+ return (rows as any[])[0] ? this.mapRow((rows as any[])[0]) : null;
144
+ }
145
+ return null;
146
+ }
147
+
148
+ private async findSql(
149
+ options: FindOptions,
150
+ page: number,
151
+ limit: number,
152
+ offset: number,
153
+ sortBy: string,
154
+ sortDir: string,
155
+ ): Promise<FindResult> {
156
+ let whereClause = "1=1";
157
+ const params: any[] = [];
158
+ let paramIndex = 1;
159
+
160
+ const isPostgres = this.dbType === "postgres";
161
+ const getParam = () => (isPostgres ? `$${paramIndex++}` : "?");
162
+
163
+ if (options.search) {
164
+ const likeOp = this.dbType === "postgres" ? "ILIKE" : "LIKE";
165
+ if (isPostgres) {
166
+ whereClause += ` AND (filename ${likeOp} $${paramIndex} OR title ${likeOp} $${paramIndex})`;
167
+ params.push(`%${options.search}%`);
168
+ paramIndex++;
169
+ } else {
170
+ whereClause += ` AND (filename ${likeOp} ? OR title ${likeOp} ?)`;
171
+ params.push(`%${options.search}%`, `%${options.search}%`);
172
+ }
173
+ }
174
+
175
+ if (options.folder) {
176
+ if (isPostgres) {
177
+ whereClause += ` AND (folder = $${paramIndex} OR folder LIKE $${paramIndex + 1})`;
178
+ params.push(options.folder, `${options.folder}/%`);
179
+ paramIndex += 2;
180
+ } else {
181
+ whereClause += ` AND (folder = ? OR folder LIKE ?)`;
182
+ params.push(options.folder, `${options.folder}/%`);
183
+ }
184
+ }
185
+
186
+ if (options.type) {
187
+ const likeOp = this.dbType === "postgres" ? "ILIKE" : "LIKE";
188
+ whereClause += ` AND mime_type ${likeOp} ${getParam()}`;
189
+ params.push(`${options.type}%`);
190
+ }
191
+
192
+ let countQuery: string;
193
+ let dataQuery: string;
194
+ let countResult: any;
195
+ let dataResult: any;
196
+
197
+ if (this.dbType === "postgres") {
198
+ countQuery = `SELECT COUNT(*) as total FROM media WHERE ${whereClause}`;
199
+ dataQuery = `SELECT * FROM media WHERE ${whereClause} ORDER BY ${sortBy} ${sortDir} LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`;
200
+ params.push(limit, offset);
201
+ countResult = await this.db.query(countQuery, params.slice(0, -2));
202
+ dataResult = await this.db.query(dataQuery, params);
203
+ const totalDocs = parseInt(countResult.rows[0]?.total || "0", 10);
204
+ const docs = dataResult.rows.map((row: any) => this.mapRow(row));
205
+ return {
206
+ docs,
207
+ totalDocs,
208
+ page,
209
+ limit,
210
+ totalPages: Math.ceil(totalDocs / limit),
211
+ };
212
+ } else if (this.dbType === "sqlite") {
213
+ countQuery = `SELECT COUNT(*) as total FROM media WHERE ${whereClause}`;
214
+ dataQuery = `SELECT * FROM media WHERE ${whereClause} ORDER BY ${sortBy} ${sortDir} LIMIT ? OFFSET ?`;
215
+ params.push(limit, offset);
216
+ console.log("[MediaService.find] SQLite data query:", dataQuery);
217
+ const countStmt = this.db.prepare(countQuery);
218
+ const dataStmt = this.db.prepare(dataQuery);
219
+ let countRow: any;
220
+ try {
221
+ countRow = countStmt.get(...params.slice(0, -2));
222
+ } catch (e: any) {
223
+ console.error("[MediaService.find] Count error:", e.message);
224
+ countRow = { total: 0 };
225
+ }
226
+ const totalDocs = countRow?.total || 0;
227
+ let rows: any[];
228
+ try {
229
+ rows = dataStmt.all(...params);
230
+ } catch (e: any) {
231
+ console.error("[MediaService.find] Data error:", e.message);
232
+ rows = [];
233
+ }
234
+ const docs = rows.map((row: any) => this.mapRow(row));
235
+ return {
236
+ docs,
237
+ totalDocs,
238
+ page,
239
+ limit,
240
+ totalPages: Math.ceil(totalDocs / limit),
241
+ };
242
+ } else if (this.dbType === "mysql") {
243
+ countQuery = `SELECT COUNT(*) as total FROM media WHERE ${whereClause}`;
244
+ dataQuery = `SELECT * FROM media WHERE ${whereClause} ORDER BY ${sortBy} ${sortDir} LIMIT ? OFFSET ?`;
245
+ params.push(limit, offset);
246
+ const [countRows] = await this.db.execute(
247
+ countQuery,
248
+ params.slice(0, -2),
249
+ );
250
+ const [dataRows] = await this.db.execute(dataQuery, params);
251
+ const totalDocs = countRows[0]?.total || 0;
252
+ const docs = dataRows.map((row: any) => this.mapRow(row));
253
+ return {
254
+ docs,
255
+ totalDocs,
256
+ page,
257
+ limit,
258
+ totalPages: Math.ceil(totalDocs / limit),
259
+ };
260
+ }
261
+
262
+ return { docs: [], totalDocs: 0, page, limit, totalPages: 0 };
263
+ }
264
+
265
+ private async findMongoDB(
266
+ options: FindOptions,
267
+ page: number,
268
+ limit: number,
269
+ sortBy: string,
270
+ sortDir: string,
271
+ ): Promise<FindResult> {
272
+ const collection = this.db.collection("media");
273
+ const filter: any = {};
274
+
275
+ if (options.search) {
276
+ filter.$or = [
277
+ { filename: { $regex: options.search, $options: "i" } },
278
+ { title: { $regex: options.search, $options: "i" } },
279
+ ];
280
+ }
281
+
282
+ if (options.folder) {
283
+ filter.folder = options.folder;
284
+ }
285
+
286
+ if (options.type) {
287
+ filter.mimeType = { $regex: `^${options.type}` };
288
+ }
289
+
290
+ const skip = (page - 1) * limit;
291
+ const sort: any = { [sortBy]: sortDir === "asc" ? 1 : -1 };
292
+
293
+ const [docs, totalDocs] = await Promise.all([
294
+ collection.find(filter).sort(sort).skip(skip).limit(limit).toArray(),
295
+ collection.countDocuments(filter),
296
+ ]);
297
+
298
+ const mappedDocs = docs.map((row: any) => ({
299
+ id: row._id?.toString() || row.id,
300
+ filename: row.filename,
301
+ title: row.title,
302
+ originalName: row.originalName || row.original_name,
303
+ mimeType: row.mimeType || row.mime_type,
304
+ fileSize: row.fileSize || row.file_size,
305
+ width: row.width,
306
+ height: row.height,
307
+ url: row.url,
308
+ thumbnailUrl: row.thumbnailUrl || row.thumbnail_url,
309
+ folder: row.folder,
310
+ provider: row.provider,
311
+ alt: row.alt,
312
+ caption: row.caption,
313
+ metadata: row.metadata,
314
+ createdAt: row.createdAt || row.created_at,
315
+ updatedAt: row.updatedAt || row.updated_at,
316
+ }));
317
+
318
+ return {
319
+ docs: mappedDocs,
320
+ totalDocs,
321
+ page,
322
+ limit,
323
+ totalPages: Math.ceil(totalDocs / limit),
324
+ };
325
+ }
326
+
327
+ async create(data: Partial<MediaItem>): Promise<MediaItem> {
328
+ const id = data.id || crypto.randomUUID();
329
+ const now = new Date();
330
+
331
+ if (this.dbType === "mongodb") {
332
+ const collection = this.db.collection("media");
333
+ const doc = {
334
+ _id: id,
335
+ filename: data.filename,
336
+ title: data.title,
337
+ original_name: data.originalName,
338
+ mime_type: data.mimeType,
339
+ file_size: data.fileSize,
340
+ width: data.width,
341
+ height: data.height,
342
+ url: data.url,
343
+ thumbnail_url: data.thumbnailUrl,
344
+ folder: data.folder,
345
+ provider: data.provider || "local",
346
+ alt: data.alt,
347
+ caption: data.caption,
348
+ metadata: data.metadata,
349
+ created_at: now,
350
+ updated_at: now,
351
+ };
352
+ await collection.insertOne(doc);
353
+ return this.mapRow(doc);
354
+ }
355
+
356
+ const sql =
357
+ this.dbType === "postgres"
358
+ ? `INSERT INTO media (id, filename, title, original_name, mime_type, file_size, width, height, url, thumbnail_url, folder, provider, alt, caption, metadata, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, NOW(), NOW()) RETURNING *`
359
+ : this.dbType === "sqlite"
360
+ ? `INSERT INTO media (id, filename, title, original_name, mime_type, file_size, width, height, url, thumbnail_url, folder, provider, alt, caption, metadata, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
361
+ : `INSERT INTO media (id, filename, title, original_name, mime_type, file_size, width, height, url, thumbnail_url, folder, provider, alt, caption, metadata, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())`;
362
+
363
+ const values = [
364
+ id,
365
+ data.filename,
366
+ data.title || null,
367
+ data.originalName,
368
+ data.mimeType,
369
+ data.fileSize,
370
+ data.width || null,
371
+ data.height || null,
372
+ data.url,
373
+ data.thumbnailUrl || null,
374
+ data.folder || null,
375
+ data.provider || "local",
376
+ data.alt || null,
377
+ data.caption || null,
378
+ data.metadata ? JSON.stringify(data.metadata) : null,
379
+ ];
380
+
381
+ let result: any;
382
+
383
+ if (this.dbType === "postgres") {
384
+ result = await this.db.query(sql, values);
385
+ return this.mapRow(result.rows[0]);
386
+ } else if (this.dbType === "sqlite") {
387
+ const stmt = this.db.prepare(sql);
388
+ stmt.run(...values);
389
+ const getStmt = this.db.prepare("SELECT * FROM media WHERE id = ?");
390
+ const row = getStmt.get(id);
391
+ return this.mapRow(row);
392
+ } else if (this.dbType === "mysql") {
393
+ await this.db.execute(sql, values);
394
+ const [rows] = await this.db.execute("SELECT * FROM media WHERE id = ?", [
395
+ id,
396
+ ]);
397
+ return this.mapRow(rows[0]);
398
+ }
399
+
400
+ throw new Error("Unsupported database type");
401
+ }
402
+
403
+ async update(
404
+ id: string,
405
+ data: Partial<MediaItem>,
406
+ ): Promise<MediaItem | null> {
407
+ if (this.dbType === "mongodb") {
408
+ const collection = this.db.collection("media");
409
+ const update: any = { $set: { updated_at: new Date() } };
410
+ if (data.title !== undefined) update.$set.title = data.title;
411
+ if (data.alt !== undefined) update.$set.alt = data.alt;
412
+ if (data.caption !== undefined) update.$set.caption = data.caption;
413
+ if (data.folder !== undefined) update.$set.folder = data.folder;
414
+ if (data.thumbnailUrl !== undefined)
415
+ update.$set.thumbnail_url = data.thumbnailUrl;
416
+
417
+ await collection.updateOne({ _id: id }, update);
418
+ const doc = await collection.findOne({ _id: id });
419
+ return doc ? this.mapRow(doc) : null;
420
+ }
421
+
422
+ const updates: string[] = [];
423
+ const params: any[] = [];
424
+ let paramIndex = 1;
425
+
426
+ const getParam = () =>
427
+ this.dbType === "postgres" ? `$${paramIndex++}` : "?";
428
+
429
+ if (data.title !== undefined) {
430
+ updates.push(`title = ${getParam()}`);
431
+ params.push(data.title);
432
+ }
433
+ if (data.filename !== undefined) {
434
+ updates.push(`filename = ${getParam()}`);
435
+ params.push(data.filename);
436
+ }
437
+ if (data.url !== undefined) {
438
+ updates.push(`url = ${getParam()}`);
439
+ params.push(data.url);
440
+ }
441
+ if (data.thumbnailUrl !== undefined) {
442
+ updates.push(`thumbnail_url = ${getParam()}`);
443
+ params.push(data.thumbnailUrl);
444
+ }
445
+ if (data.alt !== undefined) {
446
+ updates.push(`alt = ${getParam()}`);
447
+ params.push(data.alt);
448
+ }
449
+ if (data.caption !== undefined) {
450
+ updates.push(`caption = ${getParam()}`);
451
+ params.push(data.caption);
452
+ }
453
+ if (data.folder !== undefined) {
454
+ updates.push(`folder = ${getParam()}`);
455
+ params.push(data.folder);
456
+ }
457
+
458
+ if (updates.length === 0) return null;
459
+
460
+ if (this.dbType === "postgres") {
461
+ updates.push(`updated_at = NOW()`);
462
+ params.push(id);
463
+ const sql = `UPDATE media SET ${updates.join(", ")} WHERE id = $${paramIndex} RETURNING *`;
464
+ const result = await this.db.query(sql, params);
465
+ return result.rows[0] ? this.mapRow(result.rows[0]) : null;
466
+ } else if (this.dbType === "sqlite") {
467
+ updates.push(`updated_at = datetime('now')`);
468
+ params.push(id);
469
+ const sql = `UPDATE media SET ${updates.join(", ")} WHERE id = ?`;
470
+ const stmt = this.db.prepare(sql);
471
+ stmt.run(...params);
472
+ const getStmt = this.db.prepare("SELECT * FROM media WHERE id = ?");
473
+ const row = getStmt.get(id);
474
+ return row ? this.mapRow(row) : null;
475
+ } else if (this.dbType === "mysql") {
476
+ updates.push(`updated_at = NOW()`);
477
+ params.push(id);
478
+ const sql = `UPDATE media SET ${updates.join(", ")} WHERE id = ?`;
479
+ await this.db.execute(sql, params);
480
+ const [rows] = await this.db.execute("SELECT * FROM media WHERE id = ?", [
481
+ id,
482
+ ]);
483
+ return rows[0] ? this.mapRow(rows[0]) : null;
484
+ }
485
+
486
+ return null;
487
+ }
488
+
489
+ async delete(id: string): Promise<boolean> {
490
+ if (this.dbType === "mongodb") {
491
+ const collection = this.db.collection("media");
492
+ const result = await collection.deleteOne({ _id: id });
493
+ return result.deletedCount > 0;
494
+ }
495
+
496
+ if (this.dbType === "postgres") {
497
+ const result = await this.db.query(
498
+ "DELETE FROM media WHERE id = $1 RETURNING id",
499
+ [id],
500
+ );
501
+ return result.rows.length > 0;
502
+ } else if (this.dbType === "sqlite") {
503
+ const stmt = this.db.prepare("DELETE FROM media WHERE id = ?");
504
+ const result = stmt.run(id);
505
+ return result.changes > 0;
506
+ } else if (this.dbType === "mysql") {
507
+ const result = await this.db.execute("DELETE FROM media WHERE id = ?", [
508
+ id,
509
+ ]);
510
+ return (result.affectedRows || 0) > 0;
511
+ }
512
+
513
+ return false;
514
+ }
515
+
516
+ async close(): Promise<void> {
517
+ if (this.db) {
518
+ if (this.dbType === "postgres") {
519
+ await this.db.end();
520
+ } else if (this.dbType === "mysql") {
521
+ await this.db.end();
522
+ } else if (this.dbType === "mongodb") {
523
+ await this.db.client.close();
524
+ } else {
525
+ this.db.close();
526
+ }
527
+ }
528
+ }
529
+ }
530
+
531
+ let mediaServiceInstance: AdminMediaService | null = null;
532
+
533
+ export async function getMediaService(): Promise<AdminMediaService> {
534
+ if (!mediaServiceInstance) {
535
+ mediaServiceInstance = new AdminMediaService();
536
+ await mediaServiceInstance.init();
537
+ }
538
+ return mediaServiceInstance;
539
+ }
540
+
541
+ export type { MediaItem, FindOptions, FindResult };