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.
- package/.dockerignore +58 -0
- package/.env.example +49 -0
- package/.github/ISSUE_TEMPLATE/bug_report.yml +83 -0
- package/.github/ISSUE_TEMPLATE/config.yml +11 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +79 -0
- package/.github/copilot-instructions.md +147 -0
- package/.github/workflows/build-image.yml +65 -0
- package/.github/workflows/ci-premerge-check.yml +24 -0
- package/.github/workflows/deploy-aws.yml +130 -0
- package/.github/workflows/lint-and-format.yml +33 -0
- package/.prettierignore +65 -0
- package/.prettierrc +9 -0
- package/CHANGELOG.md +3 -0
- package/CONTRIBUTING.md +126 -0
- package/Dockerfile +27 -0
- package/GITHUB_OAUTH_SETUP.md +49 -0
- package/GOOGLE_OAUTH_SETUP.md +148 -0
- package/LICENSE +201 -0
- package/README.md +134 -0
- package/assets/Dark.svg +23 -0
- package/assets/archDiagram.png +0 -0
- package/assets/banner.png +0 -0
- package/assets/mcpInstallv2.png +0 -0
- package/assets/sampleResponse.png +0 -0
- package/assets/signin.png +0 -0
- package/assets/userflow.png +0 -0
- package/backend/migrations/000_create-base-tables.sql +142 -0
- package/backend/migrations/001_create-helper-functions.sql +41 -0
- package/backend/migrations/002_rename-auth-tables.sql +30 -0
- package/backend/migrations/003_create-users-table.sql +56 -0
- package/backend/migrations/004_add-reload-postgrest-func.sql +24 -0
- package/backend/migrations/005_enable-project-admin-modify-users.sql +30 -0
- package/backend/migrations/006_modify-ai-usage-table.sql +25 -0
- package/backend/migrations/007_drop-metadata-table.sql +2 -0
- package/backend/migrations/008_add-system-tables.sql +77 -0
- package/backend/migrations/009_add-function-secrets.sql +24 -0
- package/backend/migrations/010_modify-ai-config-modalities.sql +93 -0
- package/backend/migrations/011_refactor-secrets-table.sql +15 -0
- package/backend/migrations/012_add-storage-uploaded-by.sql +8 -0
- package/backend/package.json +75 -0
- package/backend/src/api/middleware/auth.ts +240 -0
- package/backend/src/api/middleware/error.ts +231 -0
- package/backend/src/api/middleware/upload.ts +59 -0
- package/backend/src/api/routes/agent.ts +29 -0
- package/backend/src/api/routes/ai.ts +472 -0
- package/backend/src/api/routes/auth.oauth.ts +482 -0
- package/backend/src/api/routes/auth.ts +386 -0
- package/backend/src/api/routes/database.advance.ts +275 -0
- package/backend/src/api/routes/database.records.ts +246 -0
- package/backend/src/api/routes/database.tables.ts +161 -0
- package/backend/src/api/routes/docs.ts +66 -0
- package/backend/src/api/routes/functions.ts +183 -0
- package/backend/src/api/routes/logs.ts +150 -0
- package/backend/src/api/routes/metadata.ts +160 -0
- package/backend/src/api/routes/openapi.ts +82 -0
- package/backend/src/api/routes/secrets.ts +199 -0
- package/backend/src/api/routes/storage.ts +547 -0
- package/backend/src/api/routes/usage.ts +96 -0
- package/backend/src/core/ai/chat.ts +207 -0
- package/backend/src/core/ai/client.ts +242 -0
- package/backend/src/core/ai/config.ts +187 -0
- package/backend/src/core/ai/image.ts +156 -0
- package/backend/src/core/ai/model.ts +117 -0
- package/backend/src/core/ai/usage.ts +290 -0
- package/backend/src/core/auth/auth.ts +781 -0
- package/backend/src/core/auth/oauth.ts +398 -0
- package/backend/src/core/database/advance.ts +1074 -0
- package/backend/src/core/database/manager.ts +178 -0
- package/backend/src/core/database/table.ts +772 -0
- package/backend/src/core/documentation/agent.ts +689 -0
- package/backend/src/core/documentation/openapi.ts +856 -0
- package/backend/src/core/functions/functions.ts +310 -0
- package/backend/src/core/logs/analytics.ts +76 -0
- package/backend/src/core/logs/audit.ts +255 -0
- package/backend/src/core/logs/providers/base.provider.ts +83 -0
- package/backend/src/core/logs/providers/cloudwatch.provider.ts +510 -0
- package/backend/src/core/logs/providers/localdb.provider.ts +246 -0
- package/backend/src/core/secrets/encryption.ts +58 -0
- package/backend/src/core/secrets/secrets.ts +410 -0
- package/backend/src/core/socket/socket.ts +388 -0
- package/backend/src/core/socket/types.ts +79 -0
- package/backend/src/core/storage/storage.ts +923 -0
- package/backend/src/server.ts +288 -0
- package/backend/src/types/ai.ts +46 -0
- package/backend/src/types/auth.ts +90 -0
- package/backend/src/types/database.ts +136 -0
- package/backend/src/types/error-constants.ts +86 -0
- package/backend/src/types/logs.ts +47 -0
- package/backend/src/types/profile.ts +55 -0
- package/backend/src/types/storage.ts +23 -0
- package/backend/src/utils/cloud-token.ts +39 -0
- package/backend/src/utils/constants.ts +1 -0
- package/backend/src/utils/environment.ts +35 -0
- package/backend/src/utils/helpers.ts +49 -0
- package/backend/src/utils/logger.ts +13 -0
- package/backend/src/utils/response.ts +62 -0
- package/backend/src/utils/seed.ts +205 -0
- package/backend/src/utils/sql-parser.ts +63 -0
- package/backend/src/utils/uuid.ts +9 -0
- package/backend/src/utils/validations.ts +129 -0
- package/backend/tests/README.md +134 -0
- package/backend/tests/cleanup-all-test-data.sh +231 -0
- package/backend/tests/cloud/test-s3-multitenant.sh +132 -0
- package/backend/tests/local/comprehensive-curl-tests.sh +156 -0
- package/backend/tests/local/test-auth-router.sh +144 -0
- package/backend/tests/local/test-database-router.sh +222 -0
- package/backend/tests/local/test-e2e.sh +241 -0
- package/backend/tests/local/test-fk-errors.sh +97 -0
- package/backend/tests/local/test-id-field.sh +201 -0
- package/backend/tests/local/test-public-bucket.sh +265 -0
- package/backend/tests/local/test-secrets.sh +248 -0
- package/backend/tests/local/test-serverless-functions.sh.disabled +325 -0
- package/backend/tests/local/test-traditional-rest.sh +209 -0
- package/backend/tests/manual/README.md +51 -0
- package/backend/tests/manual/create-large-table-simple.sql +11 -0
- package/backend/tests/manual/seed-large-table.sql +101 -0
- package/backend/tests/manual/setup-large-table-extras.sql +34 -0
- package/backend/tests/manual/test-better-auth.sh +303 -0
- package/backend/tests/manual/test-bulk-upsert.sh +410 -0
- package/backend/tests/manual/test-database-advance.sh +297 -0
- package/backend/tests/manual/test-postgrest-stability.sh +192 -0
- package/backend/tests/manual/test-rawsql-export-import.sh +412 -0
- package/backend/tests/manual/test-universal-storage.sh +264 -0
- package/backend/tests/manual/test-users.sql +18 -0
- package/backend/tests/run-all-tests.sh +140 -0
- package/backend/tests/setup.ts +22 -0
- package/backend/tests/test-config.sh +303 -0
- package/backend/tsconfig.json +23 -0
- package/backend/tsup.config.ts +18 -0
- package/backend/vitest.config.ts +22 -0
- package/docker-compose.prod.yml +145 -0
- package/docker-compose.yml +167 -0
- package/docker-init/db/db-init.sql +125 -0
- package/docker-init/db/jwt.sql +5 -0
- package/docker-init/db/logs.sql +9 -0
- package/docker-init/db/postgresql.conf +17 -0
- package/docs/deprecated/insforge-auth-api.md +215 -0
- package/docs/deprecated/insforge-auth-sdk.md +100 -0
- package/docs/deprecated/insforge-db-api.md +359 -0
- package/docs/deprecated/insforge-db-sdk.md +140 -0
- package/docs/deprecated/insforge-debug-sdk.md +157 -0
- package/docs/deprecated/insforge-debug.md +65 -0
- package/docs/deprecated/insforge-instructions.md +124 -0
- package/docs/deprecated/insforge-project.md +118 -0
- package/docs/deprecated/insforge-storage-api.md +279 -0
- package/docs/deprecated/insforge-storage-sdk.md +159 -0
- package/docs/insforge-instructions-sdk.md +407 -0
- package/eslint.config.js +317 -0
- package/examples/oauth/frontend-oauth-example.html +251 -0
- package/examples/response-examples.md +444 -0
- package/frontend/README.md +112 -0
- package/frontend/components.json +17 -0
- package/frontend/index.html +13 -0
- package/frontend/package.json +63 -0
- package/frontend/public/favicon.ico +0 -0
- package/frontend/src/App.tsx +106 -0
- package/frontend/src/assets/icons/checkbox_checked.svg +6 -0
- package/frontend/src/assets/icons/checkbox_undetermined.svg +6 -0
- package/frontend/src/assets/icons/checked.svg +3 -0
- package/frontend/src/assets/icons/error.svg +3 -0
- package/frontend/src/assets/icons/pencil.svg +4 -0
- package/frontend/src/assets/icons/refresh.svg +4 -0
- package/frontend/src/assets/icons/step_active.svg +3 -0
- package/frontend/src/assets/icons/step_inactive.svg +11 -0
- package/frontend/src/assets/icons/warning.svg +3 -0
- package/frontend/src/assets/logos/amazon.svg +1 -0
- package/frontend/src/assets/logos/claude_code.svg +3 -0
- package/frontend/src/assets/logos/cline.svg +6 -0
- package/frontend/src/assets/logos/cursor.svg +20 -0
- package/frontend/src/assets/logos/discord.svg +9 -0
- package/frontend/src/assets/logos/gemini.svg +19 -0
- package/frontend/src/assets/logos/github.svg +5 -0
- package/frontend/src/assets/logos/google.svg +13 -0
- package/frontend/src/assets/logos/grok.svg +10 -0
- package/frontend/src/assets/logos/insforge_dark.svg +15 -0
- package/frontend/src/assets/logos/insforge_light.svg +15 -0
- package/frontend/src/assets/logos/openai.svg +10 -0
- package/frontend/src/assets/logos/roo_code.svg +9 -0
- package/frontend/src/assets/logos/trae.svg +3 -0
- package/frontend/src/assets/logos/windsurf.svg +10 -0
- package/frontend/src/components/ButtonWithLoading.tsx +27 -0
- package/frontend/src/components/Checkbox.tsx +61 -0
- package/frontend/src/components/CodeBlock.tsx +32 -0
- package/frontend/src/components/ConfirmDialog.tsx +96 -0
- package/frontend/src/components/CopyButton.tsx +69 -0
- package/frontend/src/components/DeleteActionButton.tsx +42 -0
- package/frontend/src/components/EmptyState.tsx +41 -0
- package/frontend/src/components/ErrorState.tsx +35 -0
- package/frontend/src/components/FeatureSidebar.tsx +126 -0
- package/frontend/src/components/FeatureSidebarItem.tsx +101 -0
- package/frontend/src/components/JsonHighlight.tsx +61 -0
- package/frontend/src/components/LoadingState.tsx +16 -0
- package/frontend/src/components/PaginationControls.tsx +54 -0
- package/frontend/src/components/PromptDialog.tsx +68 -0
- package/frontend/src/components/SearchInput.tsx +90 -0
- package/frontend/src/components/SelectionClearButton.tsx +26 -0
- package/frontend/src/components/Stepper.tsx +139 -0
- package/frontend/src/components/ThemeToggle.tsx +58 -0
- package/frontend/src/components/TypeBadge.tsx +20 -0
- package/frontend/src/components/datagrid/DataGrid.tsx +264 -0
- package/frontend/src/components/datagrid/DefaultCellRenderer.tsx +114 -0
- package/frontend/src/components/datagrid/IdCell.tsx +44 -0
- package/frontend/src/components/datagrid/SortableHeader.tsx +74 -0
- package/frontend/src/components/datagrid/cell-editors/BooleanCellEditor.tsx +54 -0
- package/frontend/src/components/datagrid/cell-editors/DateCellEditor.tsx +483 -0
- package/frontend/src/components/datagrid/cell-editors/JsonCellEditor.tsx +362 -0
- package/frontend/src/components/datagrid/cell-editors/TextCellEditor.tsx +38 -0
- package/frontend/src/components/datagrid/cell-editors/index.ts +14 -0
- package/frontend/src/components/datagrid/cell-editors/types.ts +43 -0
- package/frontend/src/components/datagrid/datagridTypes.tsx +72 -0
- package/frontend/src/components/datagrid/index.tsx +20 -0
- package/frontend/src/components/index.ts +39 -0
- package/frontend/src/components/layout/AppHeader.tsx +146 -0
- package/frontend/src/components/layout/AppSidebar.tsx +190 -0
- package/frontend/src/components/layout/CloudLayout.tsx +95 -0
- package/frontend/src/components/layout/Layout.tsx +43 -0
- package/frontend/src/components/radix/Alert.tsx +45 -0
- package/frontend/src/components/radix/AlertDialog.tsx +115 -0
- package/frontend/src/components/radix/Avatar.tsx +45 -0
- package/frontend/src/components/radix/Badge.tsx +33 -0
- package/frontend/src/components/radix/Button.tsx +50 -0
- package/frontend/src/components/radix/Card.tsx +58 -0
- package/frontend/src/components/radix/Dialog.tsx +98 -0
- package/frontend/src/components/radix/DropdownMenu.tsx +185 -0
- package/frontend/src/components/radix/Form.tsx +167 -0
- package/frontend/src/components/radix/Input.tsx +22 -0
- package/frontend/src/components/radix/Label.tsx +19 -0
- package/frontend/src/components/radix/Popover.tsx +29 -0
- package/frontend/src/components/radix/ScrollArea.tsx +44 -0
- package/frontend/src/components/radix/Select.tsx +151 -0
- package/frontend/src/components/radix/Separator.tsx +26 -0
- package/frontend/src/components/radix/Sheet.tsx +119 -0
- package/frontend/src/components/radix/Skeleton.tsx +7 -0
- package/frontend/src/components/radix/Switch.tsx +29 -0
- package/frontend/src/components/radix/Tabs.tsx +50 -0
- package/frontend/src/components/radix/Textarea.tsx +21 -0
- package/frontend/src/components/radix/Tooltip.tsx +28 -0
- package/frontend/src/features/ai/components/AIConfigCard.tsx +154 -0
- package/frontend/src/features/ai/components/AIConfigDialog.tsx +76 -0
- package/frontend/src/features/ai/components/AIConfigForm.tsx +222 -0
- package/frontend/src/features/ai/components/AIEmptyState.tsx +18 -0
- package/frontend/src/features/ai/components/fields/ModalityField.tsx +87 -0
- package/frontend/src/features/ai/components/fields/ModelSelectionField.tsx +134 -0
- package/frontend/src/features/ai/components/fields/SystemPromptField.tsx +33 -0
- package/frontend/src/features/ai/helpers.ts +155 -0
- package/frontend/src/features/ai/hooks/useAIConfigs.ts +221 -0
- package/frontend/src/features/ai/hooks/useAIUsage.ts +77 -0
- package/frontend/src/features/ai/page/AIPage.tsx +178 -0
- package/frontend/src/features/ai/services/ai.service.ts +148 -0
- package/frontend/src/features/auth/components/AddOAuthDialog.tsx +106 -0
- package/frontend/src/features/auth/components/AuthMethodTab.tsx +238 -0
- package/frontend/src/features/auth/components/OAuthConfigDialog.tsx +303 -0
- package/frontend/src/features/auth/components/OAuthEmptyState.tsx +15 -0
- package/frontend/src/features/auth/components/UserFormDialog.tsx +248 -0
- package/frontend/src/features/auth/components/UsersDataGrid.tsx +183 -0
- package/frontend/src/features/auth/components/UsersTab.tsx +114 -0
- package/frontend/src/features/auth/hooks/useOAuthConfig.ts +129 -0
- package/frontend/src/features/auth/hooks/useUsers.ts +57 -0
- package/frontend/src/features/auth/index.ts +9 -0
- package/frontend/src/features/auth/page/AuthenticationPage.tsx +169 -0
- package/frontend/src/features/auth/services/auth.service.ts +112 -0
- package/frontend/src/features/auth/services/oauth.service.ts +49 -0
- package/frontend/src/features/dashboard/page/DashboardPage.tsx +194 -0
- package/frontend/src/features/database/components/ColumnTypeSelect.tsx +64 -0
- package/frontend/src/features/database/components/DatabaseDataGrid.tsx +282 -0
- package/frontend/src/features/database/components/ForeignKeyCell.tsx +187 -0
- package/frontend/src/features/database/components/ForeignKeyPopover.tsx +378 -0
- package/frontend/src/features/database/components/LinkRecordModal.tsx +288 -0
- package/frontend/src/features/database/components/RecordFormDialog.tsx +164 -0
- package/frontend/src/features/database/components/RecordFormField.tsx +568 -0
- package/frontend/src/features/database/components/TableEmptyState.tsx +21 -0
- package/frontend/src/features/database/components/TableForm.tsx +656 -0
- package/frontend/src/features/database/components/TableFormColumn.tsx +137 -0
- package/frontend/src/features/database/components/TableListSkeleton.tsx +9 -0
- package/frontend/src/features/database/components/TableSidebar.tsx +47 -0
- package/frontend/src/features/database/constants.ts +26 -0
- package/frontend/src/features/database/helpers.ts +125 -0
- package/frontend/src/features/database/hooks/UseLinkModal.tsx +78 -0
- package/frontend/src/features/database/index.ts +12 -0
- package/frontend/src/features/database/page/DatabasePage.tsx +626 -0
- package/frontend/src/features/database/schema.ts +25 -0
- package/frontend/src/features/database/services/database.service.ts +216 -0
- package/frontend/src/features/functions/components/FunctionEmptyState.tsx +15 -0
- package/frontend/src/features/functions/components/FunctionRow.tsx +71 -0
- package/frontend/src/features/functions/components/FunctionViewer.tsx +46 -0
- package/frontend/src/features/functions/components/FunctionsContent.tsx +88 -0
- package/frontend/src/features/functions/components/FunctionsSidebar.tsx +56 -0
- package/frontend/src/features/functions/components/SecretEmptyState.tsx +23 -0
- package/frontend/src/features/functions/components/SecretRow.tsx +68 -0
- package/frontend/src/features/functions/components/SecretsContent.tsx +120 -0
- package/frontend/src/features/functions/hooks/useFunctions.ts +106 -0
- package/frontend/src/features/functions/page/FunctionsPage.tsx +28 -0
- package/frontend/src/features/functions/services/functions.service.ts +48 -0
- package/frontend/src/features/login/components/AuthErrorBoundary.tsx +87 -0
- package/frontend/src/features/login/components/PrivateRoute.tsx +24 -0
- package/frontend/src/features/login/page/CloudLoginPage.tsx +93 -0
- package/frontend/src/features/login/page/LoginPage.tsx +174 -0
- package/frontend/src/features/logs/components/AnalyticsLogsTable.tsx +313 -0
- package/frontend/src/features/logs/components/LogsTable.tsx +199 -0
- package/frontend/src/features/logs/hooks/useAuditLogs.ts +39 -0
- package/frontend/src/features/logs/index.ts +5 -0
- package/frontend/src/features/logs/page/AnalyticsLogsPage.tsx +530 -0
- package/frontend/src/features/logs/page/AuditsPage.tsx +192 -0
- package/frontend/src/features/logs/services/log.service.ts +171 -0
- package/frontend/src/features/metadata/hooks/useMetadata.ts +53 -0
- package/frontend/src/features/metadata/index.ts +0 -0
- package/frontend/src/features/metadata/page/MetadataPage.tsx +136 -0
- package/frontend/src/features/metadata/services/metadata.service.ts +17 -0
- package/frontend/src/features/onboard/components/CompletionCard.tsx +41 -0
- package/frontend/src/features/onboard/components/OnboardButton.tsx +84 -0
- package/frontend/src/features/onboard/components/StepContent.tsx +91 -0
- package/frontend/src/features/onboard/components/TestConnectionStep.tsx +53 -0
- package/frontend/src/features/onboard/components/mcp/CursorDeeplinkGenerator.tsx +35 -0
- package/frontend/src/features/onboard/components/mcp/McpInstallation.tsx +144 -0
- package/frontend/src/features/onboard/components/mcp/index.ts +4 -0
- package/frontend/src/features/onboard/components/mcp/mcp-helper.tsx +98 -0
- package/frontend/src/features/onboard/index.ts +3 -0
- package/frontend/src/features/onboard/page/OnBoardPage.tsx +104 -0
- package/frontend/src/features/onboard/types.ts +8 -0
- package/frontend/src/features/secrets/hooks/useSecrets.ts +139 -0
- package/frontend/src/features/secrets/services/secrets.service.ts +57 -0
- package/frontend/src/features/storage/components/BucketEmptyState.tsx +19 -0
- package/frontend/src/features/storage/components/BucketFormDialog.tsx +194 -0
- package/frontend/src/features/storage/components/BucketListSkeleton.tsx +17 -0
- package/frontend/src/features/storage/components/FilePreviewDialog.tsx +287 -0
- package/frontend/src/features/storage/components/StorageDataGrid.tsx +239 -0
- package/frontend/src/features/storage/components/StorageManager.tsx +236 -0
- package/frontend/src/features/storage/components/StorageSidebar.tsx +44 -0
- package/frontend/src/features/storage/components/UploadToast.tsx +46 -0
- package/frontend/src/features/storage/index.ts +3 -0
- package/frontend/src/features/storage/page/StoragePage.tsx +553 -0
- package/frontend/src/features/storage/services/storage.service.ts +144 -0
- package/frontend/src/features/visualizer/components/AuthNode.tsx +107 -0
- package/frontend/src/features/visualizer/components/BucketNode.tsx +34 -0
- package/frontend/src/features/visualizer/components/SchemaVisualizer.tsx +359 -0
- package/frontend/src/features/visualizer/components/TableNode.tsx +152 -0
- package/frontend/src/features/visualizer/components/VisualizerSkeleton.tsx +24 -0
- package/frontend/src/features/visualizer/components/index.ts +5 -0
- package/frontend/src/features/visualizer/page/VisualizerPage.tsx +127 -0
- package/frontend/src/index.css +248 -0
- package/frontend/src/lib/api/client.ts +163 -0
- package/frontend/src/lib/contexts/AuthContext.tsx +157 -0
- package/frontend/src/lib/contexts/OnboardStepContext.tsx +68 -0
- package/frontend/src/lib/contexts/SocketContext.tsx +303 -0
- package/frontend/src/lib/contexts/ThemeContext.tsx +125 -0
- package/frontend/src/lib/hooks/useAuth.ts +4 -0
- package/frontend/src/lib/hooks/useConfirm.ts +55 -0
- package/frontend/src/lib/hooks/useInterval.ts +27 -0
- package/frontend/src/lib/hooks/useMediaQuery.ts +59 -0
- package/frontend/src/lib/hooks/useOnboardingCompletion.ts +29 -0
- package/frontend/src/lib/hooks/usePagination.ts +27 -0
- package/frontend/src/lib/hooks/useTimeout.ts +27 -0
- package/frontend/src/lib/hooks/useToast.tsx +229 -0
- package/frontend/src/lib/utils/constants.ts +38 -0
- package/frontend/src/lib/utils/utils.ts +165 -0
- package/frontend/src/lib/utils/validation-schemas.ts +126 -0
- package/frontend/src/main.tsx +16 -0
- package/frontend/src/rdg.css +194 -0
- package/frontend/src/vite-env.d.ts +12 -0
- package/frontend/tailwind.config.js +97 -0
- package/frontend/tsconfig.json +26 -0
- package/frontend/tsconfig.node.json +10 -0
- package/frontend/vite.config.ts +37 -0
- package/frontend/vitest.config.ts +36 -0
- package/functions/deno.json +25 -0
- package/functions/server.ts +290 -0
- package/functions/worker-template.js +126 -0
- package/openapi/ai.yaml +689 -0
- package/openapi/auth.yaml +563 -0
- package/openapi/functions.yaml +476 -0
- package/openapi/health.yaml +30 -0
- package/openapi/logs.yaml +224 -0
- package/openapi/metadata.yaml +178 -0
- package/openapi/records.yaml +382 -0
- package/openapi/secrets.yaml +371 -0
- package/openapi/storage.yaml +876 -0
- package/openapi/tables.yaml +464 -0
- package/package.json +88 -0
- package/shared-schemas/package.json +31 -0
- package/shared-schemas/src/ai-api.schema.ts +167 -0
- package/shared-schemas/src/ai.schema.ts +54 -0
- package/shared-schemas/src/auth-api.schema.ts +193 -0
- package/shared-schemas/src/auth.schema.ts +94 -0
- package/shared-schemas/src/database-api.schema.ts +259 -0
- package/shared-schemas/src/database.schema.ts +69 -0
- package/shared-schemas/src/functions-api.schema.ts +25 -0
- package/shared-schemas/src/functions.schema.ts +16 -0
- package/shared-schemas/src/index.ts +13 -0
- package/shared-schemas/src/logs-api.schema.ts +49 -0
- package/shared-schemas/src/logs.schema.ts +14 -0
- package/shared-schemas/src/metadata.schema.ts +56 -0
- package/shared-schemas/src/storage-api.schema.ts +65 -0
- package/shared-schemas/src/storage.schema.ts +19 -0
- package/shared-schemas/tsconfig.json +21 -0
- 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
|
+
}
|