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,1074 @@
1
+ import { DatabaseManager } from '@/core/database/manager.js';
2
+ import { AppError } from '@/api/middleware/error.js';
3
+ import {
4
+ type RawSQLResponse,
5
+ type ExportDatabaseResponse,
6
+ type ExportDatabaseJsonData,
7
+ type ImportDatabaseResponse,
8
+ type BulkUpsertResponse,
9
+ type DatabaseMetadataSchema,
10
+ } from '@insforge/shared-schemas';
11
+ import logger from '@/utils/logger.js';
12
+ import { ERROR_CODES } from '@/types/error-constants';
13
+ import { parseSQLStatements } from '@/utils/sql-parser.js';
14
+ import { validateTableName } from '@/utils/validations.js';
15
+ import format from 'pg-format';
16
+ import { parse } from 'csv-parse/sync';
17
+
18
+ export class DatabaseAdvanceService {
19
+ private dbManager = DatabaseManager.getInstance();
20
+
21
+ /**
22
+ * Get table data using simple SELECT query
23
+ * More reliable than streaming for moderate datasets
24
+ */
25
+ private async getTableData(
26
+ client: any, // eslint-disable-line @typescript-eslint/no-explicit-any
27
+ table: string,
28
+ rowLimit: number | undefined
29
+ ): Promise<{ rows: Record<string, unknown>[]; totalRows: number; wasTruncated: boolean }> {
30
+ const query = rowLimit ? `SELECT * FROM ${table} LIMIT ${rowLimit}` : `SELECT * FROM ${table}`;
31
+
32
+ let wasTruncated = false;
33
+ let totalRows = 0;
34
+
35
+ // Check for truncation upfront if rowLimit is set
36
+ if (rowLimit) {
37
+ try {
38
+ const countResult = await client.query(`SELECT COUNT(*) FROM ${table}`);
39
+ totalRows = parseInt(countResult.rows[0].count);
40
+ wasTruncated = totalRows > rowLimit;
41
+ } catch (err) {
42
+ logger.error('Error counting rows:', err);
43
+ }
44
+ }
45
+
46
+ const result = await client.query(query);
47
+ const rows = result.rows || [];
48
+
49
+ if (!rowLimit) {
50
+ totalRows = rows.length;
51
+ }
52
+
53
+ return { rows, totalRows, wasTruncated };
54
+ }
55
+
56
+ private sanitizeQuery(query: string): string {
57
+ // Basic SQL injection prevention - check for dangerous patterns
58
+ const dangerousPatterns = [
59
+ /DROP\s+DATABASE/i,
60
+ /CREATE\s+DATABASE/i,
61
+ /ALTER\s+DATABASE/i,
62
+ /pg_catalog/i,
63
+ /information_schema/i,
64
+ ];
65
+
66
+ for (const pattern of dangerousPatterns) {
67
+ if (pattern.test(query)) {
68
+ throw new AppError('Query contains restricted operations', 403, ERROR_CODES.FORBIDDEN);
69
+ }
70
+ }
71
+
72
+ // Check for system table operations (tables starting with underscore)
73
+ // This pattern checks each statement in multi-statement queries, including schema-qualified names
74
+ const systemTablePattern =
75
+ /(?:^|\n|;)\s*(?:CREATE|ALTER|DROP|INSERT\s+INTO|UPDATE|DELETE\s+FROM|TRUNCATE)\s+(?:TABLE\s+)?(?:IF\s+(?:NOT\s+)?EXISTS\s+)?(?:\w+\.)?["']?_\w+/im;
76
+ if (systemTablePattern.test(query)) {
77
+ throw new AppError(
78
+ 'Cannot modify or create system tables (tables starting with underscore)',
79
+ 403,
80
+ ERROR_CODES.FORBIDDEN
81
+ );
82
+ }
83
+
84
+ // Check for RENAME TO system table
85
+ const renameToSystemTablePattern = /RENAME\s+TO\s+(?:\w+\.)?["']?_\w+/im;
86
+ if (renameToSystemTablePattern.test(query)) {
87
+ throw new AppError(
88
+ 'Cannot rename tables to system table names (tables starting with underscore)',
89
+ 403,
90
+ ERROR_CODES.FORBIDDEN
91
+ );
92
+ }
93
+
94
+ // Check for DROP or RENAME operations on 'users' table
95
+ const usersTablePattern =
96
+ /(?:^|\n|;)\s*(?:DROP\s+(?:TABLE\s+)?(?:IF\s+EXISTS\s+)?(?:\w+\.)?["']?users["']?|ALTER\s+TABLE\s+(?:IF\s+EXISTS\s+)?(?:\w+\.)?["']?users["']?\s+RENAME\s+TO)/im;
97
+ if (usersTablePattern.test(query)) {
98
+ throw new AppError('Cannot drop or rename the users table', 403, ERROR_CODES.FORBIDDEN);
99
+ }
100
+
101
+ return query;
102
+ }
103
+
104
+ async executeRawSQL(input_query: string, params: unknown[] = []): Promise<RawSQLResponse> {
105
+ const query = this.sanitizeQuery(input_query);
106
+ const pool = this.dbManager.getPool();
107
+ const client = await pool.connect();
108
+
109
+ try {
110
+ // Execute query with timeout
111
+ const result = (await Promise.race([
112
+ client.query(query, params),
113
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Query timeout')), 30000)),
114
+ ])) as { rows: unknown[]; rowCount: number; fields?: { name: string; dataTypeID: number }[] };
115
+
116
+ // Refresh schema cache if it was a DDL operation
117
+ if (/CREATE|ALTER|DROP/i.test(query)) {
118
+ await client.query(`NOTIFY pgrst, 'reload schema';`);
119
+ // Metadata is now updated on-demand
120
+ }
121
+
122
+ const response: RawSQLResponse = {
123
+ rows: result.rows || [],
124
+ rowCount: result.rowCount,
125
+ fields: result.fields?.map((field: { name: string; dataTypeID: number }) => ({
126
+ name: field.name,
127
+ dataTypeID: field.dataTypeID,
128
+ })),
129
+ };
130
+
131
+ return response;
132
+ } finally {
133
+ client.release();
134
+ }
135
+ }
136
+
137
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
138
+ private async exportTableSchemaBySQL(client: any, table: string): Promise<string> {
139
+ let sqlExport = '';
140
+ // Always export table schema with defaults
141
+ const schemaResult = await client.query(
142
+ `
143
+ SELECT 'CREATE TABLE IF NOT EXISTS ' || table_name || ' (' ||
144
+ string_agg(column_name || ' ' ||
145
+ CASE
146
+ WHEN data_type = 'character varying' THEN 'varchar' || COALESCE('(' || character_maximum_length || ')', '')
147
+ WHEN data_type = 'timestamp with time zone' THEN 'timestamptz'
148
+ ELSE data_type
149
+ END ||
150
+ CASE WHEN is_nullable = 'NO' THEN ' NOT NULL' ELSE '' END ||
151
+ CASE WHEN column_default IS NOT NULL THEN ' DEFAULT ' || column_default ELSE '' END,
152
+ ', ') || ');' as create_statement
153
+ FROM information_schema.columns
154
+ WHERE table_schema = 'public' AND table_name = $1
155
+ GROUP BY table_name
156
+ `,
157
+ [table]
158
+ );
159
+
160
+ if (schemaResult.rows.length > 0) {
161
+ sqlExport += `-- Table: ${table}\n`;
162
+ sqlExport += schemaResult.rows[0].create_statement + '\n\n';
163
+ }
164
+
165
+ // Export indexes (excluding primary key indexes)
166
+ const indexesResult = await client.query(
167
+ `
168
+ SELECT
169
+ indexname,
170
+ indexdef
171
+ FROM pg_indexes
172
+ WHERE tablename = $1
173
+ AND schemaname = 'public'
174
+ AND indexname NOT LIKE '%_pkey'
175
+ ORDER BY indexname
176
+ `,
177
+ [table]
178
+ );
179
+
180
+ if (indexesResult.rows.length > 0) {
181
+ sqlExport += `-- Indexes for table: ${table}\n`;
182
+ for (const indexRow of indexesResult.rows) {
183
+ sqlExport += indexRow.indexdef + ';\n';
184
+ }
185
+ sqlExport += '\n';
186
+ }
187
+
188
+ // Export foreign key constraints
189
+ const foreignKeysResult = await client.query(
190
+ `
191
+ SELECT DISTINCT
192
+ 'ALTER TABLE ' || quote_ident(tc.table_name) ||
193
+ ' ADD CONSTRAINT ' || quote_ident(tc.constraint_name) ||
194
+ ' FOREIGN KEY (' || quote_ident(kcu.column_name) || ')' ||
195
+ ' REFERENCES ' || quote_ident(ccu.table_name) ||
196
+ ' (' || quote_ident(ccu.column_name) || ')' ||
197
+ CASE
198
+ WHEN rc.delete_rule != 'NO ACTION' THEN ' ON DELETE ' || rc.delete_rule
199
+ ELSE ''
200
+ END ||
201
+ CASE
202
+ WHEN rc.update_rule != 'NO ACTION' THEN ' ON UPDATE ' || rc.update_rule
203
+ ELSE ''
204
+ END || ';' as fk_statement,
205
+ tc.constraint_name
206
+ FROM information_schema.table_constraints AS tc
207
+ JOIN information_schema.key_column_usage AS kcu
208
+ ON tc.constraint_name = kcu.constraint_name
209
+ AND tc.table_schema = kcu.table_schema
210
+ AND kcu.table_name = tc.table_name
211
+ JOIN information_schema.constraint_column_usage AS ccu
212
+ ON ccu.constraint_name = tc.constraint_name
213
+ AND ccu.table_schema = tc.table_schema
214
+ LEFT JOIN information_schema.referential_constraints AS rc
215
+ ON tc.constraint_name = rc.constraint_name
216
+ AND tc.table_schema = rc.constraint_schema
217
+ WHERE tc.constraint_type = 'FOREIGN KEY'
218
+ AND tc.table_name = $1
219
+ AND tc.table_schema = 'public'
220
+ ORDER BY tc.constraint_name
221
+ `,
222
+ [table]
223
+ );
224
+
225
+ if (foreignKeysResult.rows.length > 0) {
226
+ sqlExport += `-- Foreign key constraints for table: ${table}\n`;
227
+ for (const fkRow of foreignKeysResult.rows) {
228
+ sqlExport += fkRow.fk_statement + '\n';
229
+ }
230
+ sqlExport += '\n';
231
+ }
232
+
233
+ // Check if RLS is enabled on the table
234
+ const rlsResult = await client.query(
235
+ `
236
+ SELECT relrowsecurity
237
+ FROM pg_class
238
+ WHERE relname = $1
239
+ AND relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'public')
240
+ `,
241
+ [table]
242
+ );
243
+ const rlsEnabled =
244
+ rlsResult.rows.length > 0 &&
245
+ (rlsResult.rows[0].relrowsecurity === true || rlsResult.rows[0].relrowsecurity === 1);
246
+ if (rlsEnabled) {
247
+ sqlExport += `-- RLS enabled for table: ${table}\n`;
248
+ sqlExport += `ALTER TABLE ${table} ENABLE ROW LEVEL SECURITY;\n\n`;
249
+ }
250
+
251
+ // Export RLS policies
252
+ const policiesResult = await client.query(
253
+ `
254
+ SELECT
255
+ 'CREATE POLICY ' || quote_ident(policyname) || ' ON ' || quote_ident(tablename) ||
256
+ ' FOR ' || cmd ||
257
+ CASE
258
+ WHEN roles != '{}'::name[] THEN ' TO ' || array_to_string(roles, ', ')
259
+ ELSE ''
260
+ END ||
261
+ CASE
262
+ WHEN qual IS NOT NULL THEN ' USING (' || qual || ')'
263
+ ELSE ''
264
+ END ||
265
+ CASE
266
+ WHEN with_check IS NOT NULL THEN ' WITH CHECK (' || with_check || ')'
267
+ ELSE ''
268
+ END || ';' as policy_statement
269
+ FROM pg_policies
270
+ WHERE schemaname = 'public' AND tablename = $1
271
+ ORDER BY policyname
272
+ `,
273
+ [table]
274
+ );
275
+
276
+ if (policiesResult.rows.length > 0) {
277
+ sqlExport += `-- RLS policies for table: ${table}\n`;
278
+ for (const policyRow of policiesResult.rows) {
279
+ sqlExport += policyRow.policy_statement + '\n';
280
+ }
281
+ sqlExport += '\n';
282
+ }
283
+
284
+ // Export triggers for this table
285
+ const triggersResult = await client.query(
286
+ `
287
+ SELECT
288
+ 'CREATE TRIGGER ' || quote_ident(trigger_name) ||
289
+ ' ' || action_timing || ' ' || event_manipulation ||
290
+ ' ON ' || quote_ident(event_object_table) ||
291
+ CASE
292
+ WHEN action_reference_new_table IS NOT NULL OR action_reference_old_table IS NOT NULL
293
+ THEN ' REFERENCING ' ||
294
+ CASE WHEN action_reference_new_table IS NOT NULL
295
+ THEN 'NEW TABLE AS ' || quote_ident(action_reference_new_table)
296
+ ELSE ''
297
+ END ||
298
+ CASE WHEN action_reference_old_table IS NOT NULL
299
+ THEN ' OLD TABLE AS ' || quote_ident(action_reference_old_table)
300
+ ELSE ''
301
+ END
302
+ ELSE ''
303
+ END ||
304
+ ' FOR EACH ' || action_orientation ||
305
+ CASE
306
+ WHEN action_condition IS NOT NULL
307
+ THEN ' WHEN (' || action_condition || ')'
308
+ ELSE ''
309
+ END ||
310
+ ' ' || action_statement || ';' as trigger_statement
311
+ FROM information_schema.triggers
312
+ WHERE event_object_schema = 'public'
313
+ AND event_object_table = $1
314
+ ORDER BY trigger_name
315
+ `,
316
+ [table]
317
+ );
318
+
319
+ if (triggersResult.rows.length > 0) {
320
+ sqlExport += `-- Triggers for table: ${table}\n`;
321
+ for (const triggerRow of triggersResult.rows) {
322
+ sqlExport += triggerRow.trigger_statement + '\n';
323
+ }
324
+ sqlExport += '\n';
325
+ }
326
+ return sqlExport;
327
+ }
328
+
329
+ async exportDatabase(
330
+ tables?: string[],
331
+ format: 'sql' | 'json' = 'sql',
332
+ includeData: boolean = true,
333
+ includeFunctions: boolean = false,
334
+ includeSequences: boolean = false,
335
+ includeViews: boolean = false,
336
+ rowLimit?: number
337
+ ): Promise<ExportDatabaseResponse> {
338
+ const pool = this.dbManager.getPool();
339
+ const client = await pool.connect();
340
+
341
+ try {
342
+ // Get tables to export
343
+ let tablesToExport: string[];
344
+ if (tables && tables.length > 0) {
345
+ tablesToExport = tables;
346
+ } else {
347
+ const tablesResult = await client.query(`
348
+ SELECT tablename
349
+ FROM pg_tables
350
+ WHERE schemaname = 'public'
351
+ ORDER BY tablename
352
+ `);
353
+ tablesToExport = tablesResult.rows.map((row: { tablename: string }) => row.tablename);
354
+ }
355
+ logger.info(
356
+ `Exporting tables: ${tablesToExport.join(', ')}, format: ${format}, includeData: ${includeData}, includeFunctions: ${includeFunctions}, includeSequences: ${includeSequences}, includeViews: ${includeViews}, rowLimit: ${rowLimit}`
357
+ );
358
+
359
+ const timestamp = new Date().toISOString();
360
+ const truncatedTables: string[] = [];
361
+
362
+ if (format === 'sql') {
363
+ let sqlExport = `-- Database Export\n-- Generated on: ${timestamp}\n-- Format: SQL\n-- Include Data: ${includeData}\n`;
364
+ if (rowLimit) {
365
+ sqlExport += `-- Row Limit: ${rowLimit} rows per table\n`;
366
+ }
367
+ sqlExport += '\n';
368
+
369
+ for (const table of tablesToExport) {
370
+ sqlExport += await this.exportTableSchemaBySQL(client, table);
371
+
372
+ // Export data if requested - using simple SELECT query
373
+ if (includeData) {
374
+ let tableDataSql = '';
375
+
376
+ const { rows, wasTruncated } = await this.getTableData(client, table, rowLimit);
377
+
378
+ if (rows.length > 0) {
379
+ tableDataSql += `-- Data for table: ${table}\n`;
380
+
381
+ for (const row of rows) {
382
+ const columns = Object.keys(row);
383
+ const values = Object.values(row).map((val) => {
384
+ if (val === null) {
385
+ return 'NULL';
386
+ } else if (typeof val === 'string') {
387
+ return `'${val.replace(/'/g, "''")}'`;
388
+ } else if (val instanceof Date) {
389
+ return `'${val.toISOString()}'`;
390
+ } else if (typeof val === 'object') {
391
+ // Handle JSON/JSONB columns
392
+ return `'${JSON.stringify(val).replace(/'/g, "''")}'`;
393
+ } else if (typeof val === 'boolean') {
394
+ return val ? 'true' : 'false';
395
+ } else {
396
+ return String(val);
397
+ }
398
+ });
399
+ tableDataSql += `INSERT INTO ${table} (${columns.join(', ')}) VALUES (${values.join(', ')});\n`;
400
+ }
401
+ }
402
+
403
+ if (wasTruncated) {
404
+ const countResult = await client.query(`SELECT COUNT(*) FROM ${table}`);
405
+ const totalRowsInTable = parseInt(countResult.rows[0].count);
406
+ tableDataSql =
407
+ `-- WARNING: Table contains ${totalRowsInTable} rows, but only ${rowLimit} rows exported due to row limit\n` +
408
+ tableDataSql;
409
+ truncatedTables.push(table);
410
+ }
411
+
412
+ if (tableDataSql) {
413
+ sqlExport += tableDataSql + '\n';
414
+ }
415
+ }
416
+ }
417
+
418
+ // Export all functions in public schema
419
+ if (includeFunctions) {
420
+ const functionsResult = await client.query(`
421
+ SELECT
422
+ pg_get_functiondef(p.oid) || ';' as function_def,
423
+ p.proname as function_name
424
+ FROM pg_proc p
425
+ JOIN pg_namespace n ON p.pronamespace = n.oid
426
+ WHERE n.nspname = 'public'
427
+ AND p.prokind IN ('f', 'p', 'w') -- functions, procedures, window functions
428
+ AND NOT EXISTS (
429
+ SELECT 1 FROM pg_depend d
430
+ JOIN pg_extension e ON d.refobjid = e.oid
431
+ WHERE d.objid = p.oid
432
+ ) -- Exclude extension functions
433
+ ORDER BY p.proname
434
+ `);
435
+
436
+ if (functionsResult.rows.length > 0) {
437
+ sqlExport += `-- Functions and Procedures\n`;
438
+ for (const funcRow of functionsResult.rows) {
439
+ sqlExport += `-- Function: ${funcRow.function_name}\n`;
440
+ sqlExport += funcRow.function_def + '\n\n';
441
+ }
442
+ }
443
+ }
444
+
445
+ // Export all sequences in public schema
446
+ if (includeSequences) {
447
+ const sequencesResult = await client.query(`
448
+ SELECT
449
+ 'CREATE SEQUENCE IF NOT EXISTS ' || quote_ident(sequence_name) ||
450
+ ' START WITH ' || start_value ||
451
+ ' INCREMENT BY ' || increment ||
452
+ CASE WHEN minimum_value IS NOT NULL THEN ' MINVALUE ' || minimum_value ELSE ' NO MINVALUE' END ||
453
+ CASE WHEN maximum_value IS NOT NULL THEN ' MAXVALUE ' || maximum_value ELSE ' NO MAXVALUE' END ||
454
+ CASE WHEN cycle_option = 'YES' THEN ' CYCLE' ELSE ' NO CYCLE' END ||
455
+ ';' as sequence_statement,
456
+ sequence_name
457
+ FROM information_schema.sequences
458
+ WHERE sequence_schema = 'public'
459
+ ORDER BY sequence_name
460
+ `);
461
+
462
+ if (sequencesResult.rows.length > 0) {
463
+ sqlExport += `-- Sequences\n`;
464
+ for (const seqRow of sequencesResult.rows) {
465
+ sqlExport += seqRow.sequence_statement + '\n';
466
+ }
467
+ sqlExport += '\n';
468
+ }
469
+ }
470
+
471
+ // Export all views in public schema
472
+ if (includeViews) {
473
+ const viewsResult = await client.query(`
474
+ SELECT
475
+ 'CREATE OR REPLACE VIEW ' || quote_ident(table_name) || ' AS ' ||
476
+ view_definition as view_statement,
477
+ table_name as view_name
478
+ FROM information_schema.views
479
+ WHERE table_schema = 'public'
480
+ ORDER BY table_name
481
+ `);
482
+
483
+ if (viewsResult.rows.length > 0) {
484
+ sqlExport += `-- Views\n`;
485
+ for (const viewRow of viewsResult.rows) {
486
+ sqlExport += `-- View: ${viewRow.view_name}\n`;
487
+ sqlExport += viewRow.view_statement + '\n\n';
488
+ }
489
+ }
490
+ }
491
+
492
+ return {
493
+ format: 'sql',
494
+ data: sqlExport,
495
+ timestamp,
496
+ ...(truncatedTables.length > 0 && {
497
+ truncatedTables,
498
+ rowLimit,
499
+ }),
500
+ };
501
+ } else {
502
+ // JSON format
503
+ const jsonData: ExportDatabaseJsonData = {
504
+ timestamp,
505
+ tables: {},
506
+ functions: [],
507
+ sequences: [],
508
+ views: [],
509
+ };
510
+
511
+ for (const table of tablesToExport) {
512
+ // Get schema
513
+ const schemaResult = await client.query(
514
+ `
515
+ SELECT
516
+ column_name as "columnName",
517
+ data_type as "dataType",
518
+ character_maximum_length as "characterMaximumLength",
519
+ is_nullable as "isNullable",
520
+ column_default as "columnDefault"
521
+ FROM information_schema.columns
522
+ WHERE table_schema = 'public' AND table_name = $1
523
+ ORDER BY ordinal_position
524
+ `,
525
+ [table]
526
+ );
527
+
528
+ // Get indexes
529
+ const indexesResult = await client.query(
530
+ `
531
+ SELECT DISTINCT
532
+ pi.indexname,
533
+ pi.indexdef,
534
+ idx.indisunique as "isUnique",
535
+ idx.indisprimary as "isPrimary"
536
+ FROM pg_indexes pi
537
+ JOIN pg_class cls ON cls.relname = pi.indexname
538
+ AND cls.relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = pi.schemaname)
539
+ JOIN pg_index idx ON idx.indexrelid = cls.oid
540
+ WHERE pi.tablename = $1
541
+ AND pi.schemaname = 'public'
542
+ ORDER BY pi.indexname
543
+ `,
544
+ [table]
545
+ );
546
+
547
+ // Get foreign keys
548
+ const foreignKeysResult = await client.query(
549
+ `
550
+ SELECT DISTINCT
551
+ tc.constraint_name as "constraintName",
552
+ kcu.column_name as "columnName",
553
+ ccu.table_name as "foreignTableName",
554
+ ccu.column_name as "foreignColumnName",
555
+ rc.delete_rule as "deleteRule",
556
+ rc.update_rule as "updateRule"
557
+ FROM information_schema.table_constraints AS tc
558
+ JOIN information_schema.key_column_usage AS kcu
559
+ ON tc.constraint_name = kcu.constraint_name
560
+ AND tc.table_schema = kcu.table_schema
561
+ AND kcu.table_name = tc.table_name
562
+ JOIN information_schema.constraint_column_usage AS ccu
563
+ ON ccu.constraint_name = tc.constraint_name
564
+ AND ccu.table_schema = tc.table_schema
565
+ LEFT JOIN information_schema.referential_constraints AS rc
566
+ ON tc.constraint_name = rc.constraint_name
567
+ AND tc.table_schema = rc.constraint_schema
568
+ WHERE tc.constraint_type = 'FOREIGN KEY'
569
+ AND tc.table_name = $1
570
+ AND tc.table_schema = 'public'
571
+ ORDER BY "constraintName", "columnName"
572
+ `,
573
+ [table]
574
+ );
575
+
576
+ // Check if RLS is enabled on the table
577
+ const rlsResult = await client.query(
578
+ `
579
+ SELECT relrowsecurity
580
+ FROM pg_class
581
+ WHERE relname = $1
582
+ AND relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'public')
583
+ `,
584
+ [table]
585
+ );
586
+
587
+ const rlsEnabled =
588
+ rlsResult.rows.length > 0 &&
589
+ (rlsResult.rows[0].relrowsecurity === true || rlsResult.rows[0].relrowsecurity === 1);
590
+
591
+ // Get policies
592
+ const policiesResult = await client.query(
593
+ `
594
+ SELECT
595
+ policyname,
596
+ cmd,
597
+ roles,
598
+ qual,
599
+ with_check as "withCheck"
600
+ FROM pg_policies
601
+ WHERE schemaname = 'public' AND tablename = $1
602
+ `,
603
+ [table]
604
+ );
605
+
606
+ // Get triggers
607
+ const triggersResult = await client.query(
608
+ `
609
+ SELECT
610
+ trigger_name as "triggerName",
611
+ action_timing as "actionTiming",
612
+ event_manipulation as "eventManipulation",
613
+ action_orientation as "actionOrientation",
614
+ action_condition as "actionCondition",
615
+ action_statement as "actionStatement",
616
+ action_reference_new_table as "newTable",
617
+ action_reference_old_table as "oldTable"
618
+ FROM information_schema.triggers
619
+ WHERE event_object_schema = 'public'
620
+ AND event_object_table = $1
621
+ ORDER BY trigger_name
622
+ `,
623
+ [table]
624
+ );
625
+
626
+ // Get data if requested - using streaming to avoid memory issues
627
+ const rows: unknown[] = [];
628
+ let truncated = false;
629
+ let totalRowCount: number | undefined;
630
+
631
+ if (includeData) {
632
+ const tableData = await this.getTableData(client, table, rowLimit);
633
+
634
+ rows.push(...tableData.rows);
635
+ truncated = tableData.wasTruncated;
636
+
637
+ if (truncated) {
638
+ totalRowCount = tableData.totalRows;
639
+ truncatedTables.push(table);
640
+ }
641
+ }
642
+
643
+ jsonData.tables[table] = {
644
+ schema: schemaResult.rows,
645
+ indexes: indexesResult.rows,
646
+ foreignKeys: foreignKeysResult.rows,
647
+ rlsEnabled,
648
+ policies: policiesResult.rows,
649
+ triggers: triggersResult.rows,
650
+ rows,
651
+ ...(truncated && {
652
+ truncated: true,
653
+ exportedRowCount: rows.length,
654
+ totalRowCount,
655
+ }),
656
+ };
657
+ }
658
+
659
+ // Get all functions
660
+ if (includeFunctions) {
661
+ const functionsResult = await client.query(`
662
+ SELECT
663
+ p.proname as "functionName",
664
+ pg_get_functiondef(p.oid) as "functionDef",
665
+ p.prokind as "kind"
666
+ FROM pg_proc p
667
+ JOIN pg_namespace n ON p.pronamespace = n.oid
668
+ WHERE n.nspname = 'public'
669
+ AND p.prokind IN ('f', 'p', 'w')
670
+ AND NOT EXISTS (
671
+ SELECT 1 FROM pg_depend d
672
+ JOIN pg_extension e ON d.refobjid = e.oid
673
+ WHERE d.objid = p.oid
674
+ )
675
+ ORDER BY p.proname
676
+ `);
677
+ jsonData.functions = functionsResult.rows;
678
+ }
679
+
680
+ // Get all sequences
681
+ if (includeSequences) {
682
+ const sequencesResult = await client.query(`
683
+ SELECT
684
+ sequence_name as "sequenceName",
685
+ start_value as "startValue",
686
+ increment as "increment",
687
+ minimum_value as "minValue",
688
+ maximum_value as "maxValue",
689
+ cycle_option as "cycle"
690
+ FROM information_schema.sequences
691
+ WHERE sequence_schema = 'public'
692
+ ORDER BY sequence_name
693
+ `);
694
+ jsonData.sequences = sequencesResult.rows;
695
+ }
696
+
697
+ // Get all views
698
+ if (includeViews) {
699
+ const viewsResult = await client.query(`
700
+ SELECT
701
+ table_name as "viewName",
702
+ view_definition as "definition"
703
+ FROM information_schema.views
704
+ WHERE table_schema = 'public'
705
+ ORDER BY table_name
706
+ `);
707
+ jsonData.views = viewsResult.rows;
708
+ }
709
+
710
+ return {
711
+ format: 'json',
712
+ data: jsonData,
713
+ timestamp,
714
+ ...(truncatedTables.length > 0 && {
715
+ truncatedTables,
716
+ rowLimit,
717
+ }),
718
+ };
719
+ }
720
+ } finally {
721
+ client.release();
722
+ }
723
+ }
724
+
725
+ async importDatabase(
726
+ fileBuffer: Buffer,
727
+ filename: string,
728
+ fileSize: number,
729
+ truncate: boolean = false
730
+ ): Promise<ImportDatabaseResponse> {
731
+ // Validate file type
732
+ const allowedExtensions = ['.sql', '.txt'];
733
+ const fileExtension = filename.toLowerCase().substring(filename.lastIndexOf('.'));
734
+
735
+ if (!allowedExtensions.includes(fileExtension)) {
736
+ throw new AppError('Only .sql/.txt files are allowed', 400, ERROR_CODES.INVALID_INPUT);
737
+ }
738
+
739
+ // Convert buffer to string
740
+ const raw_data = fileBuffer.toString('utf-8');
741
+ const data = this.sanitizeQuery(raw_data);
742
+ const pool = this.dbManager.getPool();
743
+ const client = await pool.connect();
744
+
745
+ try {
746
+ await client.query('BEGIN');
747
+
748
+ const importedTables: string[] = [];
749
+ let totalRows = 0;
750
+
751
+ // If truncate is requested, truncate all public tables first
752
+ if (truncate) {
753
+ const tablesResult = await client.query(`
754
+ SELECT tablename
755
+ FROM pg_tables
756
+ WHERE schemaname = 'public'
757
+ `);
758
+
759
+ for (const row of tablesResult.rows) {
760
+ try {
761
+ await client.query(`TRUNCATE TABLE ${row.tablename} CASCADE`);
762
+ logger.info(`Truncated table: ${row.tablename}`);
763
+ } catch (err) {
764
+ logger.warn(`Could not truncate table ${row.tablename}:`, err);
765
+ }
766
+ }
767
+ }
768
+
769
+ // Process SQL file using our SQL parser utility
770
+ let statements: string[] = [];
771
+
772
+ try {
773
+ statements = parseSQLStatements(data);
774
+ logger.info(`Parsed ${statements.length} SQL statements from import file`);
775
+ } catch (parseError) {
776
+ logger.warn('Failed to parse SQL file:', parseError);
777
+ throw new AppError(
778
+ 'Invalid SQL file format. Please ensure the file contains valid SQL statements.',
779
+ 400,
780
+ ERROR_CODES.INVALID_INPUT
781
+ );
782
+ }
783
+
784
+ for (const statement of statements) {
785
+ // Basic validation to prevent dangerous operations
786
+ this.sanitizeQuery(statement);
787
+
788
+ try {
789
+ const result = await client.query(statement);
790
+
791
+ // Track INSERT operations
792
+ if (statement.toUpperCase().startsWith('INSERT')) {
793
+ totalRows += result.rowCount || 0;
794
+
795
+ // Extract table name from INSERT statement
796
+ const tableMatch = statement.match(/INSERT\s+INTO\s+([a-zA-Z_][a-zA-Z0-9_]*)/i);
797
+ if (tableMatch && !importedTables.includes(tableMatch[1])) {
798
+ importedTables.push(tableMatch[1]);
799
+ }
800
+ }
801
+
802
+ // Track CREATE TABLE operations
803
+ if (statement.toUpperCase().includes('CREATE TABLE')) {
804
+ // Extract table name from CREATE TABLE statement
805
+ const tableMatch = statement.match(
806
+ /CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?([a-zA-Z_][a-zA-Z0-9_]*)/i
807
+ );
808
+ if (tableMatch && !importedTables.includes(tableMatch[1])) {
809
+ importedTables.push(tableMatch[1]);
810
+ }
811
+ }
812
+ } catch (err: unknown) {
813
+ logger.warn(`Failed to execute statement: ${statement.substring(0, 100)}...`, err);
814
+ throw new AppError(
815
+ `Import failed: ${err instanceof Error ? err.message : 'Unknown error'}`,
816
+ 400,
817
+ ERROR_CODES.INVALID_INPUT
818
+ );
819
+ }
820
+ }
821
+
822
+ await client.query(`NOTIFY pgrst, 'reload schema';`);
823
+ await client.query('COMMIT');
824
+ // Metadata is now updated on-demand
825
+
826
+ return {
827
+ success: true,
828
+ message: 'SQL file imported successfully',
829
+ filename,
830
+ tables: importedTables,
831
+ rowsImported: totalRows,
832
+ fileSize,
833
+ };
834
+ } catch (error) {
835
+ await client.query('ROLLBACK');
836
+ throw error;
837
+ } finally {
838
+ client.release();
839
+ }
840
+ }
841
+
842
+ async bulkUpsertFromFile(
843
+ table: string,
844
+ fileBuffer: Buffer,
845
+ filename: string,
846
+ upsertKey?: string
847
+ ): Promise<BulkUpsertResponse> {
848
+ validateTableName(table);
849
+
850
+ const fileExtension = filename.toLowerCase().substring(filename.lastIndexOf('.'));
851
+ let records: Record<string, unknown>[] = [];
852
+
853
+ // Parse file based on type
854
+ try {
855
+ if (fileExtension === '.csv') {
856
+ records = parse(fileBuffer, {
857
+ columns: true,
858
+ skip_empty_lines: true,
859
+ });
860
+ } else if (fileExtension === '.json') {
861
+ const jsonContent = fileBuffer.toString('utf-8');
862
+ const parsed = JSON.parse(jsonContent);
863
+ records = Array.isArray(parsed) ? parsed : [parsed];
864
+ } else {
865
+ throw new AppError(
866
+ 'Unsupported file type. Use .csv or .json',
867
+ 400,
868
+ ERROR_CODES.INVALID_INPUT
869
+ );
870
+ }
871
+ } catch (parseError) {
872
+ if (parseError instanceof AppError) {
873
+ throw parseError;
874
+ }
875
+ throw new AppError(
876
+ `Failed to parse ${fileExtension} file: ${parseError instanceof Error ? parseError.message : 'Unknown error'}`,
877
+ 400,
878
+ ERROR_CODES.INVALID_INPUT
879
+ );
880
+ }
881
+
882
+ if (!records || records.length === 0) {
883
+ throw new AppError('No records found in file', 400, ERROR_CODES.INVALID_INPUT);
884
+ }
885
+
886
+ // Perform the bulk insert
887
+ const result = await this.bulkInsert(table, records, upsertKey);
888
+
889
+ return {
890
+ success: true,
891
+ message: `Successfully inserted ${result.rowCount} rows into ${table}`,
892
+ table,
893
+ rowsAffected: result.rowCount,
894
+ totalRecords: records.length,
895
+ filename,
896
+ };
897
+ }
898
+
899
+ private async bulkInsert(
900
+ table: string,
901
+ records: Record<string, unknown>[],
902
+ upsertKey?: string
903
+ ): Promise<{ rowCount: number; rows?: unknown[] }> {
904
+ if (!records || records.length === 0) {
905
+ throw new AppError('No records to insert', 400, ERROR_CODES.INVALID_INPUT);
906
+ }
907
+
908
+ const pool = this.dbManager.getPool();
909
+ const client = await pool.connect();
910
+
911
+ try {
912
+ // Get column names from first record
913
+ const columns = Object.keys(records[0]);
914
+
915
+ // Convert records to array format for pg-format
916
+ const values = records.map((record) =>
917
+ columns.map((col) => {
918
+ const value = record[col];
919
+ // pg-format handles NULL, dates, JSON automatically
920
+ // Convert empty strings to NULL for consistency
921
+ return value === '' ? null : value;
922
+ })
923
+ );
924
+
925
+ let query: string;
926
+
927
+ if (upsertKey) {
928
+ // Validate upsert key exists in columns
929
+ if (!columns.includes(upsertKey)) {
930
+ throw new AppError(
931
+ `Upsert key '${upsertKey}' not found in record columns`,
932
+ 400,
933
+ ERROR_CODES.INVALID_INPUT
934
+ );
935
+ }
936
+
937
+ // Build upsert query with pg-format
938
+ const updateColumns = columns.filter((c) => c !== upsertKey);
939
+
940
+ if (updateColumns.length > 0) {
941
+ // Build UPDATE SET clause
942
+ const updateClause = updateColumns
943
+ .map((col) => format('%I = EXCLUDED.%I', col, col))
944
+ .join(', ');
945
+
946
+ query = format(
947
+ 'INSERT INTO %I (%I) VALUES %L ON CONFLICT (%I) DO UPDATE SET %s',
948
+ table,
949
+ columns,
950
+ values,
951
+ upsertKey,
952
+ updateClause
953
+ );
954
+ } else {
955
+ // No columns to update, just do nothing on conflict
956
+ query = format(
957
+ 'INSERT INTO %I (%I) VALUES %L ON CONFLICT (%I) DO NOTHING',
958
+ table,
959
+ columns,
960
+ values,
961
+ upsertKey
962
+ );
963
+ }
964
+ } else {
965
+ // Simple insert
966
+ query = format('INSERT INTO %I (%I) VALUES %L', table, columns, values);
967
+ }
968
+
969
+ // Execute query
970
+ const result = await client.query(query);
971
+
972
+ // Refresh schema cache if needed
973
+ await client.query(`NOTIFY pgrst, 'reload schema';`);
974
+
975
+ return {
976
+ rowCount: result.rowCount || 0,
977
+ rows: result.rows,
978
+ };
979
+ } catch (error) {
980
+ // Log the error for debugging
981
+ logger.error('Bulk insert error:', error);
982
+
983
+ // Re-throw with better error message
984
+ if (error instanceof AppError) {
985
+ throw error;
986
+ }
987
+
988
+ const message = error instanceof Error ? error.message : 'Bulk insert failed';
989
+ throw new AppError(message, 400, ERROR_CODES.INVALID_INPUT);
990
+ } finally {
991
+ client.release();
992
+ }
993
+ }
994
+
995
+ /**
996
+ * Get database metadata
997
+ */
998
+ async getMetadata(): Promise<DatabaseMetadataSchema> {
999
+ // Get all tables excluding system tables (those starting with _)
1000
+ const allTables = await this.dbManager.getUserTables();
1001
+ const dbMetadata = await this.exportDatabase(allTables, 'json', false, false, false, false);
1002
+
1003
+ const databaseSize = await this.getDatabaseSizeInGB();
1004
+
1005
+ // Get record counts for each table
1006
+ const tablesSchemas = (dbMetadata.data as ExportDatabaseJsonData).tables;
1007
+ const db = this.dbManager.getDb();
1008
+
1009
+ for (const tableName of allTables) {
1010
+ let recordCount = 0;
1011
+ try {
1012
+ // there is a race condition here, if the table is immeditely deleted, so added an extra check to see if the table exists
1013
+ const tableExists = (await db
1014
+ .prepare(
1015
+ `
1016
+ SELECT EXISTS (
1017
+ SELECT 1 FROM pg_class
1018
+ WHERE relname = ?
1019
+ AND relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'public')
1020
+ AND relkind = 'r'
1021
+ ) as exists
1022
+ `
1023
+ )
1024
+ .get(tableName)) as { exists: boolean } | null;
1025
+
1026
+ if (tableExists?.exists) {
1027
+ const countResult = (await db
1028
+ .prepare(`SELECT COUNT(*) as count FROM "${tableName}"`)
1029
+ .get()) as { count: number } | null;
1030
+ recordCount = countResult?.count || 0;
1031
+ }
1032
+ } catch (error) {
1033
+ // Handle any unexpected errors
1034
+ logger.warn('Could not get record count for table', {
1035
+ table: tableName,
1036
+ error: error instanceof Error ? error.message : String(error),
1037
+ });
1038
+ recordCount = 0;
1039
+ }
1040
+
1041
+ // Only add recordCount if the table exists in tablesSchemas
1042
+ if (tablesSchemas[tableName]) {
1043
+ tablesSchemas[tableName].recordCount = recordCount;
1044
+ }
1045
+ }
1046
+
1047
+ return {
1048
+ tables: tablesSchemas,
1049
+ totalSize: databaseSize,
1050
+ };
1051
+ }
1052
+
1053
+ async getDatabaseSizeInGB(): Promise<number> {
1054
+ try {
1055
+ const db = this.dbManager.getDb();
1056
+ // Query PostgreSQL for database size
1057
+ const result = (await db
1058
+ .prepare(
1059
+ `
1060
+ SELECT pg_database_size(current_database()) as size
1061
+ `
1062
+ )
1063
+ .get()) as { size: number } | null;
1064
+
1065
+ // PostgreSQL returns size in bytes, convert to GB
1066
+ return (result?.size || 0) / (1024 * 1024 * 1024);
1067
+ } catch (error) {
1068
+ logger.error('Error getting database size', {
1069
+ error: error instanceof Error ? error.message : String(error),
1070
+ });
1071
+ return 0;
1072
+ }
1073
+ }
1074
+ }