opacacms 0.1.0 → 0.1.2

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 (247) hide show
  1. package/dist/admin/index.js +49 -0
  2. package/dist/{chunk-2zm8cy1w.js → admin/webcomponent.js} +116 -168
  3. package/dist/{chunk-6dhs73zq.js → chunk-2yz1nsxs.js} +1 -1
  4. package/dist/chunk-fa5mg0hr.js +96 -0
  5. package/dist/{chunk-kwp83w8b.js → chunk-m09hahe2.js} +7 -7
  6. package/dist/{chunk-hmhcense.js → chunk-ry15hke8.js} +253 -4
  7. package/dist/chunk-vtvqfhgy.js +2442 -0
  8. package/dist/{chunk-f3nvxn63.js → chunk-y8hc6nm4.js} +1 -1
  9. package/dist/{src/cli → cli}/index.js +10 -10
  10. package/dist/{src/client.js → client.js} +2 -2
  11. package/dist/{src/db → db}/bun-sqlite.js +10 -10
  12. package/dist/{src/db → db}/d1.js +8 -8
  13. package/dist/db/index.d.ts +2 -0
  14. package/dist/db/index.js +7 -0
  15. package/dist/db/migration.d.ts +39 -0
  16. package/dist/{src/db → db}/postgres.js +10 -10
  17. package/dist/{src/db → db}/sqlite.js +8 -8
  18. package/dist/index.d.ts +0 -2
  19. package/dist/index.js +13 -0
  20. package/dist/{src/runtimes → runtimes}/bun.js +5 -6
  21. package/dist/{src/runtimes → runtimes}/cloudflare-workers.js +5 -6
  22. package/dist/{src/runtimes → runtimes}/next.js +5 -6
  23. package/dist/{src/runtimes → runtimes}/node.js +5 -6
  24. package/dist/{src/server.js → server.js} +7 -8
  25. package/dist/storage/index.d.ts +0 -3
  26. package/dist/storage/index.js +35 -0
  27. package/dist/types.d.ts +5 -2
  28. package/package.json +161 -39
  29. package/bun.lock +0 -34
  30. package/dist/api.d.ts +0 -6
  31. package/dist/chunk-8gkhn1d4.js +0 -309
  32. package/dist/chunk-dy5t83hr.js +0 -261
  33. package/dist/src/admin/index.js +0 -176
  34. package/dist/src/admin/webcomponent.js +0 -19
  35. package/dist/src/api.js +0 -27
  36. package/dist/src/index.js +0 -20
  37. package/dist/src/storage/index.js +0 -355
  38. package/global.d.ts +0 -11
  39. package/src/admin/api-client.ts +0 -63
  40. package/src/admin/auth-client.ts +0 -40
  41. package/src/admin/custom-field.ts +0 -179
  42. package/src/admin/index.ts +0 -15
  43. package/src/admin/react.tsx +0 -72
  44. package/src/admin/router.ts +0 -9
  45. package/src/admin/stores/admin-queries.ts +0 -121
  46. package/src/admin/stores/auth.ts +0 -61
  47. package/src/admin/stores/column-visibility.ts +0 -67
  48. package/src/admin/stores/config.ts +0 -15
  49. package/src/admin/stores/media.ts +0 -95
  50. package/src/admin/stores/query.ts +0 -13
  51. package/src/admin/stores/ui.ts +0 -29
  52. package/src/admin/ui/admin-client.tsx +0 -283
  53. package/src/admin/ui/admin-layout.tsx +0 -276
  54. package/src/admin/ui/components/ColumnVisibilityToggle.tsx +0 -141
  55. package/src/admin/ui/components/DataDetailSheet.tsx +0 -141
  56. package/src/admin/ui/components/DataDetailView.tsx +0 -175
  57. package/src/admin/ui/components/Table.tsx +0 -67
  58. package/src/admin/ui/components/fields/ArrayField.tsx +0 -166
  59. package/src/admin/ui/components/fields/BlocksField.tsx +0 -202
  60. package/src/admin/ui/components/fields/BooleanField.tsx +0 -50
  61. package/src/admin/ui/components/fields/CollapsibleField.tsx +0 -75
  62. package/src/admin/ui/components/fields/DateField.tsx +0 -45
  63. package/src/admin/ui/components/fields/FileField.tsx +0 -322
  64. package/src/admin/ui/components/fields/GroupField.tsx +0 -50
  65. package/src/admin/ui/components/fields/JoinField.tsx +0 -23
  66. package/src/admin/ui/components/fields/NumberField.tsx +0 -46
  67. package/src/admin/ui/components/fields/RadioField.tsx +0 -62
  68. package/src/admin/ui/components/fields/RelationshipField.tsx +0 -278
  69. package/src/admin/ui/components/fields/RowField.tsx +0 -40
  70. package/src/admin/ui/components/fields/SelectField.tsx +0 -59
  71. package/src/admin/ui/components/fields/TabsField.tsx +0 -101
  72. package/src/admin/ui/components/fields/TextAreaField.tsx +0 -54
  73. package/src/admin/ui/components/fields/TextField.tsx +0 -49
  74. package/src/admin/ui/components/fields/VirtualField.tsx +0 -53
  75. package/src/admin/ui/components/fields/index.tsx +0 -371
  76. package/src/admin/ui/components/fields/richtext-editor/index.tsx +0 -211
  77. package/src/admin/ui/components/fields/richtext-editor/nodes/ImageComponent.tsx +0 -142
  78. package/src/admin/ui/components/fields/richtext-editor/nodes/ImageNode.tsx +0 -95
  79. package/src/admin/ui/components/fields/richtext-editor/plugins/ComponentPickerPlugin.tsx +0 -226
  80. package/src/admin/ui/components/fields/richtext-editor/plugins/EditableSyncPlugin.tsx +0 -16
  81. package/src/admin/ui/components/fields/richtext-editor/plugins/NotionToolbarPlugin.tsx +0 -184
  82. package/src/admin/ui/components/fields/richtext-editor/plugins/SimpleToolbarPlugin.tsx +0 -240
  83. package/src/admin/ui/components/fields/richtext-editor/plugins/ValueSyncPlugin.tsx +0 -40
  84. package/src/admin/ui/components/fields/utils.ts +0 -1
  85. package/src/admin/ui/components/link.tsx +0 -41
  86. package/src/admin/ui/components/media/AssetManagerModal.tsx +0 -334
  87. package/src/admin/ui/components/toast.tsx +0 -72
  88. package/src/admin/ui/components/ui/accordion.tsx +0 -51
  89. package/src/admin/ui/components/ui/alert-dialog.tsx +0 -98
  90. package/src/admin/ui/components/ui/blocks.tsx +0 -32
  91. package/src/admin/ui/components/ui/breadcrumbs.tsx +0 -59
  92. package/src/admin/ui/components/ui/button.tsx +0 -26
  93. package/src/admin/ui/components/ui/collapsible.tsx +0 -124
  94. package/src/admin/ui/components/ui/dialog.tsx +0 -79
  95. package/src/admin/ui/components/ui/group.tsx +0 -20
  96. package/src/admin/ui/components/ui/index.ts +0 -17
  97. package/src/admin/ui/components/ui/input.tsx +0 -12
  98. package/src/admin/ui/components/ui/join.tsx +0 -53
  99. package/src/admin/ui/components/ui/label.tsx +0 -11
  100. package/src/admin/ui/components/ui/radio-group.tsx +0 -75
  101. package/src/admin/ui/components/ui/relationship-detail-sheet.tsx +0 -122
  102. package/src/admin/ui/components/ui/relationship.tsx +0 -58
  103. package/src/admin/ui/components/ui/scroll-area.tsx +0 -19
  104. package/src/admin/ui/components/ui/select.tsx +0 -187
  105. package/src/admin/ui/components/ui/separator.tsx +0 -21
  106. package/src/admin/ui/components/ui/sheet.tsx +0 -106
  107. package/src/admin/ui/components/ui/tabs.tsx +0 -116
  108. package/src/admin/ui/components/ui/utils.ts +0 -3
  109. package/src/admin/ui/hooks/use-debounce.ts +0 -15
  110. package/src/admin/ui/styles/_locale-switcher.scss +0 -33
  111. package/src/admin/ui/styles/accordion.scss +0 -60
  112. package/src/admin/ui/styles/animations.scss +0 -41
  113. package/src/admin/ui/styles/asset-manager.scss +0 -547
  114. package/src/admin/ui/styles/badge.scss +0 -13
  115. package/src/admin/ui/styles/base.scss +0 -22
  116. package/src/admin/ui/styles/button.scss +0 -161
  117. package/src/admin/ui/styles/card.scss +0 -13
  118. package/src/admin/ui/styles/collapsible.scss +0 -75
  119. package/src/admin/ui/styles/data-detail.scss +0 -92
  120. package/src/admin/ui/styles/dialog.scss +0 -102
  121. package/src/admin/ui/styles/empty-state.scss +0 -22
  122. package/src/admin/ui/styles/group.scss +0 -19
  123. package/src/admin/ui/styles/index.scss +0 -33
  124. package/src/admin/ui/styles/input.scss +0 -80
  125. package/src/admin/ui/styles/label.scss +0 -12
  126. package/src/admin/ui/styles/layout.scss +0 -56
  127. package/src/admin/ui/styles/lexical.scss +0 -469
  128. package/src/admin/ui/styles/loading.scss +0 -102
  129. package/src/admin/ui/styles/media-registry.scss +0 -597
  130. package/src/admin/ui/styles/pagination.scss +0 -20
  131. package/src/admin/ui/styles/radio-group.scss +0 -66
  132. package/src/admin/ui/styles/row.scss +0 -17
  133. package/src/admin/ui/styles/scrollbar.scss +0 -36
  134. package/src/admin/ui/styles/select.scss +0 -121
  135. package/src/admin/ui/styles/separator.scss +0 -14
  136. package/src/admin/ui/styles/sheet.scss +0 -152
  137. package/src/admin/ui/styles/sidebar.scss +0 -148
  138. package/src/admin/ui/styles/switch.scss +0 -59
  139. package/src/admin/ui/styles/table.scss +0 -207
  140. package/src/admin/ui/styles/tabs.scss +0 -62
  141. package/src/admin/ui/styles/toast.scss +0 -45
  142. package/src/admin/ui/styles/variables.scss +0 -24
  143. package/src/admin/ui/views/collection-list-view.tsx +0 -720
  144. package/src/admin/ui/views/dashboard-view.tsx +0 -263
  145. package/src/admin/ui/views/document-edit-view.tsx +0 -384
  146. package/src/admin/ui/views/global-edit-view.tsx +0 -226
  147. package/src/admin/ui/views/init-view.tsx +0 -182
  148. package/src/admin/ui/views/login-view.tsx +0 -123
  149. package/src/admin/ui/views/media-registry-view.tsx +0 -1104
  150. package/src/admin/ui/views/settings-view.tsx +0 -729
  151. package/src/admin/webcomponent.tsx +0 -15
  152. package/src/api.ts +0 -9
  153. package/src/auth/index.ts +0 -194
  154. package/src/auth/migrations.ts +0 -87
  155. package/src/auth/premissions.ts +0 -46
  156. package/src/cli/commands/generate-types.ts +0 -116
  157. package/src/cli/commands/init.ts +0 -95
  158. package/src/cli/commands/migrate-commands.ts +0 -160
  159. package/src/cli/commands/seed-command.ts +0 -11
  160. package/src/cli/d1-mock.ts +0 -101
  161. package/src/cli/index.test.ts +0 -84
  162. package/src/cli/index.ts +0 -183
  163. package/src/cli/r2-mock.ts +0 -217
  164. package/src/cli/seeding.ts +0 -405
  165. package/src/client.ts +0 -181
  166. package/src/config-utils.ts +0 -102
  167. package/src/config.ts +0 -49
  168. package/src/db/adapter.ts +0 -53
  169. package/src/db/better-sqlite.ts +0 -630
  170. package/src/db/bun-sqlite.ts +0 -646
  171. package/src/db/d1.ts +0 -711
  172. package/src/db/kysely/data-mapper.ts +0 -142
  173. package/src/db/kysely/field-mapper.ts +0 -148
  174. package/src/db/kysely/migration-generator.ts +0 -223
  175. package/src/db/kysely/query-builder.ts +0 -92
  176. package/src/db/kysely/schema-builder.ts +0 -439
  177. package/src/db/kysely/sql-utils.ts +0 -13
  178. package/src/db/postgres.ts +0 -621
  179. package/src/db/sqlite.ts +0 -658
  180. package/src/db/system-schema.ts +0 -121
  181. package/src/index.ts +0 -13
  182. package/src/runtimes/README.md +0 -59
  183. package/src/runtimes/bun.ts +0 -49
  184. package/src/runtimes/cloudflare-workers.ts +0 -38
  185. package/src/runtimes/next.ts +0 -26
  186. package/src/runtimes/node.ts +0 -52
  187. package/src/schema/collection.ts +0 -184
  188. package/src/schema/fields/base.ts +0 -164
  189. package/src/schema/fields/index.ts +0 -427
  190. package/src/schema/global.ts +0 -145
  191. package/src/schema/index.ts +0 -4
  192. package/src/schema/infer.ts +0 -72
  193. package/src/server/admin-router.ts +0 -20
  194. package/src/server/admin.ts +0 -142
  195. package/src/server/assets.ts +0 -306
  196. package/src/server/collection-router.ts +0 -55
  197. package/src/server/handlers.ts +0 -722
  198. package/src/server/middlewares/admin.ts +0 -27
  199. package/src/server/middlewares/auth.ts +0 -89
  200. package/src/server/middlewares/context.ts +0 -17
  201. package/src/server/middlewares/cors.ts +0 -24
  202. package/src/server/middlewares/database-init.ts +0 -74
  203. package/src/server/middlewares/rate-limit.ts +0 -71
  204. package/src/server/router.ts +0 -47
  205. package/src/server/setup-middlewares.ts +0 -58
  206. package/src/server/system-router.ts +0 -35
  207. package/src/server.ts +0 -9
  208. package/src/storage/adapters/cloudflare-r2.ts +0 -136
  209. package/src/storage/adapters/local.ts +0 -146
  210. package/src/storage/adapters/s3.ts +0 -186
  211. package/src/storage/errors.ts +0 -46
  212. package/src/storage/index.ts +0 -5
  213. package/src/storage/types.ts +0 -39
  214. package/src/types.ts +0 -577
  215. package/src/utils/lexical.ts +0 -37
  216. package/src/utils/logger.ts +0 -73
  217. package/src/validation.ts +0 -429
  218. package/src/validator.ts +0 -179
  219. package/test/admin-custom-field.test.ts +0 -162
  220. package/test/admin-react-field.test.tsx +0 -134
  221. package/test/api-features.test.ts +0 -78
  222. package/test/api.test.ts +0 -178
  223. package/test/auth.test.ts +0 -62
  224. package/test/cli-integration.test.ts +0 -146
  225. package/test/cli.test.ts +0 -25
  226. package/test/db/postgres.test.ts +0 -95
  227. package/test/db/sqlite-filter.test.ts +0 -53
  228. package/test/db/sqlite.test.ts +0 -82
  229. package/test/engine-features.test.ts +0 -79
  230. package/test/globals.test.ts +0 -74
  231. package/test/integration-tmp/db-app/opacacms.config.ts +0 -15
  232. package/test/integration-tmp/my-sqlite-app/opacacms.config.ts +0 -25
  233. package/test/integration-tmp/my-test-app/index.ts +0 -8
  234. package/test/integration-tmp/my-test-app/opacacms.config.ts +0 -16
  235. package/test/integration-tmp/my-test-app/package.json +0 -12
  236. package/test/populate.test.ts +0 -79
  237. package/test/runtimes.test.ts +0 -43
  238. package/test/schema-builder.test.ts +0 -107
  239. package/test/schema-features.test.ts +0 -63
  240. package/test/seeding.test.ts +0 -68
  241. package/test/storage/local.test.ts +0 -72
  242. package/test/storage/s3.test.ts +0 -60
  243. package/test/structural-data.test.ts +0 -100
  244. package/test/test-setup.ts +0 -11
  245. package/test/validation.test.ts +0 -162
  246. package/tsconfig.json +0 -42
  247. /package/dist/{src/admin/index.css → admin/webcomponent.css} +0 -0
@@ -1,217 +0,0 @@
1
- import { Database } from "bun:sqlite";
2
- import crypto from "node:crypto";
3
- import fs from "node:fs";
4
- import path from "node:path";
5
-
6
- /**
7
- * A minimal R2Bucket mock for OpacaCMS CLI that uses Wrangler's local state.
8
- * Stores metadata in _mf_objects table and blobs in .blobs sibling directory.
9
- */
10
- export function createR2Mock(dbPath?: string) {
11
- let sqlite: Database;
12
- let blobsDir: string;
13
- let finalDbPath: string | undefined;
14
-
15
- // 1. Try to find Wrangler's local R2 state if no path provided
16
- const wranglerR2Dir = path.resolve(
17
- process.cwd(),
18
- ".wrangler/state/v3/r2/miniflare-R2BucketObject",
19
- );
20
-
21
- if (!dbPath && fs.existsSync(wranglerR2Dir)) {
22
- const files = fs.readdirSync(wranglerR2Dir);
23
- const sqliteFile = files.find((f) => f.endsWith(".sqlite"));
24
- if (sqliteFile) {
25
- finalDbPath = path.join(wranglerR2Dir, sqliteFile);
26
- console.log(`[OpacaCMS] Using Wrangler R2 local state: ${sqliteFile}`);
27
- }
28
- }
29
-
30
- // 2. Handle specific path or fallback
31
- if (!finalDbPath) {
32
- const inputPath = dbPath || ".opaca/local-r2/mock-bucket.sqlite";
33
- const absolutePath = path.isAbsolute(inputPath)
34
- ? inputPath
35
- : path.resolve(process.cwd(), inputPath);
36
-
37
- // If it's a directory, append default filename
38
- if (fs.existsSync(absolutePath) && fs.statSync(absolutePath).isDirectory()) {
39
- finalDbPath = path.join(absolutePath, "mock-bucket.sqlite");
40
- } else if (!absolutePath.endsWith(".sqlite")) {
41
- finalDbPath = absolutePath.endsWith("/")
42
- ? path.join(absolutePath, "mock-bucket.sqlite")
43
- : absolutePath + ".sqlite";
44
- } else {
45
- finalDbPath = absolutePath;
46
- }
47
- console.log(`[OpacaCMS] Using local R2 mock: ${path.basename(finalDbPath)}`);
48
- }
49
-
50
- // Ensure directory exists
51
- const dir = path.dirname(finalDbPath);
52
- if (!fs.existsSync(dir)) {
53
- fs.mkdirSync(dir, { recursive: true });
54
- }
55
-
56
- try {
57
- sqlite = new Database(finalDbPath);
58
- } catch (err: any) {
59
- throw new Error(`Failed to open R2 mock database at ${finalDbPath}: ${err.message}`);
60
- }
61
-
62
- blobsDir = finalDbPath.replace(".sqlite", ".blobs");
63
- if (!fs.existsSync(blobsDir)) {
64
- fs.mkdirSync(dir, { recursive: true });
65
- fs.mkdirSync(blobsDir, { recursive: true });
66
- }
67
-
68
- // Ensure table exists (miniflare 3 schema)
69
- sqlite.exec(`
70
- CREATE TABLE IF NOT EXISTS _mf_objects (
71
- key TEXT PRIMARY KEY,
72
- blob_id TEXT,
73
- version TEXT,
74
- size INTEGER,
75
- etag TEXT,
76
- uploaded INTEGER,
77
- checksums TEXT,
78
- http_metadata TEXT,
79
- custom_metadata TEXT
80
- )
81
- `);
82
-
83
- const getBlobPath = (blobId: string) => path.join(blobsDir, blobId);
84
-
85
- return {
86
- async put(key: string, value: any, options?: any) {
87
- const blobId = crypto.randomUUID();
88
- const filePath = getBlobPath(blobId);
89
-
90
- const buffer =
91
- value instanceof Uint8Array
92
- ? value
93
- : value instanceof ArrayBuffer
94
- ? new Uint8Array(value)
95
- : typeof value === "string" || value instanceof Buffer
96
- ? Buffer.from(value)
97
- : new Uint8Array(await (value as any).arrayBuffer());
98
-
99
- fs.writeFileSync(filePath, buffer);
100
-
101
- // MD5 for ETag (standard for R2/Miniflare)
102
- const etag = crypto.createHash("md5").update(buffer).digest("hex");
103
-
104
- // Miniflare 3 internal SQLite state often use MILLIS for 'uploaded'
105
- // despite the R2 API returning seconds. Let's use MILLIS.
106
- const uploaded = Date.now();
107
- const size = buffer.length;
108
-
109
- // Ensure we use the exact keys expected by Workerd/Miniflare
110
- const httpMetadataObj: Record<string, string> = {};
111
- if (options?.httpMetadata?.contentType) {
112
- httpMetadataObj.contentType = options.httpMetadata.contentType;
113
- }
114
-
115
- const customMetadataObj: Record<string, string> = { ...options?.customMetadata };
116
- if (options?.customMetadata?.sourceUrl) {
117
- customMetadataObj.sourceUrl = options.customMetadata.sourceUrl;
118
- }
119
-
120
- const httpMetadata = JSON.stringify(httpMetadataObj);
121
- const customMetadata = JSON.stringify(customMetadataObj);
122
-
123
- sqlite
124
- .prepare(`
125
- INSERT OR REPLACE INTO _mf_objects
126
- (key, blob_id, version, size, etag, uploaded, checksums, http_metadata, custom_metadata)
127
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
128
- `)
129
- .run(key, blobId, "v1", size, etag, uploaded, "{}", httpMetadata, customMetadata);
130
-
131
- return {
132
- key,
133
- size,
134
- etag,
135
- httpMetadata: options?.httpMetadata || {},
136
- customMetadata: options?.customMetadata || {},
137
- };
138
- },
139
-
140
- async get(key: string) {
141
- const row = sqlite.prepare("SELECT * FROM _mf_objects WHERE key = ?").get(key) as any;
142
- if (!row) return null;
143
-
144
- const filePath = getBlobPath(row.blob_id);
145
- if (!fs.existsSync(filePath)) return null;
146
-
147
- const buffer = fs.readFileSync(filePath);
148
- const stream = new ReadableStream({
149
- start(controller) {
150
- controller.enqueue(new Uint8Array(buffer));
151
- controller.close();
152
- },
153
- });
154
-
155
- return {
156
- key: row.key,
157
- size: row.size,
158
- etag: row.etag,
159
- uploaded: new Date(row.uploaded), // Works with both s and ms
160
- httpMetadata: JSON.parse(row.http_metadata || "{}"),
161
- customMetadata: JSON.parse(row.custom_metadata || "{}"),
162
- body: stream,
163
- bodyUsed: false,
164
- async arrayBuffer() {
165
- return buffer.buffer;
166
- },
167
- async text() {
168
- return buffer.toString();
169
- },
170
- async json() {
171
- return JSON.parse(buffer.toString());
172
- },
173
- };
174
- },
175
-
176
- async head(key: string) {
177
- const row = sqlite
178
- .prepare(
179
- "SELECT key, size, etag, http_metadata, custom_metadata, uploaded FROM _mf_objects WHERE key = ?",
180
- )
181
- .get(key) as any;
182
- if (!row) return null;
183
- return {
184
- key: row.key,
185
- size: row.size,
186
- etag: row.etag,
187
- uploaded: new Date(row.uploaded),
188
- httpMetadata: JSON.parse(row.http_metadata || "{}"),
189
- customMetadata: JSON.parse(row.custom_metadata || "{}"),
190
- };
191
- },
192
-
193
- async delete(key: string) {
194
- const row = sqlite.prepare("SELECT blob_id FROM _mf_objects WHERE key = ?").get(key) as any;
195
- if (row) {
196
- const filePath = getBlobPath(row.blob_id);
197
- if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
198
- sqlite.prepare("DELETE FROM _mf_objects WHERE key = ?").run(key);
199
- }
200
- },
201
-
202
- async list() {
203
- const rows = sqlite.prepare("SELECT * FROM _mf_objects").all() as any[];
204
- return {
205
- objects: rows.map((row) => ({
206
- key: row.key,
207
- size: row.size,
208
- etag: row.etag,
209
- uploaded: new Date(row.uploaded),
210
- httpMetadata: JSON.parse(row.http_metadata || "{}"),
211
- customMetadata: JSON.parse(row.custom_metadata || "{}"),
212
- })),
213
- truncated: false,
214
- };
215
- },
216
- };
217
- }
@@ -1,405 +0,0 @@
1
- import { faker } from "@faker-js/faker";
2
- import type { Collection, DatabaseAdapter, Field, OpacaConfig } from "../types";
3
-
4
- /**
5
- * Default generators for each field type.
6
- */
7
- export const defaultFieldGenerators: Record<string, () => any> = {
8
- text: () => faker.lorem.words(3),
9
- textarea: () => faker.lorem.paragraph(),
10
- number: () => faker.number.int({ min: 1, max: 1000 }),
11
- richtext: () =>
12
- JSON.stringify({
13
- root: {
14
- children: [
15
- {
16
- children: [
17
- {
18
- detail: 0,
19
- format: 0,
20
- mode: "normal",
21
- style: "",
22
- text: faker.lorem.sentence(),
23
- type: "text",
24
- version: 1,
25
- },
26
- ],
27
- direction: "ltr",
28
- format: "",
29
- indent: 0,
30
- type: "heading",
31
- tag: "h1",
32
- version: 1,
33
- },
34
- {
35
- children: [
36
- {
37
- detail: 0,
38
- format: 0,
39
- mode: "normal",
40
- style: "",
41
- text: faker.lorem.paragraphs(2),
42
- type: "text",
43
- version: 1,
44
- },
45
- ],
46
- direction: "ltr",
47
- format: "",
48
- indent: 0,
49
- type: "paragraph",
50
- version: 1,
51
- },
52
- ],
53
- direction: "ltr",
54
- format: "",
55
- indent: 0,
56
- type: "root",
57
- version: 1,
58
- },
59
- }),
60
- boolean: () => faker.datatype.boolean(),
61
- date: () => faker.date.recent().toISOString(),
62
- email: () => faker.internet.email(),
63
- json: () => ({ [faker.lorem.word()]: faker.lorem.sentence() }),
64
- select: () => faker.lorem.word(),
65
- radio: () => faker.lorem.word(),
66
- // Add block generator template
67
- blocks: () => [],
68
- };
69
-
70
- async function getRandomIds(
71
- db: DatabaseAdapter,
72
- relationTo: string,
73
- count: number = 1,
74
- ): Promise<any[]> {
75
- try {
76
- const result = await db.find(relationTo, {}, { limit: 50 });
77
- const docs = result.docs || [];
78
-
79
- if (docs.length === 0) {
80
- // Don't log for system collections or if it's expected
81
- if (!relationTo.startsWith("_")) {
82
- console.warn(`[Seeding] No documents found in '${relationTo}' for relationship.`);
83
- }
84
- return [];
85
- }
86
-
87
- // Shuffle and pick
88
- const shuffled = [...docs].sort(() => 0.5 - Math.random());
89
- const selected = shuffled.slice(0, count).map((doc) => (doc as any).id);
90
- return selected;
91
- } catch (e) {
92
- console.error(`[Seeding] Failed to fetch random IDs for ${relationTo}:`, e);
93
- return [];
94
- }
95
- }
96
-
97
- /**
98
- * Resolves a relationship by picking a random ID from the target collection.
99
- */
100
-
101
- /**
102
- * Generates data for a list of fields, recursively handling groups and layouts.
103
- */
104
- async function generateDataForFields(
105
- fields: Field[],
106
- db: DatabaseAdapter,
107
- locales: string[] = [],
108
- ): Promise<Record<string, any>> {
109
- const record: Record<string, any> = {};
110
-
111
- for (const field of fields) {
112
- // 1. Handle Layout Fields (Recursive)
113
- if (field.type === "row" || field.type === "collapsible") {
114
- Object.assign(record, await generateDataForFields(field.fields, db, locales));
115
- continue;
116
- }
117
-
118
- if (field.type === "tabs") {
119
- for (const tab of field.tabs) {
120
- Object.assign(record, await generateDataForFields(tab.fields, db, locales));
121
- }
122
- continue;
123
- }
124
-
125
- // 2. Handle Group (Nested)
126
- if (field.type === "group" && field.name) {
127
- record[field.name] = await generateDataForFields(field.fields, db, locales);
128
- continue;
129
- }
130
-
131
- // 3. Handle Normal Fields with name
132
- if (field.name) {
133
- const isLocalized = !!(field as any).localized && locales.length > 0;
134
-
135
- const generateFieldValue = async () => {
136
- // Handle Relationships
137
- if (field.type === "relationship" && "relationTo" in field) {
138
- if (field.hasMany) {
139
- const count = faker.number.int({ min: 1, max: 3 });
140
- return await getRandomIds(db, field.relationTo, count);
141
- }
142
- const ids = await getRandomIds(db, field.relationTo, 1);
143
- return ids[0] || null;
144
- }
145
-
146
- // Handle Blocks
147
- if (field.type === "blocks" && field.blocks) {
148
- const blockCount = faker.number.int({ min: 1, max: 3 });
149
- const generatedBlocks = [];
150
-
151
- for (let i = 0; i < blockCount; i++) {
152
- const blockType =
153
- field.blocks[faker.number.int({ min: 0, max: field.blocks.length - 1 })];
154
- if (!blockType) continue;
155
-
156
- const blockData = await generateDataForFields(blockType.fields, db, locales);
157
- generatedBlocks.push({
158
- ...blockData,
159
- blockType: blockType.slug,
160
- id: faker.string.uuid(),
161
- });
162
- }
163
-
164
- return generatedBlocks;
165
- }
166
-
167
- // Default Generators
168
- const generator = defaultFieldGenerators[field.type];
169
- if (generator) {
170
- if (field.type === "select" || field.type === "radio") {
171
- const options = (field as any).options;
172
- const choices = options?.choices || [];
173
- if (choices.length > 0) {
174
- const choice = choices[Math.floor(Math.random() * choices.length)];
175
- return typeof choice === "string" ? choice : choice.value;
176
- }
177
- }
178
- return generator();
179
- }
180
- return null;
181
- };
182
-
183
- if (isLocalized) {
184
- const localizedValue: Record<string, any> = {};
185
- for (const locale of locales) {
186
- localizedValue[locale] = await generateFieldValue();
187
- }
188
- record[field.name] = localizedValue;
189
- } else {
190
- record[field.name] = await generateFieldValue();
191
- }
192
- }
193
- }
194
-
195
- return record;
196
- }
197
-
198
- /**
199
- * Generates a single record for a collection.
200
- */
201
- export async function generateRecord(db: DatabaseAdapter, collection: Collection, locales: string[] = []) {
202
- return generateDataForFields(collection.fields as Field[], db, locales);
203
- }
204
-
205
- /**
206
- * Topologically sorts collections based on their dependencies.
207
- */
208
- export function sortCollections(collections: Collection[]): Collection[] {
209
- const sorted: Collection[] = [];
210
- const visited = new Set<string>();
211
- const visiting = new Set<string>();
212
-
213
- const visit = (collection: Collection) => {
214
- if (visited.has(collection.slug)) return;
215
- if (visiting.has(collection.slug)) {
216
- throw new Error(`Circular dependency detected: ${collection.slug}`);
217
- }
218
-
219
- visiting.add(collection.slug);
220
-
221
- // Implicit dependencies from relationships
222
- const deps: string[] = [];
223
-
224
- // Recursive search for relationships
225
- const findDeps = (fields: Field[]) => {
226
- for (const f of fields) {
227
- if (f.type === "relationship") {
228
- deps.push((f as any).relationTo);
229
- } else if (f.type === "blocks" && f.blocks) {
230
- f.blocks.forEach((b) => findDeps(b.fields as Field[]));
231
- } else if ("fields" in f && f.fields) {
232
- findDeps(f.fields as Field[]);
233
- } else if ("tabs" in f && f.tabs) {
234
- f.tabs.forEach((t) => findDeps(t.fields as Field[]));
235
- }
236
- }
237
- };
238
- findDeps(collection.fields as Field[]);
239
-
240
- for (const depSlug of deps) {
241
- const depColl = collections.find((c) => c.slug === depSlug);
242
- if (depColl) visit(depColl);
243
- }
244
-
245
- visiting.delete(collection.slug);
246
- visited.add(collection.slug);
247
- sorted.push(collection);
248
- };
249
-
250
- for (const collection of collections) {
251
- visit(collection);
252
- }
253
-
254
- return sorted;
255
- }
256
-
257
- /**
258
- * Seeds the database with mock data.
259
- */
260
- export async function autoSeed(
261
- config: OpacaConfig,
262
- countPerCollection = 10,
263
- reset = false,
264
- type: "collections" | "assets" | "all" = "all",
265
- ) {
266
- const { collections, db, globals, storages, serverURL } = config;
267
-
268
- console.log(`🌱 Starting automatic seed (${countPerCollection} records per collection)...`);
269
-
270
- // Include system collections (like _opaca_assets)
271
- const { getSystemCollections } = await import("../db/system-schema.js");
272
- const systemCollections = getSystemCollections().filter((c) => c.slug === "_opaca_assets");
273
- const allCollections = [...systemCollections, ...collections];
274
-
275
- let collectionsToSeed = sortCollections(allCollections);
276
-
277
- if (type === "assets") {
278
- collectionsToSeed = collectionsToSeed.filter((c) => c.slug === "_opaca_assets");
279
- console.log("📁 Seeding only assets...");
280
- } else if (type === "collections") {
281
- collectionsToSeed = collectionsToSeed.filter((c) => c.slug !== "_opaca_assets");
282
- console.log("📚 Seeding only user collections...");
283
- }
284
-
285
- // Connect and ensure adapter knows the schema (pass ALL collections for proper whitelisting)
286
- await db.connect();
287
- await db.migrate(allCollections, globals || []);
288
-
289
- try {
290
- if (reset) {
291
- console.log("🧹 Resetting data (deleting existing records)...");
292
- // Delete in reverse order to respect foreign key constraints if any
293
- const reversed = [...collectionsToSeed].reverse();
294
- for (const collection of reversed) {
295
- console.log(`Cleaning ${collection.slug}...`);
296
- await db.deleteMany?.(collection.slug, {});
297
- }
298
- }
299
-
300
- const storageAdapter = (storages as any)?.default || storages;
301
-
302
- const locales = config.i18n?.locales || [];
303
-
304
- for (const collection of collectionsToSeed) {
305
- console.log(`Seeding ${collection.slug}...`);
306
-
307
- const isAssetCollection = collection.slug === "_opaca_assets";
308
-
309
- for (let i = 0; i < countPerCollection; i++) {
310
- let data: any;
311
-
312
- if (isAssetCollection) {
313
- // ... (asset logic remains the same)
314
- const id = faker.string.uuid();
315
- const width = faker.number.int({ min: 400, max: 1200 });
316
- const height = faker.number.int({ min: 300, max: 800 });
317
- const color = faker.color.rgb({ prefix: "" });
318
- const textColor = faker.color.rgb({ prefix: "" });
319
-
320
- // Randomize between Picsum and Placehold.co
321
- const usePicsum = Math.random() > 0.5;
322
- let imageUrl = "";
323
-
324
- if (usePicsum) {
325
- const categories = ["nature", "city", "tech", "people", "animals", "architecture"];
326
- const category = faker.helpers.arrayElement(categories);
327
- imageUrl = `https://picsum.photos/seed/${category}-${id}/${width}/${height}`;
328
- } else {
329
- imageUrl = `https://placehold.co/${width}x${height}/${color}/${textColor}.png?text=Seed+${i}`;
330
- }
331
-
332
- // 1. Fetch real image
333
- const res = await fetch(imageUrl);
334
- if (!res.ok) {
335
- throw new Error(`Failed to fetch placeholder image: ${imageUrl}`);
336
- }
337
-
338
- const arrayBuffer = await res.arrayBuffer();
339
- const mime_type = res.headers.get("content-type")?.split(";")[0] || "image/png";
340
-
341
- const extMap: Record<string, string> = {
342
- "image/jpeg": "jpg",
343
- "image/png": "png",
344
- "image/webp": "webp",
345
- "image/gif": "gif",
346
- "image/svg+xml": "svg",
347
- };
348
-
349
- const ext = extMap[mime_type] || "png";
350
-
351
- // 2. Build FileRecord
352
- const fileRecord = {
353
- filename: `seed-image-${i}.${ext}`,
354
- original_filename: `seed-image-${i}.${ext}`,
355
- mime_type,
356
- filesize: arrayBuffer.byteLength,
357
- buffer: new Uint8Array(arrayBuffer),
358
- };
359
-
360
- // 3. Upload via adapter
361
- if (!storageAdapter || typeof storageAdapter.upload !== "function") {
362
- throw new Error(
363
- "Storage adapter is required for seeding assets and must have an 'upload' method.",
364
- );
365
- }
366
-
367
- const uploaded = await storageAdapter.upload(fileRecord, {
368
- generateUniqueName: true,
369
- customMetadata: {
370
- sourceUrl: imageUrl,
371
- },
372
- });
373
-
374
- // 4. Persist metadata real
375
- data = {
376
- id,
377
- key: uploaded.filename,
378
- filename: uploaded.filename,
379
- originalFilename: fileRecord.original_filename,
380
- mimeType: uploaded.mime_type,
381
- filesize: uploaded.filesize,
382
- width,
383
- height,
384
- bucket: "default",
385
- url: uploaded.url,
386
- thumbnailUrl: uploaded.url, // (placeholder)
387
- altText: faker.lorem.sentence(),
388
- };
389
-
390
- // LOG THE FULL URL
391
- const baseURL = serverURL || "http://localhost:8787";
392
- console.log(`[Asset] Source: ${imageUrl}`);
393
- console.log(`[Asset] Seeded: ${baseURL}/api/assets/${id}/view (${uploaded.filename})`);
394
- } else {
395
- data = await generateRecord(db, collection, locales);
396
- }
397
-
398
- await db.create(collection.slug, data);
399
- }
400
- }
401
- console.log("✅ Seeding completed.");
402
- } finally {
403
- await db.disconnect();
404
- }
405
- }