insforge 0.3.1

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 (395) hide show
  1. package/.dockerignore +58 -0
  2. package/.env.example +49 -0
  3. package/.github/ISSUE_TEMPLATE/bug_report.yml +83 -0
  4. package/.github/ISSUE_TEMPLATE/config.yml +11 -0
  5. package/.github/ISSUE_TEMPLATE/feature_request.yml +79 -0
  6. package/.github/copilot-instructions.md +147 -0
  7. package/.github/workflows/build-image.yml +65 -0
  8. package/.github/workflows/ci-premerge-check.yml +24 -0
  9. package/.github/workflows/deploy-aws.yml +130 -0
  10. package/.github/workflows/lint-and-format.yml +33 -0
  11. package/.prettierignore +65 -0
  12. package/.prettierrc +9 -0
  13. package/CHANGELOG.md +3 -0
  14. package/CONTRIBUTING.md +126 -0
  15. package/Dockerfile +27 -0
  16. package/GITHUB_OAUTH_SETUP.md +49 -0
  17. package/GOOGLE_OAUTH_SETUP.md +148 -0
  18. package/LICENSE +201 -0
  19. package/README.md +134 -0
  20. package/assets/Dark.svg +23 -0
  21. package/assets/archDiagram.png +0 -0
  22. package/assets/banner.png +0 -0
  23. package/assets/mcpInstallv2.png +0 -0
  24. package/assets/sampleResponse.png +0 -0
  25. package/assets/signin.png +0 -0
  26. package/assets/userflow.png +0 -0
  27. package/backend/migrations/000_create-base-tables.sql +142 -0
  28. package/backend/migrations/001_create-helper-functions.sql +41 -0
  29. package/backend/migrations/002_rename-auth-tables.sql +30 -0
  30. package/backend/migrations/003_create-users-table.sql +56 -0
  31. package/backend/migrations/004_add-reload-postgrest-func.sql +24 -0
  32. package/backend/migrations/005_enable-project-admin-modify-users.sql +30 -0
  33. package/backend/migrations/006_modify-ai-usage-table.sql +25 -0
  34. package/backend/migrations/007_drop-metadata-table.sql +2 -0
  35. package/backend/migrations/008_add-system-tables.sql +77 -0
  36. package/backend/migrations/009_add-function-secrets.sql +24 -0
  37. package/backend/migrations/010_modify-ai-config-modalities.sql +93 -0
  38. package/backend/migrations/011_refactor-secrets-table.sql +15 -0
  39. package/backend/migrations/012_add-storage-uploaded-by.sql +8 -0
  40. package/backend/package.json +75 -0
  41. package/backend/src/api/middleware/auth.ts +240 -0
  42. package/backend/src/api/middleware/error.ts +231 -0
  43. package/backend/src/api/middleware/upload.ts +59 -0
  44. package/backend/src/api/routes/agent.ts +29 -0
  45. package/backend/src/api/routes/ai.ts +472 -0
  46. package/backend/src/api/routes/auth.oauth.ts +482 -0
  47. package/backend/src/api/routes/auth.ts +386 -0
  48. package/backend/src/api/routes/database.advance.ts +275 -0
  49. package/backend/src/api/routes/database.records.ts +246 -0
  50. package/backend/src/api/routes/database.tables.ts +161 -0
  51. package/backend/src/api/routes/docs.ts +66 -0
  52. package/backend/src/api/routes/functions.ts +183 -0
  53. package/backend/src/api/routes/logs.ts +150 -0
  54. package/backend/src/api/routes/metadata.ts +160 -0
  55. package/backend/src/api/routes/openapi.ts +82 -0
  56. package/backend/src/api/routes/secrets.ts +199 -0
  57. package/backend/src/api/routes/storage.ts +547 -0
  58. package/backend/src/api/routes/usage.ts +96 -0
  59. package/backend/src/core/ai/chat.ts +207 -0
  60. package/backend/src/core/ai/client.ts +242 -0
  61. package/backend/src/core/ai/config.ts +187 -0
  62. package/backend/src/core/ai/image.ts +156 -0
  63. package/backend/src/core/ai/model.ts +117 -0
  64. package/backend/src/core/ai/usage.ts +290 -0
  65. package/backend/src/core/auth/auth.ts +781 -0
  66. package/backend/src/core/auth/oauth.ts +398 -0
  67. package/backend/src/core/database/advance.ts +1074 -0
  68. package/backend/src/core/database/manager.ts +178 -0
  69. package/backend/src/core/database/table.ts +772 -0
  70. package/backend/src/core/documentation/agent.ts +689 -0
  71. package/backend/src/core/documentation/openapi.ts +856 -0
  72. package/backend/src/core/functions/functions.ts +310 -0
  73. package/backend/src/core/logs/analytics.ts +76 -0
  74. package/backend/src/core/logs/audit.ts +255 -0
  75. package/backend/src/core/logs/providers/base.provider.ts +83 -0
  76. package/backend/src/core/logs/providers/cloudwatch.provider.ts +510 -0
  77. package/backend/src/core/logs/providers/localdb.provider.ts +246 -0
  78. package/backend/src/core/secrets/encryption.ts +58 -0
  79. package/backend/src/core/secrets/secrets.ts +410 -0
  80. package/backend/src/core/socket/socket.ts +388 -0
  81. package/backend/src/core/socket/types.ts +79 -0
  82. package/backend/src/core/storage/storage.ts +923 -0
  83. package/backend/src/server.ts +288 -0
  84. package/backend/src/types/ai.ts +46 -0
  85. package/backend/src/types/auth.ts +90 -0
  86. package/backend/src/types/database.ts +136 -0
  87. package/backend/src/types/error-constants.ts +86 -0
  88. package/backend/src/types/logs.ts +47 -0
  89. package/backend/src/types/profile.ts +55 -0
  90. package/backend/src/types/storage.ts +23 -0
  91. package/backend/src/utils/cloud-token.ts +39 -0
  92. package/backend/src/utils/constants.ts +1 -0
  93. package/backend/src/utils/environment.ts +35 -0
  94. package/backend/src/utils/helpers.ts +49 -0
  95. package/backend/src/utils/logger.ts +13 -0
  96. package/backend/src/utils/response.ts +62 -0
  97. package/backend/src/utils/seed.ts +205 -0
  98. package/backend/src/utils/sql-parser.ts +63 -0
  99. package/backend/src/utils/uuid.ts +9 -0
  100. package/backend/src/utils/validations.ts +129 -0
  101. package/backend/tests/README.md +134 -0
  102. package/backend/tests/cleanup-all-test-data.sh +231 -0
  103. package/backend/tests/cloud/test-s3-multitenant.sh +132 -0
  104. package/backend/tests/local/comprehensive-curl-tests.sh +156 -0
  105. package/backend/tests/local/test-auth-router.sh +144 -0
  106. package/backend/tests/local/test-database-router.sh +222 -0
  107. package/backend/tests/local/test-e2e.sh +241 -0
  108. package/backend/tests/local/test-fk-errors.sh +97 -0
  109. package/backend/tests/local/test-id-field.sh +201 -0
  110. package/backend/tests/local/test-public-bucket.sh +265 -0
  111. package/backend/tests/local/test-secrets.sh +248 -0
  112. package/backend/tests/local/test-serverless-functions.sh.disabled +325 -0
  113. package/backend/tests/local/test-traditional-rest.sh +209 -0
  114. package/backend/tests/manual/README.md +51 -0
  115. package/backend/tests/manual/create-large-table-simple.sql +11 -0
  116. package/backend/tests/manual/seed-large-table.sql +101 -0
  117. package/backend/tests/manual/setup-large-table-extras.sql +34 -0
  118. package/backend/tests/manual/test-better-auth.sh +303 -0
  119. package/backend/tests/manual/test-bulk-upsert.sh +410 -0
  120. package/backend/tests/manual/test-database-advance.sh +297 -0
  121. package/backend/tests/manual/test-postgrest-stability.sh +192 -0
  122. package/backend/tests/manual/test-rawsql-export-import.sh +412 -0
  123. package/backend/tests/manual/test-universal-storage.sh +264 -0
  124. package/backend/tests/manual/test-users.sql +18 -0
  125. package/backend/tests/run-all-tests.sh +140 -0
  126. package/backend/tests/setup.ts +22 -0
  127. package/backend/tests/test-config.sh +303 -0
  128. package/backend/tsconfig.json +23 -0
  129. package/backend/tsup.config.ts +18 -0
  130. package/backend/vitest.config.ts +22 -0
  131. package/docker-compose.prod.yml +145 -0
  132. package/docker-compose.yml +167 -0
  133. package/docker-init/db/db-init.sql +125 -0
  134. package/docker-init/db/jwt.sql +5 -0
  135. package/docker-init/db/logs.sql +9 -0
  136. package/docker-init/db/postgresql.conf +17 -0
  137. package/docs/deprecated/insforge-auth-api.md +215 -0
  138. package/docs/deprecated/insforge-auth-sdk.md +100 -0
  139. package/docs/deprecated/insforge-db-api.md +359 -0
  140. package/docs/deprecated/insforge-db-sdk.md +140 -0
  141. package/docs/deprecated/insforge-debug-sdk.md +157 -0
  142. package/docs/deprecated/insforge-debug.md +65 -0
  143. package/docs/deprecated/insforge-instructions.md +124 -0
  144. package/docs/deprecated/insforge-project.md +118 -0
  145. package/docs/deprecated/insforge-storage-api.md +279 -0
  146. package/docs/deprecated/insforge-storage-sdk.md +159 -0
  147. package/docs/insforge-instructions-sdk.md +407 -0
  148. package/eslint.config.js +317 -0
  149. package/examples/oauth/frontend-oauth-example.html +251 -0
  150. package/examples/response-examples.md +444 -0
  151. package/frontend/README.md +112 -0
  152. package/frontend/components.json +17 -0
  153. package/frontend/index.html +13 -0
  154. package/frontend/package.json +63 -0
  155. package/frontend/public/favicon.ico +0 -0
  156. package/frontend/src/App.tsx +106 -0
  157. package/frontend/src/assets/icons/checkbox_checked.svg +6 -0
  158. package/frontend/src/assets/icons/checkbox_undetermined.svg +6 -0
  159. package/frontend/src/assets/icons/checked.svg +3 -0
  160. package/frontend/src/assets/icons/error.svg +3 -0
  161. package/frontend/src/assets/icons/pencil.svg +4 -0
  162. package/frontend/src/assets/icons/refresh.svg +4 -0
  163. package/frontend/src/assets/icons/step_active.svg +3 -0
  164. package/frontend/src/assets/icons/step_inactive.svg +11 -0
  165. package/frontend/src/assets/icons/warning.svg +3 -0
  166. package/frontend/src/assets/logos/amazon.svg +1 -0
  167. package/frontend/src/assets/logos/claude_code.svg +3 -0
  168. package/frontend/src/assets/logos/cline.svg +6 -0
  169. package/frontend/src/assets/logos/cursor.svg +20 -0
  170. package/frontend/src/assets/logos/discord.svg +9 -0
  171. package/frontend/src/assets/logos/gemini.svg +19 -0
  172. package/frontend/src/assets/logos/github.svg +5 -0
  173. package/frontend/src/assets/logos/google.svg +13 -0
  174. package/frontend/src/assets/logos/grok.svg +10 -0
  175. package/frontend/src/assets/logos/insforge_dark.svg +15 -0
  176. package/frontend/src/assets/logos/insforge_light.svg +15 -0
  177. package/frontend/src/assets/logos/openai.svg +10 -0
  178. package/frontend/src/assets/logos/roo_code.svg +9 -0
  179. package/frontend/src/assets/logos/trae.svg +3 -0
  180. package/frontend/src/assets/logos/windsurf.svg +10 -0
  181. package/frontend/src/components/ButtonWithLoading.tsx +27 -0
  182. package/frontend/src/components/Checkbox.tsx +61 -0
  183. package/frontend/src/components/CodeBlock.tsx +32 -0
  184. package/frontend/src/components/ConfirmDialog.tsx +96 -0
  185. package/frontend/src/components/CopyButton.tsx +69 -0
  186. package/frontend/src/components/DeleteActionButton.tsx +42 -0
  187. package/frontend/src/components/EmptyState.tsx +41 -0
  188. package/frontend/src/components/ErrorState.tsx +35 -0
  189. package/frontend/src/components/FeatureSidebar.tsx +126 -0
  190. package/frontend/src/components/FeatureSidebarItem.tsx +101 -0
  191. package/frontend/src/components/JsonHighlight.tsx +61 -0
  192. package/frontend/src/components/LoadingState.tsx +16 -0
  193. package/frontend/src/components/PaginationControls.tsx +54 -0
  194. package/frontend/src/components/PromptDialog.tsx +68 -0
  195. package/frontend/src/components/SearchInput.tsx +90 -0
  196. package/frontend/src/components/SelectionClearButton.tsx +26 -0
  197. package/frontend/src/components/Stepper.tsx +139 -0
  198. package/frontend/src/components/ThemeToggle.tsx +58 -0
  199. package/frontend/src/components/TypeBadge.tsx +20 -0
  200. package/frontend/src/components/datagrid/DataGrid.tsx +264 -0
  201. package/frontend/src/components/datagrid/DefaultCellRenderer.tsx +114 -0
  202. package/frontend/src/components/datagrid/IdCell.tsx +44 -0
  203. package/frontend/src/components/datagrid/SortableHeader.tsx +74 -0
  204. package/frontend/src/components/datagrid/cell-editors/BooleanCellEditor.tsx +54 -0
  205. package/frontend/src/components/datagrid/cell-editors/DateCellEditor.tsx +483 -0
  206. package/frontend/src/components/datagrid/cell-editors/JsonCellEditor.tsx +362 -0
  207. package/frontend/src/components/datagrid/cell-editors/TextCellEditor.tsx +38 -0
  208. package/frontend/src/components/datagrid/cell-editors/index.ts +14 -0
  209. package/frontend/src/components/datagrid/cell-editors/types.ts +43 -0
  210. package/frontend/src/components/datagrid/datagridTypes.tsx +72 -0
  211. package/frontend/src/components/datagrid/index.tsx +20 -0
  212. package/frontend/src/components/index.ts +39 -0
  213. package/frontend/src/components/layout/AppHeader.tsx +146 -0
  214. package/frontend/src/components/layout/AppSidebar.tsx +190 -0
  215. package/frontend/src/components/layout/CloudLayout.tsx +95 -0
  216. package/frontend/src/components/layout/Layout.tsx +43 -0
  217. package/frontend/src/components/radix/Alert.tsx +45 -0
  218. package/frontend/src/components/radix/AlertDialog.tsx +115 -0
  219. package/frontend/src/components/radix/Avatar.tsx +45 -0
  220. package/frontend/src/components/radix/Badge.tsx +33 -0
  221. package/frontend/src/components/radix/Button.tsx +50 -0
  222. package/frontend/src/components/radix/Card.tsx +58 -0
  223. package/frontend/src/components/radix/Dialog.tsx +98 -0
  224. package/frontend/src/components/radix/DropdownMenu.tsx +185 -0
  225. package/frontend/src/components/radix/Form.tsx +167 -0
  226. package/frontend/src/components/radix/Input.tsx +22 -0
  227. package/frontend/src/components/radix/Label.tsx +19 -0
  228. package/frontend/src/components/radix/Popover.tsx +29 -0
  229. package/frontend/src/components/radix/ScrollArea.tsx +44 -0
  230. package/frontend/src/components/radix/Select.tsx +151 -0
  231. package/frontend/src/components/radix/Separator.tsx +26 -0
  232. package/frontend/src/components/radix/Sheet.tsx +119 -0
  233. package/frontend/src/components/radix/Skeleton.tsx +7 -0
  234. package/frontend/src/components/radix/Switch.tsx +29 -0
  235. package/frontend/src/components/radix/Tabs.tsx +50 -0
  236. package/frontend/src/components/radix/Textarea.tsx +21 -0
  237. package/frontend/src/components/radix/Tooltip.tsx +28 -0
  238. package/frontend/src/features/ai/components/AIConfigCard.tsx +154 -0
  239. package/frontend/src/features/ai/components/AIConfigDialog.tsx +76 -0
  240. package/frontend/src/features/ai/components/AIConfigForm.tsx +222 -0
  241. package/frontend/src/features/ai/components/AIEmptyState.tsx +18 -0
  242. package/frontend/src/features/ai/components/fields/ModalityField.tsx +87 -0
  243. package/frontend/src/features/ai/components/fields/ModelSelectionField.tsx +134 -0
  244. package/frontend/src/features/ai/components/fields/SystemPromptField.tsx +33 -0
  245. package/frontend/src/features/ai/helpers.ts +155 -0
  246. package/frontend/src/features/ai/hooks/useAIConfigs.ts +221 -0
  247. package/frontend/src/features/ai/hooks/useAIUsage.ts +77 -0
  248. package/frontend/src/features/ai/page/AIPage.tsx +178 -0
  249. package/frontend/src/features/ai/services/ai.service.ts +148 -0
  250. package/frontend/src/features/auth/components/AddOAuthDialog.tsx +106 -0
  251. package/frontend/src/features/auth/components/AuthMethodTab.tsx +238 -0
  252. package/frontend/src/features/auth/components/OAuthConfigDialog.tsx +303 -0
  253. package/frontend/src/features/auth/components/OAuthEmptyState.tsx +15 -0
  254. package/frontend/src/features/auth/components/UserFormDialog.tsx +248 -0
  255. package/frontend/src/features/auth/components/UsersDataGrid.tsx +183 -0
  256. package/frontend/src/features/auth/components/UsersTab.tsx +114 -0
  257. package/frontend/src/features/auth/hooks/useOAuthConfig.ts +129 -0
  258. package/frontend/src/features/auth/hooks/useUsers.ts +57 -0
  259. package/frontend/src/features/auth/index.ts +9 -0
  260. package/frontend/src/features/auth/page/AuthenticationPage.tsx +169 -0
  261. package/frontend/src/features/auth/services/auth.service.ts +112 -0
  262. package/frontend/src/features/auth/services/oauth.service.ts +49 -0
  263. package/frontend/src/features/dashboard/page/DashboardPage.tsx +194 -0
  264. package/frontend/src/features/database/components/ColumnTypeSelect.tsx +64 -0
  265. package/frontend/src/features/database/components/DatabaseDataGrid.tsx +282 -0
  266. package/frontend/src/features/database/components/ForeignKeyCell.tsx +187 -0
  267. package/frontend/src/features/database/components/ForeignKeyPopover.tsx +378 -0
  268. package/frontend/src/features/database/components/LinkRecordModal.tsx +288 -0
  269. package/frontend/src/features/database/components/RecordFormDialog.tsx +164 -0
  270. package/frontend/src/features/database/components/RecordFormField.tsx +568 -0
  271. package/frontend/src/features/database/components/TableEmptyState.tsx +21 -0
  272. package/frontend/src/features/database/components/TableForm.tsx +656 -0
  273. package/frontend/src/features/database/components/TableFormColumn.tsx +137 -0
  274. package/frontend/src/features/database/components/TableListSkeleton.tsx +9 -0
  275. package/frontend/src/features/database/components/TableSidebar.tsx +47 -0
  276. package/frontend/src/features/database/constants.ts +26 -0
  277. package/frontend/src/features/database/helpers.ts +125 -0
  278. package/frontend/src/features/database/hooks/UseLinkModal.tsx +78 -0
  279. package/frontend/src/features/database/index.ts +12 -0
  280. package/frontend/src/features/database/page/DatabasePage.tsx +626 -0
  281. package/frontend/src/features/database/schema.ts +25 -0
  282. package/frontend/src/features/database/services/database.service.ts +216 -0
  283. package/frontend/src/features/functions/components/FunctionEmptyState.tsx +15 -0
  284. package/frontend/src/features/functions/components/FunctionRow.tsx +71 -0
  285. package/frontend/src/features/functions/components/FunctionViewer.tsx +46 -0
  286. package/frontend/src/features/functions/components/FunctionsContent.tsx +88 -0
  287. package/frontend/src/features/functions/components/FunctionsSidebar.tsx +56 -0
  288. package/frontend/src/features/functions/components/SecretEmptyState.tsx +23 -0
  289. package/frontend/src/features/functions/components/SecretRow.tsx +68 -0
  290. package/frontend/src/features/functions/components/SecretsContent.tsx +120 -0
  291. package/frontend/src/features/functions/hooks/useFunctions.ts +106 -0
  292. package/frontend/src/features/functions/page/FunctionsPage.tsx +28 -0
  293. package/frontend/src/features/functions/services/functions.service.ts +48 -0
  294. package/frontend/src/features/login/components/AuthErrorBoundary.tsx +87 -0
  295. package/frontend/src/features/login/components/PrivateRoute.tsx +24 -0
  296. package/frontend/src/features/login/page/CloudLoginPage.tsx +93 -0
  297. package/frontend/src/features/login/page/LoginPage.tsx +174 -0
  298. package/frontend/src/features/logs/components/AnalyticsLogsTable.tsx +313 -0
  299. package/frontend/src/features/logs/components/LogsTable.tsx +199 -0
  300. package/frontend/src/features/logs/hooks/useAuditLogs.ts +39 -0
  301. package/frontend/src/features/logs/index.ts +5 -0
  302. package/frontend/src/features/logs/page/AnalyticsLogsPage.tsx +530 -0
  303. package/frontend/src/features/logs/page/AuditsPage.tsx +192 -0
  304. package/frontend/src/features/logs/services/log.service.ts +171 -0
  305. package/frontend/src/features/metadata/hooks/useMetadata.ts +53 -0
  306. package/frontend/src/features/metadata/index.ts +0 -0
  307. package/frontend/src/features/metadata/page/MetadataPage.tsx +136 -0
  308. package/frontend/src/features/metadata/services/metadata.service.ts +17 -0
  309. package/frontend/src/features/onboard/components/CompletionCard.tsx +41 -0
  310. package/frontend/src/features/onboard/components/OnboardButton.tsx +84 -0
  311. package/frontend/src/features/onboard/components/StepContent.tsx +91 -0
  312. package/frontend/src/features/onboard/components/TestConnectionStep.tsx +53 -0
  313. package/frontend/src/features/onboard/components/mcp/CursorDeeplinkGenerator.tsx +35 -0
  314. package/frontend/src/features/onboard/components/mcp/McpInstallation.tsx +144 -0
  315. package/frontend/src/features/onboard/components/mcp/index.ts +4 -0
  316. package/frontend/src/features/onboard/components/mcp/mcp-helper.tsx +98 -0
  317. package/frontend/src/features/onboard/index.ts +3 -0
  318. package/frontend/src/features/onboard/page/OnBoardPage.tsx +104 -0
  319. package/frontend/src/features/onboard/types.ts +8 -0
  320. package/frontend/src/features/secrets/hooks/useSecrets.ts +139 -0
  321. package/frontend/src/features/secrets/services/secrets.service.ts +57 -0
  322. package/frontend/src/features/storage/components/BucketEmptyState.tsx +19 -0
  323. package/frontend/src/features/storage/components/BucketFormDialog.tsx +194 -0
  324. package/frontend/src/features/storage/components/BucketListSkeleton.tsx +17 -0
  325. package/frontend/src/features/storage/components/FilePreviewDialog.tsx +287 -0
  326. package/frontend/src/features/storage/components/StorageDataGrid.tsx +239 -0
  327. package/frontend/src/features/storage/components/StorageManager.tsx +236 -0
  328. package/frontend/src/features/storage/components/StorageSidebar.tsx +44 -0
  329. package/frontend/src/features/storage/components/UploadToast.tsx +46 -0
  330. package/frontend/src/features/storage/index.ts +3 -0
  331. package/frontend/src/features/storage/page/StoragePage.tsx +553 -0
  332. package/frontend/src/features/storage/services/storage.service.ts +144 -0
  333. package/frontend/src/features/visualizer/components/AuthNode.tsx +107 -0
  334. package/frontend/src/features/visualizer/components/BucketNode.tsx +34 -0
  335. package/frontend/src/features/visualizer/components/SchemaVisualizer.tsx +359 -0
  336. package/frontend/src/features/visualizer/components/TableNode.tsx +152 -0
  337. package/frontend/src/features/visualizer/components/VisualizerSkeleton.tsx +24 -0
  338. package/frontend/src/features/visualizer/components/index.ts +5 -0
  339. package/frontend/src/features/visualizer/page/VisualizerPage.tsx +127 -0
  340. package/frontend/src/index.css +248 -0
  341. package/frontend/src/lib/api/client.ts +163 -0
  342. package/frontend/src/lib/contexts/AuthContext.tsx +157 -0
  343. package/frontend/src/lib/contexts/OnboardStepContext.tsx +68 -0
  344. package/frontend/src/lib/contexts/SocketContext.tsx +303 -0
  345. package/frontend/src/lib/contexts/ThemeContext.tsx +125 -0
  346. package/frontend/src/lib/hooks/useAuth.ts +4 -0
  347. package/frontend/src/lib/hooks/useConfirm.ts +55 -0
  348. package/frontend/src/lib/hooks/useInterval.ts +27 -0
  349. package/frontend/src/lib/hooks/useMediaQuery.ts +59 -0
  350. package/frontend/src/lib/hooks/useOnboardingCompletion.ts +29 -0
  351. package/frontend/src/lib/hooks/usePagination.ts +27 -0
  352. package/frontend/src/lib/hooks/useTimeout.ts +27 -0
  353. package/frontend/src/lib/hooks/useToast.tsx +229 -0
  354. package/frontend/src/lib/utils/constants.ts +38 -0
  355. package/frontend/src/lib/utils/utils.ts +165 -0
  356. package/frontend/src/lib/utils/validation-schemas.ts +126 -0
  357. package/frontend/src/main.tsx +16 -0
  358. package/frontend/src/rdg.css +194 -0
  359. package/frontend/src/vite-env.d.ts +12 -0
  360. package/frontend/tailwind.config.js +97 -0
  361. package/frontend/tsconfig.json +26 -0
  362. package/frontend/tsconfig.node.json +10 -0
  363. package/frontend/vite.config.ts +37 -0
  364. package/frontend/vitest.config.ts +36 -0
  365. package/functions/deno.json +25 -0
  366. package/functions/server.ts +290 -0
  367. package/functions/worker-template.js +126 -0
  368. package/openapi/ai.yaml +689 -0
  369. package/openapi/auth.yaml +563 -0
  370. package/openapi/functions.yaml +476 -0
  371. package/openapi/health.yaml +30 -0
  372. package/openapi/logs.yaml +224 -0
  373. package/openapi/metadata.yaml +178 -0
  374. package/openapi/records.yaml +382 -0
  375. package/openapi/secrets.yaml +371 -0
  376. package/openapi/storage.yaml +876 -0
  377. package/openapi/tables.yaml +464 -0
  378. package/package.json +88 -0
  379. package/shared-schemas/package.json +31 -0
  380. package/shared-schemas/src/ai-api.schema.ts +167 -0
  381. package/shared-schemas/src/ai.schema.ts +54 -0
  382. package/shared-schemas/src/auth-api.schema.ts +193 -0
  383. package/shared-schemas/src/auth.schema.ts +94 -0
  384. package/shared-schemas/src/database-api.schema.ts +259 -0
  385. package/shared-schemas/src/database.schema.ts +69 -0
  386. package/shared-schemas/src/functions-api.schema.ts +25 -0
  387. package/shared-schemas/src/functions.schema.ts +16 -0
  388. package/shared-schemas/src/index.ts +13 -0
  389. package/shared-schemas/src/logs-api.schema.ts +49 -0
  390. package/shared-schemas/src/logs.schema.ts +14 -0
  391. package/shared-schemas/src/metadata.schema.ts +56 -0
  392. package/shared-schemas/src/storage-api.schema.ts +65 -0
  393. package/shared-schemas/src/storage.schema.ts +19 -0
  394. package/shared-schemas/tsconfig.json +21 -0
  395. package/tsconfig.json +8 -0
@@ -0,0 +1,553 @@
1
+ import React, { useState, useEffect, useRef, useCallback, type DragEvent } from 'react';
2
+ import { useQuery, useQueryClient, useMutation } from '@tanstack/react-query';
3
+ import { Upload } from 'lucide-react';
4
+ import PencilIcon from '@/assets/icons/pencil.svg?react';
5
+ import RefreshIcon from '@/assets/icons/refresh.svg?react';
6
+ import { storageService } from '@/features/storage/services/storage.service';
7
+ import { Button } from '@/components/radix/Button';
8
+ import { Alert, AlertDescription } from '@/components/radix/Alert';
9
+ import { StorageSidebar } from '@/features/storage/components/StorageSidebar';
10
+ import { StorageManager } from '@/features/storage/components/StorageManager';
11
+ import { BucketFormDialog } from '@/features/storage/components/BucketFormDialog';
12
+ import { ConfirmDialog } from '@/components/ConfirmDialog';
13
+ import { EmptyState } from '@/components/EmptyState';
14
+ import {
15
+ Tooltip,
16
+ TooltipContent,
17
+ TooltipProvider,
18
+ TooltipTrigger,
19
+ } from '@/components/radix/Tooltip';
20
+ import { useConfirm } from '@/lib/hooks/useConfirm';
21
+ import { useToast } from '@/lib/hooks/useToast';
22
+ import { useUploadToast } from '@/features/storage/components/UploadToast';
23
+ import { SearchInput, SelectionClearButton, DeleteActionButton } from '@/components';
24
+ import {
25
+ DataUpdatePayload,
26
+ DataUpdateResourceType,
27
+ ServerEvents,
28
+ SocketMessage,
29
+ useSocket,
30
+ } from '@/lib/contexts/SocketContext';
31
+
32
+ interface BucketFormState {
33
+ mode: 'create' | 'edit';
34
+ name: string | null;
35
+ isPublic: boolean;
36
+ }
37
+
38
+ export default function StoragePage() {
39
+ const [selectedBucket, setSelectedBucket] = useState<string | null>(null);
40
+ const [searchQuery, setSearchQuery] = useState('');
41
+ const [isDragging, setIsDragging] = useState(false);
42
+ const [isUploading, setIsUploading] = useState(false);
43
+ const [selectedFiles, setSelectedFiles] = useState<Set<string>>(new Set());
44
+ const [isRefreshing, setIsRefreshing] = useState(false);
45
+ // Bucket form state
46
+ const [bucketFormOpen, setBucketFormOpen] = useState(false);
47
+ const [bucketFormState, setBucketFormState] = useState<BucketFormState>({
48
+ mode: 'create',
49
+ name: null,
50
+ isPublic: false,
51
+ });
52
+ const { confirm, confirmDialogProps } = useConfirm();
53
+ const { showToast } = useToast();
54
+ const { showUploadToast, updateUploadProgress, cancelUpload } = useUploadToast();
55
+ const queryClient = useQueryClient();
56
+ const fileInputRef = useRef<HTMLInputElement>(null);
57
+ const uploadAbortControllerRef = useRef<AbortController | null>(null);
58
+
59
+ const { socket, isConnected } = useSocket();
60
+
61
+ // Fetch buckets
62
+ const {
63
+ data: buckets = [],
64
+ isLoading,
65
+ error: bucketsError,
66
+ refetch: refetchBuckets,
67
+ } = useQuery({
68
+ queryKey: ['storage', 'buckets'],
69
+ queryFn: () => storageService.listBuckets(),
70
+ });
71
+
72
+ // Fetch bucket statistics
73
+ const { data: bucketStats } = useQuery({
74
+ queryKey: ['storage', 'bucket-stats', buckets],
75
+ queryFn: async () => {
76
+ const stats: Record<
77
+ string,
78
+ { fileCount: number; totalSize: number; public: boolean; createdAt?: string }
79
+ > = {};
80
+ const currentBuckets = buckets;
81
+ const promises = currentBuckets.map(async (bucket) => {
82
+ try {
83
+ const result = await storageService.listObjects(bucket.name, { limit: 1000 });
84
+ const objects = result.objects;
85
+ const totalSize = objects.reduce((sum, file) => sum + file.size, 0);
86
+ return {
87
+ bucketName: bucket.name,
88
+ stats: {
89
+ fileCount: result.pagination.total,
90
+ totalSize: totalSize,
91
+ public: bucket.public,
92
+ createdAt: bucket.createdAt,
93
+ },
94
+ };
95
+ } catch (error) {
96
+ if (error) {
97
+ console.error(error);
98
+ return null;
99
+ }
100
+ return {
101
+ bucketName: bucket.name,
102
+ stats: {
103
+ fileCount: 0,
104
+ totalSize: 0,
105
+ public: bucket.public,
106
+ createdAt: bucket.createdAt,
107
+ },
108
+ };
109
+ }
110
+ });
111
+ const results = await Promise.all(promises);
112
+ results.forEach((result) => {
113
+ if (result) {
114
+ stats[result.bucketName] = result.stats;
115
+ }
116
+ });
117
+ return stats;
118
+ },
119
+ enabled: buckets.length > 0,
120
+ staleTime: 30000, // Cache for 30 seconds
121
+ });
122
+
123
+ // Build bucket info map
124
+ const bucketInfo = React.useMemo(() => {
125
+ return bucketStats || {};
126
+ }, [bucketStats]);
127
+
128
+ // Upload mutation
129
+ const uploadMutation = useMutation({
130
+ mutationFn: async ({
131
+ bucket,
132
+ file,
133
+ fileName,
134
+ }: {
135
+ bucket: string;
136
+ file: File;
137
+ fileName?: string;
138
+ }) => {
139
+ const key = fileName || file.name;
140
+ return await storageService.uploadObject(bucket, key, file);
141
+ },
142
+ onSuccess: () => {
143
+ void queryClient.invalidateQueries({ queryKey: ['storage'] });
144
+ },
145
+ // Remove global onError handler - errors are now handled individually in uploadFiles
146
+ });
147
+
148
+ useEffect(() => {
149
+ if (!socket || !isConnected) {
150
+ return;
151
+ }
152
+
153
+ const handleDataUpdate = (message: SocketMessage<DataUpdatePayload>) => {
154
+ if (
155
+ message.payload?.resource === DataUpdateResourceType.METADATA ||
156
+ message.payload?.resource === DataUpdateResourceType.STORAGE_SCHEMA
157
+ ) {
158
+ // Invalidate all buckets queries
159
+ void queryClient.invalidateQueries({ queryKey: ['storage'] });
160
+ }
161
+ };
162
+
163
+ socket.on(ServerEvents.DATA_UPDATE, handleDataUpdate);
164
+
165
+ return () => {
166
+ socket.off(ServerEvents.DATA_UPDATE, handleDataUpdate);
167
+ };
168
+ }, [socket, isConnected, queryClient]);
169
+
170
+ // Auto-select first bucket
171
+ useEffect(() => {
172
+ if (buckets.length > 0 && !selectedBucket) {
173
+ setSelectedBucket(buckets[0].name);
174
+ }
175
+ }, [buckets, selectedBucket]);
176
+
177
+ const handleRefresh = async () => {
178
+ setIsRefreshing(true);
179
+ try {
180
+ await Promise.all([
181
+ refetchBuckets(),
182
+ queryClient.invalidateQueries({ queryKey: ['storage'] }),
183
+ ]);
184
+ } finally {
185
+ setIsRefreshing(false);
186
+ }
187
+ };
188
+
189
+ // Handle bulk delete files
190
+ const handleBulkDeleteFiles = async (fileKeys: string[]) => {
191
+ if (!selectedBucket || fileKeys.length === 0) {
192
+ return;
193
+ }
194
+
195
+ const shouldDelete = await confirm({
196
+ title: `Delete ${fileKeys.length} ${fileKeys.length === 1 ? 'file' : 'files'}`,
197
+ description: `Are you sure you want to delete ${fileKeys.length} ${fileKeys.length === 1 ? 'file' : 'files'}? This action cannot be undone.`,
198
+ confirmText: 'Delete',
199
+ destructive: true,
200
+ });
201
+
202
+ if (shouldDelete) {
203
+ try {
204
+ await Promise.all(fileKeys.map((key) => storageService.deleteObject(selectedBucket, key)));
205
+ void queryClient.invalidateQueries({ queryKey: ['storage'] });
206
+ setSelectedFiles(new Set());
207
+ showToast(`${fileKeys.length} files deleted successfully`, 'success');
208
+ } catch {
209
+ showToast('Failed to delete some files', 'error');
210
+ }
211
+ }
212
+ void queryClient.invalidateQueries({ queryKey: ['storage'] });
213
+ };
214
+
215
+ const uploadFiles = async (files: FileList | File[] | null) => {
216
+ if (!files || files.length === 0 || !selectedBucket) {
217
+ return;
218
+ }
219
+
220
+ setIsUploading(true);
221
+
222
+ // Create abort controller for cancellation
223
+ uploadAbortControllerRef.current = new AbortController();
224
+
225
+ // Show upload toast
226
+ const toastId = showUploadToast(files.length, {
227
+ onCancel: () => {
228
+ uploadAbortControllerRef.current?.abort();
229
+ setIsUploading(false);
230
+ if (fileInputRef.current) {
231
+ fileInputRef.current.value = '';
232
+ }
233
+ },
234
+ });
235
+
236
+ let successCount = 0;
237
+
238
+ // Upload files sequentially with individual error handling
239
+ for (let i = 0; i < files.length; i++) {
240
+ if (uploadAbortControllerRef.current?.signal.aborted) {
241
+ break;
242
+ }
243
+
244
+ // Update progress
245
+ const progress = Math.round(((i + 1) / files.length) * 100);
246
+ updateUploadProgress(toastId, progress);
247
+
248
+ try {
249
+ await uploadMutation.mutateAsync({
250
+ bucket: selectedBucket,
251
+ file: files[i],
252
+ fileName: files[i].name, // Backend will auto-rename if needed
253
+ });
254
+ successCount++;
255
+ } catch (error) {
256
+ // Handle individual file upload error
257
+ const fileName = files[i].name;
258
+
259
+ // Show individual file error (but don't stop the overall process)
260
+ const errorMessage = error instanceof Error ? error.message : 'Upload failed';
261
+ showToast(`Failed to upload "${fileName}": ${errorMessage}`, 'error');
262
+ }
263
+ }
264
+ showToast(`${successCount} files uploaded successfully`, 'success');
265
+
266
+ // Complete the upload toast
267
+ cancelUpload(toastId);
268
+
269
+ // Always reset uploading state
270
+ setIsUploading(false);
271
+ uploadAbortControllerRef.current = null;
272
+
273
+ // Reset file input
274
+ if (fileInputRef.current) {
275
+ fileInputRef.current.value = '';
276
+ }
277
+ };
278
+
279
+ const handleFileUpload = useCallback(uploadFiles, [
280
+ cancelUpload,
281
+ selectedBucket,
282
+ showToast,
283
+ showUploadToast,
284
+ updateUploadProgress,
285
+ uploadMutation,
286
+ ]);
287
+
288
+ const handleFileSelect = async (event: React.ChangeEvent<HTMLInputElement>) => {
289
+ await handleFileUpload(event.target.files);
290
+ };
291
+
292
+ const handleDeleteBucket = async (bucketName: string) => {
293
+ const confirmOptions = {
294
+ title: 'Delete Bucket',
295
+ description: `Are you sure you want to delete the bucket "${bucketName}"? This will permanently delete all files in this bucket. This action cannot be undone.`,
296
+ confirmText: 'Delete',
297
+ destructive: true,
298
+ };
299
+
300
+ const shouldDelete = await confirm(confirmOptions);
301
+
302
+ if (shouldDelete) {
303
+ try {
304
+ await storageService.deleteBucket(bucketName);
305
+
306
+ // Refresh buckets list
307
+ await refetchBuckets();
308
+ showToast('Bucket deleted successfully', 'success');
309
+
310
+ // If the deleted bucket was selected, select the first available bucket
311
+ if (selectedBucket === bucketName) {
312
+ const updatedBuckets =
313
+ queryClient.getQueryData<typeof buckets>(['storage', 'buckets']) || [];
314
+ setSelectedBucket(updatedBuckets[0]?.name || null);
315
+ }
316
+ } catch (error) {
317
+ const errorMessage = error instanceof Error ? error.message : 'Failed to delete bucket';
318
+ showToast(errorMessage, 'error');
319
+ }
320
+ }
321
+ };
322
+
323
+ const handleEditBucket = (bucketName: string) => {
324
+ // Get current bucket's public status
325
+ const info = bucketInfo[bucketName];
326
+ setBucketFormState({
327
+ mode: 'edit',
328
+ name: bucketName,
329
+ isPublic: info?.public ?? false,
330
+ });
331
+ setBucketFormOpen(true);
332
+ };
333
+
334
+ const handleDragOver = useCallback((event: DragEvent<HTMLDivElement>) => {
335
+ event.preventDefault();
336
+ setIsDragging(true);
337
+ }, []);
338
+
339
+ const handleDragLeave = useCallback((_event: DragEvent<HTMLDivElement>) => {
340
+ setIsDragging(false);
341
+ }, []);
342
+
343
+ const handleDrop = useCallback(
344
+ (event: DragEvent<HTMLDivElement>) => {
345
+ event.preventDefault();
346
+ setIsDragging(false);
347
+
348
+ // To support only file uploads (not directories), we filter through
349
+ // dataTransfer.items instead of directly using dataTransfer.files.
350
+ // Ref: https://developer.mozilla.org/en-US/docs/Web/API/DataTransferItem/webkitGetAsEntry
351
+ const fileItems: File[] = Array.from(event.dataTransfer.items)
352
+ .filter((item) => item.webkitGetAsEntry()?.isFile)
353
+ .map((item) => item.getAsFile())
354
+ .filter((item) => item !== null);
355
+
356
+ void handleFileUpload(fileItems);
357
+ },
358
+ [handleFileUpload]
359
+ );
360
+
361
+ const error = bucketsError;
362
+
363
+ return (
364
+ <div className="flex h-full bg-bg-gray dark:bg-neutral-800">
365
+ {/* Secondary Sidebar - Bucket List */}
366
+ <StorageSidebar
367
+ buckets={Object.keys(bucketInfo)}
368
+ selectedBucket={selectedBucket || undefined}
369
+ onBucketSelect={setSelectedBucket}
370
+ loading={isLoading}
371
+ onNewBucket={() => {
372
+ setBucketFormState({
373
+ mode: 'create',
374
+ name: null,
375
+ isPublic: true,
376
+ });
377
+ setBucketFormOpen(true);
378
+ }}
379
+ onEditBucket={handleEditBucket}
380
+ onDeleteBucket={(bucketName) => void handleDeleteBucket(bucketName)}
381
+ />
382
+
383
+ {/* Main Content Area */}
384
+ <div className="flex-1 min-w-0 flex flex-col overflow-hidden">
385
+ {selectedBucket && (
386
+ <>
387
+ {/* Sticky Header Section */}
388
+ <div className="sticky top-0 z-30 bg-bg-gray dark:bg-neutral-800">
389
+ <div className="pl-4 pr-1.5 py-1.5 h-12">
390
+ {/* Page Header with Breadcrumb */}
391
+ <div className="flex items-center justify-between">
392
+ <div className="flex items-center gap-3">
393
+ {selectedBucket && (
394
+ <nav className="flex items-center text-base font-semibold">
395
+ <span className="text-black dark:text-white">{selectedBucket}</span>
396
+ </nav>
397
+ )}
398
+
399
+ {/* Separator */}
400
+ <div className="h-6 w-px bg-gray-200 dark:bg-neutral-700" />
401
+
402
+ {/* Action buttons group */}
403
+ <div className="flex items-center gap-1">
404
+ <TooltipProvider>
405
+ {selectedBucket && (
406
+ <Tooltip>
407
+ <TooltipTrigger asChild>
408
+ <Button
409
+ variant="ghost"
410
+ size="icon"
411
+ className="p-1 h-9 w-9"
412
+ onClick={() => handleEditBucket(selectedBucket)}
413
+ >
414
+ <PencilIcon className="h-5 w-5 text-zinc-400 dark:text-neutral-400" />
415
+ </Button>
416
+ </TooltipTrigger>
417
+ <TooltipContent side="bottom" align="center">
418
+ <p>Edit Bucket</p>
419
+ </TooltipContent>
420
+ </Tooltip>
421
+ )}
422
+ <Tooltip>
423
+ <TooltipTrigger asChild>
424
+ <Button
425
+ variant="ghost"
426
+ size="icon"
427
+ className="p-1 h-9 w-9"
428
+ onClick={() => void handleRefresh()}
429
+ disabled={isRefreshing}
430
+ >
431
+ <RefreshIcon className="h-5 w-5 text-zinc-400 dark:text-neutral-400" />
432
+ </Button>
433
+ </TooltipTrigger>
434
+ <TooltipContent side="bottom" align="center">
435
+ <p>{isRefreshing ? 'Refreshing...' : 'Refresh'}</p>
436
+ </TooltipContent>
437
+ </Tooltip>
438
+ </TooltipProvider>
439
+ </div>
440
+ </div>
441
+ </div>
442
+ </div>
443
+ <div className="pt-2 px-3 pb-4">
444
+ {/* Search Bar and Actions - only show when bucket is selected */}
445
+ {selectedBucket && (
446
+ <div className="flex items-center justify-between">
447
+ {selectedFiles.size > 0 ? (
448
+ <div className="flex items-center gap-3">
449
+ <SelectionClearButton
450
+ selectedCount={selectedFiles.size}
451
+ itemType="file"
452
+ onClear={() => setSelectedFiles(new Set())}
453
+ />
454
+ <DeleteActionButton
455
+ selectedCount={selectedFiles.size}
456
+ itemType="file"
457
+ onDelete={() => void handleBulkDeleteFiles(Array.from(selectedFiles))}
458
+ />
459
+ </div>
460
+ ) : (
461
+ <SearchInput
462
+ value={searchQuery}
463
+ onChange={setSearchQuery}
464
+ placeholder="Search Files by Name"
465
+ className="flex-1 max-w-80 dark:bg-neutral-800 dark:text-white dark:placeholder:text-neutral-400 dark:border-neutral-700"
466
+ debounceTime={300}
467
+ />
468
+ )}
469
+ <div className="flex items-center gap-2 ml-4">
470
+ {selectedFiles.size === 0 && (
471
+ <>
472
+ {/* Upload File Button - moved here when no files selected */}
473
+ <input
474
+ ref={fileInputRef}
475
+ type="file"
476
+ multiple
477
+ onChange={(e) => void handleFileSelect(e)}
478
+ className="hidden"
479
+ accept="*"
480
+ style={{ display: 'none' }}
481
+ />
482
+ <Button
483
+ className="h-10 px-4 font-medium gap-1.5 dark:bg-emerald-300 dark:text-zinc-950 dark:hover:bg-emerald-400"
484
+ onClick={() => fileInputRef.current?.click()}
485
+ disabled={isUploading}
486
+ >
487
+ <Upload className="w-5 h-5" />
488
+ {isUploading ? 'Uploading...' : 'Upload File'}
489
+ </Button>
490
+ </>
491
+ )}
492
+ </div>
493
+ </div>
494
+ )}
495
+ </div>
496
+ </div>
497
+
498
+ {/* Content (supports drag-and-drop file upload) */}
499
+ <div
500
+ className={
501
+ 'relative flex-1 flex flex-col overflow-hidden' + (isDragging ? ' opacity-25' : '')
502
+ }
503
+ onDragOver={handleDragOver}
504
+ onDragLeave={handleDragLeave}
505
+ onDrop={handleDrop}
506
+ >
507
+ {error && (
508
+ <Alert variant="destructive" className="mb-4 mx-8 mt-4">
509
+ <AlertDescription>{String(error)}</AlertDescription>
510
+ </Alert>
511
+ )}
512
+
513
+ <StorageManager
514
+ bucketName={selectedBucket}
515
+ fileCount={bucketStats?.[selectedBucket]?.fileCount || 0}
516
+ searchQuery={searchQuery}
517
+ selectedFiles={selectedFiles}
518
+ onSelectedFilesChange={setSelectedFiles}
519
+ isRefreshing={isRefreshing}
520
+ />
521
+ </div>
522
+ </>
523
+ )}
524
+ {!selectedBucket && (
525
+ <div className="flex-1 flex items-center justify-center">
526
+ <EmptyState
527
+ title="No Bucket Selected"
528
+ description="Select a bucket from the sidebar to view its files"
529
+ />
530
+ </div>
531
+ )}
532
+ </div>
533
+
534
+ {/* Bucket Form (handles both create and edit) */}
535
+ <BucketFormDialog
536
+ open={bucketFormOpen}
537
+ onOpenChange={setBucketFormOpen}
538
+ mode={bucketFormState.mode}
539
+ initialBucketName={bucketFormState.name || ''}
540
+ initialIsPublic={bucketFormState.isPublic}
541
+ onSuccess={(bucketName) => {
542
+ void refetchBuckets();
543
+ if (bucketFormState.mode === 'create' && bucketName) {
544
+ setSelectedBucket(bucketName);
545
+ }
546
+ }}
547
+ />
548
+
549
+ {/* Confirm Dialog */}
550
+ <ConfirmDialog {...confirmDialogProps} />
551
+ </div>
552
+ );
553
+ }
@@ -0,0 +1,144 @@
1
+ import { apiClient } from '@/lib/api/client';
2
+ import {
3
+ StorageFileSchema,
4
+ StorageBucketSchema,
5
+ ListObjectsResponseSchema,
6
+ } from '@insforge/shared-schemas';
7
+
8
+ export interface ListObjectsParams {
9
+ prefix?: string;
10
+ limit?: number;
11
+ offset?: number;
12
+ }
13
+
14
+ export const storageService = {
15
+ // List all buckets
16
+ async listBuckets(): Promise<StorageBucketSchema[]> {
17
+ const response = await apiClient.request('/storage/buckets', {
18
+ headers: apiClient.withAccessToken(),
19
+ });
20
+ // Traditional REST: API returns array directly
21
+ return response;
22
+ },
23
+
24
+ // List objects in a bucket
25
+ async listObjects(
26
+ bucketName: string,
27
+ params?: ListObjectsParams,
28
+ searchQuery?: string
29
+ ): Promise<ListObjectsResponseSchema> {
30
+ const searchParams = new URLSearchParams();
31
+ if (params?.prefix) {
32
+ searchParams.append('prefix', params.prefix);
33
+ }
34
+ if (params?.limit) {
35
+ searchParams.append('limit', params.limit.toString());
36
+ }
37
+ if (params?.offset) {
38
+ searchParams.append('offset', params.offset.toString());
39
+ }
40
+ if (searchQuery && searchQuery.trim()) {
41
+ searchParams.append('search', searchQuery.trim());
42
+ }
43
+
44
+ const url = `/storage/buckets/${encodeURIComponent(bucketName)}/objects${searchParams.toString() ? `?${searchParams}` : ''}`;
45
+ const response: {
46
+ data: StorageFileSchema[];
47
+ pagination: { offset: number; limit: number; total: number };
48
+ } = await apiClient.request(url, {
49
+ headers: apiClient.withAccessToken(),
50
+ });
51
+
52
+ return {
53
+ objects: response.data,
54
+ pagination: response.pagination,
55
+ };
56
+ },
57
+
58
+ // Upload an object to bucket
59
+ async uploadObject(
60
+ bucketName: string,
61
+ objectKey: string,
62
+ object: File
63
+ ): Promise<StorageFileSchema> {
64
+ const formData = new FormData();
65
+ formData.append('file', object);
66
+
67
+ // Use fetch directly for object uploads to avoid Content-Type header issues
68
+ const response = await fetch(
69
+ `/api/storage/buckets/${encodeURIComponent(bucketName)}/objects/${encodeURIComponent(objectKey)}`,
70
+ {
71
+ method: 'PUT',
72
+ headers: {
73
+ Authorization: `Bearer ${apiClient.getToken()}`,
74
+ },
75
+ body: formData,
76
+ }
77
+ );
78
+
79
+ if (!response.ok) {
80
+ const error = await response.json();
81
+ // Traditional REST error format
82
+ throw new Error(error.message || error.error || 'Upload failed');
83
+ }
84
+
85
+ const result = await response.json();
86
+ // Traditional REST: response returned directly
87
+ return result;
88
+ },
89
+
90
+ // Get download URL for an object
91
+ getDownloadUrl(bucketName: string, objectKey: string): string {
92
+ return `/api/storage/buckets/${encodeURIComponent(bucketName)}/objects/${encodeURIComponent(objectKey)}`;
93
+ },
94
+
95
+ // Download an object (returns blob)
96
+ async downloadObject(bucketName: string, objectKey: string): Promise<Blob> {
97
+ const response = await fetch(this.getDownloadUrl(bucketName, objectKey), {
98
+ headers: {
99
+ Authorization: `Bearer ${apiClient.getToken()}`,
100
+ },
101
+ });
102
+ if (!response.ok) {
103
+ throw new Error(`Failed to download object: ${response.statusText}`);
104
+ }
105
+ return await response.blob();
106
+ },
107
+
108
+ // Delete an object
109
+ async deleteObject(bucketName: string, objectKey: string): Promise<void> {
110
+ await apiClient.request(
111
+ `/storage/buckets/${encodeURIComponent(bucketName)}/objects/${encodeURIComponent(objectKey)}`,
112
+ {
113
+ method: 'DELETE',
114
+ headers: apiClient.withAccessToken(),
115
+ }
116
+ );
117
+ },
118
+
119
+ // Create a new bucket
120
+ async createBucket(bucketName: string, isPublic: boolean = true): Promise<void> {
121
+ await apiClient.request('/storage/buckets', {
122
+ method: 'POST',
123
+ headers: apiClient.withAccessToken(),
124
+ body: JSON.stringify({ bucketName: bucketName, isPublic: isPublic }),
125
+ });
126
+ },
127
+
128
+ // Delete entire bucket
129
+ async deleteBucket(bucketName: string): Promise<void> {
130
+ await apiClient.request(`/storage/buckets/${encodeURIComponent(bucketName)}`, {
131
+ method: 'DELETE',
132
+ headers: apiClient.withAccessToken(),
133
+ });
134
+ },
135
+
136
+ // Edit bucket (update visibility or other config)
137
+ async editBucket(bucketName: string, config: { isPublic: boolean }): Promise<void> {
138
+ await apiClient.request(`/storage/buckets/${encodeURIComponent(bucketName)}`, {
139
+ method: 'PATCH',
140
+ headers: apiClient.withAccessToken(),
141
+ body: JSON.stringify(config),
142
+ });
143
+ },
144
+ };