@kyro-cms/admin 0.3.1 → 0.3.4

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 (242) hide show
  1. package/dist/EditorClient-XEUOVAAC.js +466 -0
  2. package/dist/EditorClient-XEUOVAAC.js.map +1 -0
  3. package/dist/EditorClient-YLCGVDXY.cjs +468 -0
  4. package/dist/EditorClient-YLCGVDXY.cjs.map +1 -0
  5. package/dist/chunk-7KPIUCGT.js +384 -0
  6. package/dist/chunk-7KPIUCGT.js.map +1 -0
  7. package/dist/chunk-GOACG6R7.cjs +473 -0
  8. package/dist/chunk-GOACG6R7.cjs.map +1 -0
  9. package/dist/index.cjs +14861 -0
  10. package/dist/index.cjs.map +1 -0
  11. package/dist/index.css +1661 -0
  12. package/dist/index.css.map +1 -0
  13. package/dist/index.d.ts +563 -0
  14. package/dist/index.js +14784 -0
  15. package/dist/index.js.map +1 -0
  16. package/package.json +19 -19
  17. package/src/components/ActionBar.tsx +7 -43
  18. package/src/components/Admin.tsx +138 -277
  19. package/src/components/ApiKeysManager.tsx +428 -419
  20. package/src/components/AuditLogsPage.tsx +35 -39
  21. package/src/components/AuthBridge.tsx +51 -0
  22. package/src/components/AutoForm.tsx +495 -1230
  23. package/src/components/BrandingHub.tsx +18 -19
  24. package/src/components/BulkActionsBar.tsx +1 -1
  25. package/src/components/CreateView.tsx +22 -36
  26. package/src/components/Dashboard.tsx +60 -84
  27. package/src/components/DetailView.tsx +113 -91
  28. package/src/components/DeveloperCenter.tsx +200 -198
  29. package/src/components/FieldRenderer.tsx +206 -0
  30. package/src/components/GraphQLPlayground.tsx +340 -480
  31. package/src/components/ListView.tsx +828 -254
  32. package/src/components/LoginPage.tsx +3 -4
  33. package/src/components/MarketplaceManager.tsx +254 -0
  34. package/src/components/MediaGallery.tsx +856 -1192
  35. package/src/components/PluginsManager.tsx +277 -0
  36. package/src/components/RestPlayground.tsx +398 -560
  37. package/src/components/SessionsManager.tsx +211 -0
  38. package/src/components/Sidebar.astro +179 -151
  39. package/src/components/ThemeProvider.tsx +7 -161
  40. package/src/components/UserManagement.tsx +162 -146
  41. package/src/components/UserMenu.tsx +110 -0
  42. package/src/components/WebhookManager.tsx +305 -367
  43. package/src/components/blocks/AccordionBlock.tsx +4 -4
  44. package/src/components/blocks/ArrayBlock.tsx +3 -3
  45. package/src/components/blocks/BlockEditModal.tsx +8 -8
  46. package/src/components/blocks/BlockWrapper.tsx +61 -0
  47. package/src/components/blocks/ButtonBlock.tsx +4 -4
  48. package/src/components/blocks/ChildBlocksTree.tsx +23 -25
  49. package/src/components/blocks/CodeBlock.tsx +15 -15
  50. package/src/components/blocks/ColumnsBlock.tsx +6 -44
  51. package/src/components/blocks/DividerBlock.tsx +3 -3
  52. package/src/components/blocks/FileBlock.tsx +4 -4
  53. package/src/components/blocks/HeadingBlock.tsx +6 -38
  54. package/src/components/blocks/HeroBlock.tsx +4 -4
  55. package/src/components/blocks/ImageBlock.tsx +4 -4
  56. package/src/components/blocks/LinkBlock.tsx +4 -4
  57. package/src/components/blocks/ListBlock.tsx +3 -3
  58. package/src/components/blocks/ParagraphBlock.tsx +12 -42
  59. package/src/components/blocks/RelationshipBlock.tsx +4 -4
  60. package/src/components/blocks/RichTextBlock.tsx +4 -4
  61. package/src/components/blocks/VStackBlock.tsx +5 -37
  62. package/src/components/blocks/VideoBlock.tsx +4 -4
  63. package/src/components/blocks/types.ts +11 -0
  64. package/src/components/fields/AccordionField.tsx +1 -1
  65. package/src/components/fields/ArrayField.tsx +2 -2
  66. package/src/components/fields/ArrayLayout.tsx +93 -0
  67. package/src/components/fields/BlocksField.tsx +122 -111
  68. package/src/components/fields/ButtonField.tsx +1 -1
  69. package/src/components/fields/CheckboxField.tsx +14 -15
  70. package/src/components/fields/ChildrenField.tsx +2 -2
  71. package/src/components/fields/CodeField.tsx +3 -3
  72. package/src/components/fields/ColumnsField.tsx +2 -2
  73. package/src/components/fields/DateField.tsx +13 -26
  74. package/src/components/fields/EditorClient.tsx +26 -28
  75. package/src/components/fields/FieldLayout.tsx +52 -0
  76. package/src/components/fields/GroupLayout.tsx +35 -0
  77. package/src/components/fields/JSONField.tsx +7 -7
  78. package/src/components/fields/LinkField.tsx +1 -1
  79. package/src/components/fields/MarkdownField.tsx +1 -1
  80. package/src/components/fields/NumberField.tsx +13 -26
  81. package/src/components/fields/PortableTextField.tsx +4 -4
  82. package/src/components/fields/PortableTextRenderer.tsx +1 -1
  83. package/src/components/fields/RelationshipBlockField.tsx +31 -23
  84. package/src/components/fields/RelationshipField.tsx +14 -14
  85. package/src/components/fields/SelectField.tsx +17 -26
  86. package/src/components/fields/TabsLayout.tsx +69 -0
  87. package/src/components/fields/TextField.tsx +85 -38
  88. package/src/components/fields/UploadField.tsx +71 -41
  89. package/src/components/fields/VideoField.tsx +1 -1
  90. package/src/components/fields/extensions/blockComponents.tsx +2 -2
  91. package/src/components/fields/extensions/blocksStore.ts +207 -193
  92. package/src/components/fields/types.ts +22 -0
  93. package/src/components/layout/Layout.tsx +1 -1
  94. package/src/components/ui/ActionMenu.tsx +63 -0
  95. package/src/components/ui/Badge.tsx +59 -5
  96. package/src/components/ui/BlockDrawer.tsx +4 -5
  97. package/src/components/ui/CommandPalette.tsx +58 -36
  98. package/src/components/ui/CommandPaletteWrapper.tsx +18 -17
  99. package/src/components/ui/Dropdown.tsx +18 -16
  100. package/src/components/ui/EmptyState.tsx +25 -0
  101. package/src/components/ui/GlobalModal.tsx +49 -0
  102. package/src/components/ui/IconButton.tsx +44 -0
  103. package/src/components/ui/Modal.tsx +19 -20
  104. package/src/components/ui/PageHeader.tsx +158 -0
  105. package/src/components/ui/Pagination.tsx +61 -0
  106. package/src/components/ui/PromptModal.tsx +1 -1
  107. package/src/components/ui/SearchInput.tsx +57 -0
  108. package/src/components/ui/SeoPreview.tsx +31 -0
  109. package/src/components/ui/SessionModal.tsx +0 -0
  110. package/src/components/ui/SlidePanel.tsx +2 -0
  111. package/src/components/ui/Toast.tsx +65 -122
  112. package/src/components/ui/Toaster.tsx +18 -0
  113. package/src/components/ui/icons.tsx +112 -0
  114. package/src/components/users/UserDetail.tsx +290 -0
  115. package/src/components/users/UserForm.tsx +242 -0
  116. package/src/components/users/UsersList.tsx +338 -0
  117. package/src/env.d.ts +13 -13
  118. package/src/fields/index.ts +2 -1
  119. package/src/global.d.ts +7 -0
  120. package/src/hooks/data.ts +2 -9
  121. package/src/hooks/useAsyncData.ts +36 -0
  122. package/src/hooks/useAutoFormState.ts +527 -0
  123. package/src/hooks/useSelection.ts +49 -0
  124. package/src/hooks/useSession.ts +0 -0
  125. package/src/index.ts +11 -1
  126. package/src/integration.ts +86 -11
  127. package/src/kyro-cms.d.ts +209 -0
  128. package/src/layouts/AdminLayout.astro +128 -11
  129. package/src/layouts/AuthLayout.astro +21 -5
  130. package/src/lib/api.ts +175 -55
  131. package/src/lib/autoform-store.ts +435 -0
  132. package/src/lib/config.ts +82 -34
  133. package/src/lib/createRegistry.ts +29 -0
  134. package/src/lib/default-kyro-config.ts +4 -0
  135. package/src/lib/globals.ts +50 -0
  136. package/src/lib/media-utils.ts +18 -0
  137. package/src/lib/object-utils.ts +77 -0
  138. package/src/lib/paths.ts +61 -0
  139. package/src/lib/stores/index.ts +370 -0
  140. package/src/lib/types.ts +43 -0
  141. package/src/lib/useResourceManager.ts +105 -0
  142. package/src/pages/403.astro +67 -0
  143. package/src/pages/[collection]/[id].astro +14 -180
  144. package/src/pages/[collection]/index.astro +11 -6
  145. package/src/pages/api-explorer.astro +173 -0
  146. package/src/pages/audit/index.astro +2 -0
  147. package/src/pages/auth/login.astro +122 -0
  148. package/src/pages/auth/register.astro +167 -0
  149. package/src/pages/graphql-explorer.astro +59 -0
  150. package/src/pages/{admin/graphql.astro → graphql.astro} +51 -17
  151. package/src/pages/index.astro +577 -0
  152. package/src/pages/index_ALT.astro +3 -0
  153. package/src/pages/keys.astro +11 -0
  154. package/src/pages/marketplace.astro +11 -0
  155. package/src/pages/media.astro +3 -0
  156. package/src/pages/plugins.astro +8 -0
  157. package/src/pages/preview/[collection]/[id].astro +188 -123
  158. package/src/pages/rest-playground.astro +62 -0
  159. package/src/pages/roles/index.astro +183 -76
  160. package/src/pages/sessions.astro +8 -0
  161. package/src/pages/settings/[slug].astro +92 -114
  162. package/src/pages/settings/index.astro +5 -3
  163. package/src/pages/users/[id].astro +25 -154
  164. package/src/pages/users/index.astro +19 -130
  165. package/src/pages/users/new.astro +9 -86
  166. package/src/pages/webhooks.astro +11 -0
  167. package/src/routes.ts +80 -0
  168. package/src/styles/main.css +119 -79
  169. package/src/theme/tokens.ts +1 -0
  170. package/src/vite-env.d.ts +14 -0
  171. package/src/collections/auth/index.ts +0 -155
  172. package/src/collections/portfolio/index.ts +0 -343
  173. package/src/components/ApiExplorer.tsx +0 -325
  174. package/src/components/EnhancedListView.tsx +0 -889
  175. package/src/components/GraphQLExplorer.tsx +0 -675
  176. package/src/components/Icons.tsx +0 -23
  177. package/src/components/StatusBadge.tsx +0 -76
  178. package/src/lib/MediaService.ts +0 -541
  179. package/src/lib/auth/sqlite-adapter.ts +0 -319
  180. package/src/lib/dataStore.ts +0 -226
  181. package/src/lib/db/adapter.ts +0 -54
  182. package/src/lib/db/drizzle-mysql-adapter.ts +0 -194
  183. package/src/lib/db/drizzle-mysql-auth-adapter.ts +0 -327
  184. package/src/lib/db/drizzle-postgres-adapter.ts +0 -202
  185. package/src/lib/db/drizzle-postgres-auth-adapter.ts +0 -304
  186. package/src/lib/db/drizzle-sqlite-adapter.ts +0 -227
  187. package/src/lib/db/drizzle-sqlite-auth-adapter.ts +0 -548
  188. package/src/lib/db/index.ts +0 -449
  189. package/src/lib/db/mongodb-adapter.ts +0 -207
  190. package/src/lib/db/mongodb-auth-adapter.ts +0 -305
  191. package/src/lib/db/schema/mysql-auth.ts +0 -113
  192. package/src/lib/db/schema/mysql-content.ts +0 -20
  193. package/src/lib/db/schema/postgres-auth.ts +0 -116
  194. package/src/lib/db/schema/postgres-content.ts +0 -35
  195. package/src/lib/db/schema/postgres-media.ts +0 -52
  196. package/src/lib/db/schema/postgres-settings.ts +0 -11
  197. package/src/lib/db/schema/sqlite-auth.ts +0 -112
  198. package/src/lib/db/schema/sqlite-content.ts +0 -20
  199. package/src/lib/db/version-adapter.ts +0 -248
  200. package/src/lib/graphql/index.ts +0 -1
  201. package/src/lib/graphql/schema.ts +0 -443
  202. package/src/lib/rate-limit.ts +0 -267
  203. package/src/lib/storage.ts +0 -374
  204. package/src/lib/store.ts +0 -85
  205. package/src/middleware.ts +0 -177
  206. package/src/pages/admin/api-explorer.astro +0 -98
  207. package/src/pages/admin/graphql-explorer.astro +0 -40
  208. package/src/pages/admin/index.astro +0 -286
  209. package/src/pages/admin/keys.astro +0 -8
  210. package/src/pages/admin/rest-playground.astro +0 -44
  211. package/src/pages/admin/webhooks.astro +0 -8
  212. package/src/pages/api/[collection]/[id]/publish.ts +0 -52
  213. package/src/pages/api/[collection]/[id]/unpublish.ts +0 -42
  214. package/src/pages/api/[collection]/[id]/versions.ts +0 -66
  215. package/src/pages/api/[collection]/[id].ts +0 -213
  216. package/src/pages/api/[collection]/index.ts +0 -209
  217. package/src/pages/api/auth/[id].ts +0 -121
  218. package/src/pages/api/auth/audit-logs.ts +0 -57
  219. package/src/pages/api/auth/login.ts +0 -211
  220. package/src/pages/api/auth/logout.ts +0 -66
  221. package/src/pages/api/auth/me.ts +0 -36
  222. package/src/pages/api/auth/refresh.ts +0 -119
  223. package/src/pages/api/auth/register.ts +0 -188
  224. package/src/pages/api/auth/users.ts +0 -97
  225. package/src/pages/api/collections.ts +0 -59
  226. package/src/pages/api/globals/[slug].ts +0 -42
  227. package/src/pages/api/graphql.ts +0 -90
  228. package/src/pages/api/health.ts +0 -426
  229. package/src/pages/api/keys/[id].ts +0 -26
  230. package/src/pages/api/keys/index.ts +0 -75
  231. package/src/pages/api/media/[id].ts +0 -309
  232. package/src/pages/api/media/folders.ts +0 -609
  233. package/src/pages/api/media/index.ts +0 -146
  234. package/src/pages/api/media/resize.ts +0 -267
  235. package/src/pages/api/search.ts +0 -82
  236. package/src/pages/api/slug-availability.ts +0 -70
  237. package/src/pages/api/storage-config.ts +0 -20
  238. package/src/pages/api/storage-status.ts +0 -206
  239. package/src/pages/api/upload.ts +0 -334
  240. package/src/pages/api/webhooks/index.ts +0 -71
  241. package/src/pages/login.astro +0 -82
  242. package/src/pages/register.astro +0 -102
@@ -1,76 +0,0 @@
1
- import React, { type ReactNode } from "react";
2
-
3
- interface StatusBadgeProps {
4
- status:
5
- | "draft"
6
- | "published"
7
- | "scheduled"
8
- | "archived"
9
- | "active"
10
- | "inactive"
11
- | "pending"
12
- | "completed"
13
- | "cancelled";
14
- children?: ReactNode;
15
- }
16
-
17
- export function StatusBadge({ status, children }: StatusBadgeProps) {
18
- const statusConfig: Record<string, { class: string; defaultLabel?: string }> =
19
- {
20
- draft: { class: "bg-gray-100 text-gray-600", defaultLabel: "Draft" },
21
- published: {
22
- class: "bg-green-100 text-green-700",
23
- defaultLabel: "Published",
24
- },
25
- scheduled: {
26
- class: "bg-blue-100 text-blue-700",
27
- defaultLabel: "Scheduled",
28
- },
29
- archived: {
30
- class: "bg-yellow-100 text-yellow-700",
31
- defaultLabel: "Archived",
32
- },
33
- active: { class: "bg-green-100 text-green-700", defaultLabel: "Active" },
34
- inactive: {
35
- class: "bg-gray-100 text-gray-600",
36
- defaultLabel: "Inactive",
37
- },
38
- pending: {
39
- class: "bg-yellow-100 text-yellow-700",
40
- defaultLabel: "Pending",
41
- },
42
- completed: {
43
- class: "bg-green-100 text-green-700",
44
- defaultLabel: "Completed",
45
- },
46
- cancelled: {
47
- class: "bg-red-100 text-red-700",
48
- defaultLabel: "Cancelled",
49
- },
50
- };
51
-
52
- const config = statusConfig[status] || { class: "bg-gray-100 text-gray-600" };
53
-
54
- return (
55
- <span
56
- className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${config.class}`}
57
- >
58
- {children || config.defaultLabel || status}
59
- </span>
60
- );
61
- }
62
-
63
- interface CountBadgeProps {
64
- count: number;
65
- max?: number;
66
- }
67
-
68
- export function CountBadge({ count, max = 99 }: CountBadgeProps) {
69
- if (count === 0) return null;
70
-
71
- return (
72
- <span className="inline-flex items-center justify-center min-w-[20px] h-5 px-1.5 text-xs font-medium bg-gray-200 text-gray-700 rounded-full">
73
- {count > max ? `${max}+` : count}
74
- </span>
75
- );
76
- }
@@ -1,541 +0,0 @@
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 };