@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,319 @@
1
+ import type { AuthAdapter, AuthUser, Session, UserRole } from "@kyro-cms/core";
2
+ import bcrypt from "bcryptjs";
3
+ import { randomBytes } from "crypto";
4
+ import Database from "better-sqlite3";
5
+ import path from "path";
6
+ import fs from "fs";
7
+
8
+ const DB_DIR = path.join(process.cwd(), "data");
9
+ const DB_PATH = path.join(DB_DIR, "kyro.db");
10
+
11
+ if (!fs.existsSync(DB_DIR)) {
12
+ fs.mkdirSync(DB_DIR, { recursive: true });
13
+ }
14
+
15
+ export const db = new Database(DB_PATH);
16
+ db.pragma("journal_mode = WAL");
17
+
18
+ db.exec(`
19
+ CREATE TABLE IF NOT EXISTS users (
20
+ id TEXT PRIMARY KEY,
21
+ email TEXT UNIQUE NOT NULL,
22
+ password_hash TEXT NOT NULL,
23
+ name TEXT,
24
+ role TEXT DEFAULT 'customer',
25
+ created_at TEXT NOT NULL,
26
+ updated_at TEXT NOT NULL
27
+ );
28
+
29
+ CREATE TABLE IF NOT EXISTS sessions (
30
+ token TEXT PRIMARY KEY,
31
+ user_id TEXT NOT NULL,
32
+ expires_at TEXT NOT NULL,
33
+ created_at TEXT NOT NULL
34
+ );
35
+
36
+ CREATE TABLE IF NOT EXISTS roles (
37
+ id TEXT PRIMARY KEY,
38
+ name TEXT UNIQUE NOT NULL,
39
+ level INTEGER DEFAULT 50,
40
+ inherits TEXT DEFAULT '[]',
41
+ permissions TEXT DEFAULT '[]',
42
+ description TEXT,
43
+ created_at TEXT NOT NULL
44
+ );
45
+
46
+ CREATE TABLE IF NOT EXISTS audit_logs (
47
+ id TEXT PRIMARY KEY,
48
+ user_id TEXT,
49
+ action TEXT NOT NULL,
50
+ entity_type TEXT,
51
+ entity_id TEXT,
52
+ metadata TEXT DEFAULT '{}',
53
+ ip_address TEXT,
54
+ user_agent TEXT,
55
+ created_at TEXT NOT NULL
56
+ );
57
+ `);
58
+
59
+ export interface SQLiteAuthAdapterOptions {
60
+ tokenExpiration?: number;
61
+ refreshTokenExpiration?: number;
62
+ }
63
+
64
+ const DEFAULT_TOKEN_EXPIRATION = 86400;
65
+ const DEFAULT_REFRESH_EXPIRATION = 604800;
66
+
67
+ export class SQLiteAuthAdapter implements AuthAdapter {
68
+ private tokenExpiration: number;
69
+ private refreshExpiration: number;
70
+
71
+ constructor(options: SQLiteAuthAdapterOptions = {}) {
72
+ this.tokenExpiration = options.tokenExpiration || DEFAULT_TOKEN_EXPIRATION;
73
+ this.refreshExpiration =
74
+ options.refreshTokenExpiration || DEFAULT_REFRESH_EXPIRATION;
75
+ }
76
+
77
+ private getTimestamp(): string {
78
+ return new Date().toISOString();
79
+ }
80
+
81
+ private generateId(): string {
82
+ return randomBytes(16).toString("hex");
83
+ }
84
+
85
+ private getDb(): Database.Database {
86
+ return db;
87
+ }
88
+
89
+ async findUserById(id: string): Promise<AuthUser | null> {
90
+ const user = db.prepare("SELECT * FROM users WHERE id = ?").get(id) as any;
91
+ if (!user) return null;
92
+ return {
93
+ id: user.id,
94
+ email: user.email,
95
+ name: user.name,
96
+ role: user.role as UserRole,
97
+ createdAt: user.created_at,
98
+ updatedAt: user.updated_at,
99
+ };
100
+ }
101
+
102
+ async findUserByEmail(email: string): Promise<AuthUser | null> {
103
+ const user = db
104
+ .prepare("SELECT * FROM users WHERE email = ?")
105
+ .get(email) as any;
106
+ if (!user) return null;
107
+ return {
108
+ id: user.id,
109
+ email: user.email,
110
+ name: user.name,
111
+ role: user.role as UserRole,
112
+ createdAt: user.created_at,
113
+ updatedAt: user.updated_at,
114
+ };
115
+ }
116
+
117
+ async createUser(
118
+ userData: Partial<AuthUser> & { password: string },
119
+ ): Promise<AuthUser> {
120
+ const now = this.getTimestamp();
121
+ const id = this.generateId();
122
+ const passwordHash = await bcrypt.hash(userData.password, 10);
123
+
124
+ db.prepare(
125
+ "INSERT INTO users (id, email, password_hash, name, role, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)",
126
+ ).run(
127
+ id,
128
+ userData.email,
129
+ passwordHash,
130
+ userData.name || null,
131
+ userData.role || "customer",
132
+ now,
133
+ now,
134
+ );
135
+
136
+ return {
137
+ id,
138
+ email: userData.email,
139
+ name: userData.name,
140
+ role: userData.role || "customer",
141
+ createdAt: now,
142
+ updatedAt: now,
143
+ };
144
+ }
145
+
146
+ async updateUser(
147
+ id: string,
148
+ data: Partial<AuthUser> & { password?: string },
149
+ ): Promise<AuthUser | null> {
150
+ const existing = await this.findUserById(id);
151
+ if (!existing) return null;
152
+
153
+ const now = this.getTimestamp();
154
+ const updates: string[] = [];
155
+ const values: any[] = [];
156
+
157
+ if (data.email !== undefined) {
158
+ updates.push("email = ?");
159
+ values.push(data.email);
160
+ }
161
+ if (data.name !== undefined) {
162
+ updates.push("name = ?");
163
+ values.push(data.name);
164
+ }
165
+ if (data.role !== undefined) {
166
+ updates.push("role = ?");
167
+ values.push(data.role);
168
+ }
169
+ if (data.password !== undefined) {
170
+ updates.push("password_hash = ?");
171
+ values.push(await bcrypt.hash(data.password, 10));
172
+ }
173
+
174
+ updates.push("updated_at = ?");
175
+ values.push(now);
176
+ values.push(id);
177
+
178
+ db.prepare(`UPDATE users SET ${updates.join(", ")} WHERE id = ?`).run(
179
+ ...values,
180
+ );
181
+
182
+ return this.findUserById(id);
183
+ }
184
+
185
+ async deleteUser(id: string): Promise<boolean> {
186
+ const result = db.prepare("DELETE FROM users WHERE id = ?").run(id);
187
+ if (result.changes > 0) {
188
+ db.prepare("DELETE FROM sessions WHERE user_id = ?").run(id);
189
+ return true;
190
+ }
191
+ return false;
192
+ }
193
+
194
+ async verifyPassword(
195
+ email: string,
196
+ password: string,
197
+ ): Promise<AuthUser | null> {
198
+ const user = db
199
+ .prepare("SELECT * FROM users WHERE email = ?")
200
+ .get(email) as any;
201
+ if (!user) return null;
202
+
203
+ const valid = await bcrypt.compare(password, user.password_hash);
204
+ if (!valid) return null;
205
+
206
+ return {
207
+ id: user.id,
208
+ email: user.email,
209
+ name: user.name,
210
+ role: user.role as UserRole,
211
+ createdAt: user.created_at,
212
+ updatedAt: user.updated_at,
213
+ };
214
+ }
215
+
216
+ async createSession(userId: string): Promise<Session> {
217
+ const now = new Date();
218
+ const expiresAt = new Date(now.getTime() + this.tokenExpiration * 1000);
219
+ const refreshExpiresAt = new Date(
220
+ now.getTime() + this.refreshExpiration * 1000,
221
+ );
222
+
223
+ const token = randomBytes(32).toString("hex");
224
+ const refreshToken = randomBytes(32).toString("hex");
225
+
226
+ db.prepare(
227
+ "INSERT INTO sessions (token, user_id, expires_at, created_at) VALUES (?, ?, ?, ?)",
228
+ ).run(token, userId, expiresAt.toISOString(), now.toISOString());
229
+
230
+ return {
231
+ token,
232
+ refreshToken,
233
+ expiresAt: expiresAt.toISOString(),
234
+ userId,
235
+ createdAt: now.toISOString(),
236
+ };
237
+ }
238
+
239
+ async validateSession(token: string): Promise<Session | null> {
240
+ const now = new Date().toISOString();
241
+ const session = db
242
+ .prepare("SELECT * FROM sessions WHERE token = ? AND expires_at > ?")
243
+ .get(token, now) as any;
244
+
245
+ if (!session) return null;
246
+
247
+ return {
248
+ token: session.token,
249
+ userId: session.user_id,
250
+ expiresAt: session.expires_at,
251
+ createdAt: session.created_at,
252
+ };
253
+ }
254
+
255
+ async refreshSession(token: string): Promise<Session | null> {
256
+ const session = await this.validateSession(token);
257
+ if (!session) return null;
258
+
259
+ return this.createSession(session.userId);
260
+ }
261
+
262
+ async revokeSession(token: string): Promise<void> {
263
+ db.prepare("DELETE FROM sessions WHERE token = ?").run(token);
264
+ }
265
+
266
+ async revokeAllUserSessions(userId: string): Promise<void> {
267
+ db.prepare("DELETE FROM sessions WHERE user_id = ?").run(userId);
268
+ }
269
+
270
+ async findUserRoles(): Promise<UserRole[]> {
271
+ const roles = db
272
+ .prepare("SELECT * FROM roles ORDER BY level DESC")
273
+ .all() as any[];
274
+ return roles.map((r) => ({
275
+ id: r.id,
276
+ name: r.name,
277
+ level: r.level,
278
+ inherits: JSON.parse(r.inherits || "[]"),
279
+ permissions: JSON.parse(r.permissions || "[]"),
280
+ description: r.description,
281
+ createdAt: r.created_at,
282
+ }));
283
+ }
284
+
285
+ async createRole(role: Partial<UserRole>): Promise<UserRole> {
286
+ const now = this.getTimestamp();
287
+ const id = role.id || this.generateId();
288
+
289
+ db.prepare(
290
+ "INSERT INTO roles (id, name, level, inherits, permissions, description, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)",
291
+ ).run(
292
+ id,
293
+ role.name,
294
+ role.level || 50,
295
+ JSON.stringify(role.inherits || []),
296
+ JSON.stringify(role.permissions || []),
297
+ role.description || null,
298
+ now,
299
+ );
300
+
301
+ return {
302
+ id,
303
+ name: role.name!,
304
+ level: role.level || 50,
305
+ inherits: role.inherits || [],
306
+ permissions: role.permissions || [],
307
+ description: role.description,
308
+ createdAt: now,
309
+ };
310
+ }
311
+
312
+ async findUserByIdWithPassword(
313
+ id: string,
314
+ ): Promise<{ id: string; email: string; password_hash: string } | null> {
315
+ return db
316
+ .prepare("SELECT id, email, password_hash FROM users WHERE id = ?")
317
+ .get(id) as any;
318
+ }
319
+ }
package/src/lib/config.ts CHANGED
@@ -1,8 +1,8 @@
1
- import type { CollectionConfig, GlobalConfig } from "@kyro-cms/core";
2
- import { blogCollections } from "../../../src/index";
3
- import { ecommerceCollections } from "../../../src/index";
4
- import { minimalCollections } from "../../../src/index";
5
- import { kitchenSinkCollections } from "../../../src/index";
1
+ import type { CollectionConfig, GlobalConfig } from "@kyro-cms/core/client";
2
+ import { blogCollections } from "../../../src/templates/blog";
3
+ import { ecommerceCollections } from "../../../src/templates/ecommerce";
4
+ import { minimalCollections } from "../../../src/templates/minimal";
5
+ import { kitchenSinkCollections } from "../../../src/templates/kitchen-sink";
6
6
  import { mediaCollections } from "../../../src/templates/media";
7
7
  import {
8
8
  allSettingsGlobals,
@@ -10,8 +10,14 @@ import {
10
10
  ecommerceSettingsGlobals,
11
11
  } from "../../../src/templates/settings";
12
12
  import { authCollections } from "../collections/auth";
13
+ import { portfolioCollections } from "../collections/portfolio";
13
14
 
14
- export type AdminTemplate = "minimal" | "blog" | "ecommerce" | "kitchen-sink";
15
+ export type AdminTemplate =
16
+ | "minimal"
17
+ | "blog"
18
+ | "ecommerce"
19
+ | "kitchen-sink"
20
+ | "portfolio";
15
21
 
16
22
  export function getAdminConfig(template: AdminTemplate = "blog") {
17
23
  const collections: CollectionConfig[] = [];
@@ -42,6 +48,11 @@ export function getAdminConfig(template: AdminTemplate = "blog") {
42
48
  );
43
49
  globals.push(...allSettingsGlobals);
44
50
  break;
51
+ case "portfolio":
52
+ collections.push(...Object.values(blogCollections));
53
+ collections.push(...Object.values(portfolioCollections));
54
+ globals.push(...coreSettingsGlobals);
55
+ break;
45
56
  }
46
57
 
47
58
  const collectionsMap = collections.reduce(
@@ -63,6 +74,11 @@ export function getAdminConfig(template: AdminTemplate = "blog") {
63
74
  return { collections: collectionsMap, globals: globalsMap };
64
75
  }
65
76
 
66
- export const adminConfig = getAdminConfig("blog");
77
+ export const adminConfig = getAdminConfig("minimal");
67
78
  export const collections = adminConfig.collections;
68
79
  export const globals = adminConfig.globals;
80
+
81
+ export const authCollectionSlugs = ["users", "roles", "audit_logs"];
82
+ export const nonAuthCollections = Object.values(collections).filter(
83
+ (c) => !authCollectionSlugs.includes(c.slug),
84
+ );
@@ -1,111 +1,226 @@
1
- import type { CollectionConfig } from "@kyro-cms/core";
1
+ import type { CollectionConfig } from "@kyro-cms/core/client";
2
+ import { randomUUID } from "crypto";
3
+ import {
4
+ initializeDatabase,
5
+ getDatabaseAdapter,
6
+ isDatabaseInitialized,
7
+ } from "./db";
8
+ import type { Version, VersionDiff, DraftPublishConfig } from "@kyro-cms/core";
9
+ import { VersionManager, createVersionManager } from "@kyro-cms/core";
10
+ import { DataStoreVersionAdapter } from "./db/version-adapter.js";
2
11
 
3
- interface Document {
4
- id: string;
5
- [key: string]: any;
6
- }
7
-
8
- class DataStore {
9
- private data: Map<string, Document[]> = new Map();
10
- private metadata: Map<string, { createdAt: string; updatedAt: string }> =
11
- new Map();
12
- private idCounters: Map<string, number> = new Map();
12
+ class DataStoreWrapper {
13
+ private db: ReturnType<typeof getDatabaseAdapter> | null = null;
14
+ private versionManager: VersionManager | null = null;
13
15
 
14
- initialize(collections: Record<string, CollectionConfig>) {
15
- for (const [slug, config] of Object.entries(collections)) {
16
- if (!this.data.has(slug)) {
17
- this.data.set(slug, []);
18
- this.idCounters.set(slug, 1);
19
- }
16
+ private async getStore() {
17
+ if (!isDatabaseInitialized()) {
18
+ await initializeDatabase();
20
19
  }
20
+ return getDatabaseAdapter();
21
+ }
22
+
23
+ private async getVersionManager(): Promise<VersionManager> {
24
+ if (this.versionManager) return this.versionManager;
25
+ const db = await this.getStore();
26
+ const adapter = new DataStoreVersionAdapter(db);
27
+ this.versionManager = createVersionManager(adapter, {
28
+ versioningEnabled: true,
29
+ maxVersionsPerDocument: 50,
30
+ } as DraftPublishConfig);
31
+ return this.versionManager;
21
32
  }
22
33
 
23
- private generateId(slug: string): string {
24
- const counter = this.idCounters.get(slug) || 1;
25
- this.idCounters.set(slug, counter + 1);
26
- return `${slug}-${counter}`;
34
+ initialize(collections: Record<string, CollectionConfig>) {
35
+ initializeDatabase(collections);
27
36
  }
28
37
 
29
- private getTimestamp(): string {
38
+ private async getTimestamp(): Promise<string> {
30
39
  return new Date().toISOString();
31
40
  }
32
41
 
33
- find<T = any>(
42
+ private generateId(): string {
43
+ return randomUUID();
44
+ }
45
+
46
+ async find<T = any>(
34
47
  slug: string,
35
48
  options: { page?: number; limit?: number } = {},
36
- ): { docs: T[]; totalDocs: number; totalPages: number; page: number } {
37
- const docs = this.data.get(slug) || [];
38
- const page = options.page || 1;
39
- const limit = options.limit || 25;
40
- const start = (page - 1) * limit;
41
- const end = start + limit;
42
-
43
- return {
44
- docs: docs.slice(start, end) as T[],
45
- totalDocs: docs.length,
46
- totalPages: Math.ceil(docs.length / limit),
47
- page,
48
- };
49
+ ): Promise<{
50
+ docs: T[];
51
+ totalDocs: number;
52
+ totalPages: number;
53
+ page: number;
54
+ }> {
55
+ const store = await this.getStore();
56
+ return store.find(slug, options);
49
57
  }
50
58
 
51
- findById<T = any>(slug: string, id: string): T | null {
52
- const docs = this.data.get(slug) || [];
53
- return (docs.find((d) => d.id === id) as T) || null;
59
+ async findById<T = any>(slug: string, id: string): Promise<T | null> {
60
+ const store = await this.getStore();
61
+ return store.findById(slug, id);
54
62
  }
55
63
 
56
- create<T = any>(slug: string, data: Partial<T>): T {
57
- const docs = this.data.get(slug) || [];
58
- const now = this.getTimestamp();
64
+ async create<T = any>(slug: string, data: Partial<T>): Promise<T> {
65
+ const store = await this.getStore();
66
+ const now = await this.getTimestamp();
67
+ const id = (data as any)?.id || this.generateId();
59
68
  const newDoc = {
60
- id: this.generateId(slug),
61
69
  ...data,
70
+ id,
62
71
  createdAt: now,
63
72
  updatedAt: now,
64
73
  } as T;
65
- docs.push(newDoc as any);
66
- this.data.set(slug, docs);
67
- this.metadata.set(`${slug}:${(newDoc as any).id}`, {
68
- createdAt: now,
69
- updatedAt: now,
70
- });
71
- return newDoc;
74
+ return store.create(slug, newDoc);
72
75
  }
73
76
 
74
- update<T = any>(slug: string, id: string, data: Partial<T>): T | null {
75
- const docs = this.data.get(slug) || [];
76
- const index = docs.findIndex((d) => d.id === id);
77
- if (index === -1) return null;
77
+ async update<T = any>(
78
+ slug: string,
79
+ id: string,
80
+ data: Partial<T>,
81
+ options?: {
82
+ versionStatus?: "draft" | "published";
83
+ changeDescription?: string;
84
+ },
85
+ ): Promise<T | null> {
86
+ const store = await this.getStore();
87
+ const existing = await store.findById(slug, id);
88
+ if (!existing) return null;
78
89
 
79
- const now = this.getTimestamp();
90
+ const now = await this.getTimestamp();
80
91
  const updated = {
81
- ...docs[index],
92
+ ...existing,
82
93
  ...data,
83
94
  id,
84
95
  updatedAt: now,
85
96
  };
86
- docs[index] = updated;
87
- this.data.set(slug, docs);
88
- this.metadata.set(`${slug}:${id}`, {
89
- createdAt: docs[index].createdAt,
90
- updatedAt: now,
97
+
98
+ // Save version history before updating
99
+ const versionStatus = options?.versionStatus || "draft";
100
+ const changeDescription = options?.changeDescription;
101
+
102
+ await this.createVersion(slug, id, existing, {
103
+ status: versionStatus,
104
+ changeDescription,
91
105
  });
92
- return updated as T;
106
+
107
+ return store.update(slug, id, updated);
108
+ }
109
+
110
+ async findVersions(parentCollection: string, parentId: string) {
111
+ const vm = await this.getVersionManager();
112
+ const versions = await vm.getVersionHistory(
113
+ parentCollection,
114
+ parentId,
115
+ 100,
116
+ );
117
+ return (versions as Version[])
118
+ .map((v) => ({
119
+ id: v.id,
120
+ version: v.version,
121
+ createdAt: v.createdAt,
122
+ status: v.status,
123
+ createdBy: v.createdBy,
124
+ data: v.data,
125
+ changeDescription: v.changeDescription,
126
+ }))
127
+ .sort((a, b) => b.version - a.version);
93
128
  }
94
129
 
95
- delete(slug: string, id: string): boolean {
96
- const docs = this.data.get(slug) || [];
97
- const index = docs.findIndex((d) => d.id === id);
98
- if (index === -1) return false;
130
+ async createVersion(
131
+ parentCollection: string,
132
+ parentId: string,
133
+ data: any,
134
+ options?: {
135
+ status?: string;
136
+ createdBy?: string;
137
+ changeDescription?: string;
138
+ },
139
+ ) {
140
+ const vm = await this.getVersionManager();
141
+ try {
142
+ return await vm.createVersion({
143
+ collection: parentCollection,
144
+ documentId: parentId,
145
+ data,
146
+ status: (options?.status || "draft") as "draft" | "published",
147
+ createdBy: options?.createdBy || "system",
148
+ changeDescription: options?.changeDescription,
149
+ });
150
+ } catch (e) {
151
+ console.error("Failed to create version snapshot:", e);
152
+ return null;
153
+ }
154
+ }
155
+
156
+ async restoreVersion(
157
+ parentCollection: string,
158
+ parentId: string,
159
+ versionId: string,
160
+ ) {
161
+ const vm = await this.getVersionManager();
162
+ const store = await this.getStore();
163
+ const version = await store.findById("_versions", versionId);
164
+ if (!version || version.parentId !== parentId) return null;
165
+
166
+ return this.update(parentCollection, parentId, version.data);
167
+ }
168
+
169
+ async compareVersions(
170
+ collection: string,
171
+ documentId: string,
172
+ versionA: string | number,
173
+ versionB: string | number,
174
+ ): Promise<VersionDiff[]> {
175
+ const vm = await this.getVersionManager();
176
+ return vm.compareTwoVersions(collection, documentId, versionA, versionB);
177
+ }
178
+
179
+ async delete(slug: string, id: string): Promise<boolean> {
180
+ const store = await this.getStore();
181
+ return store.delete(slug, id);
182
+ }
183
+
184
+ async findOne(
185
+ slug: string,
186
+ filter: Record<string, any>,
187
+ ): Promise<any | null> {
188
+ const store = await this.getStore();
189
+ const results = await store.find(slug, { limit: 1 });
190
+ return results.docs.length > 0 ? results.docs[0] : null;
191
+ }
192
+
193
+ async findGlobal<T = any>(slug: string): Promise<T> {
194
+ const store = await this.getStore();
195
+ return store.findGlobal(slug);
196
+ }
197
+
198
+ async updateGlobal<T = any>(slug: string, data: Partial<T>): Promise<T> {
199
+ const store = await this.getStore();
200
+ const current = await store.findGlobal(slug);
201
+ const updated = { ...current, ...data };
202
+ return store.updateGlobal(slug, updated);
203
+ }
204
+
205
+ async seedGlobal(slug: string, data: any): Promise<void> {
206
+ const store = await this.getStore();
207
+ return store.seedGlobal(slug, data);
208
+ }
209
+
210
+ async count(slug: string): Promise<number> {
211
+ const store = await this.getStore();
212
+ return store.count(slug);
213
+ }
99
214
 
100
- docs.splice(index, 1);
101
- this.data.set(slug, docs);
102
- this.metadata.delete(`${slug}:${id}`);
103
- return true;
215
+ async seed(slug: string, docs: any[]): Promise<void> {
216
+ const store = await this.getStore();
217
+ return store.seed(slug, docs);
104
218
  }
105
219
 
106
- count(slug: string): number {
107
- return (this.data.get(slug) || []).length;
220
+ async isSeeded(slug: string): Promise<boolean> {
221
+ const store = await this.getStore();
222
+ return store.isSeeded(slug);
108
223
  }
109
224
  }
110
225
 
111
- export const dataStore = new DataStore();
226
+ export const dataStore = new DataStoreWrapper();