@kyro-cms/admin 0.1.6 → 0.1.8

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 (179) hide show
  1. package/README.md +149 -51
  2. package/package.json +54 -5
  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 +137 -28
  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 +2155 -770
  11. package/src/components/BrandingHub.tsx +267 -0
  12. package/src/components/BulkActionsBar.tsx +3 -3
  13. package/src/components/CreateView.tsx +4 -4
  14. package/src/components/Dashboard.tsx +393 -0
  15. package/src/components/DetailView.tsx +200 -58
  16. package/src/components/DeveloperCenter.tsx +403 -0
  17. package/src/components/EnhancedListView.tsx +890 -0
  18. package/src/components/GraphQLExplorer.tsx +675 -0
  19. package/src/components/GraphQLPlayground.tsx +627 -0
  20. package/src/components/ListView.tsx +192 -54
  21. package/src/components/MediaGallery.tsx +1569 -0
  22. package/src/components/Modal.tsx +206 -0
  23. package/src/components/RestPlayground.tsx +951 -0
  24. package/src/components/Sidebar.astro +237 -0
  25. package/src/components/ThemeProvider.tsx +8 -2
  26. package/src/components/UserManagement.tsx +204 -0
  27. package/src/components/VersionHistoryPanel.tsx +3 -3
  28. package/src/components/WebhookManager.tsx +608 -0
  29. package/src/components/blocks/AccordionBlock.tsx +65 -0
  30. package/src/components/blocks/ArrayBlock.tsx +84 -0
  31. package/src/components/blocks/BlockEditModal.tsx +363 -0
  32. package/src/components/blocks/ButtonBlock.tsx +64 -0
  33. package/src/components/blocks/ChildBlocksTree.tsx +551 -0
  34. package/src/components/blocks/CodeBlock.tsx +114 -0
  35. package/src/components/blocks/ColumnsBlock.tsx +93 -0
  36. package/src/components/blocks/DividerBlock.tsx +43 -0
  37. package/src/components/blocks/FileBlock.tsx +63 -0
  38. package/src/components/blocks/HeadingBlock.tsx +59 -0
  39. package/src/components/blocks/HeroBlock.tsx +99 -0
  40. package/src/components/blocks/ImageBlock.tsx +82 -0
  41. package/src/components/blocks/LinkBlock.tsx +65 -0
  42. package/src/components/blocks/ListBlock.tsx +60 -0
  43. package/src/components/blocks/ParagraphBlock.tsx +61 -0
  44. package/src/components/blocks/RelationshipBlock.tsx +72 -0
  45. package/src/components/blocks/RichTextBlock.tsx +66 -0
  46. package/src/components/blocks/VStackBlock.tsx +61 -0
  47. package/src/components/blocks/VideoBlock.tsx +65 -0
  48. package/src/components/blocks/index.ts +10 -0
  49. package/src/components/fields/AccordionField.tsx +213 -0
  50. package/src/components/fields/ArrayField.tsx +241 -0
  51. package/src/components/fields/BlocksField.tsx +323 -0
  52. package/src/components/fields/ButtonField.tsx +53 -0
  53. package/src/components/fields/CheckboxField.tsx +18 -8
  54. package/src/components/fields/ChildrenField.tsx +48 -0
  55. package/src/components/fields/CodeField.tsx +294 -0
  56. package/src/components/fields/ColumnsField.tsx +137 -0
  57. package/src/components/fields/DateField.tsx +24 -12
  58. package/src/components/fields/EditorClient.tsx +537 -0
  59. package/src/components/fields/HeadingField.tsx +31 -0
  60. package/src/components/fields/HeroField.tsx +101 -0
  61. package/src/components/fields/JSONField.tsx +341 -0
  62. package/src/components/fields/LinkField.tsx +81 -0
  63. package/src/components/fields/ListField.tsx +74 -0
  64. package/src/components/fields/MarkdownField.tsx +260 -0
  65. package/src/components/fields/NumberField.tsx +25 -13
  66. package/src/components/fields/PortableTextField.tsx +155 -0
  67. package/src/components/fields/PortableTextRenderer.tsx +68 -0
  68. package/src/components/fields/RelationshipBlockField.tsx +233 -0
  69. package/src/components/fields/RelationshipField.tsx +278 -60
  70. package/src/components/fields/SelectField.tsx +28 -16
  71. package/src/components/fields/TextField.tsx +31 -15
  72. package/src/components/fields/UploadField.tsx +613 -0
  73. package/src/components/fields/VideoField.tsx +73 -0
  74. package/src/components/fields/extensions/blockComponents.tsx +247 -0
  75. package/src/components/fields/extensions/blocksStore.ts +273 -0
  76. package/src/components/fields/index.ts +24 -0
  77. package/src/components/index.ts +1 -2
  78. package/src/components/layout/Header.tsx +2 -2
  79. package/src/components/layout/Layout.tsx +3 -3
  80. package/src/components/ui/Badge.tsx +9 -4
  81. package/src/components/ui/BlockDrawer.tsx +79 -0
  82. package/src/components/ui/Button.tsx +1 -1
  83. package/src/components/ui/CommandPalette.tsx +362 -0
  84. package/src/components/ui/CommandPaletteWrapper.tsx +97 -0
  85. package/src/components/ui/Dropdown.tsx +1 -1
  86. package/src/components/ui/Modal.tsx +37 -12
  87. package/src/components/ui/PromptModal.tsx +94 -0
  88. package/src/components/ui/SlidePanel.tsx +43 -16
  89. package/src/components/ui/Toast.tsx +80 -14
  90. package/src/env.d.ts +16 -0
  91. package/src/env.ts +20 -0
  92. package/src/index.ts +0 -1
  93. package/src/layouts/AdminLayout.astro +164 -170
  94. package/src/layouts/AuthLayout.astro +23 -6
  95. package/src/lib/MediaService.ts +541 -0
  96. package/src/lib/api.ts +163 -0
  97. package/src/lib/auth/sqlite-adapter.ts +319 -0
  98. package/src/lib/config.ts +23 -7
  99. package/src/lib/dataStore.ts +188 -73
  100. package/src/lib/date-utils.ts +69 -0
  101. package/src/lib/db/adapter.ts +54 -0
  102. package/src/lib/db/drizzle-mysql-adapter.ts +194 -0
  103. package/src/lib/db/drizzle-mysql-auth-adapter.ts +327 -0
  104. package/src/lib/db/drizzle-postgres-adapter.ts +202 -0
  105. package/src/lib/db/drizzle-postgres-auth-adapter.ts +304 -0
  106. package/src/lib/db/drizzle-sqlite-adapter.ts +227 -0
  107. package/src/lib/db/drizzle-sqlite-auth-adapter.ts +548 -0
  108. package/src/lib/db/index.ts +449 -0
  109. package/src/lib/db/mongodb-adapter.ts +207 -0
  110. package/src/lib/db/mongodb-auth-adapter.ts +305 -0
  111. package/src/lib/db/schema/mysql-auth.ts +113 -0
  112. package/src/lib/db/schema/mysql-content.ts +20 -0
  113. package/src/lib/db/schema/postgres-auth.ts +116 -0
  114. package/src/lib/db/schema/postgres-content.ts +35 -0
  115. package/src/lib/db/schema/postgres-media.ts +52 -0
  116. package/src/lib/db/schema/postgres-settings.ts +11 -0
  117. package/src/lib/db/schema/sqlite-auth.ts +112 -0
  118. package/src/lib/db/schema/sqlite-content.ts +20 -0
  119. package/src/lib/db/version-adapter.ts +248 -0
  120. package/src/lib/graphql/index.ts +1 -0
  121. package/src/lib/graphql/schema.ts +443 -0
  122. package/src/lib/i18n.tsx +353 -0
  123. package/src/lib/rate-limit.ts +267 -0
  124. package/src/lib/slugify.ts +15 -0
  125. package/src/lib/storage.ts +374 -0
  126. package/src/lib/store.ts +85 -0
  127. package/src/lib/validation.ts +250 -0
  128. package/src/middleware.ts +70 -11
  129. package/src/pages/[collection]/[id].astro +178 -122
  130. package/src/pages/[collection]/index.astro +24 -156
  131. package/src/pages/admin/api-explorer.astro +98 -0
  132. package/src/pages/admin/graphql-explorer.astro +40 -0
  133. package/src/pages/admin/graphql.astro +97 -0
  134. package/src/pages/admin/index.astro +200 -139
  135. package/src/pages/admin/keys.astro +8 -0
  136. package/src/pages/admin/rest-playground.astro +44 -0
  137. package/src/pages/admin/webhooks.astro +8 -0
  138. package/src/pages/api/[collection]/[id]/publish.ts +52 -0
  139. package/src/pages/api/[collection]/[id]/unpublish.ts +42 -0
  140. package/src/pages/api/[collection]/[id]/versions.ts +66 -0
  141. package/src/pages/api/[collection]/[id].ts +114 -159
  142. package/src/pages/api/[collection]/index.ts +150 -230
  143. package/src/pages/api/auth/[id].ts +48 -69
  144. package/src/pages/api/auth/audit-logs.ts +20 -43
  145. package/src/pages/api/auth/login.ts +159 -45
  146. package/src/pages/api/auth/logout.ts +42 -24
  147. package/src/pages/api/auth/refresh.ts +119 -0
  148. package/src/pages/api/auth/register.ts +110 -40
  149. package/src/pages/api/auth/users.ts +22 -97
  150. package/src/pages/api/collections.ts +59 -0
  151. package/src/pages/api/globals/[slug]/test.ts +172 -0
  152. package/src/pages/api/globals/[slug].ts +42 -0
  153. package/src/pages/api/graphql.ts +90 -0
  154. package/src/pages/api/health.ts +417 -40
  155. package/src/pages/api/keys/[id].ts +26 -0
  156. package/src/pages/api/keys/index.ts +75 -0
  157. package/src/pages/api/media/[id].ts +309 -0
  158. package/src/pages/api/media/folders.ts +609 -0
  159. package/src/pages/api/media/index.ts +146 -0
  160. package/src/pages/api/media/resize.ts +267 -0
  161. package/src/pages/api/search.ts +82 -0
  162. package/src/pages/api/slug-availability.ts +70 -0
  163. package/src/pages/api/storage-config.ts +20 -0
  164. package/src/pages/api/storage-status.ts +206 -0
  165. package/src/pages/api/upload.ts +334 -0
  166. package/src/pages/api/webhooks/index.ts +71 -0
  167. package/src/pages/audit/index.astro +2 -104
  168. package/src/pages/login.astro +11 -11
  169. package/src/pages/media.astro +10 -0
  170. package/src/pages/preview/[collection]/[id].astro +178 -0
  171. package/src/pages/register.astro +13 -13
  172. package/src/pages/roles/index.astro +21 -21
  173. package/src/pages/settings/[slug].astro +162 -0
  174. package/src/pages/settings/index.astro +9 -0
  175. package/src/pages/users/[id].astro +29 -21
  176. package/src/pages/users/index.astro +22 -17
  177. package/src/pages/users/new.astro +18 -17
  178. package/src/styles/main.css +563 -128
  179. 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 };
package/src/lib/api.ts ADDED
@@ -0,0 +1,163 @@
1
+ export interface ApiResponse<T = any> {
2
+ docs?: T[];
3
+ doc?: T;
4
+ totalDocs?: number;
5
+ error?: string;
6
+ }
7
+
8
+ export async function apiGet<T = any>(
9
+ url: string,
10
+ options?: RequestInit,
11
+ ): Promise<T> {
12
+ const response = await fetch(url, {
13
+ ...options,
14
+ credentials: "include",
15
+ });
16
+ if (!response.ok) {
17
+ throw new Error(`API Error: ${response.status}`);
18
+ }
19
+ return response.json();
20
+ }
21
+
22
+ export async function apiPost<T = any>(
23
+ url: string,
24
+ body?: any,
25
+ options?: RequestInit,
26
+ ): Promise<T> {
27
+ const response = await fetch(url, {
28
+ method: "POST",
29
+ headers: {
30
+ "Content-Type": "application/json",
31
+ ...options?.headers,
32
+ },
33
+ body: body ? JSON.stringify(body) : undefined,
34
+ credentials: "include",
35
+ ...options,
36
+ });
37
+ if (!response.ok) {
38
+ throw new Error(`API Error: ${response.status}`);
39
+ }
40
+ return response.json();
41
+ }
42
+
43
+ export async function apiPatch<T = any>(
44
+ url: string,
45
+ body?: any,
46
+ options?: RequestInit,
47
+ ): Promise<T> {
48
+ const response = await fetch(url, {
49
+ method: "PATCH",
50
+ headers: {
51
+ "Content-Type": "application/json",
52
+ ...options?.headers,
53
+ },
54
+ body: body ? JSON.stringify(body) : undefined,
55
+ credentials: "include",
56
+ ...options,
57
+ });
58
+ if (!response.ok) {
59
+ throw new Error(`API Error: ${response.status}`);
60
+ }
61
+ return response.json();
62
+ }
63
+
64
+ export async function apiPatchNoThrow<T = any>(
65
+ url: string,
66
+ body?: any,
67
+ ): Promise<{ ok: boolean; data?: T; error?: string }> {
68
+ const response = await fetch(url, {
69
+ method: "PATCH",
70
+ headers: { "Content-Type": "application/json" },
71
+ body: body ? JSON.stringify(body) : undefined,
72
+ credentials: "include",
73
+ });
74
+ if (!response.ok) {
75
+ return { ok: false, error: `Error: ${response.status}` };
76
+ }
77
+ const data = await response.json();
78
+ return { ok: true, data };
79
+ }
80
+
81
+ export async function apiDelete<T = any>(
82
+ url: string,
83
+ options?: RequestInit,
84
+ ): Promise<T> {
85
+ const response = await fetch(url, {
86
+ method: "DELETE",
87
+ credentials: "include",
88
+ ...options,
89
+ });
90
+ if (!response.ok) {
91
+ throw new Error(`API Error: ${response.status}`);
92
+ }
93
+ return response.json();
94
+ }
95
+
96
+ export function buildQueryString(params: Record<string, any>): string {
97
+ const urlParams = new URLSearchParams();
98
+ for (const [key, value] of Object.entries(params)) {
99
+ if (value !== undefined && value !== null && value !== "") {
100
+ urlParams.set(key, String(value));
101
+ }
102
+ }
103
+ return urlParams.toString();
104
+ }
105
+
106
+ export function withCacheBust(url: string): string {
107
+ const separator = url.includes("?") ? "&" : "?";
108
+ return `${url}${separator}t=${Date.now()}`;
109
+ }
110
+
111
+ export function buildSearchQuery(
112
+ search: string,
113
+ fields: string[],
114
+ limit: number = 50,
115
+ ): string {
116
+ if (!search || fields.length === 0) {
117
+ return `limit=${limit}`;
118
+ }
119
+ const searchQuery = fields
120
+ .map((f) => `where[${f}][contains]=${encodeURIComponent(search)}`)
121
+ .join("&");
122
+ return `${searchQuery}&limit=${limit}`;
123
+ }
124
+
125
+ export function buildCollectionUrl(
126
+ collection: string,
127
+ params?: Record<string, any>,
128
+ ): string {
129
+ let url = `/api/${collection}`;
130
+ if (params) {
131
+ const query = buildQueryString(params);
132
+ if (query) url += `?${query}`;
133
+ }
134
+ return withCacheBust(url);
135
+ }
136
+
137
+ export function buildDocumentUrl(
138
+ collection: string,
139
+ id: string,
140
+ params?: Record<string, any>,
141
+ ): string {
142
+ let url = `/api/${collection}/${id}`;
143
+ if (params) {
144
+ const query = buildQueryString(params);
145
+ if (query) url += `?${query}`;
146
+ }
147
+ return url;
148
+ }
149
+
150
+ export async function apiUpload<T = any>(
151
+ url: string,
152
+ body: FormData,
153
+ ): Promise<T> {
154
+ const response = await fetch(url, {
155
+ method: "POST",
156
+ body,
157
+ credentials: "include",
158
+ });
159
+ if (!response.ok) {
160
+ throw new Error(`Upload Error: ${response.status}`);
161
+ }
162
+ return response.json();
163
+ }