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,722 @@
1
+ import type { Context } from "hono";
2
+ import type { Collection, Global, OpacaConfig } from "../types";
3
+ import { logger } from "../utils/logger";
4
+ import { generateSchemaForCollection } from "../validator";
5
+
6
+ /**
7
+ * Hydrates a document with virtual fields and nested fields.
8
+ * @param doc document to hydrate
9
+ * @param fields fields to hydrate
10
+ * @param c context
11
+ * @returns hydrated document
12
+ */
13
+ export const hydrateDoc = async (doc: any, fields: any[], c: Context, config: OpacaConfig) => {
14
+ if (!doc) return doc;
15
+
16
+ const user = c.get("user");
17
+ const session = c.get("session");
18
+ const apiKey = c.get("apiKey");
19
+
20
+ const hydratePromises = fields.map(async (field) => {
21
+ // 1. Resolve Virtual/Computed Fields
22
+ if (field.type === "virtual" && typeof field.resolve === "function") {
23
+ try {
24
+ doc[field.name] = await field.resolve({
25
+ data: doc,
26
+ req: c,
27
+ user,
28
+ session,
29
+ apiKey,
30
+ });
31
+ } catch (err) {
32
+ console.error(`[OpacaCMS] Failed to resolve virtual field ${field.name} `, err);
33
+ doc[field.name] = null;
34
+ }
35
+ }
36
+
37
+ // 2. Handle Nested Layout Fields (Recursive)
38
+ if (field.fields && Array.isArray(field.fields)) {
39
+ await hydrateDoc(doc, field.fields, c, config);
40
+ }
41
+
42
+ // 3. Handle Tabs (Recursive)
43
+ if (field.tabs && Array.isArray(field.tabs)) {
44
+ for (const tab of field.tabs) {
45
+ if (tab.fields && Array.isArray(tab.fields)) {
46
+ await hydrateDoc(doc, tab.fields, c, config);
47
+ }
48
+ }
49
+ }
50
+
51
+ // 4. Handle Groups (Recursive - value stays in place unless explicitly mapped)
52
+ if (field.type === "group" && field.fields && doc[field.name]) {
53
+ await hydrateDoc(doc[field.name], field.fields, c, config);
54
+ }
55
+
56
+ // 5. Handle Arrays (Recursive for each item)
57
+ if (field.type === "array" && field.fields && Array.isArray(doc[field.name])) {
58
+ await Promise.all(
59
+ doc[field.name].map((item: any) => hydrateDoc(item, field.fields, c, config)),
60
+ );
61
+ }
62
+
63
+ // 6. Handle Blocks (Recursive for each block type)
64
+ if (field.type === "blocks" && field.blocks && Array.isArray(doc[field.name])) {
65
+ await Promise.all(
66
+ doc[field.name].map((item: any) => {
67
+ const block = field.blocks.find((b: any) => b.slug === item.block_type);
68
+ if (block && block.fields) {
69
+ return hydrateDoc(item, block.fields, c, config);
70
+ }
71
+ return Promise.resolve();
72
+ }),
73
+ );
74
+ }
75
+ // 7. Handle Localized Fields
76
+ if (field.localized && doc[field.name] && typeof doc[field.name] === "object") {
77
+ const i18nConfig = config.i18n;
78
+ const requestedLocale = c.req.header("x-opaca-locale") || c.req.query("locale");
79
+
80
+ // Resolve target locale: explicit request > defaultLocale > fallback to "en"
81
+ const targetLocale = requestedLocale || i18nConfig?.defaultLocale || "en";
82
+
83
+ if (targetLocale !== "all") {
84
+ const localizedValue =
85
+ doc[field.name][targetLocale] ?? doc[field.name][i18nConfig?.defaultLocale || "en"] ?? "";
86
+ doc[field.name] = localizedValue;
87
+ }
88
+ }
89
+ });
90
+
91
+ await Promise.all(hydratePromises);
92
+ return doc;
93
+ };
94
+
95
+ /**
96
+ * Populates a document with related documents.
97
+ * This version is generic and takes the DB adapter and fields list.
98
+ */
99
+ const populateDoc = async (db: any, fields: any[], doc: any, populateKeys: string[]) => {
100
+ if (!doc) return doc;
101
+
102
+ const populatePromises = fields
103
+ .filter((f) => {
104
+ const field = f as any;
105
+ return (
106
+ field.type === "relationship" &&
107
+ field.relationTo &&
108
+ populateKeys.includes(field.name) &&
109
+ doc[field.name]
110
+ );
111
+ })
112
+ .map(async (f) => {
113
+ const field = f as any;
114
+ try {
115
+ const relatedDoc = await db.findOne(field.relationTo, { id: doc[field.name] });
116
+ if (relatedDoc) {
117
+ doc[field.name] = relatedDoc;
118
+ }
119
+ } catch (err) {
120
+ console.error(`[OpacaCMS] Failed to populate relationship ${field.name} `, err);
121
+ }
122
+ });
123
+
124
+ await Promise.all(populatePromises);
125
+ return doc;
126
+ };
127
+
128
+ /**
129
+ * Creates handlers for a collection
130
+ * @param config opaca config
131
+ * @param collection collection config
132
+ * @param getAuth auth function
133
+ * @returns collection handlers
134
+ */
135
+ export function createHandlers(config: OpacaConfig, collection: Collection, getAuth?: () => any) {
136
+ const { db } = config;
137
+
138
+ /**
139
+ * Checks access to a collection
140
+ * @param c context
141
+ * @param action action to check
142
+ * @param data data to check
143
+ * @returns true if access is allowed, false otherwise
144
+ */
145
+ const checkAccess = async (
146
+ c: Context,
147
+ action: keyof NonNullable<Collection["access"]>,
148
+ data?: unknown,
149
+ ) => {
150
+ const access = collection.access?.[action];
151
+
152
+ // 1. Check API Key Permissions
153
+ const apiKey = c.get("apiKey");
154
+
155
+ // Check if API Key is strictly required for this collection
156
+ if (collection.access?.requireApiKey && !apiKey) {
157
+ const user = c.get("user");
158
+ // Allow access for authenticated admins even without an API Key
159
+ if (user?.role === "admin" || (Array.isArray(user?.role) && user.role.includes("admin"))) {
160
+ // Proceed to normal access function check
161
+ } else {
162
+ return false;
163
+ }
164
+ }
165
+
166
+ if (apiKey) {
167
+ // If the key has specific permissions for this collection, we enforce them.
168
+ if (apiKey.permissions) {
169
+ const collectionPermissions = apiKey.permissions[collection.slug];
170
+ if (collectionPermissions) {
171
+ if (!collectionPermissions.includes(action as string)) {
172
+ return false;
173
+ }
174
+ return true;
175
+ }
176
+ }
177
+ }
178
+
179
+ // 2. Fallback to Collection Access Function
180
+ if (access === undefined) return true;
181
+ if (typeof access === "boolean") return access;
182
+
183
+ const user = c.get("user");
184
+ const session = c.get("session");
185
+
186
+ return await access({
187
+ req: c,
188
+ user,
189
+ session,
190
+ apiKey,
191
+ data,
192
+ });
193
+ };
194
+
195
+ /**
196
+ * Gets field access permissions
197
+ * @param c context
198
+ * @param operation operation type
199
+ * @param fields fields to check
200
+ * @param data data to check
201
+ * @returns field access permissions
202
+ */
203
+ const getFieldAccessPermissions = async (
204
+ c: Context,
205
+ operation: "read" | "create" | "update",
206
+ fields: any[],
207
+ data?: unknown,
208
+ ) => {
209
+ const permissions: Record<string, any> = {};
210
+ const user = c.get("user");
211
+ const session = c.get("session");
212
+ const apiKey = c.get("apiKey");
213
+
214
+ const accessArgs = {
215
+ req: c,
216
+ user,
217
+ session,
218
+ apiKey,
219
+ data,
220
+ operation,
221
+ };
222
+
223
+ for (const field of fields) {
224
+ if (field.name) {
225
+ if (!field.access) {
226
+ permissions[field.name] = { hidden: false, readOnly: false, disabled: false };
227
+ } else {
228
+ const hidden =
229
+ typeof field.access.hidden === "function"
230
+ ? await field.access.hidden(accessArgs as any)
231
+ : !!field.access.hidden;
232
+ const readOnly =
233
+ typeof field.access.readOnly === "function"
234
+ ? await field.access.readOnly(accessArgs as any)
235
+ : !!field.access.readOnly;
236
+ const disabled =
237
+ typeof field.access.disabled === "function"
238
+ ? await field.access.disabled(accessArgs as any)
239
+ : !!field.access.disabled;
240
+
241
+ permissions[field.name] = { hidden, readOnly, disabled };
242
+ }
243
+ }
244
+
245
+ // Recursively check nested fields if any
246
+ if (field.fields && Array.isArray(field.fields)) {
247
+ const nestedPermissions = await getFieldAccessPermissions(c, operation, field.fields, data);
248
+ Object.assign(permissions, nestedPermissions);
249
+ }
250
+
251
+ if (field.tabs && Array.isArray(field.tabs)) {
252
+ for (const tab of field.tabs) {
253
+ if (tab.fields && Array.isArray(tab.fields)) {
254
+ const nestedPermissions = await getFieldAccessPermissions(
255
+ c,
256
+ operation,
257
+ tab.fields,
258
+ data,
259
+ );
260
+ Object.assign(permissions, nestedPermissions);
261
+ }
262
+ }
263
+ }
264
+ }
265
+
266
+ return permissions;
267
+ };
268
+
269
+ /**
270
+ * Saves a document version to the _versions table
271
+ */
272
+ const saveVersion = async (doc: any, status?: string) => {
273
+ if (!collection.versions) return;
274
+
275
+ const versionsTable = `${collection.slug}_versions`.toLowerCase();
276
+ const versionDoc = {
277
+ id: crypto.randomUUID(),
278
+ _parent_id: doc.id,
279
+ _version_data: JSON.stringify(doc),
280
+ _status: status || doc._status || "published",
281
+ created_at: new Date().toISOString(),
282
+ };
283
+
284
+ try {
285
+ await (db as any).unsafe(
286
+ `INSERT INTO ${versionsTable} (id, _parent_id, _version_data, _status, created_at) VALUES(?, ?, ?, ?, ?)`,
287
+ [
288
+ versionDoc.id,
289
+ versionDoc._parent_id,
290
+ versionDoc._version_data,
291
+ versionDoc._status,
292
+ versionDoc.created_at,
293
+ ],
294
+ );
295
+ } catch (err) {
296
+ console.error(`[OpacaCMS] Failed to save version for ${collection.slug}: `, err);
297
+ }
298
+ };
299
+
300
+ return {
301
+ async find(c: Context) {
302
+ if (!(await checkAccess(c, "read"))) {
303
+ return c.json({ message: "Forbidden" }, 403);
304
+ }
305
+
306
+ const queries = c.req.query();
307
+ const maxLimit = config.api?.maxLimit ?? 100;
308
+ const page = queries.page ? parseInt(queries.page, 10) : 1;
309
+ let limit = queries.limit ? parseInt(queries.limit, 10) : 10;
310
+ if (limit > maxLimit) limit = maxLimit;
311
+
312
+ const sort = queries.sort as string;
313
+ const populate = queries.populate ? queries.populate.split(",") : [];
314
+
315
+ const filter: Record<string, unknown> = {};
316
+ for (const [key, value] of Object.entries(queries)) {
317
+ if (["page", "limit", "sort", "populate", "locale", "draft"].includes(key)) continue;
318
+ const match = key.match(/^([^[]+)\[([^\]]+)\]$/);
319
+ if (match) {
320
+ const field = match[1] as string;
321
+ const op = match[2] as string;
322
+ if (!filter[field]) filter[field] = {};
323
+ (filter[field] as Record<string, unknown>)[op] = value;
324
+ } else {
325
+ filter[key] = value;
326
+ }
327
+ }
328
+
329
+ const results = await db.find(collection.slug, filter, { page, limit, sort });
330
+
331
+ if (populate.length > 0) {
332
+ results.docs = await Promise.all(
333
+ results.docs.map((doc) => populateDoc(db, collection.fields, doc, populate)),
334
+ );
335
+ }
336
+
337
+ results.docs = await Promise.all(
338
+ results.docs.map((doc) => hydrateDoc(doc, collection.fields, c, config)),
339
+ );
340
+
341
+ return c.json(results);
342
+ },
343
+
344
+ async findOne(c: Context) {
345
+ if (!(await checkAccess(c, "read"))) {
346
+ return c.json({ message: "Forbidden" }, 403);
347
+ }
348
+
349
+ const queries = c.req.query();
350
+ const populate = queries.populate ? queries.populate.split(",") : [];
351
+ const id = c.req.param("id");
352
+
353
+ let doc = await db.findOne(collection.slug, { id });
354
+ if (!doc) return c.json({ message: "Not found" }, 404);
355
+
356
+ if (populate.length > 0) {
357
+ doc = await populateDoc(db, collection.fields, doc, populate);
358
+ }
359
+
360
+ doc = await hydrateDoc(doc, collection.fields, c, config);
361
+
362
+ const permissions = await getFieldAccessPermissions(c, "read", collection.fields, doc);
363
+ const cleanDoc = { ...doc } as any;
364
+ for (const [field, perm] of Object.entries(permissions)) {
365
+ if ((perm as any).hidden) {
366
+ delete cleanDoc[field];
367
+ }
368
+ }
369
+
370
+ return c.json(cleanDoc);
371
+ },
372
+
373
+ async create(c: Context) {
374
+ const body = await c.req.json();
375
+ if (!(await checkAccess(c, "create", body))) {
376
+ return c.json({ message: "Forbidden" }, 403);
377
+ }
378
+
379
+ // Handle Slug Auto-generation
380
+ for (const field of collection.fields) {
381
+ if (field.type === "slug" && !body[field.name]) {
382
+ const fromValue = body[field.from];
383
+ if (fromValue && typeof fromValue === "string") {
384
+ const formatted = field.format
385
+ ? field.format(fromValue)
386
+ : fromValue
387
+ .toLowerCase()
388
+ .replace(/[^a-z0-9]+/g, "-")
389
+ .replace(/^-+|-+$/g, "");
390
+ body[field.name] = formatted;
391
+ }
392
+ }
393
+ }
394
+
395
+ const schema = generateSchemaForCollection(collection);
396
+ const validation = schema.safeParse(body);
397
+ if (!validation.success) {
398
+ return c.json({ message: "Validation Error", errors: validation.error.format() }, 400);
399
+ }
400
+
401
+ let data = validation.data;
402
+ const locale = c.req.header("x-opaca-locale");
403
+
404
+ // Wrap localized fields if a specific locale is provided
405
+ if (locale && locale !== "all") {
406
+ for (const field of collection.fields) {
407
+ if (field.name && field.localized && data[field.name] !== undefined) {
408
+ data[field.name] = { [locale]: data[field.name] };
409
+ }
410
+ }
411
+ }
412
+ if (collection.hooks?.beforeCreate) {
413
+ data = (await collection.hooks.beforeCreate(data as never)) as any;
414
+ }
415
+
416
+ const doc = await db.create(collection.slug, data as Record<string, unknown>);
417
+
418
+ if (collection.hooks?.afterCreate) {
419
+ await collection.hooks.afterCreate(doc as never);
420
+ }
421
+
422
+ if (collection.webhooks) {
423
+ const afterCreateWebhooks = collection.webhooks.filter((w) =>
424
+ w.events.includes("afterCreate"),
425
+ );
426
+ for (const webhook of afterCreateWebhooks) {
427
+ const hookPromise = fetch(webhook.url, {
428
+ method: "POST",
429
+ headers: { "Content-Type": "application/json", ...webhook.headers },
430
+ body: JSON.stringify(doc),
431
+ }).catch((e) => logger.error(`Webhook afterCreate failed for ${collection.slug}: `, e));
432
+ if (c.executionCtx?.waitUntil) {
433
+ c.executionCtx.waitUntil(hookPromise);
434
+ }
435
+ }
436
+ }
437
+
438
+ if (collection.versions) {
439
+ await saveVersion(doc, body._status);
440
+ }
441
+
442
+ const hydratedDoc = await hydrateDoc(doc, collection.fields, c, config);
443
+
444
+ return c.json(hydratedDoc, 201);
445
+ },
446
+
447
+ async update(c: Context) {
448
+ const id = c.req.param("id");
449
+ const body = await c.req.json();
450
+ if (!(await checkAccess(c, "update", body))) {
451
+ return c.json({ message: "Forbidden" }, 403);
452
+ }
453
+
454
+ // Handle Slug Auto-generation
455
+ for (const field of collection.fields) {
456
+ if (field.type === "slug" && !body[field.name]) {
457
+ const fromValue = body[field.from];
458
+ if (fromValue && typeof fromValue === "string") {
459
+ const formatted = field.format
460
+ ? field.format(fromValue)
461
+ : fromValue
462
+ .toLowerCase()
463
+ .replace(/[^a-z0-9]+/g, "-")
464
+ .replace(/^-+|-+$/g, "");
465
+ body[field.name] = formatted;
466
+ }
467
+ }
468
+ }
469
+
470
+ const schema = generateSchemaForCollection(collection, true);
471
+ const validation = schema.safeParse(body);
472
+ if (!validation.success) {
473
+ return c.json({ message: "Validation Error", errors: validation.error.format() }, 400);
474
+ }
475
+
476
+ let data = validation.data;
477
+ const locale = c.req.header("x-opaca-locale");
478
+
479
+ // Merge into localized fields if a specific locale is provided
480
+ if (locale && locale !== "all") {
481
+ let existing: any = null;
482
+ for (const field of collection.fields) {
483
+ if (field.name && field.localized && data[field.name] !== undefined) {
484
+ if (!existing) {
485
+ existing = await db.findOne(collection.slug, { id });
486
+ }
487
+ const currentObj = (existing?.[field.name] || {}) as Record<string, any>;
488
+ data[field.name] = { ...currentObj, [locale]: data[field.name] };
489
+ }
490
+ }
491
+ }
492
+ if (collection.hooks?.beforeUpdate) {
493
+ data = (await collection.hooks.beforeUpdate(data as never)) as any;
494
+ }
495
+
496
+ const doc = await db.update(collection.slug, { id }, data as Record<string, unknown>);
497
+
498
+ if (collection.hooks?.afterUpdate) {
499
+ await collection.hooks.afterUpdate(doc as never);
500
+ }
501
+
502
+ if (collection.webhooks) {
503
+ const afterUpdateWebhooks = collection.webhooks.filter((w) =>
504
+ w.events.includes("afterUpdate"),
505
+ );
506
+ for (const webhook of afterUpdateWebhooks) {
507
+ const hookPromise = fetch(webhook.url, {
508
+ method: "POST",
509
+ headers: { "Content-Type": "application/json", ...webhook.headers },
510
+ body: JSON.stringify(doc),
511
+ }).catch((e) => logger.error(`Webhook afterUpdate failed for ${collection.slug}: `, e));
512
+ if (c.executionCtx?.waitUntil) {
513
+ c.executionCtx.waitUntil(hookPromise);
514
+ }
515
+ }
516
+ }
517
+
518
+ if (collection.versions) {
519
+ await saveVersion(doc, body._status);
520
+ }
521
+
522
+ const hydratedDoc = await hydrateDoc(doc, collection.fields, c, config);
523
+
524
+ return c.json(hydratedDoc);
525
+ },
526
+
527
+ async delete(c: Context) {
528
+ if (!(await checkAccess(c, "delete"))) {
529
+ return c.json({ message: "Forbidden" }, 403);
530
+ }
531
+
532
+ const id = c.req.param("id");
533
+
534
+ // Optionally fetch doc before deleting to pass to webhook
535
+ let docToPass: any = { id };
536
+ try {
537
+ if (
538
+ collection.webhooks &&
539
+ collection.webhooks.some((w) => w.events.includes("afterDelete"))
540
+ ) {
541
+ docToPass = await db.findOne(collection.slug, { id });
542
+ }
543
+ } catch (e) {
544
+ // Ignore
545
+ }
546
+
547
+ if (collection.hooks?.beforeDelete) {
548
+ await collection.hooks.beforeDelete(id as never);
549
+ }
550
+
551
+ await db.delete(collection.slug, { id });
552
+
553
+ if (collection.hooks?.afterDelete) {
554
+ await collection.hooks.afterDelete(id as never);
555
+ }
556
+
557
+ if (collection.webhooks) {
558
+ const afterDeleteWebhooks = collection.webhooks.filter((w) =>
559
+ w.events.includes("afterDelete"),
560
+ );
561
+ for (const webhook of afterDeleteWebhooks) {
562
+ const hookPromise = fetch(webhook.url, {
563
+ method: "POST",
564
+ headers: { "Content-Type": "application/json", ...webhook.headers },
565
+ body: JSON.stringify(docToPass || { id }),
566
+ }).catch((e) => logger.error(`Webhook afterDelete failed for ${collection.slug}: `, e));
567
+ if (c.executionCtx?.waitUntil) {
568
+ c.executionCtx.waitUntil(hookPromise);
569
+ }
570
+ }
571
+ }
572
+
573
+ return c.json({ message: "Deleted" });
574
+ },
575
+
576
+ async findVersions(c: Context) {
577
+ if (!(await checkAccess(c, "read"))) {
578
+ return c.json({ message: "Forbidden" }, 403);
579
+ }
580
+ const parentId = c.req.query("parentId");
581
+ const versionsTable = `${collection.slug} _versions`.toLowerCase();
582
+
583
+ const query = parentId
584
+ ? `SELECT * FROM ${versionsTable} WHERE _parent_id = ? ORDER BY created_at DESC`
585
+ : `SELECT * FROM ${versionsTable} ORDER BY created_at DESC`;
586
+ const params = parentId ? [parentId] : [];
587
+
588
+ try {
589
+ const rows = (await (db as any).unsafe(query, params)) as any[];
590
+ return c.json({ docs: rows });
591
+ } catch (err) {
592
+ return c.json({ message: "Failed to fetch versions", error: (err as any).message }, 500);
593
+ }
594
+ },
595
+
596
+ async restoreVersion(c: Context) {
597
+ if (!(await checkAccess(c, "update"))) {
598
+ return c.json({ message: "Forbidden" }, 403);
599
+ }
600
+ const versionId = c.req.param("versionId");
601
+ const versionsTable = `${collection.slug} _versions`.toLowerCase();
602
+
603
+ try {
604
+ const versionRows = (await (db as any).unsafe(
605
+ `SELECT * FROM ${versionsTable} WHERE id = ? `,
606
+ [versionId],
607
+ )) as any[];
608
+ if (versionRows.length === 0) return c.json({ message: "Version not found" }, 404);
609
+
610
+ const version = versionRows[0];
611
+ const data = JSON.parse(version._version_data);
612
+
613
+ // Remove ID and other internal fields for update
614
+ const id = version._parent_id;
615
+ delete data.id;
616
+ delete data.created_at;
617
+ delete data.updated_at;
618
+
619
+ const doc = await db.update(collection.slug, { id }, data);
620
+ await saveVersion(doc, "published"); // Save a new version tracking the restoration
621
+
622
+ return c.json(doc);
623
+ } catch (err) {
624
+ return c.json({ message: "Failed to restore version", error: (err as any).message }, 500);
625
+ }
626
+ },
627
+ };
628
+ }
629
+
630
+ /**
631
+ * Creates handlers for global collections
632
+ * @param config opaca config
633
+ * @param globalConfig global config
634
+ * @param getAuth auth function
635
+ * @returns global handlers
636
+ */
637
+ export function createGlobalHandlers(
638
+ config: OpacaConfig,
639
+ globalConfig: Global,
640
+ getAuth?: () => any,
641
+ ) {
642
+ const { db } = config;
643
+
644
+ /**
645
+ * Checks access to a global collection
646
+ * @param c context
647
+ * @param action action to check
648
+ * @param data data to check
649
+ * @returns true if access is allowed, false otherwise
650
+ */
651
+ const checkAccess = async (
652
+ c: Context,
653
+ action: keyof NonNullable<Global["access"]>,
654
+ data?: unknown,
655
+ ) => {
656
+ const access = globalConfig.access?.[action];
657
+ if (access === undefined) return true;
658
+ if (typeof access === "boolean") return access;
659
+
660
+ const user = c.get("user");
661
+ const session = c.get("session");
662
+ const apiKey = c.get("apiKey");
663
+
664
+ return await access({
665
+ req: c,
666
+ user,
667
+ session,
668
+ apiKey,
669
+ data,
670
+ });
671
+ };
672
+
673
+ return {
674
+ async find(c: Context) {
675
+ if (!(await checkAccess(c, "read"))) {
676
+ return c.json({ message: "Forbidden" }, 403);
677
+ }
678
+
679
+ if (!db.findGlobal) {
680
+ return c.json({ message: "Globals are not supported by this database adapter" }, 501);
681
+ }
682
+
683
+ const doc = await db.findGlobal(globalConfig.slug);
684
+ const hydratedDoc = await hydrateDoc(doc || {}, globalConfig.fields, c, config);
685
+ return c.json(hydratedDoc);
686
+ },
687
+
688
+ async update(c: Context) {
689
+ const body = await c.req.json();
690
+
691
+ if (!(await checkAccess(c, "update", body))) {
692
+ return c.json({ message: "Forbidden" }, 403);
693
+ }
694
+
695
+ if (!db.updateGlobal) {
696
+ return c.json({ message: "Globals are not supported by this database adapter" }, 501);
697
+ }
698
+
699
+ const schema = generateSchemaForCollection(globalConfig, true);
700
+ const validation = schema.safeParse(body);
701
+
702
+ if (!validation.success) {
703
+ logger.error(
704
+ `Validation Error on Global ${globalConfig.slug}: `,
705
+ validation.error.format(),
706
+ );
707
+ return c.json(
708
+ {
709
+ message: "Validation Error",
710
+ errors: validation.error.format(),
711
+ },
712
+ 400,
713
+ );
714
+ }
715
+
716
+ const doc = await db.updateGlobal(globalConfig.slug, validation.data);
717
+ const hydratedDoc = await hydrateDoc(doc, globalConfig.fields, c, config);
718
+
719
+ return c.json(hydratedDoc);
720
+ },
721
+ };
722
+ }