opacacms 0.1.0

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 (399) hide show
  1. package/bun.lock +34 -0
  2. package/dist/admin/api-client.d.ts +8 -0
  3. package/dist/admin/auth-client.d.ts +940 -0
  4. package/dist/admin/custom-field.d.ts +71 -0
  5. package/dist/admin/index.d.ts +11 -0
  6. package/dist/admin/react.d.ts +3 -0
  7. package/dist/admin/router.d.ts +7 -0
  8. package/dist/admin/stores/admin-queries.d.ts +32 -0
  9. package/dist/admin/stores/auth.d.ts +33 -0
  10. package/dist/admin/stores/column-visibility.d.ts +21 -0
  11. package/dist/admin/stores/config.d.ts +7 -0
  12. package/dist/admin/stores/media.d.ts +44 -0
  13. package/dist/admin/stores/query.d.ts +4 -0
  14. package/dist/admin/stores/ui.d.ts +11 -0
  15. package/dist/admin/ui/admin-client.d.ts +7 -0
  16. package/dist/admin/ui/admin-layout.d.ts +14 -0
  17. package/dist/admin/ui/components/ColumnVisibilityToggle.d.ts +10 -0
  18. package/dist/admin/ui/components/DataDetailSheet.d.ts +13 -0
  19. package/dist/admin/ui/components/DataDetailView.d.ts +9 -0
  20. package/dist/admin/ui/components/Table.d.ts +10 -0
  21. package/dist/admin/ui/components/fields/ArrayField.d.ts +13 -0
  22. package/dist/admin/ui/components/fields/BlocksField.d.ts +17 -0
  23. package/dist/admin/ui/components/fields/BooleanField.d.ts +13 -0
  24. package/dist/admin/ui/components/fields/CollapsibleField.d.ts +16 -0
  25. package/dist/admin/ui/components/fields/DateField.d.ts +13 -0
  26. package/dist/admin/ui/components/fields/FileField.d.ts +23 -0
  27. package/dist/admin/ui/components/fields/GroupField.d.ts +13 -0
  28. package/dist/admin/ui/components/fields/JoinField.d.ts +15 -0
  29. package/dist/admin/ui/components/fields/NumberField.d.ts +14 -0
  30. package/dist/admin/ui/components/fields/RadioField.d.ts +17 -0
  31. package/dist/admin/ui/components/fields/RelationshipField.d.ts +16 -0
  32. package/dist/admin/ui/components/fields/RowField.d.ts +12 -0
  33. package/dist/admin/ui/components/fields/SelectField.d.ts +18 -0
  34. package/dist/admin/ui/components/fields/TabsField.d.ts +15 -0
  35. package/dist/admin/ui/components/fields/TextAreaField.d.ts +14 -0
  36. package/dist/admin/ui/components/fields/TextField.d.ts +14 -0
  37. package/dist/admin/ui/components/fields/VirtualField.d.ts +8 -0
  38. package/dist/admin/ui/components/fields/index.d.ts +28 -0
  39. package/dist/admin/ui/components/fields/richtext-editor/index.d.ts +10 -0
  40. package/dist/admin/ui/components/fields/richtext-editor/nodes/ImageComponent.d.ts +7 -0
  41. package/dist/admin/ui/components/fields/richtext-editor/nodes/ImageNode.d.ts +27 -0
  42. package/dist/admin/ui/components/fields/richtext-editor/plugins/ComponentPickerPlugin.d.ts +1 -0
  43. package/dist/admin/ui/components/fields/richtext-editor/plugins/EditableSyncPlugin.d.ts +5 -0
  44. package/dist/admin/ui/components/fields/richtext-editor/plugins/NotionToolbarPlugin.d.ts +1 -0
  45. package/dist/admin/ui/components/fields/richtext-editor/plugins/SimpleToolbarPlugin.d.ts +1 -0
  46. package/dist/admin/ui/components/fields/richtext-editor/plugins/ValueSyncPlugin.d.ts +5 -0
  47. package/dist/admin/ui/components/fields/utils.d.ts +1 -0
  48. package/dist/admin/ui/components/link.d.ts +8 -0
  49. package/dist/admin/ui/components/media/AssetManagerModal.d.ts +17 -0
  50. package/dist/admin/ui/components/toast.d.ts +10 -0
  51. package/dist/admin/ui/components/ui/accordion.d.ts +11 -0
  52. package/dist/admin/ui/components/ui/alert-dialog.d.ts +12 -0
  53. package/dist/admin/ui/components/ui/blocks.d.ts +5 -0
  54. package/dist/admin/ui/components/ui/breadcrumbs.d.ts +7 -0
  55. package/dist/admin/ui/components/ui/button.d.ts +7 -0
  56. package/dist/admin/ui/components/ui/collapsible.d.ts +16 -0
  57. package/dist/admin/ui/components/ui/dialog.d.ts +27 -0
  58. package/dist/admin/ui/components/ui/group.d.ts +6 -0
  59. package/dist/admin/ui/components/ui/index.d.ts +17 -0
  60. package/dist/admin/ui/components/ui/input.d.ts +5 -0
  61. package/dist/admin/ui/components/ui/join.d.ts +7 -0
  62. package/dist/admin/ui/components/ui/label.d.ts +3 -0
  63. package/dist/admin/ui/components/ui/radio-group.d.ts +13 -0
  64. package/dist/admin/ui/components/ui/relationship-detail-sheet.d.ts +9 -0
  65. package/dist/admin/ui/components/ui/relationship.d.ts +8 -0
  66. package/dist/admin/ui/components/ui/scroll-area.d.ts +7 -0
  67. package/dist/admin/ui/components/ui/select.d.ts +37 -0
  68. package/dist/admin/ui/components/ui/separator.d.ts +8 -0
  69. package/dist/admin/ui/components/ui/sheet.d.ts +28 -0
  70. package/dist/admin/ui/components/ui/tabs.d.ts +17 -0
  71. package/dist/admin/ui/components/ui/utils.d.ts +1 -0
  72. package/dist/admin/ui/hooks/use-debounce.d.ts +1 -0
  73. package/dist/admin/ui/views/collection-list-view.d.ts +5 -0
  74. package/dist/admin/ui/views/dashboard-view.d.ts +10 -0
  75. package/dist/admin/ui/views/document-edit-view.d.ts +7 -0
  76. package/dist/admin/ui/views/global-edit-view.d.ts +19 -0
  77. package/dist/admin/ui/views/init-view.d.ts +4 -0
  78. package/dist/admin/ui/views/login-view.d.ts +4 -0
  79. package/dist/admin/ui/views/media-registry-view.d.ts +7 -0
  80. package/dist/admin/ui/views/settings-view.d.ts +7 -0
  81. package/dist/admin/webcomponent.d.ts +1 -0
  82. package/dist/api.d.ts +6 -0
  83. package/dist/auth/index.d.ts +2107 -0
  84. package/dist/auth/migrations.d.ts +5 -0
  85. package/dist/auth/premissions.d.ts +6 -0
  86. package/dist/chunk-16vgcf3k.js +88 -0
  87. package/dist/chunk-2zm8cy1w.js +9482 -0
  88. package/dist/chunk-5gvbp2qa.js +167 -0
  89. package/dist/chunk-62ev8gnc.js +41 -0
  90. package/dist/chunk-6dhs73zq.js +126 -0
  91. package/dist/chunk-6ew02s0c.js +472 -0
  92. package/dist/chunk-7a9kn0np.js +116 -0
  93. package/dist/chunk-8gkhn1d4.js +309 -0
  94. package/dist/chunk-8sqjbsgt.js +42 -0
  95. package/dist/chunk-9kxpbcb1.js +85 -0
  96. package/dist/chunk-cvdd4eqh.js +110 -0
  97. package/dist/chunk-d3ffeqp9.js +87 -0
  98. package/dist/chunk-dy5t83hr.js +261 -0
  99. package/dist/chunk-f3nvxn63.js +17 -0
  100. package/dist/chunk-hmhcense.js +1352 -0
  101. package/dist/chunk-j4d50hrx.js +20 -0
  102. package/dist/chunk-jwjk85ze.js +15 -0
  103. package/dist/chunk-kwp83w8b.js +250 -0
  104. package/dist/chunk-s8mqwnm1.js +14 -0
  105. package/dist/chunk-srsac177.js +85 -0
  106. package/dist/chunk-v521d72w.js +10 -0
  107. package/dist/chunk-xa7rjsn2.js +20 -0
  108. package/dist/chunk-xg35h5a3.js +15 -0
  109. package/dist/chunk-ybbbqj63.js +130 -0
  110. package/dist/chunk-zvwb67nd.js +332 -0
  111. package/dist/cli/commands/generate-types.d.ts +1 -0
  112. package/dist/cli/commands/init.d.ts +1 -0
  113. package/dist/cli/commands/migrate-commands.d.ts +5 -0
  114. package/dist/cli/commands/seed-command.d.ts +2 -0
  115. package/dist/cli/d1-mock.d.ts +30 -0
  116. package/dist/cli/index.d.ts +5 -0
  117. package/dist/cli/index.test.d.ts +1 -0
  118. package/dist/cli/r2-mock.d.ts +46 -0
  119. package/dist/cli/seeding.d.ts +17 -0
  120. package/dist/client.d.ts +51 -0
  121. package/dist/config-utils.d.ts +6 -0
  122. package/dist/config.d.ts +10 -0
  123. package/dist/db/adapter.d.ts +34 -0
  124. package/dist/db/better-sqlite.d.ts +40 -0
  125. package/dist/db/bun-sqlite.d.ts +40 -0
  126. package/dist/db/d1.d.ts +42 -0
  127. package/dist/db/kysely/data-mapper.d.ts +6 -0
  128. package/dist/db/kysely/field-mapper.d.ts +22 -0
  129. package/dist/db/kysely/migration-generator.d.ts +9 -0
  130. package/dist/db/kysely/query-builder.d.ts +9 -0
  131. package/dist/db/kysely/schema-builder.d.ts +15 -0
  132. package/dist/db/kysely/sql-utils.d.ts +1 -0
  133. package/dist/db/postgres.d.ts +51 -0
  134. package/dist/db/sqlite.d.ts +41 -0
  135. package/dist/db/system-schema.d.ts +2 -0
  136. package/dist/index.d.ts +6 -0
  137. package/dist/runtimes/bun.d.ts +17 -0
  138. package/dist/runtimes/cloudflare-workers.d.ts +10 -0
  139. package/dist/runtimes/next.d.ts +16 -0
  140. package/dist/runtimes/node.d.ts +18 -0
  141. package/dist/schema/collection.d.ts +100 -0
  142. package/dist/schema/fields/base.d.ts +83 -0
  143. package/dist/schema/fields/index.d.ts +135 -0
  144. package/dist/schema/global.d.ts +82 -0
  145. package/dist/schema/index.d.ts +4 -0
  146. package/dist/schema/infer.d.ts +55 -0
  147. package/dist/server/admin-router.d.ts +9 -0
  148. package/dist/server/admin.d.ts +18 -0
  149. package/dist/server/assets.d.ts +47 -0
  150. package/dist/server/collection-router.d.ts +14 -0
  151. package/dist/server/handlers.d.ts +76 -0
  152. package/dist/server/middlewares/admin.d.ts +6 -0
  153. package/dist/server/middlewares/auth.d.ts +16 -0
  154. package/dist/server/middlewares/context.d.ts +9 -0
  155. package/dist/server/middlewares/cors.d.ts +3 -0
  156. package/dist/server/middlewares/database-init.d.ts +11 -0
  157. package/dist/server/middlewares/rate-limit.d.ts +3 -0
  158. package/dist/server/router.d.ts +7 -0
  159. package/dist/server/setup-middlewares.d.ts +17 -0
  160. package/dist/server/system-router.d.ts +9 -0
  161. package/dist/server.d.ts +6 -0
  162. package/dist/src/admin/index.css +47 -0
  163. package/dist/src/admin/index.js +176 -0
  164. package/dist/src/admin/webcomponent.js +19 -0
  165. package/dist/src/api.js +27 -0
  166. package/dist/src/cli/index.js +157 -0
  167. package/dist/src/client.js +9 -0
  168. package/dist/src/db/bun-sqlite.js +523 -0
  169. package/dist/src/db/d1.js +568 -0
  170. package/dist/src/db/postgres.js +520 -0
  171. package/dist/src/db/sqlite.js +534 -0
  172. package/dist/src/index.js +20 -0
  173. package/dist/src/runtimes/bun.js +36 -0
  174. package/dist/src/runtimes/cloudflare-workers.js +29 -0
  175. package/dist/src/runtimes/next.js +26 -0
  176. package/dist/src/runtimes/node.js +38 -0
  177. package/dist/src/server.js +27 -0
  178. package/dist/src/storage/index.js +355 -0
  179. package/dist/storage/adapters/cloudflare-r2.d.ts +6 -0
  180. package/dist/storage/adapters/local.d.ts +6 -0
  181. package/dist/storage/adapters/s3.d.ts +13 -0
  182. package/dist/storage/errors.d.ts +12 -0
  183. package/dist/storage/index.d.ts +5 -0
  184. package/dist/storage/types.d.ts +31 -0
  185. package/dist/types.d.ts +484 -0
  186. package/dist/utils/lexical.d.ts +5 -0
  187. package/dist/utils/logger.d.ts +35 -0
  188. package/dist/validation.d.ts +300 -0
  189. package/dist/validator.d.ts +9 -0
  190. package/global.d.ts +11 -0
  191. package/package.json +151 -0
  192. package/src/admin/api-client.ts +63 -0
  193. package/src/admin/auth-client.ts +40 -0
  194. package/src/admin/custom-field.ts +179 -0
  195. package/src/admin/index.ts +15 -0
  196. package/src/admin/react.tsx +72 -0
  197. package/src/admin/router.ts +9 -0
  198. package/src/admin/stores/admin-queries.ts +121 -0
  199. package/src/admin/stores/auth.ts +61 -0
  200. package/src/admin/stores/column-visibility.ts +67 -0
  201. package/src/admin/stores/config.ts +15 -0
  202. package/src/admin/stores/media.ts +95 -0
  203. package/src/admin/stores/query.ts +13 -0
  204. package/src/admin/stores/ui.ts +29 -0
  205. package/src/admin/ui/admin-client.tsx +283 -0
  206. package/src/admin/ui/admin-layout.tsx +276 -0
  207. package/src/admin/ui/components/ColumnVisibilityToggle.tsx +141 -0
  208. package/src/admin/ui/components/DataDetailSheet.tsx +141 -0
  209. package/src/admin/ui/components/DataDetailView.tsx +175 -0
  210. package/src/admin/ui/components/Table.tsx +67 -0
  211. package/src/admin/ui/components/fields/ArrayField.tsx +166 -0
  212. package/src/admin/ui/components/fields/BlocksField.tsx +202 -0
  213. package/src/admin/ui/components/fields/BooleanField.tsx +50 -0
  214. package/src/admin/ui/components/fields/CollapsibleField.tsx +75 -0
  215. package/src/admin/ui/components/fields/DateField.tsx +45 -0
  216. package/src/admin/ui/components/fields/FileField.tsx +322 -0
  217. package/src/admin/ui/components/fields/GroupField.tsx +50 -0
  218. package/src/admin/ui/components/fields/JoinField.tsx +23 -0
  219. package/src/admin/ui/components/fields/NumberField.tsx +46 -0
  220. package/src/admin/ui/components/fields/RadioField.tsx +62 -0
  221. package/src/admin/ui/components/fields/RelationshipField.tsx +278 -0
  222. package/src/admin/ui/components/fields/RowField.tsx +40 -0
  223. package/src/admin/ui/components/fields/SelectField.tsx +59 -0
  224. package/src/admin/ui/components/fields/TabsField.tsx +101 -0
  225. package/src/admin/ui/components/fields/TextAreaField.tsx +54 -0
  226. package/src/admin/ui/components/fields/TextField.tsx +49 -0
  227. package/src/admin/ui/components/fields/VirtualField.tsx +53 -0
  228. package/src/admin/ui/components/fields/index.tsx +371 -0
  229. package/src/admin/ui/components/fields/richtext-editor/index.tsx +211 -0
  230. package/src/admin/ui/components/fields/richtext-editor/nodes/ImageComponent.tsx +142 -0
  231. package/src/admin/ui/components/fields/richtext-editor/nodes/ImageNode.tsx +95 -0
  232. package/src/admin/ui/components/fields/richtext-editor/plugins/ComponentPickerPlugin.tsx +226 -0
  233. package/src/admin/ui/components/fields/richtext-editor/plugins/EditableSyncPlugin.tsx +16 -0
  234. package/src/admin/ui/components/fields/richtext-editor/plugins/NotionToolbarPlugin.tsx +184 -0
  235. package/src/admin/ui/components/fields/richtext-editor/plugins/SimpleToolbarPlugin.tsx +240 -0
  236. package/src/admin/ui/components/fields/richtext-editor/plugins/ValueSyncPlugin.tsx +40 -0
  237. package/src/admin/ui/components/fields/utils.ts +1 -0
  238. package/src/admin/ui/components/link.tsx +41 -0
  239. package/src/admin/ui/components/media/AssetManagerModal.tsx +334 -0
  240. package/src/admin/ui/components/toast.tsx +72 -0
  241. package/src/admin/ui/components/ui/accordion.tsx +51 -0
  242. package/src/admin/ui/components/ui/alert-dialog.tsx +98 -0
  243. package/src/admin/ui/components/ui/blocks.tsx +32 -0
  244. package/src/admin/ui/components/ui/breadcrumbs.tsx +59 -0
  245. package/src/admin/ui/components/ui/button.tsx +26 -0
  246. package/src/admin/ui/components/ui/collapsible.tsx +124 -0
  247. package/src/admin/ui/components/ui/dialog.tsx +79 -0
  248. package/src/admin/ui/components/ui/group.tsx +20 -0
  249. package/src/admin/ui/components/ui/index.ts +17 -0
  250. package/src/admin/ui/components/ui/input.tsx +12 -0
  251. package/src/admin/ui/components/ui/join.tsx +53 -0
  252. package/src/admin/ui/components/ui/label.tsx +11 -0
  253. package/src/admin/ui/components/ui/radio-group.tsx +75 -0
  254. package/src/admin/ui/components/ui/relationship-detail-sheet.tsx +122 -0
  255. package/src/admin/ui/components/ui/relationship.tsx +58 -0
  256. package/src/admin/ui/components/ui/scroll-area.tsx +19 -0
  257. package/src/admin/ui/components/ui/select.tsx +187 -0
  258. package/src/admin/ui/components/ui/separator.tsx +21 -0
  259. package/src/admin/ui/components/ui/sheet.tsx +106 -0
  260. package/src/admin/ui/components/ui/tabs.tsx +116 -0
  261. package/src/admin/ui/components/ui/utils.ts +3 -0
  262. package/src/admin/ui/hooks/use-debounce.ts +15 -0
  263. package/src/admin/ui/styles/_locale-switcher.scss +33 -0
  264. package/src/admin/ui/styles/accordion.scss +60 -0
  265. package/src/admin/ui/styles/animations.scss +41 -0
  266. package/src/admin/ui/styles/asset-manager.scss +547 -0
  267. package/src/admin/ui/styles/badge.scss +13 -0
  268. package/src/admin/ui/styles/base.scss +22 -0
  269. package/src/admin/ui/styles/button.scss +161 -0
  270. package/src/admin/ui/styles/card.scss +13 -0
  271. package/src/admin/ui/styles/collapsible.scss +75 -0
  272. package/src/admin/ui/styles/data-detail.scss +92 -0
  273. package/src/admin/ui/styles/dialog.scss +102 -0
  274. package/src/admin/ui/styles/empty-state.scss +22 -0
  275. package/src/admin/ui/styles/group.scss +19 -0
  276. package/src/admin/ui/styles/index.scss +33 -0
  277. package/src/admin/ui/styles/input.scss +80 -0
  278. package/src/admin/ui/styles/label.scss +12 -0
  279. package/src/admin/ui/styles/layout.scss +56 -0
  280. package/src/admin/ui/styles/lexical.scss +469 -0
  281. package/src/admin/ui/styles/loading.scss +102 -0
  282. package/src/admin/ui/styles/media-registry.scss +597 -0
  283. package/src/admin/ui/styles/pagination.scss +20 -0
  284. package/src/admin/ui/styles/radio-group.scss +66 -0
  285. package/src/admin/ui/styles/row.scss +17 -0
  286. package/src/admin/ui/styles/scrollbar.scss +36 -0
  287. package/src/admin/ui/styles/select.scss +121 -0
  288. package/src/admin/ui/styles/separator.scss +14 -0
  289. package/src/admin/ui/styles/sheet.scss +152 -0
  290. package/src/admin/ui/styles/sidebar.scss +148 -0
  291. package/src/admin/ui/styles/switch.scss +59 -0
  292. package/src/admin/ui/styles/table.scss +207 -0
  293. package/src/admin/ui/styles/tabs.scss +62 -0
  294. package/src/admin/ui/styles/toast.scss +45 -0
  295. package/src/admin/ui/styles/variables.scss +24 -0
  296. package/src/admin/ui/views/collection-list-view.tsx +720 -0
  297. package/src/admin/ui/views/dashboard-view.tsx +263 -0
  298. package/src/admin/ui/views/document-edit-view.tsx +384 -0
  299. package/src/admin/ui/views/global-edit-view.tsx +226 -0
  300. package/src/admin/ui/views/init-view.tsx +182 -0
  301. package/src/admin/ui/views/login-view.tsx +123 -0
  302. package/src/admin/ui/views/media-registry-view.tsx +1104 -0
  303. package/src/admin/ui/views/settings-view.tsx +729 -0
  304. package/src/admin/webcomponent.tsx +15 -0
  305. package/src/api.ts +9 -0
  306. package/src/auth/index.ts +194 -0
  307. package/src/auth/migrations.ts +87 -0
  308. package/src/auth/premissions.ts +46 -0
  309. package/src/cli/commands/generate-types.ts +116 -0
  310. package/src/cli/commands/init.ts +95 -0
  311. package/src/cli/commands/migrate-commands.ts +160 -0
  312. package/src/cli/commands/seed-command.ts +11 -0
  313. package/src/cli/d1-mock.ts +101 -0
  314. package/src/cli/index.test.ts +84 -0
  315. package/src/cli/index.ts +183 -0
  316. package/src/cli/r2-mock.ts +217 -0
  317. package/src/cli/seeding.ts +405 -0
  318. package/src/client.ts +181 -0
  319. package/src/config-utils.ts +102 -0
  320. package/src/config.ts +49 -0
  321. package/src/db/adapter.ts +53 -0
  322. package/src/db/better-sqlite.ts +630 -0
  323. package/src/db/bun-sqlite.ts +646 -0
  324. package/src/db/d1.ts +711 -0
  325. package/src/db/kysely/data-mapper.ts +142 -0
  326. package/src/db/kysely/field-mapper.ts +148 -0
  327. package/src/db/kysely/migration-generator.ts +223 -0
  328. package/src/db/kysely/query-builder.ts +92 -0
  329. package/src/db/kysely/schema-builder.ts +439 -0
  330. package/src/db/kysely/sql-utils.ts +13 -0
  331. package/src/db/postgres.ts +621 -0
  332. package/src/db/sqlite.ts +658 -0
  333. package/src/db/system-schema.ts +121 -0
  334. package/src/index.ts +13 -0
  335. package/src/runtimes/README.md +59 -0
  336. package/src/runtimes/bun.ts +49 -0
  337. package/src/runtimes/cloudflare-workers.ts +38 -0
  338. package/src/runtimes/next.ts +26 -0
  339. package/src/runtimes/node.ts +52 -0
  340. package/src/schema/collection.ts +184 -0
  341. package/src/schema/fields/base.ts +164 -0
  342. package/src/schema/fields/index.ts +427 -0
  343. package/src/schema/global.ts +145 -0
  344. package/src/schema/index.ts +4 -0
  345. package/src/schema/infer.ts +72 -0
  346. package/src/server/admin-router.ts +20 -0
  347. package/src/server/admin.ts +142 -0
  348. package/src/server/assets.ts +306 -0
  349. package/src/server/collection-router.ts +55 -0
  350. package/src/server/handlers.ts +722 -0
  351. package/src/server/middlewares/admin.ts +27 -0
  352. package/src/server/middlewares/auth.ts +89 -0
  353. package/src/server/middlewares/context.ts +17 -0
  354. package/src/server/middlewares/cors.ts +24 -0
  355. package/src/server/middlewares/database-init.ts +74 -0
  356. package/src/server/middlewares/rate-limit.ts +71 -0
  357. package/src/server/router.ts +47 -0
  358. package/src/server/setup-middlewares.ts +58 -0
  359. package/src/server/system-router.ts +35 -0
  360. package/src/server.ts +9 -0
  361. package/src/storage/adapters/cloudflare-r2.ts +136 -0
  362. package/src/storage/adapters/local.ts +146 -0
  363. package/src/storage/adapters/s3.ts +186 -0
  364. package/src/storage/errors.ts +46 -0
  365. package/src/storage/index.ts +5 -0
  366. package/src/storage/types.ts +39 -0
  367. package/src/types.ts +577 -0
  368. package/src/utils/lexical.ts +37 -0
  369. package/src/utils/logger.ts +73 -0
  370. package/src/validation.ts +429 -0
  371. package/src/validator.ts +179 -0
  372. package/test/admin-custom-field.test.ts +162 -0
  373. package/test/admin-react-field.test.tsx +134 -0
  374. package/test/api-features.test.ts +78 -0
  375. package/test/api.test.ts +178 -0
  376. package/test/auth.test.ts +62 -0
  377. package/test/cli-integration.test.ts +146 -0
  378. package/test/cli.test.ts +25 -0
  379. package/test/db/postgres.test.ts +95 -0
  380. package/test/db/sqlite-filter.test.ts +53 -0
  381. package/test/db/sqlite.test.ts +82 -0
  382. package/test/engine-features.test.ts +79 -0
  383. package/test/globals.test.ts +74 -0
  384. package/test/integration-tmp/db-app/opacacms.config.ts +15 -0
  385. package/test/integration-tmp/my-sqlite-app/opacacms.config.ts +25 -0
  386. package/test/integration-tmp/my-test-app/index.ts +8 -0
  387. package/test/integration-tmp/my-test-app/opacacms.config.ts +16 -0
  388. package/test/integration-tmp/my-test-app/package.json +12 -0
  389. package/test/populate.test.ts +79 -0
  390. package/test/runtimes.test.ts +43 -0
  391. package/test/schema-builder.test.ts +107 -0
  392. package/test/schema-features.test.ts +63 -0
  393. package/test/seeding.test.ts +68 -0
  394. package/test/storage/local.test.ts +72 -0
  395. package/test/storage/s3.test.ts +60 -0
  396. package/test/structural-data.test.ts +100 -0
  397. package/test/test-setup.ts +11 -0
  398. package/test/validation.test.ts +162 -0
  399. package/tsconfig.json +42 -0
@@ -0,0 +1,217 @@
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
+ }
@@ -0,0 +1,405 @@
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
+ }