opacacms 0.1.11 → 0.1.13

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 (246) hide show
  1. package/dist/admin/index.js +9464 -21
  2. package/dist/admin/webcomponent.d.ts +1 -1
  3. package/dist/admin/webcomponent.js +9620 -6
  4. package/dist/admin.css +1 -0
  5. package/dist/{chunk-6dhs73zq.js → chunk-0am1m47g.js} +1 -1
  6. package/dist/{chunk-0nf7fe26.js → chunk-0d9aqz6z.js} +1 -1
  7. package/dist/{chunk-cvdd4eqh.js → chunk-2kyhqvhc.js} +5 -1
  8. package/dist/{chunk-gjjcc4hm.js → chunk-2z8wxx9g.js} +21 -6
  9. package/dist/{chunk-xg35h5a3.js → chunk-7fyepksb.js} +1 -1
  10. package/dist/{chunk-njytmdb4.js → chunk-pxh5encs.js} +34 -24
  11. package/dist/{chunk-n8aekdnr.js → chunk-qkn1ykrj.js} +33 -23
  12. package/dist/{chunk-kwp83w8b.js → chunk-wmvjvn7b.js} +4 -4
  13. package/dist/{chunk-qrt22f6e.js → chunk-wq314kkx.js} +35 -25
  14. package/dist/{chunk-eqtsfyjf.js → chunk-x2ejaftz.js} +52 -28
  15. package/dist/{chunk-6ew02s0c.js → chunk-xtwc125q.js} +18 -18
  16. package/dist/cli/index.js +5 -5
  17. package/dist/db/better-sqlite.d.ts +1 -0
  18. package/dist/db/better-sqlite.js +3 -3
  19. package/dist/db/bun-sqlite.d.ts +1 -0
  20. package/dist/db/bun-sqlite.js +3 -3
  21. package/dist/db/d1.js +3 -3
  22. package/dist/db/index.d.ts +3 -0
  23. package/dist/db/index.js +17 -13
  24. package/dist/db/postgres.js +3 -3
  25. package/dist/db/sqlite.js +3 -3
  26. package/dist/runtimes/bun.js +2 -2
  27. package/dist/runtimes/cloudflare-workers.js +2 -2
  28. package/dist/runtimes/next.js +2 -2
  29. package/dist/runtimes/node.js +2 -2
  30. package/dist/server.js +2 -2
  31. package/package.json +8 -2
  32. package/bun.lock +0 -34
  33. package/dist/admin/index.css +0 -47
  34. package/dist/api.d.ts +0 -6
  35. package/dist/api.js +0 -27
  36. package/dist/chunk-2zm8cy1w.js +0 -9482
  37. package/global.d.ts +0 -11
  38. package/src/admin/api-client.ts +0 -63
  39. package/src/admin/auth-client.ts +0 -40
  40. package/src/admin/custom-field.ts +0 -179
  41. package/src/admin/index.ts +0 -15
  42. package/src/admin/react.tsx +0 -72
  43. package/src/admin/router.ts +0 -9
  44. package/src/admin/stores/admin-queries.ts +0 -121
  45. package/src/admin/stores/auth.ts +0 -61
  46. package/src/admin/stores/column-visibility.ts +0 -67
  47. package/src/admin/stores/config.ts +0 -15
  48. package/src/admin/stores/media.ts +0 -95
  49. package/src/admin/stores/query.ts +0 -13
  50. package/src/admin/stores/ui.ts +0 -29
  51. package/src/admin/ui/admin-client.tsx +0 -283
  52. package/src/admin/ui/admin-layout.tsx +0 -276
  53. package/src/admin/ui/components/ColumnVisibilityToggle.tsx +0 -141
  54. package/src/admin/ui/components/DataDetailSheet.tsx +0 -141
  55. package/src/admin/ui/components/DataDetailView.tsx +0 -175
  56. package/src/admin/ui/components/Table.tsx +0 -67
  57. package/src/admin/ui/components/fields/ArrayField.tsx +0 -166
  58. package/src/admin/ui/components/fields/BlocksField.tsx +0 -202
  59. package/src/admin/ui/components/fields/BooleanField.tsx +0 -50
  60. package/src/admin/ui/components/fields/CollapsibleField.tsx +0 -75
  61. package/src/admin/ui/components/fields/DateField.tsx +0 -45
  62. package/src/admin/ui/components/fields/FileField.tsx +0 -322
  63. package/src/admin/ui/components/fields/GroupField.tsx +0 -50
  64. package/src/admin/ui/components/fields/JoinField.tsx +0 -23
  65. package/src/admin/ui/components/fields/NumberField.tsx +0 -46
  66. package/src/admin/ui/components/fields/RadioField.tsx +0 -62
  67. package/src/admin/ui/components/fields/RelationshipField.tsx +0 -278
  68. package/src/admin/ui/components/fields/RowField.tsx +0 -40
  69. package/src/admin/ui/components/fields/SelectField.tsx +0 -59
  70. package/src/admin/ui/components/fields/TabsField.tsx +0 -101
  71. package/src/admin/ui/components/fields/TextAreaField.tsx +0 -54
  72. package/src/admin/ui/components/fields/TextField.tsx +0 -49
  73. package/src/admin/ui/components/fields/VirtualField.tsx +0 -53
  74. package/src/admin/ui/components/fields/index.tsx +0 -371
  75. package/src/admin/ui/components/fields/richtext-editor/index.tsx +0 -211
  76. package/src/admin/ui/components/fields/richtext-editor/nodes/ImageComponent.tsx +0 -142
  77. package/src/admin/ui/components/fields/richtext-editor/nodes/ImageNode.tsx +0 -95
  78. package/src/admin/ui/components/fields/richtext-editor/plugins/ComponentPickerPlugin.tsx +0 -226
  79. package/src/admin/ui/components/fields/richtext-editor/plugins/EditableSyncPlugin.tsx +0 -16
  80. package/src/admin/ui/components/fields/richtext-editor/plugins/NotionToolbarPlugin.tsx +0 -184
  81. package/src/admin/ui/components/fields/richtext-editor/plugins/SimpleToolbarPlugin.tsx +0 -240
  82. package/src/admin/ui/components/fields/richtext-editor/plugins/ValueSyncPlugin.tsx +0 -40
  83. package/src/admin/ui/components/fields/utils.ts +0 -1
  84. package/src/admin/ui/components/link.tsx +0 -41
  85. package/src/admin/ui/components/media/AssetManagerModal.tsx +0 -334
  86. package/src/admin/ui/components/toast.tsx +0 -72
  87. package/src/admin/ui/components/ui/accordion.tsx +0 -51
  88. package/src/admin/ui/components/ui/alert-dialog.tsx +0 -98
  89. package/src/admin/ui/components/ui/blocks.tsx +0 -32
  90. package/src/admin/ui/components/ui/breadcrumbs.tsx +0 -59
  91. package/src/admin/ui/components/ui/button.tsx +0 -26
  92. package/src/admin/ui/components/ui/collapsible.tsx +0 -124
  93. package/src/admin/ui/components/ui/dialog.tsx +0 -79
  94. package/src/admin/ui/components/ui/group.tsx +0 -20
  95. package/src/admin/ui/components/ui/index.ts +0 -17
  96. package/src/admin/ui/components/ui/input.tsx +0 -12
  97. package/src/admin/ui/components/ui/join.tsx +0 -53
  98. package/src/admin/ui/components/ui/label.tsx +0 -11
  99. package/src/admin/ui/components/ui/radio-group.tsx +0 -75
  100. package/src/admin/ui/components/ui/relationship-detail-sheet.tsx +0 -122
  101. package/src/admin/ui/components/ui/relationship.tsx +0 -58
  102. package/src/admin/ui/components/ui/scroll-area.tsx +0 -19
  103. package/src/admin/ui/components/ui/select.tsx +0 -187
  104. package/src/admin/ui/components/ui/separator.tsx +0 -21
  105. package/src/admin/ui/components/ui/sheet.tsx +0 -106
  106. package/src/admin/ui/components/ui/tabs.tsx +0 -116
  107. package/src/admin/ui/components/ui/utils.ts +0 -3
  108. package/src/admin/ui/hooks/use-debounce.ts +0 -15
  109. package/src/admin/ui/styles/_locale-switcher.scss +0 -33
  110. package/src/admin/ui/styles/accordion.scss +0 -60
  111. package/src/admin/ui/styles/animations.scss +0 -41
  112. package/src/admin/ui/styles/asset-manager.scss +0 -547
  113. package/src/admin/ui/styles/badge.scss +0 -13
  114. package/src/admin/ui/styles/base.scss +0 -22
  115. package/src/admin/ui/styles/button.scss +0 -161
  116. package/src/admin/ui/styles/card.scss +0 -13
  117. package/src/admin/ui/styles/collapsible.scss +0 -75
  118. package/src/admin/ui/styles/data-detail.scss +0 -92
  119. package/src/admin/ui/styles/dialog.scss +0 -102
  120. package/src/admin/ui/styles/empty-state.scss +0 -22
  121. package/src/admin/ui/styles/group.scss +0 -19
  122. package/src/admin/ui/styles/index.scss +0 -33
  123. package/src/admin/ui/styles/input.scss +0 -80
  124. package/src/admin/ui/styles/label.scss +0 -12
  125. package/src/admin/ui/styles/layout.scss +0 -56
  126. package/src/admin/ui/styles/lexical.scss +0 -469
  127. package/src/admin/ui/styles/loading.scss +0 -102
  128. package/src/admin/ui/styles/media-registry.scss +0 -597
  129. package/src/admin/ui/styles/pagination.scss +0 -20
  130. package/src/admin/ui/styles/radio-group.scss +0 -66
  131. package/src/admin/ui/styles/row.scss +0 -17
  132. package/src/admin/ui/styles/scrollbar.scss +0 -36
  133. package/src/admin/ui/styles/select.scss +0 -121
  134. package/src/admin/ui/styles/separator.scss +0 -14
  135. package/src/admin/ui/styles/sheet.scss +0 -152
  136. package/src/admin/ui/styles/sidebar.scss +0 -148
  137. package/src/admin/ui/styles/switch.scss +0 -59
  138. package/src/admin/ui/styles/table.scss +0 -207
  139. package/src/admin/ui/styles/tabs.scss +0 -62
  140. package/src/admin/ui/styles/toast.scss +0 -45
  141. package/src/admin/ui/styles/variables.scss +0 -24
  142. package/src/admin/ui/views/collection-list-view.tsx +0 -720
  143. package/src/admin/ui/views/dashboard-view.tsx +0 -263
  144. package/src/admin/ui/views/document-edit-view.tsx +0 -384
  145. package/src/admin/ui/views/global-edit-view.tsx +0 -226
  146. package/src/admin/ui/views/init-view.tsx +0 -182
  147. package/src/admin/ui/views/login-view.tsx +0 -123
  148. package/src/admin/ui/views/media-registry-view.tsx +0 -1104
  149. package/src/admin/ui/views/settings-view.tsx +0 -729
  150. package/src/admin/webcomponent.tsx +0 -15
  151. package/src/api.ts +0 -9
  152. package/src/auth/index.ts +0 -194
  153. package/src/auth/migrations.ts +0 -87
  154. package/src/auth/premissions.ts +0 -46
  155. package/src/cli/commands/generate-types.ts +0 -116
  156. package/src/cli/commands/init.ts +0 -95
  157. package/src/cli/commands/migrate-commands.ts +0 -160
  158. package/src/cli/commands/seed-command.ts +0 -11
  159. package/src/cli/d1-mock.ts +0 -101
  160. package/src/cli/index.test.ts +0 -84
  161. package/src/cli/index.ts +0 -183
  162. package/src/cli/r2-mock.ts +0 -217
  163. package/src/cli/seeding.ts +0 -409
  164. package/src/client.ts +0 -181
  165. package/src/config-utils.ts +0 -102
  166. package/src/config.ts +0 -49
  167. package/src/db/adapter.ts +0 -53
  168. package/src/db/better-sqlite.ts +0 -632
  169. package/src/db/bun-sqlite.ts +0 -646
  170. package/src/db/d1.ts +0 -711
  171. package/src/db/index.ts +0 -6
  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 -660
  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 -77
  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 -148
  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
@@ -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,409 +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
- for (const b of f.blocks) {
231
- findDeps(b.fields as Field[]);
232
- }
233
- } else if ("fields" in f && f.fields) {
234
- findDeps(f.fields as Field[]);
235
- } else if ("tabs" in f && f.tabs) {
236
- for (const t of f.tabs) {
237
- findDeps(t.fields as Field[]);
238
- }
239
- }
240
- }
241
- };
242
- findDeps(collection.fields as Field[]);
243
-
244
- for (const depSlug of deps) {
245
- const depColl = collections.find((c) => c.slug === depSlug);
246
- if (depColl) visit(depColl);
247
- }
248
-
249
- visiting.delete(collection.slug);
250
- visited.add(collection.slug);
251
- sorted.push(collection);
252
- };
253
-
254
- for (const collection of collections) {
255
- visit(collection);
256
- }
257
-
258
- return sorted;
259
- }
260
-
261
- /**
262
- * Seeds the database with mock data.
263
- */
264
- export async function autoSeed(
265
- config: OpacaConfig,
266
- countPerCollection = 10,
267
- reset = false,
268
- type: "collections" | "assets" | "all" = "all",
269
- ) {
270
- const { collections, db, globals, storages, serverURL } = config;
271
-
272
- console.log(`🌱 Starting automatic seed (${countPerCollection} records per collection)...`);
273
-
274
- // Include system collections (like _opaca_assets)
275
- const { getSystemCollections } = await import("../db/system-schema.js");
276
- const systemCollections = getSystemCollections().filter((c) => c.slug === "_opaca_assets");
277
- const allCollections = [...systemCollections, ...collections];
278
-
279
- let collectionsToSeed = sortCollections(allCollections);
280
-
281
- if (type === "assets") {
282
- collectionsToSeed = collectionsToSeed.filter((c) => c.slug === "_opaca_assets");
283
- console.log("📁 Seeding only assets...");
284
- } else if (type === "collections") {
285
- collectionsToSeed = collectionsToSeed.filter((c) => c.slug !== "_opaca_assets");
286
- console.log("📚 Seeding only user collections...");
287
- }
288
-
289
- // Connect and ensure adapter knows the schema (pass ALL collections for proper whitelisting)
290
- await db.connect();
291
- await db.migrate(allCollections, globals || []);
292
-
293
- try {
294
- if (reset) {
295
- console.log("🧹 Resetting data (deleting existing records)...");
296
- // Delete in reverse order to respect foreign key constraints if any
297
- const reversed = [...collectionsToSeed].reverse();
298
- for (const collection of reversed) {
299
- console.log(`Cleaning ${collection.slug}...`);
300
- await db.deleteMany?.(collection.slug, {});
301
- }
302
- }
303
-
304
- const storageAdapter = (storages as any)?.default || storages;
305
-
306
- const locales = config.i18n?.locales || [];
307
-
308
- for (const collection of collectionsToSeed) {
309
- console.log(`Seeding ${collection.slug}...`);
310
-
311
- const isAssetCollection = collection.slug === "_opaca_assets";
312
-
313
- for (let i = 0; i < countPerCollection; i++) {
314
- let data: any;
315
-
316
- if (isAssetCollection) {
317
- // ... (asset logic remains the same)
318
- const id = faker.string.uuid();
319
- const width = faker.number.int({ min: 400, max: 1200 });
320
- const height = faker.number.int({ min: 300, max: 800 });
321
- const color = faker.color.rgb({ prefix: "" });
322
- const textColor = faker.color.rgb({ prefix: "" });
323
-
324
- // Randomize between Picsum and Placehold.co
325
- const usePicsum = Math.random() > 0.5;
326
- let imageUrl = "";
327
-
328
- if (usePicsum) {
329
- const categories = ["nature", "city", "tech", "people", "animals", "architecture"];
330
- const category = faker.helpers.arrayElement(categories);
331
- imageUrl = `https://picsum.photos/seed/${category}-${id}/${width}/${height}`;
332
- } else {
333
- imageUrl = `https://placehold.co/${width}x${height}/${color}/${textColor}.png?text=Seed+${i}`;
334
- }
335
-
336
- // 1. Fetch real image
337
- const res = await fetch(imageUrl);
338
- if (!res.ok) {
339
- throw new Error(`Failed to fetch placeholder image: ${imageUrl}`);
340
- }
341
-
342
- const arrayBuffer = await res.arrayBuffer();
343
- const mime_type = res.headers.get("content-type")?.split(";")[0] || "image/png";
344
-
345
- const extMap: Record<string, string> = {
346
- "image/jpeg": "jpg",
347
- "image/png": "png",
348
- "image/webp": "webp",
349
- "image/gif": "gif",
350
- "image/svg+xml": "svg",
351
- };
352
-
353
- const ext = extMap[mime_type] || "png";
354
-
355
- // 2. Build FileRecord
356
- const fileRecord = {
357
- filename: `seed-image-${i}.${ext}`,
358
- original_filename: `seed-image-${i}.${ext}`,
359
- mime_type,
360
- filesize: arrayBuffer.byteLength,
361
- buffer: new Uint8Array(arrayBuffer),
362
- };
363
-
364
- // 3. Upload via adapter
365
- if (!storageAdapter || typeof storageAdapter.upload !== "function") {
366
- throw new Error(
367
- "Storage adapter is required for seeding assets and must have an 'upload' method.",
368
- );
369
- }
370
-
371
- const uploaded = await storageAdapter.upload(fileRecord, {
372
- generateUniqueName: true,
373
- customMetadata: {
374
- sourceUrl: imageUrl,
375
- },
376
- });
377
-
378
- // 4. Persist metadata real
379
- data = {
380
- id,
381
- key: uploaded.filename,
382
- filename: uploaded.filename,
383
- originalFilename: fileRecord.original_filename,
384
- mimeType: uploaded.mime_type,
385
- filesize: uploaded.filesize,
386
- width,
387
- height,
388
- bucket: "default",
389
- url: uploaded.url,
390
- thumbnailUrl: uploaded.url, // (placeholder)
391
- altText: faker.lorem.sentence(),
392
- };
393
-
394
- // LOG THE FULL URL
395
- const baseURL = serverURL || "http://localhost:8787";
396
- console.log(`[Asset] Source: ${imageUrl}`);
397
- console.log(`[Asset] Seeded: ${baseURL}/api/assets/${id}/view (${uploaded.filename})`);
398
- } else {
399
- data = await generateRecord(db, collection, locales);
400
- }
401
-
402
- await db.create(collection.slug, data);
403
- }
404
- }
405
- console.log("✅ Seeding completed.");
406
- } finally {
407
- // Keep connection alive for potential reuse in tests
408
- }
409
- }