@tulip-systems/core 0.7.0 → 0.8.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 (367) hide show
  1. package/dist/auth/server.d.mts +3 -3
  2. package/dist/auth/server.mjs +3 -3
  3. package/dist/components/editor/components/editor.client.d.mts +4 -3
  4. package/dist/components/editor/components/editor.client.d.mts.map +1 -1
  5. package/dist/components/editor/components/editor.client.mjs +5 -2
  6. package/dist/components/editor/components/editor.client.mjs.map +1 -1
  7. package/dist/components/editor/extensions/file-handler/extension.d.mts +4 -4
  8. package/dist/components/editor/extensions/file-handler/extension.d.mts.map +1 -1
  9. package/dist/components/editor/extensions/file-handler/extension.mjs.map +1 -1
  10. package/dist/components/editor/extensions/file-handler/strategy.d.mts +4 -6
  11. package/dist/components/editor/extensions/file-handler/strategy.d.mts.map +1 -1
  12. package/dist/components/editor/extensions/file-handler/strategy.mjs +11 -11
  13. package/dist/components/editor/extensions/file-handler/strategy.mjs.map +1 -1
  14. package/dist/components/editor/extensions/file-handler/utils.mjs +1 -1
  15. package/dist/components/editor/extensions/file-handler/utils.mjs.map +1 -1
  16. package/dist/components/editor/extensions/image/extension.mjs +9 -9
  17. package/dist/components/editor/extensions/image/extension.mjs.map +1 -1
  18. package/dist/components/editor/lib/constants.d.mts +1 -1
  19. package/dist/components/editor/lib/constants.mjs +1 -1
  20. package/dist/components/editor/lib/extensions.d.mts +1 -1
  21. package/dist/components/editor/lib/helpers.d.mts +11 -3
  22. package/dist/components/editor/lib/helpers.d.mts.map +1 -1
  23. package/dist/components/editor/lib/helpers.mjs +27 -13
  24. package/dist/components/editor/lib/helpers.mjs.map +1 -1
  25. package/dist/components/ui/badge.d.mts +1 -1
  26. package/dist/components/ui/button-group.d.mts +1 -1
  27. package/dist/components/ui/button.d.mts +2 -2
  28. package/dist/components/ui/combobox-dropdown.client.mjs +1 -0
  29. package/dist/components/ui/combobox-dropdown.client.mjs.map +1 -1
  30. package/dist/components/ui/combobox.client.mjs +1 -1
  31. package/dist/components/ui/combobox.client.mjs.map +1 -1
  32. package/dist/components/ui/field.client.d.mts +1 -1
  33. package/dist/components/ui/item.d.mts +1 -1
  34. package/dist/components.d.mts +2 -2
  35. package/dist/components.mjs +2 -2
  36. package/dist/config/server.d.mts +1 -3
  37. package/dist/config/server.mjs +1 -4
  38. package/dist/config.d.mts +2 -2
  39. package/dist/config.mjs +1 -1
  40. package/dist/data-tables/client.d.mts +2 -1
  41. package/dist/data-tables/client.mjs +2 -1
  42. package/dist/data-tables.d.mts +1 -1
  43. package/dist/database/client.d.mts +1 -0
  44. package/dist/database/client.mjs +1 -0
  45. package/dist/database/server.d.mts +2 -0
  46. package/dist/database/server.mjs +3 -0
  47. package/dist/database.d.mts +3 -0
  48. package/dist/database.mjs +3 -0
  49. package/dist/emails/client.d.mts +1 -0
  50. package/dist/emails/client.mjs +1 -0
  51. package/dist/emails/server.d.mts +2 -0
  52. package/dist/emails/server.mjs +3 -0
  53. package/dist/emails.d.mts +1 -0
  54. package/dist/emails.mjs +1 -0
  55. package/dist/lib/utils/markdown.d.mts +10 -0
  56. package/dist/lib/utils/markdown.d.mts.map +1 -0
  57. package/dist/lib/utils/markdown.mjs +15 -0
  58. package/dist/lib/utils/markdown.mjs.map +1 -0
  59. package/dist/lib/utils/url.mjs +2 -1
  60. package/dist/lib/utils/url.mjs.map +1 -1
  61. package/dist/lib/utils/user-agent.mjs +15 -0
  62. package/dist/lib/utils/user-agent.mjs.map +1 -1
  63. package/dist/lib.d.mts +2 -2
  64. package/dist/lib.mjs +2 -2
  65. package/dist/modules/auth/components/create-first-user-guard.server.d.mts +16 -0
  66. package/dist/modules/auth/components/create-first-user-guard.server.d.mts.map +1 -0
  67. package/dist/modules/auth/components/create-first-user-guard.server.mjs +16 -0
  68. package/dist/modules/auth/components/create-first-user-guard.server.mjs.map +1 -0
  69. package/dist/modules/auth/components/guard.server.d.mts +2 -2
  70. package/dist/modules/auth/components/guard.server.mjs +1 -1
  71. package/dist/modules/auth/components/guard.server.mjs.map +1 -1
  72. package/dist/modules/auth/db/schema.d.mts +1 -1
  73. package/dist/modules/auth/db/schema.mjs +2 -2
  74. package/dist/modules/auth/handler/create-client.client.d.mts +4838 -229
  75. package/dist/modules/auth/handler/create-client.client.d.mts.map +1 -1
  76. package/dist/modules/auth/handler/create-client.client.mjs.map +1 -1
  77. package/dist/modules/auth/handler/proxy.server.mjs +2 -2
  78. package/dist/modules/auth/handler/proxy.server.mjs.map +1 -1
  79. package/dist/modules/auth/handler/route.server.d.mts +2 -2
  80. package/dist/modules/auth/handler/route.server.d.mts.map +1 -1
  81. package/dist/modules/auth/handler/route.server.mjs.map +1 -1
  82. package/dist/modules/auth/handler/{init.d.mts → service.server.d.mts} +322 -90
  83. package/dist/modules/auth/handler/service.server.d.mts.map +1 -0
  84. package/dist/modules/auth/handler/{init.mjs → service.server.mjs} +19 -8
  85. package/dist/modules/auth/handler/service.server.mjs.map +1 -0
  86. package/dist/modules/auth/hooks/use-session.d.mts +9 -4
  87. package/dist/modules/auth/hooks/use-session.d.mts.map +1 -1
  88. package/dist/modules/auth/lib/helpers.server.d.mts +1 -1
  89. package/dist/modules/auth/lib/permissions.d.mts +1 -1
  90. package/dist/modules/auth/lib/validators.mjs +1 -1
  91. package/dist/modules/config/lib/context.d.mts +9 -10
  92. package/dist/modules/config/lib/context.d.mts.map +1 -1
  93. package/dist/modules/config/lib/context.mjs.map +1 -1
  94. package/dist/modules/data-tables/lib/converters/search.d.mts +1 -1
  95. package/dist/modules/data-tables/lib/converters/sorting.d.mts +1 -1
  96. package/dist/modules/data-tables/server/get-data.server.d.mts +3 -3
  97. package/dist/modules/data-tables/server/get-data.server.mjs +1 -1
  98. package/dist/modules/data-tables/server/get-data.server.mjs.map +1 -1
  99. package/dist/modules/data-tables/strategies/infinite/strategy.d.mts +1 -1
  100. package/dist/modules/data-tables/strategies/infinite/strategy.mjs +3 -0
  101. package/dist/modules/data-tables/strategies/infinite/strategy.mjs.map +1 -1
  102. package/dist/modules/data-tables/tables/data-table/components/row.mjs +5 -15
  103. package/dist/modules/data-tables/tables/data-table/components/row.mjs.map +1 -1
  104. package/dist/modules/data-tables/tables/inline-table/components/body.mjs +1 -1
  105. package/dist/modules/data-tables/tables/inline-table/components/body.mjs.map +1 -1
  106. package/dist/modules/data-tables/tables/inline-table/components/row.client.mjs +13 -23
  107. package/dist/modules/data-tables/tables/inline-table/components/row.client.mjs.map +1 -1
  108. package/dist/modules/data-tables/tables/inline-table/components/table.d.mts +1 -0
  109. package/dist/modules/data-tables/tables/inline-table/components/table.d.mts.map +1 -1
  110. package/dist/modules/data-tables/tables/inline-table/components/table.mjs +2 -1
  111. package/dist/modules/data-tables/tables/inline-table/components/table.mjs.map +1 -1
  112. package/dist/modules/data-tables/tables/inline-table/hooks/context.client.d.mts +5 -1
  113. package/dist/modules/data-tables/tables/inline-table/hooks/context.client.d.mts.map +1 -1
  114. package/dist/modules/data-tables/tables/inline-table/hooks/context.client.mjs +2 -1
  115. package/dist/modules/data-tables/tables/inline-table/hooks/context.client.mjs.map +1 -1
  116. package/dist/modules/data-tables/tables/inline-table/hooks/use-hotkeys.client.d.mts +30 -0
  117. package/dist/modules/data-tables/tables/inline-table/hooks/use-hotkeys.client.d.mts.map +1 -0
  118. package/dist/modules/data-tables/tables/inline-table/hooks/use-hotkeys.client.mjs +77 -9
  119. package/dist/modules/data-tables/tables/inline-table/hooks/use-hotkeys.client.mjs.map +1 -1
  120. package/dist/modules/{config/db → database/lib}/helpers.d.mts +2 -2
  121. package/dist/modules/database/lib/helpers.d.mts.map +1 -0
  122. package/dist/modules/{config/db → database/lib}/helpers.mjs +1 -1
  123. package/dist/modules/database/lib/helpers.mjs.map +1 -0
  124. package/dist/modules/database/lib/service.server.d.mts +34 -0
  125. package/dist/modules/database/lib/service.server.d.mts.map +1 -0
  126. package/dist/modules/database/lib/service.server.mjs +24 -0
  127. package/dist/modules/database/lib/service.server.mjs.map +1 -0
  128. package/dist/modules/{config/db → database/lib}/types.d.mts +1 -1
  129. package/dist/modules/database/lib/types.d.mts.map +1 -0
  130. package/dist/modules/emails/lib/service.server.d.mts +29 -0
  131. package/dist/modules/emails/lib/service.server.d.mts.map +1 -0
  132. package/dist/modules/emails/lib/service.server.mjs +21 -0
  133. package/dist/modules/emails/lib/service.server.mjs.map +1 -0
  134. package/dist/modules/inline-edit/components/date-input.client.mjs +1 -1
  135. package/dist/modules/inline-edit/components/date-input.client.mjs.map +1 -1
  136. package/dist/modules/inline-edit/components/date-picker.client.mjs +1 -0
  137. package/dist/modules/inline-edit/components/date-picker.client.mjs.map +1 -1
  138. package/dist/modules/inline-edit/components/date-time.client.mjs +1 -0
  139. package/dist/modules/inline-edit/components/date-time.client.mjs.map +1 -1
  140. package/dist/modules/inline-edit/components/editor.client.mjs +1 -0
  141. package/dist/modules/inline-edit/components/editor.client.mjs.map +1 -1
  142. package/dist/modules/inline-edit/components/input-recipient.client.mjs +1 -0
  143. package/dist/modules/inline-edit/components/input-recipient.client.mjs.map +1 -1
  144. package/dist/modules/inline-edit/components/input-toggle.client.mjs +1 -0
  145. package/dist/modules/inline-edit/components/input-toggle.client.mjs.map +1 -1
  146. package/dist/modules/inline-edit/components/input.client.d.mts.map +1 -1
  147. package/dist/modules/inline-edit/components/input.client.mjs +3 -0
  148. package/dist/modules/inline-edit/components/input.client.mjs.map +1 -1
  149. package/dist/modules/inline-edit/components/select.client.d.mts.map +1 -1
  150. package/dist/modules/inline-edit/components/select.client.mjs +1 -0
  151. package/dist/modules/inline-edit/components/select.client.mjs.map +1 -1
  152. package/dist/modules/inline-edit/components/switch.client.mjs +1 -0
  153. package/dist/modules/inline-edit/components/switch.client.mjs.map +1 -1
  154. package/dist/modules/inline-edit/components/toggle.client.mjs +1 -0
  155. package/dist/modules/inline-edit/components/toggle.client.mjs.map +1 -1
  156. package/dist/modules/inline-edit/lib/variants.d.mts +1 -1
  157. package/dist/modules/router/handler/context.server.d.mts +12 -10
  158. package/dist/modules/router/handler/context.server.d.mts.map +1 -1
  159. package/dist/modules/router/handler/init.server.d.mts +13 -11
  160. package/dist/modules/router/handler/init.server.d.mts.map +1 -1
  161. package/dist/modules/router/handler/init.server.mjs +2 -2
  162. package/dist/modules/router/handler/init.server.mjs.map +1 -1
  163. package/dist/modules/router/handler/route.server.d.mts +1 -1
  164. package/dist/modules/storage/components/dropzone.client.d.mts +2 -2
  165. package/dist/modules/storage/components/dropzone.client.d.mts.map +1 -1
  166. package/dist/modules/storage/components/dropzone.client.mjs.map +1 -1
  167. package/dist/modules/storage/components/image-grid.client.d.mts +3 -3
  168. package/dist/modules/storage/components/image-grid.client.d.mts.map +1 -1
  169. package/dist/modules/storage/components/image-grid.client.mjs +20 -22
  170. package/dist/modules/storage/components/image-grid.client.mjs.map +1 -1
  171. package/dist/modules/storage/components/image.client.d.mts +8 -0
  172. package/dist/modules/storage/components/image.client.d.mts.map +1 -0
  173. package/dist/modules/storage/components/image.client.mjs +17 -0
  174. package/dist/modules/storage/components/image.client.mjs.map +1 -0
  175. package/dist/modules/storage/components/upload-button.client.d.mts +12 -0
  176. package/dist/modules/storage/components/upload-button.client.d.mts.map +1 -0
  177. package/dist/modules/storage/components/upload-button.client.mjs +34 -0
  178. package/dist/modules/storage/components/upload-button.client.mjs.map +1 -0
  179. package/dist/modules/storage/components/upload-zone-context.client.d.mts +5 -5
  180. package/dist/modules/storage/components/upload-zone-context.client.d.mts.map +1 -1
  181. package/dist/modules/storage/components/upload-zone-context.client.mjs +2 -2
  182. package/dist/modules/storage/components/upload-zone-context.client.mjs.map +1 -1
  183. package/dist/modules/storage/components/upload-zone.client.d.mts +4 -4
  184. package/dist/modules/storage/components/upload-zone.client.d.mts.map +1 -1
  185. package/dist/modules/storage/components/upload-zone.client.mjs +16 -9
  186. package/dist/modules/storage/components/upload-zone.client.mjs.map +1 -1
  187. package/dist/modules/storage/lib/constants.d.mts +1 -5
  188. package/dist/modules/storage/lib/constants.d.mts.map +1 -1
  189. package/dist/modules/storage/lib/constants.mjs +1 -13
  190. package/dist/modules/storage/lib/constants.mjs.map +1 -1
  191. package/dist/modules/storage/lib/helpers.d.mts +14 -28
  192. package/dist/modules/storage/lib/helpers.d.mts.map +1 -1
  193. package/dist/modules/storage/lib/helpers.mjs +17 -75
  194. package/dist/modules/storage/lib/helpers.mjs.map +1 -1
  195. package/dist/modules/storage/lib/procedures.server.d.mts +1991 -0
  196. package/dist/modules/{auth/handler/init.d.mts.map → storage/lib/procedures.server.d.mts.map} +1 -1
  197. package/dist/modules/storage/lib/procedures.server.mjs +22 -0
  198. package/dist/modules/storage/lib/procedures.server.mjs.map +1 -0
  199. package/dist/modules/storage/lib/router-handlers.server.d.mts +41 -0
  200. package/dist/modules/storage/lib/router-handlers.server.d.mts.map +1 -0
  201. package/dist/modules/storage/lib/router-handlers.server.mjs +124 -0
  202. package/dist/modules/storage/lib/router-handlers.server.mjs.map +1 -0
  203. package/dist/modules/storage/lib/schema.d.mts +68 -958
  204. package/dist/modules/storage/lib/schema.d.mts.map +1 -1
  205. package/dist/modules/storage/lib/schema.mjs +28 -65
  206. package/dist/modules/storage/lib/schema.mjs.map +1 -1
  207. package/dist/modules/storage/lib/service.server.d.mts +2155 -141
  208. package/dist/modules/storage/lib/service.server.d.mts.map +1 -1
  209. package/dist/modules/storage/lib/service.server.mjs +453 -242
  210. package/dist/modules/storage/lib/service.server.mjs.map +1 -1
  211. package/dist/modules/storage/lib/upload.client.d.mts +58 -0
  212. package/dist/modules/storage/lib/upload.client.d.mts.map +1 -0
  213. package/dist/modules/storage/lib/upload.client.mjs +87 -0
  214. package/dist/modules/storage/lib/upload.client.mjs.map +1 -0
  215. package/dist/modules/storage/lib/validators.d.mts +297 -835
  216. package/dist/modules/storage/lib/validators.d.mts.map +1 -1
  217. package/dist/modules/storage/lib/validators.mjs +32 -76
  218. package/dist/modules/storage/lib/validators.mjs.map +1 -1
  219. package/dist/modules/storage/providers/adapters/s3.server.d.mts +19 -0
  220. package/dist/modules/storage/providers/adapters/s3.server.d.mts.map +1 -0
  221. package/dist/modules/storage/providers/adapters/s3.server.mjs +173 -0
  222. package/dist/modules/storage/providers/adapters/s3.server.mjs.map +1 -0
  223. package/dist/modules/storage/providers/lib/constants.d.mts +6 -0
  224. package/dist/modules/storage/providers/lib/constants.d.mts.map +1 -0
  225. package/dist/modules/storage/providers/lib/constants.mjs +6 -0
  226. package/dist/modules/storage/providers/lib/constants.mjs.map +1 -0
  227. package/dist/modules/storage/providers/lib/errors.d.mts +12 -0
  228. package/dist/modules/storage/providers/lib/errors.d.mts.map +1 -0
  229. package/dist/modules/storage/providers/lib/errors.mjs +13 -0
  230. package/dist/modules/storage/providers/lib/errors.mjs.map +1 -0
  231. package/dist/modules/storage/providers/lib/types.d.mts +21 -0
  232. package/dist/modules/storage/providers/lib/types.d.mts.map +1 -0
  233. package/dist/modules/storage/providers/lib/validators.d.mts +112 -0
  234. package/dist/modules/storage/providers/lib/validators.d.mts.map +1 -0
  235. package/dist/modules/storage/providers/lib/validators.mjs +75 -0
  236. package/dist/modules/storage/providers/lib/validators.mjs.map +1 -0
  237. package/dist/router/server.d.mts +1 -1
  238. package/dist/storage/client.d.mts +4 -2
  239. package/dist/storage/client.mjs +4 -2
  240. package/dist/storage/server.d.mts +5 -4
  241. package/dist/storage/server.mjs +5 -4
  242. package/dist/storage.d.mts +9 -6
  243. package/dist/storage.mjs +8 -6
  244. package/package.json +18 -5
  245. package/src/components/editor/components/editor.client.tsx +9 -1
  246. package/src/components/editor/extensions/file-handler/extension.ts +4 -4
  247. package/src/components/editor/extensions/file-handler/strategy.ts +15 -40
  248. package/src/components/editor/extensions/file-handler/utils.ts +1 -1
  249. package/src/components/editor/extensions/image/extension.ts +10 -10
  250. package/src/components/editor/lib/helpers.ts +28 -11
  251. package/src/components/ui/combobox-dropdown.client.tsx +1 -0
  252. package/src/components/ui/combobox.client.tsx +1 -1
  253. package/src/entry.ts +12 -51
  254. package/src/lib/entry.ts +1 -5
  255. package/src/lib/utils/markdown.ts +10 -0
  256. package/src/lib/utils/url.ts +2 -1
  257. package/src/lib/utils/user-agent.ts +15 -0
  258. package/src/modules/auth/components/{guard-first-user.server.tsx → create-first-user-guard.server.tsx} +8 -8
  259. package/src/modules/auth/components/guard.server.tsx +1 -1
  260. package/src/modules/auth/entry.server.ts +4 -5
  261. package/src/modules/auth/handler/create-client.client.ts +2 -2
  262. package/src/modules/auth/handler/proxy.server.ts +1 -1
  263. package/src/modules/auth/handler/route.server.ts +2 -2
  264. package/src/modules/auth/handler/{init.ts → service.server.ts} +30 -9
  265. package/src/modules/config/entry.server.ts +0 -9
  266. package/src/modules/config/entry.ts +2 -2
  267. package/src/modules/config/lib/context.ts +9 -9
  268. package/src/modules/data-tables/entry.client.ts +1 -0
  269. package/src/modules/data-tables/server/get-data.server.ts +1 -1
  270. package/src/modules/data-tables/strategies/infinite/strategy.ts +4 -1
  271. package/src/modules/data-tables/tables/data-table/components/row.tsx +12 -21
  272. package/src/modules/data-tables/tables/inline-table/components/body.tsx +1 -1
  273. package/src/modules/data-tables/tables/inline-table/components/row.client.tsx +24 -30
  274. package/src/modules/data-tables/tables/inline-table/components/table.tsx +6 -1
  275. package/src/modules/data-tables/tables/inline-table/hooks/context.client.tsx +5 -0
  276. package/src/modules/data-tables/tables/inline-table/hooks/use-hotkeys.client.ts +119 -91
  277. package/src/modules/database/entry.client.ts +0 -0
  278. package/src/modules/database/entry.server.ts +4 -0
  279. package/src/modules/database/entry.ts +5 -0
  280. package/src/modules/database/lib/service.server.ts +33 -0
  281. package/src/modules/emails/entry.client.ts +0 -0
  282. package/src/modules/emails/entry.server.ts +4 -0
  283. package/src/modules/emails/entry.ts +0 -0
  284. package/src/modules/emails/lib/service.server.ts +29 -0
  285. package/src/modules/inline-edit/components/date-input.client.tsx +1 -1
  286. package/src/modules/inline-edit/components/date-picker.client.tsx +1 -0
  287. package/src/modules/inline-edit/components/date-time.client.tsx +1 -0
  288. package/src/modules/inline-edit/components/editor.client.tsx +3 -0
  289. package/src/modules/inline-edit/components/input-recipient.client.tsx +1 -0
  290. package/src/modules/inline-edit/components/input-toggle.client.tsx +1 -0
  291. package/src/modules/inline-edit/components/input.client.tsx +3 -0
  292. package/src/modules/inline-edit/components/select.client.tsx +5 -1
  293. package/src/modules/inline-edit/components/switch.client.tsx +1 -0
  294. package/src/modules/inline-edit/components/toggle.client.tsx +1 -0
  295. package/src/modules/router/handler/init.server.ts +2 -2
  296. package/src/modules/storage/components/dropzone.client.tsx +1 -1
  297. package/src/modules/storage/components/image-grid.client.tsx +23 -20
  298. package/src/modules/storage/components/image.client.tsx +8 -0
  299. package/src/modules/storage/components/upload-zone-context.client.tsx +11 -8
  300. package/src/modules/storage/components/upload-zone.client.tsx +22 -16
  301. package/src/modules/storage/entry.client.ts +3 -1
  302. package/src/modules/storage/entry.server.ts +9 -3
  303. package/src/modules/storage/entry.ts +13 -1
  304. package/src/modules/storage/lib/constants.ts +0 -11
  305. package/src/modules/storage/lib/helpers.ts +18 -65
  306. package/src/modules/storage/lib/procedures.server.ts +60 -0
  307. package/src/modules/storage/lib/router-handlers.server.ts +178 -0
  308. package/src/modules/storage/lib/schema.ts +26 -97
  309. package/src/modules/storage/lib/service.server.ts +636 -374
  310. package/src/modules/storage/lib/upload.client.ts +156 -0
  311. package/src/modules/storage/lib/validators.ts +50 -111
  312. package/src/modules/storage/providers/adapters/s3.server.ts +281 -0
  313. package/src/modules/storage/providers/lib/constants.ts +3 -0
  314. package/src/modules/storage/providers/lib/errors.ts +21 -0
  315. package/src/modules/storage/providers/lib/types.ts +28 -0
  316. package/src/modules/storage/providers/lib/validators.ts +122 -0
  317. package/dist/lib/config/constants.d.mts +0 -5
  318. package/dist/lib/config/constants.d.mts.map +0 -1
  319. package/dist/lib/config/constants.mjs +0 -6
  320. package/dist/lib/config/constants.mjs.map +0 -1
  321. package/dist/modules/auth/components/guard-first-user.server.d.mts +0 -18
  322. package/dist/modules/auth/components/guard-first-user.server.d.mts.map +0 -1
  323. package/dist/modules/auth/components/guard-first-user.server.mjs +0 -16
  324. package/dist/modules/auth/components/guard-first-user.server.mjs.map +0 -1
  325. package/dist/modules/auth/handler/init.mjs.map +0 -1
  326. package/dist/modules/config/db/helpers.d.mts.map +0 -1
  327. package/dist/modules/config/db/helpers.mjs.map +0 -1
  328. package/dist/modules/config/db/init.d.mts +0 -20
  329. package/dist/modules/config/db/init.d.mts.map +0 -1
  330. package/dist/modules/config/db/init.mjs +0 -15
  331. package/dist/modules/config/db/init.mjs.map +0 -1
  332. package/dist/modules/config/db/types.d.mts.map +0 -1
  333. package/dist/modules/config/providers/email.d.mts +0 -12
  334. package/dist/modules/config/providers/email.d.mts.map +0 -1
  335. package/dist/modules/config/providers/email.mjs +0 -11
  336. package/dist/modules/config/providers/email.mjs.map +0 -1
  337. package/dist/modules/storage/config/filters.d.mts +0 -17
  338. package/dist/modules/storage/config/filters.d.mts.map +0 -1
  339. package/dist/modules/storage/config/filters.mjs +0 -17
  340. package/dist/modules/storage/config/filters.mjs.map +0 -1
  341. package/dist/modules/storage/lib/create-client.server.d.mts +0 -11
  342. package/dist/modules/storage/lib/create-client.server.d.mts.map +0 -1
  343. package/dist/modules/storage/lib/create-client.server.mjs +0 -11
  344. package/dist/modules/storage/lib/create-client.server.mjs.map +0 -1
  345. package/dist/modules/storage/lib/create-upload.client.d.mts +0 -56
  346. package/dist/modules/storage/lib/create-upload.client.d.mts.map +0 -1
  347. package/dist/modules/storage/lib/create-upload.client.mjs +0 -98
  348. package/dist/modules/storage/lib/create-upload.client.mjs.map +0 -1
  349. package/dist/modules/storage/lib/proxy.server.d.mts +0 -21
  350. package/dist/modules/storage/lib/proxy.server.d.mts.map +0 -1
  351. package/dist/modules/storage/lib/proxy.server.mjs +0 -46
  352. package/dist/modules/storage/lib/proxy.server.mjs.map +0 -1
  353. package/dist/modules/storage/lib/router.server.d.mts +0 -31002
  354. package/dist/modules/storage/lib/router.server.d.mts.map +0 -1
  355. package/dist/modules/storage/lib/router.server.mjs +0 -86
  356. package/dist/modules/storage/lib/router.server.mjs.map +0 -1
  357. package/src/lib/config/constants.ts +0 -1
  358. package/src/lib/utils/time-picker.ts +0 -139
  359. package/src/modules/config/db/init.ts +0 -21
  360. package/src/modules/config/providers/email.ts +0 -13
  361. package/src/modules/storage/config/filters.ts +0 -12
  362. package/src/modules/storage/lib/create-client.server.ts +0 -14
  363. package/src/modules/storage/lib/create-upload.client.ts +0 -134
  364. package/src/modules/storage/lib/proxy.server.ts +0 -63
  365. package/src/modules/storage/lib/router.server.ts +0 -182
  366. /package/src/modules/{config/db → database/lib}/helpers.ts +0 -0
  367. /package/src/modules/{config/db → database/lib}/types.ts +0 -0
@@ -1,490 +1,752 @@
1
- import {
2
- DeleteObjectsCommand,
3
- GetObjectCommand,
4
- PutObjectCommand,
5
- S3Client,
6
- type S3ClientConfig,
7
- } from "@aws-sdk/client-s3";
8
- import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
9
- import { addSeconds } from "date-fns";
10
- import { and, asc, eq, inArray, isNotNull, isNull, type SQL } from "drizzle-orm";
11
- import { after } from "next/server";
12
- import { BUCKET_NAME } from "@/lib/config/constants";
13
- import { generateDefaultUUID, type TDatabaseSchema } from "@/modules/config/entry";
14
- import type { DatabaseClient } from "@/modules/config/entry.server";
15
- import {
16
- convertOrderByToQueryParams,
17
- convertSearchToQueryParams,
18
- } from "@/modules/data-tables/entry.server";
19
- import type { BulkActionSchema } from "@/modules/router/entry";
1
+ import { and, desc, eq, inArray, isNotNull, isNull } from "drizzle-orm";
2
+ import z from "zod";
3
+ import { generateDefaultUUID } from "@/modules/database/lib/helpers";
4
+ import type { Database } from "@/modules/database/lib/service.server";
5
+ import type { TDatabaseSchema } from "@/modules/database/lib/types";
20
6
  import { ServerError } from "@/modules/router/lib/error.server";
21
- import { deviceSizes } from "./constants";
22
- import { getDriveBucketKey, inferNodeSubtype, isFile, isFolder } from "./helpers";
23
- import { nodePresignedUrls, nodes, nodeVariants } from "./schema";
7
+ import { StorageAdapterError } from "../providers/lib/errors";
8
+ import type { StorageAdapter } from "../providers/lib/types";
9
+ import type { GetObjectURLOptions } from "../providers/lib/validators";
10
+ import { storageAssets } from "./schema";
24
11
  import {
25
- type CreateFolderNodeSchema,
26
- type FileNode,
27
- type GetFileURLSchema,
28
- type GetNodesByParentIdInput,
29
- type GetObjectInput,
30
- getFileURLSchemaDefaults,
31
- getObjectSchema,
32
- type Node,
33
- type PresignFileSchema,
34
- type PutObjectInput,
35
- putObjectSchema,
36
- type UpdateNodeSchema,
37
- type UploadFileSchema,
12
+ type ConfirmUploadInput,
13
+ confirmUploadInputSchema,
14
+ type PresignUploadInput,
15
+ presignUploadInputSchema,
16
+ type UploadInput,
17
+ uploadInputSchema,
38
18
  } from "./validators";
39
19
 
40
- /**
41
- * Storage Service Config
42
- */
43
- export type StorageServiceConfig<TSchema extends TDatabaseSchema> = {
44
- db: DatabaseClient<TSchema>;
45
- config: S3ClientConfig;
20
+ export type StorageConfig<TSchema extends TDatabaseSchema> = {
21
+ db: Database<TSchema>;
22
+ adapter: StorageAdapter;
23
+ prefix?: string;
46
24
  };
47
25
 
48
26
  /**
49
- * Storage Service
27
+ * Storage service for working with asset metadata and object storage.
28
+ *
29
+ * Use `Storage.init()` to create a fully configured instance in app code.
30
+ *
31
+ * @param props - Storage configuration, including `db` and `adapter`
32
+ * @returns A ready-to-use `Storage` instance
33
+ * @example
34
+ * const storage = Storage.init({
35
+ * db: drizzle(dbConnection),
36
+ * adapter: new StorageS3Adapter({
37
+ * bucketName: "my-app-uploads",
38
+ * region: "us-east-1",
39
+ * credentials: {
40
+ * accessKeyId: process.env.AWS_ACCESS_KEY_ID,
41
+ * secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
42
+ * },
43
+ * }),
44
+ * });
50
45
  */
51
- export class StorageService<TSchema extends TDatabaseSchema> {
52
- /**
53
- * S3 Client
54
- */
55
- #blob: S3Client;
56
- #db: DatabaseClient<TSchema>;
57
46
 
58
- /**
59
- * Constructor
60
- */
61
- constructor({ db, config }: StorageServiceConfig<TSchema>) {
47
+ export class Storage<TSchema extends TDatabaseSchema> {
48
+ #adapter: StorageAdapter;
49
+ #db: Database<TSchema>;
50
+ prefix: string;
51
+
52
+ private constructor({ db, adapter, prefix }: StorageConfig<TSchema>) {
62
53
  this.#db = db;
63
- this.#blob = new S3Client(config);
54
+ this.#adapter = adapter;
55
+ this.prefix = prefix ?? "uploads";
64
56
  }
65
57
 
66
58
  /**
67
- * Get Blob
59
+ * Create a storage service instance.
60
+ *
61
+ * This keeps the public API aligned with other Tulip services such as
62
+ * `Database.init()`, `Email.init()`, and `Auth.init()`.
63
+ *
64
+ * @param props - Storage configuration, including `db` and `adapter`
65
+ * @returns A new `Storage` instance
66
+ * @example
67
+ * const storage = Storage.init({
68
+ * db: drizzle(dbConnection),
69
+ * adapter: new StorageS3Adapter({
70
+ * bucketName: "my-app-uploads",
71
+ * region: "us-east-1",
72
+ * credentials: {
73
+ * accessKeyId: process.env.AWS_ACCESS_KEY_ID,
74
+ * secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
75
+ * },
76
+ * }),
77
+ * });
68
78
  */
69
- blob() {
70
- return this.#blob;
79
+ static init<TSchema extends TDatabaseSchema>(props: StorageConfig<TSchema>) {
80
+ return new Storage(props);
71
81
  }
72
82
 
73
83
  /**
74
- * Create get command
84
+ * Generates the canonical object key for a storage asset id.
85
+ *
86
+ * This keeps bucket key structure internal to the storage service,
87
+ * so callers only work with asset ids and do not manage object paths.
88
+ *
89
+ * @param input - Storage asset id (UUID)
90
+ * @returns Canonical storage key (e.g. `uploads/<id>`)
91
+ * @example
92
+ * const key = this.#generateKey("019d0051-2c0d-741e-9e3c-e5a5bc4d16a2");
93
+ * // key => "uploads/019d0051-2c0d-741e-9e3c-e5a5bc4d16a2"
75
94
  */
76
- #createGetCommand(props: GetObjectInput) {
77
- const input = getObjectSchema.parse(props);
78
-
79
- return new GetObjectCommand({
80
- Bucket: BUCKET_NAME,
81
- Key: getDriveBucketKey(input.id, input.variant),
82
- ResponseContentDisposition: input.disposition,
83
- });
95
+ #generateKey(input: string) {
96
+ const id = z.uuid().parse(input);
97
+ return `${this.prefix}/${id}`;
84
98
  }
85
99
 
86
100
  /**
87
- * Get object
101
+ * Builds a query to fetch a storage asset by its ID and the current adapter's provider.
102
+ * @param id - The asset ID to search for
103
+ * @returns A dynamic Drizzle query for fetching a single asset
104
+ * @example
105
+ * let query = storageService.getAssetByIdQuery("asset-123");
106
+ * query = query.where(eq(storageAssets.contentType, "image/png")); // Add additional conditions if needed
107
+ * const [asset] = await query;
88
108
  */
89
- async getObject(id: string, options: GetFileURLSchema = getFileURLSchemaDefaults) {
90
- const getCommand = this.#createGetCommand({ ...options, id });
91
-
92
- return await this.#blob.send(getCommand);
109
+ getAssetByIdQuery(id: string) {
110
+ return this.#db
111
+ .select()
112
+ .from(storageAssets)
113
+ .where(
114
+ and(
115
+ eq(storageAssets.id, id),
116
+ eq(storageAssets.provider, this.#adapter.key),
117
+ eq(storageAssets.bucket, this.#adapter.bucketName),
118
+ isNull(storageAssets.deletedAt),
119
+ ),
120
+ )
121
+ .limit(1)
122
+ .$dynamic();
93
123
  }
94
124
 
95
125
  /**
96
- * Create put command
126
+ * Fetches a storage asset by its ID.
127
+ * @param id - The asset ID to retrieve
128
+ * @returns The asset object if found, otherwise null
129
+ * @example
130
+ * const asset = await storageService.getAssetById("asset-123");
131
+ * if (asset) {
132
+ * console.log(asset.key, asset.contentType);
133
+ * }
97
134
  */
98
- #createPutCommand(props: PutObjectInput) {
99
- const input = putObjectSchema.parse(props);
100
-
101
- return new PutObjectCommand({
102
- Bucket: BUCKET_NAME,
103
- Key: getDriveBucketKey(input.id, input.variant),
104
- Body: input.body,
105
- ContentType: input.contentType ?? undefined,
106
- ContentLength: input.size ?? undefined,
107
- Metadata: {
108
- nodeId: input.id,
109
- },
110
- });
135
+ async getAssetById(id: string) {
136
+ const parsedId = z.uuid().parse(id);
137
+ const [asset] = await this.getAssetByIdQuery(parsedId);
138
+ return asset ?? null;
111
139
  }
112
140
 
113
141
  /**
114
- * Put object
142
+ * Builds a query to fetch a single ready storage asset by key
143
+ * within the current adapter provider scope.
144
+ *
145
+ * Notes:
146
+ * - Scoped to `this.#adapter.key` to avoid cross-provider leakage.
147
+ * - Targets ready assets by default.
148
+ * - Uniqueness is expected on `(provider, bucket, key)`.
149
+ *
150
+ * @param key - The object key stored in `storage_assets.key`
151
+ * @returns A dynamic Drizzle query returning max 1 row
152
+ * @example
153
+ * let query = storageService.getAssetByKeyQuery("uploads/abc/main");
154
+ * query = query.leftJoin(otherTable, eq(otherTable.assetId, storageAssets.id));
155
+ * const [asset] = await query;
115
156
  */
116
- async #putObject(props: PutObjectInput) {
117
- const putCommand = this.#createPutCommand(props);
118
- return await this.#blob.send(putCommand);
157
+ getAssetByKeyQuery(key: string) {
158
+ return this.#db
159
+ .select()
160
+ .from(storageAssets)
161
+ .where(
162
+ and(
163
+ eq(storageAssets.key, key),
164
+ eq(storageAssets.provider, this.#adapter.key),
165
+ eq(storageAssets.bucket, this.#adapter.bucketName),
166
+ eq(storageAssets.status, "ready"),
167
+ isNull(storageAssets.deletedAt),
168
+ ),
169
+ )
170
+ .limit(1)
171
+ .$dynamic();
119
172
  }
120
173
 
121
174
  /**
122
- * Get node by id
175
+ * Fetches a single ready storage asset by key.
176
+ *
177
+ * This is the convenience wrapper around `getAssetByKeyQuery`.
178
+ * Use this when you only need the result, not query composition.
179
+ *
180
+ * @param key - Asset key to look up
181
+ * @returns The matching asset or `null` if none is found
182
+ * @example
183
+ * const asset = await storageService.getAssetByKey("uploads/abc/main");
184
+ * if (!asset) return;
123
185
  */
124
- async getNodeById(id: string) {
125
- return this.#db.select().from(nodes).where(eq(nodes.id, id));
186
+ async getAssetByKey(key: string) {
187
+ const [result] = await this.getAssetByKeyQuery(key);
188
+ return result ?? null;
126
189
  }
127
190
 
128
191
  /**
129
- * Get nodes by parent id
192
+ * Builds a base query for listing storage assets.
193
+ *
194
+ * Scope and defaults:
195
+ * - Scoped to the current adapter provider (`this.#adapter.key`)
196
+ * - Orders by newest first (`createdAt DESC`)
197
+ *
198
+ * This method returns a dynamic query so callers can extend it with
199
+ * custom filters, joins, pagination, and limits.
200
+ *
201
+ * @returns A dynamic Drizzle query for listing assets
202
+ * @example
203
+ * const query = storageService
204
+ * .listAssetsQuery()
205
+ * .limit(50);
206
+ * const assets = await query;
130
207
  */
131
- async getNodesByParentId({ filters, ...query }: GetNodesByParentIdInput) {
132
- const orderBy = convertOrderByToQueryParams(query, nodes, asc(nodes.createdAt));
133
- const search = convertSearchToQueryParams(query, [nodes.name]);
134
-
208
+ listAssetsQuery() {
135
209
  return this.#db
136
210
  .select()
137
- .from(nodes)
211
+ .from(storageAssets)
138
212
  .where(
139
213
  and(
140
- filters.nodeIds != null ? inArray(nodes.id, filters.nodeIds) : undefined,
141
- filters.types != null ? inArray(nodes.type, filters.types) : undefined,
142
- filters.isDeleted != null ? eq(nodes.isDeleted, filters.isDeleted) : undefined,
143
- filters.isOrphaned === true
144
- ? isNotNull(nodes.orphanedAt)
145
- : filters.isOrphaned === false
146
- ? isNull(nodes.orphanedAt)
147
- : undefined,
148
- filters.hidden != null ? eq(nodes.hidden, filters.hidden) : undefined,
149
- filters.parentId ? eq(nodes.parentId, filters.parentId) : isNull(nodes.parentId),
150
- eq(nodes.namespace, filters.namespace),
151
- search,
214
+ eq(storageAssets.provider, this.#adapter.key),
215
+ eq(storageAssets.bucket, this.#adapter.bucketName),
216
+ isNull(storageAssets.deletedAt),
152
217
  ),
153
218
  )
154
- .orderBy(orderBy as SQL);
219
+ .orderBy(desc(storageAssets.createdAt))
220
+ .$dynamic();
155
221
  }
156
222
 
157
223
  /**
158
- * Get file url
224
+ * Lists storage assets using safe default pagination.
225
+ *
226
+ * This is the convenience wrapper around `listAssetsQuery`.
227
+ * Use `listAssetsQuery()` directly when you need custom query composition.
228
+ *
229
+ * @returns Up to 100 storage assets sorted by newest first
230
+ * @example
231
+ * const assets = await storageService.listAssets();
159
232
  */
160
- async getSignedURL(node: Node, options: GetFileURLSchema = getFileURLSchemaDefaults) {
161
- const [presignedUrl] = await this.#db
162
- .select({ url: nodePresignedUrls.url, expiresAt: nodePresignedUrls.expiresAt })
163
- .from(nodePresignedUrls)
164
- .where(
165
- and(
166
- eq(nodePresignedUrls.nodeId, node.id),
167
- eq(nodePresignedUrls.variant, options.variant),
168
- eq(nodePresignedUrls.disposition, options.disposition),
169
- ),
170
- );
233
+ async listAssets() {
234
+ const assets = await this.listAssetsQuery().limit(100);
235
+ return assets ?? [];
236
+ }
171
237
 
172
- if (presignedUrl && presignedUrl.expiresAt > new Date()) return presignedUrl.url;
238
+ /**
239
+ * Creates a pending storage asset record and generates a presigned upload URL.
240
+ *
241
+ * Flow:
242
+ * - Validates the input payload
243
+ * - Inserts a `pending` asset in the catalog (`storage_assets`)
244
+ * - Requests a presigned PUT URL from the storage adapter
245
+ * - Marks the asset as `error` if URL generation fails
246
+ *
247
+ * This method is intended for direct-to-storage browser uploads.
248
+ * The upload should be finalized later via a confirm step.
249
+ *
250
+ * @param props - Presign upload input (contentType, size, metadata)
251
+ * @returns The created asset id, key, and presigned URL
252
+ * @throws {ServerError} If intent creation or URL generation fails
253
+ * @example
254
+ * const result = await storageService.presignUpload({
255
+ * contentType: "image/png",
256
+ * size: 120_000,
257
+ * metadata: { uploadToken: crypto.randomUUID() },
258
+ * });
259
+ * // result => { id, uploadId, key, presignedUrl }
260
+ */
261
+ async presignUpload(props: PresignUploadInput) {
262
+ const input = presignUploadInputSchema.parse(props);
173
263
 
174
- const expiresIn = 3600 * 24;
264
+ const id = generateDefaultUUID();
265
+ const uploadId = input.uploadId ?? generateDefaultUUID();
266
+ const key = this.#generateKey(id);
175
267
 
176
- // Get the variants
177
- const variants = await this.#db
178
- .select()
179
- .from(nodeVariants)
180
- .where(eq(nodeVariants.nodeId, node.id));
181
-
182
- // If the requested variant does not exist, fallback to main
183
- const variantExists = variants.find((v) => v.variant === options.variant);
184
- const variant = variantExists ? options.variant : "main";
185
-
186
- console.info(
187
- `Generating new signed url for file: ${node.id} with variant: ${variant} and disposition: ${options.disposition}`,
188
- );
189
-
190
- // Generate the get command
191
- const getCommand = this.#createGetCommand({
192
- id: node.id,
193
- variant,
194
- disposition: `${options.disposition}; filename="${node.name}"`,
195
- });
268
+ const { contentType, size, metadata, name, visibility } = input;
269
+
270
+ const [record] = await this.#db
271
+ .insert(storageAssets)
272
+ .values({
273
+ id,
274
+ uploadId,
275
+ key,
276
+ size,
277
+ contentType,
278
+ name,
279
+ visibility,
280
+ provider: this.#adapter.key,
281
+ bucket: this.#adapter.bucketName,
282
+ status: "pending",
283
+ metadata,
284
+ })
285
+ .returning();
286
+
287
+ if (!record) {
288
+ throw new ServerError("INTERNAL_SERVER_ERROR", {
289
+ message: "Failed to create upload intent",
290
+ });
291
+ }
196
292
 
197
- // Generate the presigned url that expires in 24 hours
198
- const url = await getSignedUrl(this.#blob, getCommand, { expiresIn });
293
+ try {
294
+ const presignedUrl = await this.#adapter.putObjectURL({ key, contentType, size, metadata });
199
295
 
200
- // Add the presigned url to the database
201
- after(async () => {
296
+ return { ...record, presignedUrl };
297
+ } catch (error) {
202
298
  await this.#db
203
- .insert(nodePresignedUrls)
204
- .values({
205
- nodeId: node.id,
206
- url,
207
- variant,
208
- disposition: options.disposition,
209
- expiresAt: addSeconds(new Date(), expiresIn),
210
- })
211
- .onConflictDoUpdate({
212
- target: [
213
- nodePresignedUrls.nodeId,
214
- nodePresignedUrls.variant,
215
- nodePresignedUrls.disposition,
216
- ],
217
- set: { url, expiresAt: addSeconds(new Date(), expiresIn) },
218
- });
219
- });
299
+ .update(storageAssets)
300
+ .set({ status: "error" })
301
+ .where(eq(storageAssets.id, record.id));
220
302
 
221
- return url;
303
+ throw this.#parseError(error, {
304
+ fallbackMessage: "Failed to generate upload URL",
305
+ });
306
+ }
222
307
  }
223
308
 
224
309
  /**
225
- * Upload file to S3 and add it to the database
226
- **/
227
- async uploadFile(input: UploadFileSchema & Pick<PutObjectInput, "body">) {
228
- const id = input.id ?? generateDefaultUUID();
229
-
230
- return await this.#db.transaction(async (tx) => {
231
- const [result] = await tx
232
- .insert(nodes)
233
- .values({
234
- id,
235
- type: "file",
236
- name: input.name,
237
- namespace: input.namespace,
238
- parentId: input.parentId,
239
- size: input.size,
240
- contentType: input.contentType,
241
- mode: input.mode,
242
- subtype: inferNodeSubtype(input),
243
- })
244
- .returning();
310
+ * Confirms a direct-to-storage upload by verifying object existence
311
+ * and transitioning the asset from `pending` to `ready`.
312
+ *
313
+ * Flow:
314
+ * - Validates confirm input
315
+ * - Loads the asset record scoped to the current adapter provider
316
+ * - Returns early if asset is already `ready` (idempotent behavior)
317
+ * - Verifies object existence/metadata via `adapter.headObject`
318
+ * - Marks asset as `error` if verification fails
319
+ * - Updates status to `ready` and stores upload metadata
320
+ *
321
+ * @param props - Confirm upload payload containing the asset uploadId
322
+ * @returns The updated storage asset record
323
+ * @throws {ServerError} If the asset is missing, verification fails, or update fails
324
+ * @example
325
+ * const asset = await storageService.confirmUpload("019d0051-2c0d-741e-9e3c-e5a5bc4d16a2");
326
+ * // asset.status === "ready"
327
+ */
328
+ async confirmUpload(props: ConfirmUploadInput) {
329
+ const uploadId = confirmUploadInputSchema.parse(props);
245
330
 
246
- if (!result) {
247
- throw new ServerError("INTERNAL_SERVER_ERROR", {
248
- message: "Oep! Er is iets fout gegaan",
249
- });
250
- }
331
+ const [record] = await this.#db
332
+ .select()
333
+ .from(storageAssets)
334
+ .where(
335
+ and(
336
+ eq(storageAssets.uploadId, uploadId),
337
+ eq(storageAssets.provider, this.#adapter.key),
338
+ eq(storageAssets.bucket, this.#adapter.bucketName),
339
+ isNull(storageAssets.deletedAt),
340
+ ),
341
+ )
342
+ .limit(1);
251
343
 
252
- await this.#putObject({
253
- id,
254
- body: input.body,
255
- variant: "main",
256
- name: input.name,
257
- contentType: input.contentType,
258
- size: input.size,
344
+ if (!record) {
345
+ throw new ServerError("NOT_FOUND", { message: "Storage asset not found" });
346
+ }
347
+
348
+ if (record.status === "ready") return record;
349
+
350
+ let head: Awaited<ReturnType<StorageAdapter["headObject"]>>;
351
+
352
+ try {
353
+ head = await this.#adapter.headObject({ key: record.key });
354
+ } catch (error) {
355
+ await this.#db
356
+ .update(storageAssets)
357
+ .set({ status: "error" })
358
+ .where(eq(storageAssets.id, record.id));
359
+
360
+ throw this.#parseError(error, {
361
+ fallbackMessage: "Failed to verify uploaded object",
259
362
  });
363
+ }
260
364
 
261
- return result;
262
- });
365
+ const [updated] = await this.#db
366
+ .update(storageAssets)
367
+ .set({
368
+ status: "ready",
369
+ uploadedAt: new Date(),
370
+ size: head.size ?? record.size,
371
+ contentType: head.contentType ?? record.contentType,
372
+ })
373
+ .where(and(eq(storageAssets.id, record.id), eq(storageAssets.status, "pending")))
374
+ .returning();
375
+
376
+ if (!updated) {
377
+ throw new ServerError("INTERNAL_SERVER_ERROR", { message: "Failed to confirm upload" });
378
+ }
379
+
380
+ return updated;
263
381
  }
264
382
 
265
383
  /**
266
- * Presign a new upload
384
+ * Uploads an asset directly from the server and persists its catalog record.
385
+ *
386
+ * Flow:
387
+ * - Validates upload input
388
+ * - Creates a `pending` asset record in `storage_assets`
389
+ * - Uploads the object bytes through the storage adapter
390
+ * - Marks the record as `ready` and stores upload metadata
391
+ * - Marks the record as `error` if upload fails
392
+ *
393
+ * This method is intended for server-side uploads (non-presigned flow).
394
+ * For browser direct uploads, use `presignUpload` + `confirmUpload`.
395
+ *
396
+ * @param props - Upload payload (body, contentType, size)
397
+ * @returns The finalized storage asset record
398
+ * @throws {ServerError} If record creation, upload, or finalization fails
399
+ * @example
400
+ * const asset = await storageService.upload({
401
+ * body: fileBuffer,
402
+ * contentType: "application/pdf",
403
+ * size: fileBuffer.byteLength,
404
+ * });
267
405
  */
268
- async presignUpload(input: PresignFileSchema) {
269
- // Generate the put command
270
- const putCommand = this.#createPutCommand({
271
- id: input.id,
272
- name: input.name,
273
- variant: "main",
274
- contentType: input.contentType,
275
- size: input.size,
276
- });
406
+ async upload(props: UploadInput) {
407
+ const { body, contentType, size, metadata, name, visibility } = uploadInputSchema.parse(props);
277
408
 
278
- // Generate the presigned url
279
- const presignedUrl = await getSignedUrl(this.#blob, putCommand, { expiresIn: 3600 });
409
+ const id = generateDefaultUUID();
410
+ const key = this.#generateKey(id);
280
411
 
281
- const [node] = await this.#db
282
- .insert(nodes)
412
+ const [record] = await this.#db
413
+ .insert(storageAssets)
283
414
  .values({
284
- ...input,
285
- subtype: inferNodeSubtype(input),
286
- isPending: true,
287
- type: "file",
288
- id: input.id,
415
+ id,
416
+ key,
417
+ size,
418
+ contentType,
419
+ name,
420
+ visibility,
421
+ provider: this.#adapter.key,
422
+ bucket: this.#adapter.bucketName,
423
+ status: "pending",
424
+ metadata,
289
425
  })
290
426
  .returning();
291
427
 
292
- if (!node) {
428
+ if (!record) {
293
429
  throw new ServerError("INTERNAL_SERVER_ERROR", {
294
- message: "Oep! Er is iets fout gegaan",
430
+ message: "Failed to create upload record",
295
431
  });
296
432
  }
297
433
 
298
- // Return the result
299
- return { id: input.id, presignedUrl, node };
300
- }
434
+ try {
435
+ const uploaded = await this.#adapter.putObject({ key, body, contentType, size, metadata });
301
436
 
302
- /**
303
- * Confirm a new upload
304
- */
305
- async confirmUpload(input: { id: string }) {
306
- const [result] = await this.#db
307
- .update(nodes)
308
- .set({ isPending: false })
309
- .where(eq(nodes.id, input.id))
310
- .returning();
437
+ const [updated] = await this.#db
438
+ .update(storageAssets)
439
+ .set({
440
+ status: "ready",
441
+ uploadedAt: new Date(),
442
+ size: uploaded.size ?? size ?? record.size,
443
+ contentType: uploaded.contentType ?? contentType ?? record.contentType,
444
+ })
445
+ .where(and(eq(storageAssets.id, record.id), eq(storageAssets.status, "pending")))
446
+ .returning();
311
447
 
312
- if (!result) {
313
- throw new ServerError("NOT_FOUND", { message: "File not found" });
314
- }
448
+ if (!updated) {
449
+ throw new ServerError("INTERNAL_SERVER_ERROR", {
450
+ message: "Failed to finalize upload",
451
+ });
452
+ }
315
453
 
316
- /**
317
- * Generate the preview version of the file
318
- */
319
- // after(async () => {});
320
- await this.generatePreviews(input);
454
+ return updated;
455
+ } catch (error) {
456
+ await this.#db
457
+ .update(storageAssets)
458
+ .set({ status: "error" })
459
+ .where(and(eq(storageAssets.id, record.id), eq(storageAssets.status, "pending")));
321
460
 
322
- return result as FileNode;
461
+ throw this.#parseError(error, {
462
+ fallbackMessage: "Failed to upload object",
463
+ });
464
+ }
323
465
  }
324
466
 
325
467
  /**
326
- * Generate preview version of the file
468
+ * Retrieves the object content for a ready storage asset.
469
+ *
470
+ * Flow:
471
+ * - Looks up the asset record by id, scoped to the current adapter provider
472
+ * - Ensures the asset is in `ready` status
473
+ * - Fetches object content from the storage adapter using the stored key
474
+ *
475
+ * @param id - The storage asset id
476
+ * @returns The adapter object response (stream/body + metadata)
477
+ * @throws {ServerError} If the asset does not exist or is not ready
478
+ * @example
479
+ * const object = await storageService.getObject("asset-123");
480
+ * // object.body can be streamed/consumed by caller
327
481
  */
328
- async generatePreviews(input: { id: string }) {
329
- /**
330
- * Get the main version of the file
331
- */
332
- const getCommand = this.#createGetCommand({ id: input.id, variant: "main" });
333
-
334
- const response = await this.#blob.send(getCommand);
335
- const contentType = response.ContentType;
336
- if (!response.Body) {
337
- throw new ServerError("INTERNAL_SERVER_ERROR", {
338
- message: "Oep! Er is iets fout gegaan",
339
- });
340
- }
482
+ async getObject(input: string) {
483
+ const id = z.uuid().parse(input);
341
484
 
342
- /**
343
- * Transform the main version of the file to a buffer
344
- */
345
- const byteArray = await response.Body.transformToByteArray();
346
- const buffer = Buffer.from(byteArray);
347
-
348
- /**
349
- * Generate the preview versions for images
350
- */
351
- if (contentType?.startsWith("image/")) {
352
- const sharp = await import("sharp");
353
-
354
- // Generate the preview versions
355
- await Promise.allSettled(
356
- deviceSizes.flatMap(async (width) => {
357
- // Generate the preview
358
- const preview = await sharp.default(buffer).resize({ width }).webp().toBuffer();
359
-
360
- // Upload the preview and add the variant to the database
361
- return this.#db.transaction(async (tx) => {
362
- await this.#putObject({
363
- id: input.id,
364
- body: preview,
365
- variant: `preview-${width}`,
366
- contentType: "image/webp",
367
- size: preview.byteLength,
368
- });
369
-
370
- await tx.insert(nodeVariants).values({
371
- nodeId: input.id,
372
- variant: `preview-${width}`,
373
- width,
374
- });
375
- });
376
- }),
377
- );
485
+ const [record] = await this.#db
486
+ .select()
487
+ .from(storageAssets)
488
+ .where(
489
+ and(
490
+ eq(storageAssets.id, id),
491
+ eq(storageAssets.provider, this.#adapter.key),
492
+ eq(storageAssets.bucket, this.#adapter.bucketName),
493
+ eq(storageAssets.status, "ready"),
494
+ isNull(storageAssets.deletedAt),
495
+ ),
496
+ )
497
+ .limit(1);
498
+
499
+ if (!record) {
500
+ throw new ServerError("NOT_FOUND", { message: "Storage asset not found" });
378
501
  }
502
+
503
+ return this.#adapter.getObject(record.key).catch((error) => {
504
+ throw this.#parseError(error, {
505
+ fallbackMessage: "Failed to retrieve object",
506
+ });
507
+ });
379
508
  }
380
509
 
381
510
  /**
382
- * Create a new folder
511
+ * Generates a presigned read URL for a ready storage asset.
512
+ *
513
+ * Flow:
514
+ * - Validates the asset id
515
+ * - Resolves the asset record scoped to the current adapter provider
516
+ * - Ensures the asset is in `ready` status
517
+ * - Delegates URL signing to the storage adapter using the stored key
518
+ *
519
+ * @param input - Storage asset id (UUID)
520
+ * @param options - Optional URL options (for example expiration/disposition)
521
+ * @returns A presigned URL for reading the object
522
+ * @throws {ServerError} If the asset does not exist or is not ready
523
+ * @example
524
+ * const url = await storageService.getObjectURL("019d0051-2c0d-741e-9e3c-e5a5bc4d16a2", {
525
+ * expiresIn: 3600,
526
+ * });
383
527
  */
384
- async createFolder(input: CreateFolderNodeSchema) {
385
- const [parent] = input.parentId
386
- ? await this.#db.select().from(nodes).where(eq(nodes.id, input.parentId))
387
- : [];
388
-
389
- /**
390
- * Validate
391
- */
392
- if (input.parentId && !parent) {
393
- throw new ServerError("BAD_REQUEST", { message: "Parent not found" });
394
- }
395
528
 
396
- if (parent && !isFolder(parent)) {
397
- throw new ServerError("BAD_REQUEST", { message: "Parent is not a folder" });
529
+ async getObjectURL(input: string, options?: GetObjectURLOptions) {
530
+ const id = z.uuid().parse(input);
531
+
532
+ const [record] = await this.#db
533
+ .select()
534
+ .from(storageAssets)
535
+ .where(
536
+ and(
537
+ eq(storageAssets.id, id),
538
+ eq(storageAssets.provider, this.#adapter.key),
539
+ eq(storageAssets.bucket, this.#adapter.bucketName),
540
+ eq(storageAssets.status, "ready"),
541
+ isNull(storageAssets.deletedAt),
542
+ ),
543
+ )
544
+ .limit(1);
545
+
546
+ if (!record) {
547
+ throw new ServerError("NOT_FOUND", { message: "Storage asset not found" });
398
548
  }
399
549
 
400
- if (parent && parent.namespace !== input.namespace) {
401
- throw new ServerError("BAD_REQUEST", {
402
- message: "Parent is not in the same namespace",
550
+ return this.#adapter.getObjectURL(record.key, options).catch((error) => {
551
+ throw this.#parseError(error, {
552
+ fallbackMessage: "Failed to generate object URL",
403
553
  });
404
- }
554
+ });
555
+ }
405
556
 
406
- /**
407
- * Create the folder
408
- */
409
- const [result] = await this.#db
410
- .insert(nodes)
411
- .values({ ...input, type: "folder" })
412
- .returning();
557
+ /**
558
+ * Soft deletes a single storage asset by id.
559
+ *
560
+ * This is a convenience wrapper around `deleteAssets`.
561
+ *
562
+ * @param input - Storage asset id (UUID)
563
+ * @returns The soft-deleted asset record, or null if not found/already deleted
564
+ */
565
+ async deleteAsset(input: string) {
566
+ const id = z.uuid().parse(input);
567
+ const [deleted] = await this.deleteAssets([id]);
568
+ return deleted ?? null;
569
+ }
413
570
 
414
- if (!result) {
415
- throw new ServerError("INTERNAL_SERVER_ERROR", {
416
- message: "Folder kon niet worden aangemaakt",
417
- });
418
- }
571
+ /**
572
+ * Soft deletes multiple storage assets by setting `deletedAt`.
573
+ *
574
+ * Flow:
575
+ * - Validates and de-duplicates ids
576
+ * - Resolves provider-scoped active records
577
+ * - Marks matching rows as deleted by setting `deletedAt`
578
+ *
579
+ * @param input - Storage asset ids (UUID[])
580
+ * @returns Soft-deleted asset records
581
+ */
582
+ async deleteAssets(input: string[]) {
583
+ const ids = [...new Set(z.array(z.uuid()).parse(input))];
584
+ if (ids.length === 0) return [];
419
585
 
420
- return result;
586
+ return this.#db
587
+ .update(storageAssets)
588
+ .set({ deletedAt: new Date() })
589
+ .where(
590
+ and(
591
+ inArray(storageAssets.id, ids),
592
+ eq(storageAssets.provider, this.#adapter.key),
593
+ eq(storageAssets.bucket, this.#adapter.bucketName),
594
+ isNull(storageAssets.deletedAt),
595
+ ),
596
+ )
597
+ .returning();
421
598
  }
422
599
 
423
600
  /**
424
- * Update a node
601
+ * Restores a single soft-deleted storage asset.
602
+ *
603
+ * This is a convenience wrapper around `restoreAssets`.
604
+ *
605
+ * @param input - Storage asset id (UUID)
606
+ * @returns The restored asset record, or null if not found/not deleted
425
607
  */
426
- async updateNode(input: { id: string; data: UpdateNodeSchema }) {
427
- const [node] = await this.#db
428
- .select({ readonly: nodes.readonly })
429
- .from(nodes)
430
- .where(eq(nodes.id, input.id));
431
-
432
- if (node?.readonly) {
433
- throw new ServerError("BAD_REQUEST", { message: "Node is readonly" });
434
- }
608
+ async restoreAsset(input: string) {
609
+ const id = z.uuid().parse(input);
610
+ const [restored] = await this.restoreAssets([id]);
611
+ return restored ?? null;
612
+ }
435
613
 
436
- const [result] = await this.#db
437
- .update(nodes)
438
- .set(input.data)
439
- .where(eq(nodes.id, input.id))
614
+ /**
615
+ * Restores multiple soft-deleted storage assets by clearing `deletedAt`.
616
+ *
617
+ * @param input - Storage asset ids (UUID[])
618
+ * @returns Restored asset records
619
+ */
620
+ async restoreAssets(input: string[]) {
621
+ const ids = [...new Set(z.array(z.uuid()).parse(input))];
622
+ if (ids.length === 0) return [];
623
+
624
+ return this.#db
625
+ .update(storageAssets)
626
+ .set({ deletedAt: null })
627
+ .where(
628
+ and(
629
+ inArray(storageAssets.id, ids),
630
+ eq(storageAssets.provider, this.#adapter.key),
631
+ eq(storageAssets.bucket, this.#adapter.bucketName),
632
+ isNotNull(storageAssets.deletedAt),
633
+ ),
634
+ )
440
635
  .returning();
636
+ }
441
637
 
442
- if (!result) {
443
- throw new ServerError("INTERNAL_SERVER_ERROR", {
444
- message: "Node kon niet worden gewijzigd",
638
+ /**
639
+ * Hard deletes a single storage asset.
640
+ *
641
+ * This is a convenience wrapper around `purgeAssets`.
642
+ *
643
+ * @param input - Storage asset id (UUID)
644
+ * @returns The purged asset record, or null if not found
645
+ */
646
+ async purgeAsset(input: string) {
647
+ const id = z.uuid().parse(input);
648
+ const [deleted] = await this.purgeAssets([id]);
649
+ return deleted ?? null;
650
+ }
651
+
652
+ /**
653
+ * Hard deletes multiple storage assets.
654
+ *
655
+ * Flow:
656
+ * - Validates and de-duplicates ids
657
+ * - Resolves provider-scoped records
658
+ * - Deletes physical objects from the adapter by key
659
+ * - Hard deletes DB records
660
+ *
661
+ * @param input - Storage asset ids (UUID[])
662
+ * @returns Purged asset records
663
+ * @throws {ServerError} If provider deletion fails
664
+ */
665
+ async purgeAssets(input: string[]) {
666
+ const ids = [...new Set(z.array(z.uuid()).parse(input))];
667
+ if (ids.length === 0) return [];
668
+
669
+ const records = await this.#db
670
+ .select({
671
+ id: storageAssets.id,
672
+ key: storageAssets.key,
673
+ })
674
+ .from(storageAssets)
675
+ .where(
676
+ and(
677
+ inArray(storageAssets.id, ids),
678
+ eq(storageAssets.provider, this.#adapter.key),
679
+ eq(storageAssets.bucket, this.#adapter.bucketName),
680
+ ),
681
+ );
682
+
683
+ if (records.length === 0) return [];
684
+
685
+ const keys = records.map((r) => r.key);
686
+
687
+ try {
688
+ await this.#adapter.deleteObjects(keys);
689
+ } catch (error) {
690
+ throw this.#parseError(error, {
691
+ fallbackMessage: "Failed to delete storage objects",
445
692
  });
446
693
  }
447
694
 
448
- return result;
695
+ const deletedIds = records.map((r) => r.id);
696
+
697
+ const deleted = await this.#db
698
+ .delete(storageAssets)
699
+ .where(
700
+ and(
701
+ inArray(storageAssets.id, deletedIds),
702
+ eq(storageAssets.provider, this.#adapter.key),
703
+ eq(storageAssets.bucket, this.#adapter.bucketName),
704
+ ),
705
+ )
706
+ .returning();
707
+
708
+ return deleted;
449
709
  }
450
710
 
451
711
  /**
452
- * Delete nodes
712
+ * Normalizes unknown adapter/service errors into a consistent `ServerError`.
713
+ *
714
+ * Behavior:
715
+ * - Returns existing `ServerError` instances unchanged
716
+ * - Maps known storage adapter errors to application-level server errors
717
+ * - Falls back to a generic internal server error for unknown failures
718
+ *
719
+ * This keeps adapter-specific errors inside the storage layer while exposing
720
+ * a stable error contract to route handlers and RPC procedures.
721
+ *
722
+ * @param error - The unknown error to normalize
723
+ * @param options - Optional fallback message for non-specific failures
724
+ * @returns A normalized `ServerError`
453
725
  */
454
- async deleteNodes(input: BulkActionSchema) {
455
- const items = await this.#db
456
- .select({ id: nodes.id, type: nodes.type, readonly: nodes.readonly })
457
- .from(nodes)
458
- .where(inArray(nodes.id, input.ids));
459
-
460
- // Check if the nodes are readonly
461
- if (items.some((item) => item.readonly)) {
462
- throw new ServerError("BAD_REQUEST", { message: "Nodes are readonly" });
726
+
727
+ #parseError(error: unknown, options?: { fallbackMessage: string }) {
728
+ // If it's already a ServerError, return as-is to avoid double-wrapping
729
+ if (error instanceof ServerError) return error;
730
+
731
+ // Handle known storage adapter errors and convert to appropriate ServerError
732
+ if (error instanceof StorageAdapterError) {
733
+ if (error.code === "OBJECT_NOT_FOUND") {
734
+ return new ServerError("NOT_FOUND", {
735
+ message: "Storage asset not found",
736
+ cause: error,
737
+ });
738
+ }
739
+
740
+ return new ServerError("INTERNAL_SERVER_ERROR", {
741
+ message: options?.fallbackMessage ?? "Storage adapter error",
742
+ cause: error,
743
+ });
463
744
  }
464
745
 
465
- // Split the nodes into folders and files
466
- const folders = items.filter(isFolder).map((folder) => folder.id);
467
- const files = items.filter(isFile).map((file) => file.id);
468
-
469
- // Delete command for S3
470
- const deleteCommand =
471
- files.length > 0
472
- ? new DeleteObjectsCommand({
473
- Bucket: BUCKET_NAME,
474
- Delete: {
475
- Objects: files.map((id) => ({ Key: id })),
476
- },
477
- })
478
- : undefined;
479
-
480
- /**
481
- * Delete files and folders in a transaction
482
- */
483
- await this.#db.transaction(async (tx) => {
484
- await tx.delete(nodes).where(inArray(nodes.id, folders));
485
- await tx.delete(nodes).where(inArray(nodes.id, files));
486
-
487
- if (deleteCommand) await this.#blob.send(deleteCommand);
746
+ // For unknown errors, return a generic server error with limited information to avoid leaking details
747
+ return new ServerError("INTERNAL_SERVER_ERROR", {
748
+ message: options?.fallbackMessage ?? "Unknown storage error",
749
+ cause: error instanceof Error ? error : undefined,
488
750
  });
489
751
  }
490
752
  }