diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index b0efe68a..e457eeb7 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -14,8 +14,9 @@ Please take a moment to review this guide. It will help you understand how to co 2. [Reporting Issues](#reporting-issues) 3. [Submitting Changes](#submitting-changes) 4. [Code Style Guidelines](#code-style-guidelines) -5. [Pull Request Process](#pull-request-process) -6. [Community Guidelines](#community-guidelines) +5. [E2E Tests](#e2e-tests) +6. [Pull Request Process](#pull-request-process) +7. [Community Guidelines](#community-guidelines) --- @@ -88,6 +89,14 @@ If you encounter a bug or have a suggestion for improvement, follow these steps: --- +## E2E Tests + +E2E tests now live in a dedicated repository: [Portabase/e2e-tests](https://github.com/Portabase/e2e-tests). + +If your contribution needs corresponding end-to-end test coverage, add it there instead of this repo. + +--- + ## Pull Request Process 1. Ensure your code passes all tests and linters. diff --git a/CITATION.cff b/CITATION.cff index cd4a56a3..3a38a986 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -33,5 +33,5 @@ keywords: - web-ui - agent license: Apache-2.0 -version: 1.18.2 +version: 1.20.0 date-released: '2026-03-02' diff --git a/Makefile b/Makefile index 1dea89d1..02ceac45 100644 --- a/Makefile +++ b/Makefile @@ -47,16 +47,10 @@ export-keycloak: @docker compose -f docker-compose.func.yml start keycloak >/dev/null 2>&1 @echo "Keycloak configuration and users exported to seeds/keycloak/*.json" -end-to-end: - @echo "Starting E2E testing..." - @docker compose -f docker-compose.e2e.yml up -d - @CI=true PROJECT_URL=http://localhost:8887 pnpm playwright test --project=chromium || (docker compose -f docker-compose.e2e.yml down --volumes && exit 1) - @docker compose -f docker-compose.e2e.yml down --volumes - @echo "Finished E2E testing successfully." +seed-blob: + @chmod +x seeds/azurite/azurite-seed.sh + @bash seeds/azurite/azurite-seed.sh -e2e-manual: - @echo "Starting E2E testing..." - @docker compose -f docker-compose.e2e.yml up -d - @pnpm playwright test --ui || (docker compose -f docker-compose.e2e.yml down --volumes && exit 1) - @docker compose -f docker-compose.e2e.yml down --volumes - @echo "Finished E2E testing successfully." \ No newline at end of file +list-blob: + @chmod +x seeds/azurite/azurite-list.sh + @bash seeds/azurite/azurite-list.sh \ No newline at end of file diff --git a/README.md b/README.md index 0c7ab5ec..7d737455 100644 --- a/README.md +++ b/README.md @@ -54,12 +54,13 @@ ## Installation -You have 4 ways to install Portabase: +You have 5 ways to install Portabase: - Automated CLI (recommended) - [details](https://portabase.io/docs/dashboard/setup#cli) - Docker Run - [details](https://portabase.io/docs/dashboard/setup#docker) - Docker Compose setup - [details](https://portabase.io/docs/dashboard/setup#docker-compose) -- Kubernetes with Helm [details](https://portabase.io/docs/dashboard/setup#helm) +- Unraid Community Apps - [details](https://ca.unraid.net/apps/portabase-dashboard-1sdc97m05ufd7q) - [unraid support thread](https://forums.unraid.net/topic/199072-support-portabase-dashboard-agent-dockers/) +- Kubernetes with Helm - [details](https://portabase.io/docs/dashboard/setup#helm) - Development setup - [details](https://portabase.io/docs/dashboard/setup#development) **Ensure Docker is installed on your machine before getting started.** diff --git a/docker-compose.e2e.yml b/docker-compose.e2e.yml deleted file mode 100644 index d0412db7..00000000 --- a/docker-compose.e2e.yml +++ /dev/null @@ -1,37 +0,0 @@ -services: - app: - build: - context: . - dockerfile: docker/dockerfile/Dockerfile - target: prod - ports: - - '8887:80' - environment: - TZ: "Europe/Paris" - env_file: - - .env - volumes: - - portabase-e2e-data:/data - depends_on: - db: - condition: service_healthy - - db: - image: postgres:18-alpine - ports: - - "5433:5432" - volumes: - - postgres-e2e-data:/var/lib/postgresql/data - environment: - - POSTGRES_DB=devdb - - POSTGRES_USER=devuser - - POSTGRES_PASSWORD=changeme - healthcheck: - test: ["CMD-SHELL", "pg_isready -U devuser -d devdb"] - interval: 10s - timeout: 5s - retries: 5 - -volumes: - portabase-e2e-data: - postgres-e2e-data: diff --git a/docker-compose.yml b/docker-compose.yml index f81c2324..033f2ba9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -41,5 +41,25 @@ services: - "8025:8025" - "1025:1025" + azurite: + image: mcr.microsoft.com/azure-storage/azurite + command: azurite --blobHost 0.0.0.0 --skipApiVersionCheck + ports: + - "10000:10000" + volumes: + - azurite-data:/data + extra_hosts: + - "localhost:host-gateway" + networks: + - portabase + + # DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;QueueEndpoint=http://127.0.0.1:10001/devstoreaccount1;TableEndpoint=http://127.0.0.1:10002/devstoreaccount1; + volumes: postgres-data: + azurite-data: + +networks: + portabase: + name: portabase_network + external: true diff --git a/e2e/access-management.spec.ts b/e2e/access-management.spec.ts deleted file mode 100644 index bc1a7b45..00000000 --- a/e2e/access-management.spec.ts +++ /dev/null @@ -1,74 +0,0 @@ -import {expect, test} from "@playwright/test"; -import {users} from "./helpers/auth"; -import {changeUserRole, create, switchToDefault} from "./helpers/access-management"; -import {LOCAL_STORAGE_PATH} from "./helpers/session"; - -test.use({storageState: LOCAL_STORAGE_PATH}); - -const firstOrganization = "Organization A"; -const secondOrganization = "Organization B"; - -test.describe.serial(() => { - test("Create an organization from organizations page", async ({page}) => { - await page.goto("/dashboard/admin/organizations"); - await expect(page.getByRole("heading", {name: "Active organizations"})).toBeVisible(); - - await create(page, "button", firstOrganization); - - await expect(page.getByText(/Organization has been successfully created\./i)).toBeVisible(); - await expect(page).toHaveURL("/dashboard/admin/organizations"); - await expect(page.getByRole("button", {name: firstOrganization, exact: true})).toBeVisible(); - - await switchToDefault(page); - }); - - test("Create an organization from sidebar button", async ({page}) => { - await page.goto("/dashboard/home"); - await expect(page.getByRole("heading", {name: "Dashboard"})).toBeVisible(); - - await create(page, "sidebar", secondOrganization); - - await expect(page.getByText(/Organization has been successfully created\./i)).toBeVisible(); - await expect(page).toHaveURL("/dashboard/home"); - await expect(page.getByRole("button", {name: secondOrganization, exact: true})).toBeVisible(); - - await switchToDefault(page); - }); - - test("Change John Doe's role from pending to user", async ({page}) => { - await page.goto("/dashboard/admin/users"); - await expect(page.getByText(users.normal.email, {exact: true})).toBeVisible(); - - const userRow = page.locator("tr").filter({hasText: users.normal.email}).first(); - await expect(userRow).toBeVisible(); - await userRow.locator("button").last().click(); - await page.getByRole('menuitem', {name: 'Role'}).click(); - - await expect(page.getByRole("heading", {name: "Change the user's role"})).toBeVisible(); - - await changeUserRole(page, "user") - - await expect(page.getByText("User role changed successfully.")).toBeVisible(); - await expect(page.locator("tr").filter({hasText: users.normal.email}).getByText("user", {exact: true})).toBeVisible(); - }); - - test("Add John Doe to Organization A", async ({page}) => { - await page.goto("/dashboard/admin/organizations"); - await expect(page.getByRole("heading", {name: "Active organizations"})).toBeVisible(); - - const organizationRow = page.locator("tr").filter({hasText: firstOrganization}).first(); - await expect(organizationRow).toBeVisible(); - await organizationRow.locator('a[href*="/dashboard/admin/organizations/"], a[href*="organizations/"]').click(); - - await expect(page.getByText(firstOrganization, {exact: true})).toBeVisible(); - await page.getByRole("button", {name: /Add member/i}).click(); - await expect(page.getByRole("heading", {name: "Add member to your organization"})).toBeVisible(); - - await page.getByPlaceholder("Enter a user email").fill(users.normal.email); - await page.getByRole("option", {name: new RegExp(users.normal.email, "i")}).click(); - await page.getByRole("button", {name: "Confirm"}).click(); - - await expect(page.getByText("Member successfully added!")).toBeVisible(); - await expect(page.getByText(users.normal.email, {exact: true})).toBeVisible(); - }); -}); diff --git a/e2e/agent.spec.ts b/e2e/agent.spec.ts deleted file mode 100644 index 893eeaaa..00000000 --- a/e2e/agent.spec.ts +++ /dev/null @@ -1,116 +0,0 @@ -import {expect, test} from "@playwright/test"; -import {execSync} from "child_process"; -import fs from "fs"; -import os from "os"; -import path from "path"; -import {createAgentWithDockerDatabases} from "./helpers/agent-cli"; -import {create, edit, get, remove} from "./helpers/agent"; -import {LOCAL_STORAGE_PATH} from "./helpers/session"; - -const agent = { - aName: "Agent A", - aUpdatedName: "Agent A Updated", - bName: "Agent B", - description: "Agent created by Playwright E2E", - updatedDescription: "Agent updated by Playwright E2E", -}; - -test.use({storageState: LOCAL_STORAGE_PATH}); - -test.describe.serial(() => { - let agentWorkspace: string | null = null; - - test("Create agent A from empty state", async ({page}) => { - await page.goto("/dashboard/agents"); - await expect(page.getByRole("heading", {name: "Agents"})).toBeVisible(); - await expect(page.getByText("Create new Agent", {exact: true})).toBeVisible(); - await create(page, "emptyState", agent.aName, agent.description); - - await expect(page.getByText("Success creating agent")).toBeVisible(); - await expect(get(page, agent.aName)).toBeVisible(); - await expect(page.getByText("Create new Agent", {exact: true})).toHaveCount(0); - }); - - test("Edit agent A", async ({page}) => { - await page.goto("/dashboard/agents"); - await expect(page.getByRole("heading", {name: "Agents"})).toBeVisible(); - await expect(get(page, agent.aName)).toBeVisible(); - - await edit(page, agent.aName, agent.aUpdatedName, agent.updatedDescription); - - await expect(page.getByText("Success updating agent")).toBeVisible(); - await expect(page.getByText(agent.aUpdatedName, {exact: true})).toBeVisible(); - await expect(page.getByText(agent.updatedDescription, {exact: true})).toBeVisible(); - }); - - test("Create agent B from classic button", async ({page}) => { - await page.goto("/dashboard/agents"); - await expect(page.getByRole("heading", {name: "Agents"})).toBeVisible(); - await expect(page.getByRole("button", {name: /Create Agent/i})).toBeVisible(); - await create(page, "button", agent.bName, agent.description); - - await expect(page.getByText("Success creating agent")).toBeVisible(); - await expect(get(page, agent.bName)).toBeVisible(); - }); - - test("Delete Agent B", async ({page}) => { - await page.goto("/dashboard/agents"); - await expect(page.getByRole("heading", {name: "Agents"})).toBeVisible(); - await expect(get(page, agent.bName)).toBeVisible(); - await remove(page, agent.bName); - - await expect(page.getByText("Agent has been successfully deleted.")).toBeVisible(); - await expect(page).toHaveURL("/dashboard/agents"); - await expect(page.getByText(agent.bName)).toHaveCount(0); - }); - - // test("Launch the updated agent", async ({page}) => { - // await page.goto("/dashboard/agents"); - // await expect(page.getByRole("heading", {name: "Agents"})).toBeVisible(); - // await get(page, agent.aName).click(); - // - // await expect(page).toHaveURL(/\/dashboard\/agents\/.+/); - // await expect(page.getByText(agent.aName, {exact: true})).toBeVisible(); - // await expect(page.getByText("Registration & Setup")).toBeVisible(); - // - // const commandInput = page.locator("input[readonly]").first(); - // await page.locator("input[readonly]").first().locator("xpath=following-sibling::button[1]").click(); - // const command = await commandInput.inputValue(); - // - // agentWorkspace = fs.mkdtempSync(path.join(os.tmpdir(), "portabase-agent-")); - // await createAgentWithDockerDatabases(command, agentWorkspace); - // execSync(`portabase start "${agent.aName}"`, { - // cwd: agentWorkspace, - // stdio: "pipe", - // timeout: 120_000, - // }); - // - // await expect(page.getByText("Never connected.")).toHaveCount(0, {timeout: 120_000}); - // await expect(page.getByText("Action Required")).toHaveCount(0); - // }); - - test.afterAll(async () => { - if (agentWorkspace) { - try { - execSync(`portabase stop "${agent.aName}"`, { - cwd: agentWorkspace, - stdio: "pipe", - timeout: 30_000, - }); - } catch { - } - - try { - execSync(`portabase uninstall --force "${agent.aName}"`, { - cwd: agentWorkspace, - stdio: "pipe", - timeout: 30_000, - }); - } catch { - } - - fs.rmSync(agentWorkspace, {recursive: true, force: true}); - agentWorkspace = null; - } - }); -}); \ No newline at end of file diff --git a/e2e/auth.spec.ts b/e2e/auth.spec.ts deleted file mode 100644 index 34872cdb..00000000 --- a/e2e/auth.spec.ts +++ /dev/null @@ -1,104 +0,0 @@ -import {test, expect} from '@playwright/test'; -import {login, register, users} from "./helpers/auth"; -import {LOCAL_STORAGE_PATH} from "./helpers/session"; - -const TIMEOUT = undefined -// const TIMEOUT = 5000 - -test.use({storageState: LOCAL_STORAGE_PATH}) - -test.describe.serial( () => { - - test('Redirect to login if not connected', async ({page}) => { - await page.goto('/dashboard/projects'); - await expect(page).toHaveURL("login?redirect=%2Fdashboard%2Fprojects", {timeout: TIMEOUT}); - }); - - test('Password too short', async ({page}) => { - await page.goto('') - await page.click('text=Sign up') - await expect(page).toHaveURL('/register') - - let password = '123456' - await register(page, users["admin"].username, users["admin"].email, password, password) - - const toast = page.locator('text=Must have at least 8 character') - await expect(toast).toBeVisible() - }) - - test('Password too simple', async ({page}) => { - await page.goto('') - await page.click('text=Sign up') - await expect(page).toHaveURL('/register') - - let password = '12345678' - await register(page, users["admin"].username, users["admin"].email, password, password) - - const toast = page.locator('text=Your password must contain at least one uppercase letter, one lowercase letter, one number, and one special character.') - await expect(toast).toBeVisible() - }) - - test('Password and confirm password mismatch', async ({page}) => { - await page.goto('/') - await page.click('text=Sign up') - await expect(page).toHaveURL('/register') - - let password = 'testPASS123456!' - let confirmPassword = 'testPASS123456!!' - await register(page, users["admin"].username, users["admin"].email, password, confirmPassword) - - const toast = page.locator('text=The passwords did not match') - await expect(toast).toBeVisible() - }) - - test('Successful register for admin', async ({page}) => { - await page.goto('/') - - await page.click('text=Sign up') - await expect(page).toHaveURL('/register') - - await register(page, users["admin"].username, users["admin"].email, users["admin"].password, users["admin"].password) - - await expect(page).toHaveURL('/login', {timeout: TIMEOUT}) - }) - - test('User already exists.', async ({page}) => { - await page.goto('/') - - await page.click('text=Sign up') - await expect(page).toHaveURL('/register') - - await register(page, users["admin"].username, users["admin"].email, users["admin"].password, users["admin"].password) - - const toast = page.locator('text=User already exists. Use another email.') - await expect(toast).toBeVisible() - }) - - test('Successful register for normal', async ({page}) => { - await page.goto('/') - - await page.click('text=Sign up') - await expect(page).toHaveURL('/register') - - await register(page, users["normal"].username, users["normal"].email, users["normal"].password, users["normal"].password) - - await expect(page).toHaveURL('/login', {timeout: TIMEOUT}) - }) - - test('Failed login because account not active', async ({page}) => { - await page.goto('/login') - await login(page, users["normal"].email, users["normal"].password) - - const toast = page.locator('text=Your account is not active.') - await expect(toast).toBeVisible() - }) - - test('Successful login', async ({page}) => { - await page.goto('/login') - await login(page, users["admin"].email, users["admin"].password) - - await expect(page).toHaveURL('/dashboard/home', {timeout: TIMEOUT}) - await expect(page.getByRole('link', {name: 'Logo Portabase'})).toBeVisible() - await page.context().storageState({path: LOCAL_STORAGE_PATH}) - }) -}) diff --git a/e2e/cleanup.spec.ts b/e2e/cleanup.spec.ts deleted file mode 100644 index 95fe7fe9..00000000 --- a/e2e/cleanup.spec.ts +++ /dev/null @@ -1,32 +0,0 @@ -import {expect, test} from "@playwright/test"; -import fs from "fs"; -import {logout} from "./helpers/auth"; -import {LOCAL_STORAGE_PATH} from "./helpers/session"; - - -test.use({storageState: LOCAL_STORAGE_PATH}); - -test.describe( () => { - test.afterAll(async () => { - if (fs.existsSync(LOCAL_STORAGE_PATH)) { - fs.unlinkSync(LOCAL_STORAGE_PATH); - } - }); - - test("Remove shared storage state", async ({page}) => { - await test.step("Logout shared authenticated session", async () => { - if (!fs.existsSync(LOCAL_STORAGE_PATH)) { - return; - } - - const content = fs.readFileSync(LOCAL_STORAGE_PATH, "utf-8").trim(); - if (!content || content === "{}") { - return; - } - - await page.goto('/dashboard/home'); - await logout(page); - await expect(page).toHaveURL(/\/login(?:\?.*)?$/); - }); - }); -}); diff --git a/e2e/helpers/access-management.ts b/e2e/helpers/access-management.ts deleted file mode 100644 index dea6286d..00000000 --- a/e2e/helpers/access-management.ts +++ /dev/null @@ -1,70 +0,0 @@ -import {Page} from "@playwright/test"; - -export function getUserRow(page: Page, email: string) { - return page.locator("tr").filter({hasText: email}).first(); -} - -export function getOrganizationRow(page: Page, organizationName: string) { - return page.locator("tr").filter({hasText: organizationName}).first(); -} - -/** - * Create an organization from the selected entrypoint. - * - * Available entrypoints: - * - `button`: the organizations page create button. - * - `sidebar`: the sidebar organization switcher. - * - * Executes from: - * - `/dashboard/admin/organizations` for "button" entrypoint - * - any `/dashboard/**` page for "sidebar" entrypoint - */ -export async function create(page: Page, entrypoint: "button" | "sidebar", name: string) { - if (entrypoint === "sidebar") { - await page.getByRole("button", {name: "Default Organization"}).click(); - await page.getByText("Create organization", {exact: true}).click(); - } else { - await page.getByRole("button", {name: /Create a new organization/i}).click(); - } - - await page.getByLabel("Name").fill(name); - await page.getByRole("button", {name: "Create"}).click(); -} - -/** - * Switch the active organization from the sidebar switcher. - * - * Executes from: any `/dashboard/**` if the sidebar is visible. - */ -export async function switchTo(page: Page, name: string) { - await page.getByTestId('organization-dropdown').click(); - await page.getByRole('menuitem', {name: name}).click(); -} - -/** - * Switch back to the default organization. - * - * Executes from: any `/dashboard/**` if the sidebar is visible. - */ -export async function switchToDefault(page: Page) { - await switchTo(page, "Default Organization"); -} - -const ROLE_LABELS = { - pending: "Pending", - user: "User", - admin: "Admin", -} as const; - -/** - * Change the global role of a user from the admin users role modal. - * - * Executes from: the "Change the user's role" dialog opened from `/dashboard/admin/users`. - */ -export async function changeUserRole(page: Page, role: keyof typeof ROLE_LABELS) { - const roleOption = ROLE_LABELS[role]; - - await page.getByRole("combobox").click(); - await page.getByRole("option", {name: roleOption}).click(); - await page.getByRole("button", {name: "Validate"}).click(); -} \ No newline at end of file diff --git a/e2e/helpers/agent-cli.ts b/e2e/helpers/agent-cli.ts deleted file mode 100644 index aec53c6d..00000000 --- a/e2e/helpers/agent-cli.ts +++ /dev/null @@ -1,107 +0,0 @@ -import * as pty from "node-pty"; - -type InteractiveStep = { - match: RegExp; - reply: string; -}; - -function normalizeOutput(output: string) { - return output.replace(/\u001b\[[0-9;?]*[ -/]*[@-~]/g, ""); -} - -/** - * Run an interactive CLI command and answer prompts in sequence. - * Entry point: - * - `command` is the full CLI command executed in the PTY. - * - `cwd` is the local workspace where the command is launched. - * - `steps` defines which prompt text is matched and which reply is sent. - * - `timeoutMs` controls when the command is aborted if it stalls. - * Executes from: not page-bound; runs from a local test workspace on the Node.js side. - */ -export async function runInteractiveCommand( - command: string, - cwd: string, - steps: InteractiveStep[], - timeoutMs: number = 120_000, -) { - await new Promise((resolve, reject) => { - const child = pty.spawn("sh", ["-lc", command], { - cwd, - env: { - ...process.env, - TERM: "xterm-256color", - }, - cols: 120, - rows: 30, - }); - - let currentStep = 0; - let output = ""; - const timer = setTimeout(() => { - child.kill(); - reject(new Error(`Interactive command timed out.\n\nCommand: ${command}\n\nOutput:\n${normalizeOutput(output)}`)); - }, timeoutMs); - - const handleChunk = (chunk: string) => { - output += chunk; - const normalizedOutput = normalizeOutput(output); - - while (currentStep < steps.length && steps[currentStep].match.test(normalizedOutput)) { - child.write(steps[currentStep].reply); - currentStep += 1; - } - }; - - child.onData(handleChunk); - child.onExit(({exitCode}) => { - clearTimeout(timer); - if (exitCode === 0) { - resolve(); - return; - } - - reject(new Error(`Interactive command failed with exit code ${exitCode}.\n\nCommand: ${command}\n\nOutput:\n${normalizeOutput(output)}`)); - }); - }); -} - -/** - * Create and configure a Portabase agent with two Docker-backed databases. - * Entry point: - * - `command` is the CLI setup command copied from the agent details page. - * - `cwd` is the local workspace where the agent is installed. - * - this helper answers the wizard for PostgreSQL first, then MariaDB. - * Executes from: not page-bound; runs from a local test workspace on the Node.js side. - */ -export async function createAgentWithDockerDatabases(command: string, cwd: string) { - await runInteractiveCommand(command, cwd, [ - { - match: /configure.*database|add.*database/i, - reply: "y\n", - }, - { - match: /docker.*new local container|manual.*external|external.*existing/i, - reply: "\r", - }, - { - match: /postgresql|mariadb/i, - reply: "\r", - }, - { - match: /another.*database|add.*another|configure.*another/i, - reply: "y\n", - }, - { - match: /docker.*new local container|manual.*external|external.*existing/i, - reply: "\r", - }, - { - match: /postgresql|mariadb/i, - reply: "\u001B[B\r", - }, - { - match: /another.*database|add.*another|configure.*another/i, - reply: "n\n", - }, - ]); -} diff --git a/e2e/helpers/agent.ts b/e2e/helpers/agent.ts deleted file mode 100644 index 565f106f..00000000 --- a/e2e/helpers/agent.ts +++ /dev/null @@ -1,66 +0,0 @@ -import {Page} from "@playwright/test"; - - -/** - * Locate an agent card in the list. - - * Executes from: `/dashboard/agents`. - */ -export function get(page: Page, name: string) { - return page.locator('a[href^="/dashboard/agents"]').filter({hasText: name}).first(); -} - -/** - * Create an agent from the selected entrypoint. - * - * Available entrypoints: - * - `button`: the classic create button. - * - `emptyState`: the empty-state CTA. - * - `auto`: uses the classic create button and falls back to the empty-state CTA. - * - * Executes from: `/dashboard/agents`. - */ -export async function create(page: Page, entrypoint: "auto" | "emptyState" | "button" = "auto", agentName: string, description: string) { - if (entrypoint === "auto") { - const createButton = page.getByRole("button", {name: /Create Agent/i}); - if (await createButton.isVisible()) await page.getByRole("button", {name: /Create Agent/i}).click(); - await page.getByText("Create new Agent", {exact: true}).click(); - } else if (entrypoint === "button") { - await page.getByRole("button", {name: /Create Agent/i}).click(); - } else if (entrypoint === "emptyState") { - await page.getByText("Create new Agent", {exact: true}).click(); - } - - await page.getByLabel("Name").fill(agentName); - await page.getByLabel("Description").fill(description); - await page.getByRole("button", {name: "Create"}).click(); -} - -/** - * Edit an existing agent (name and description) from its details page. - * - * Executes from: `/dashboard/agents/[agentId]`. - */ -export async function edit(page: Page, currentName: string, updatedName: string, updatedDescription: string) { - await get(page, currentName).click(); - - await page - .getByRole("button", {name: /Delete Agent/i}) - .locator("xpath=ancestor::div[1]/preceding-sibling::div[1]/*[1]") - .click(); - - await page.getByLabel("Name").fill(updatedName); - await page.getByLabel("Description").fill(updatedDescription); - await page.getByRole("button", {name: "Update"}).click(); -} - -/** - * Delete an agent from its details page. - - * Executes from: `/dashboard/agents/[agentId]`. - */ -export async function remove(page: Page, name: string) { - await get(page, name).click(); - await page.getByRole("button", {name: /Delete Agent/i}).click(); - await page.getByRole("button", {name: "Delete", exact: true}).click(); -} diff --git a/e2e/helpers/auth.ts b/e2e/helpers/auth.ts deleted file mode 100644 index 9835c03b..00000000 --- a/e2e/helpers/auth.ts +++ /dev/null @@ -1,50 +0,0 @@ -import {Page} from "@playwright/test"; - -export type UserCredentials = { - username: string - email: string - password: string -} - -export const users: Record = { - admin: {username: "Admin", email: "admin@example.com", password: "testPASS123456!"}, - normal: {username: "John Doe", email: "john.doe@example.com", password: "testPASS123456!"}, -} - -/** - * Fill and submit the registration form. - * - * Executes from: `/register`. - */ -export async function register(page: Page, name: string, email: string, password: string, confirmPassword: string) { - await page.locator('input[name="name"]').fill(name) - await page.locator('input[name="email"]').fill(email) - await page.locator('input[name="password"]').fill(password) - await page.locator('input[name="confirmPassword"]').fill(confirmPassword) - - await page.click('button[type="submit"]') -} - -/** - * Fill and submit the login form. - * - * Executes from: `/login`. - */ -export async function login(page: Page, email: string, password: string) { - await page.locator('input[name="email"]').fill(email) - await page.locator('input[name="password"]').fill(password) - - await page.locator('button:has-text("Login")').click() -} - -/** - * Log out the current authenticated user. - * - * Executes from: any authenticated `/dashboard/**` page. - */ -export async function logout(page: Page) { - const profileButton = page.getByTestId('profile-dropdown') - await profileButton.first().click(); - - await page.getByRole("menuitem").filter({hasText: /Logout/i}).click(); -} diff --git a/e2e/helpers/env.ts b/e2e/helpers/env.ts deleted file mode 100644 index ba4d6f84..00000000 --- a/e2e/helpers/env.ts +++ /dev/null @@ -1,62 +0,0 @@ -const REQUIRED_E2E_ENV_VARS = [ - "E2E_NOTIFICATION_SMTP_HOST", - "E2E_NOTIFICATION_SMTP_PORT", - "E2E_NOTIFICATION_SMTP_USER", - "E2E_NOTIFICATION_SMTP_PASSWORD", - "E2E_NOTIFICATION_SMTP_FROM", - "E2E_NOTIFICATION_SMTP_TO", - "E2E_NOTIFICATION_SLACK_WEBHOOK", - "E2E_NOTIFICATION_DISCORD_WEBHOOK", - "E2E_NOTIFICATION_TELEGRAM_BOT_TOKEN", - "E2E_NOTIFICATION_TELEGRAM_CHAT_ID", - "E2E_NOTIFICATION_TELEGRAM_TOPIC_ID", - "E2E_NOTIFICATION_GOTIFY_SERVER_URL", - "E2E_NOTIFICATION_GOTIFY_APP_TOKEN", - "E2E_NOTIFICATION_NTFY_TOPIC", - "E2E_NOTIFICATION_NTFY_SERVER_URL", - "E2E_NOTIFICATION_NTFY_TOKEN", - "E2E_NOTIFICATION_NTFY_USERNAME", - "E2E_NOTIFICATION_NTFY_PASSWORD", - "E2E_NOTIFICATION_WEBHOOK_URL", - "E2E_NOTIFICATION_WEBHOOK_SECRET_HEADER", - "E2E_NOTIFICATION_WEBHOOK_SECRET", - "E2E_STORAGE_AWS_S3_ENDPOINT_URL", - "E2E_STORAGE_AWS_S3_REGION", - "E2E_STORAGE_AWS_S3_ACCESS_KEY", - "E2E_STORAGE_AWS_S3_SECRET_KEY", - "E2E_STORAGE_AWS_S3_BUCKET_NAME", - "E2E_STORAGE_AWS_S3_PORT", - "E2E_STORAGE_R2_ENDPOINT_URL", - "E2E_STORAGE_R2_REGION", - "E2E_STORAGE_R2_ACCESS_KEY", - "E2E_STORAGE_R2_SECRET_KEY", - "E2E_STORAGE_R2_BUCKET_NAME", - "E2E_STORAGE_R2_PORT", - "E2E_STORAGE_GOOGLE_DRIVE_CLIENT_ID", - "E2E_STORAGE_GOOGLE_DRIVE_CLIENT_SECRET", - "E2E_STORAGE_GOOGLE_DRIVE_FOLDER_ID", -] as const; - -function assertRequiredEnvVars() { - const missingVars = REQUIRED_E2E_ENV_VARS.filter((name) => { - const value = process.env[name]?.trim(); - return !value; - }); - - if (missingVars.length > 0) { - throw new Error( - `Missing required environment variables:\n${missingVars.map((name) => `- ${name}`).join("\n")}`, - ); - } -} - -assertRequiredEnvVars(); - -export function getEnv(name: string): string { - const value = process.env[name]?.trim(); - if (!value) { - throw new Error(`Missing required environment variable: ${name}`); - } - - return value; -} diff --git a/e2e/helpers/notification.ts b/e2e/helpers/notification.ts deleted file mode 100644 index 9d54983d..00000000 --- a/e2e/helpers/notification.ts +++ /dev/null @@ -1,94 +0,0 @@ -import {Page} from "@playwright/test"; - - -/** - * Locate a notification channel card in the list. - * - * Executes from: `/dashboard/notifications/channels`. - */ -export function get(page: Page, channelName: string) { - return page.locator('div.block.transition-all.duration-200.rounded-xl', { - has: page.locator('h3', {hasText: channelName}), - }).first(); -} - -/** - * Open the edit dialog for an existing notification channel. - * - * Executes from: `/dashboard/notifications/channels`. - */ -export async function edit(page: Page, channelName: string) { - const card = get(page, channelName); - await card.locator("button").nth(1).click(); -} - -/** - * Delete an existing notification channel. - * - * Executes from: `/dashboard/notifications/channels`. - */ -export async function remove(page: Page, channelName: string) { - const card = get(page, channelName); - await card.locator("button").nth(2).click(); - await page.getByRole("button", {name: "Delete"}).click(); -} - -/** - * Fill the notification channel creation form. - * - * Available entrypoints: - * - `button`: the classic add button. - * - `emptyState`: the empty-state CTA. - * - `auto` use the classic add button and falls back to the empty-state CTA. - * - * Executes from: `/dashboard/notifications/channels`. - */ -export async function create( - page: Page, - provider: "Discord" | "Gotify" | "ntfy.sh" | "Slack" | "Email" | "Telegram" | "Webhook", - channelName: string, - fillConfig: (page: Page) => Promise, - entrypoint: "auto" | "emptyState" | "button" = "auto", -) { - if (entrypoint === "auto") { - const addButton = page.getByRole("button", {name: /Add notification channel/i}); - if (await addButton.isVisible()) await page.getByRole("button", {name: /Add notification channel/i}).click(); - else await page.getByRole("button", {name: /No notification channels configured yet/i}).click(); - } else if (entrypoint === "button") { - await page.getByRole("button", {name: /Add notification channel/i}).click(); - } else if (entrypoint === "emptyState") { - await page.getByRole("button", {name: /No notification channels configured yet/i}).click(); - } - - await page.getByText(provider, {exact: true}).click(); - await page.getByLabel(/Channel Name/).fill(channelName); - await fillConfig(page); -} - -/** - * Submit the notification channel creation form. - * - * Executes from: the add notification channel dialog opened from `/dashboard/notifications/channels`. - */ -export async function submit(page: Page) { - await page.getByRole("button", {name: "Add Channel"}).click(); -} - -/** - * Open the edit dialog for a notification channel and trigger the test action. - * - * Executes from: `/dashboard/notifications/channels`. - */ -export async function testFromEdit(page: Page, channelName: string) { - await edit(page, channelName); - await page.getByRole("button", {name: /Test Channel/i}).click(); -} - -/** - * Close the current notification channel dialog without saving. - * - * Executes from: the add or edit notification channel dialog. - */ -export async function cancel(page: Page) { - await page.getByRole("button", {name: "Cancel"}).click(); -} diff --git a/e2e/helpers/project.ts b/e2e/helpers/project.ts deleted file mode 100644 index ee788866..00000000 --- a/e2e/helpers/project.ts +++ /dev/null @@ -1,59 +0,0 @@ -import {Page} from "@playwright/test"; - - -/** - * Locate a project card in the list. - * - * Executes from: `/dashboard/projects`. - */ -export function get(page: Page, projectName: string) { - return page.locator('a[href^="/dashboard/projects/"]').filter({hasText: projectName}).first(); -} - -/** - * Create a project from the selected entrypoint. - * - * Available entrypoints: - * - `emptyState`: the empty-state CTA. - * - `button`: the classic create button. - * - * * Executes from: `/dashboard/projects`. - */ -export async function create(page: Page, entrypoint: "emptyState" | "button", projectName: string) { - if (entrypoint === "emptyState") { - await page.getByText("Create new Project", {exact: true}).click(); - } else { - await page.getByRole("button", {name: /Create Project/i}).click(); - } - - await page.getByLabel("Name").fill(projectName); - await page.getByRole("button", {name: "Create"}).click(); -} - -/** - * Edit an existing project from its details page. - * - * Executes from: `/dashboard/projects/[projectId]`. - */ -export async function edit(page: Page, currentName: string, updatedName: string) { - await get(page, currentName).click(); - - await page - .getByRole("button", {name: /Delete Project/i}) - .locator("xpath=ancestor::div[1]/preceding-sibling::div[1]/*[1]") - .click(); - - await page.getByLabel("Name").fill(updatedName); - await page.getByRole("button", {name: "Update"}).click(); -} - -/** - * Delete an existing project from its details page. - * - * Executes from: `/dashboard/projects/[projectId]`. - * */ -export async function remove(page: Page, projectName: string) { - await get(page, projectName).click(); - await page.getByRole("button", {name: /Delete Project/i}).click(); - await page.getByRole("button", {name: "Delete", exact: true}).click(); -} diff --git a/e2e/helpers/session.ts b/e2e/helpers/session.ts deleted file mode 100644 index 938f8b3e..00000000 --- a/e2e/helpers/session.ts +++ /dev/null @@ -1 +0,0 @@ -export const LOCAL_STORAGE_PATH = "./e2e/local-storage.json"; diff --git a/e2e/helpers/storage.ts b/e2e/helpers/storage.ts deleted file mode 100644 index f8a7a606..00000000 --- a/e2e/helpers/storage.ts +++ /dev/null @@ -1,103 +0,0 @@ -import {Page} from "@playwright/test"; - - -/** - * Locate a storage channel card in the channels list. - * - * Executes from: `/dashboard/storages/channels`. - */ -export function get(page: Page, channelName: string) { - return page.locator('div.block.transition-all.duration-200.rounded-xl', { - has: page.locator('h3', {hasText: channelName}), - }).first(); -} - -/** - * Fill the storage channel creation form. - * - * Available entrypoints: - * - `button`: the classic add button. - * - `emptyState`: the empty-state CTA. - * - `auto`: uses the classic add button and falls back to the empty-state CTA. - * - * Executes from: `/dashboard/storages/channels`. - */ -export async function create( - page: Page, - provider: "S3" | "Google Drive", - channelName: string, - fillConfig: (page: Page) => Promise, - entrypoint: "auto" | "emptyState" | "button" = "auto", -) { - if (entrypoint === "auto") { - const addButton = page.getByRole("button", {name: /Add storage channel/i}); - if (await addButton.isVisible()) await page.getByRole("button", {name: /Add storage channel/i}).click(); - else await page.getByText("No storage channels configured yet", {exact: true}).click(); - } else if (entrypoint === "button") { - await page.getByRole("button", {name: /Add storage channel/i}).click(); - } else if (entrypoint === "emptyState") { - await page.getByText("No storage channels configured yet", {exact: true}).click(); - } - - await page.getByText(provider, {exact: true}).click(); - await page.getByLabel(/Channel Name/).fill(channelName); - await fillConfig(page); -} - -/** - * Open the edit dialog for an existing storage channel. - * - * Executes from: `/dashboard/storages/channels`. - */ -export async function edit(page: Page, channelName: string) { - const card = get(page, channelName); - await card.locator("button").nth(1).click(); -} - -/** - * Delete an existing storage channel. - * - * Executes from: `/dashboard/storages/channels`. - */ -export async function remove(page: Page, channelName: string) { - const card = get(page, channelName); - await card.locator("button").nth(2).click(); - await page.getByRole("button", {name: "Delete"}).click(); -} - -/** - * Trigger the storage connection test in the current dialog. - * - * Executes from: the add or edit storage channel dialog opened from `/dashboard/storages/channels`. - */ -export async function testConnection(page: Page) { - await page.getByRole("button", {name: /Test Storage/i}).click(); -} - -/** - * Submit the storage channel creation form. - * - * Executes from: the add storage channel dialog opened from `/dashboard/storages/channels`. - */ -export async function submit(page: Page) { - await page.getByRole("button", {name: "Add Channel"}).click(); -} - -/** - * Open the edit dialog for a storage channel and trigger the test action. - * - * Executes from: `/dashboard/storages/channels`. - */ -export async function testFromEdit(page: Page, channelName: string) { - await edit(page, channelName); - await testConnection(page); -} - -/** - * Close the current storage channel dialog without saving. - * - * Executes from: the add or edit storage channel dialog. - */ -export async function cancel(page: Page) { - await page.getByRole("button", {name: "Cancel"}).click(); -} diff --git a/e2e/notification/discord.spec.ts b/e2e/notification/discord.spec.ts deleted file mode 100644 index 4e5f4706..00000000 --- a/e2e/notification/discord.spec.ts +++ /dev/null @@ -1,71 +0,0 @@ -import {expect, test} from "@playwright/test"; -import { - cancel, create, get, remove, submit, testFromEdit, -} from "../helpers/notification"; -import {getEnv} from "../helpers/env"; -import {LOCAL_STORAGE_PATH} from "../helpers/session"; - -test.use({storageState: LOCAL_STORAGE_PATH}); - -const validChannelName = "Discord E2E Required"; -const invalidChannelName = "Discord E2E Invalid"; - -// test.describe.serial("Valid channel", () => { -// test("Create and test a valid Discord channel", async ({page}) => { -// await page.goto("/dashboard/notifications/channels"); -// await expect(page.getByRole("heading", {name: "Notification channels"})).toBeVisible(); -// await create(page, "Discord", validChannelName, async (page) => { -// await page.getByLabel(/Discord Webhook URL/).fill(getEnv("E2E_NOTIFICATION_DISCORD_WEBHOOK")); -// }); -// await expect(page.getByRole("heading", {name: "Add Notification Channel"})).toBeVisible(); -// await submit(page); -// await expect(page.getByText("Notification channel has been successfully created.")).toBeVisible(); -// await expect(get(page, validChannelName)).toBeVisible(); -// -// await testFromEdit(page, validChannelName); -// await expect(page.getByRole("heading", {name: "Edit Notification Channel"})).toBeVisible(); -// await expect(page.getByText("Sent to Discord")).toBeVisible(); -// await cancel(page); -// await expect(page.getByRole("heading", {name: "Edit Notification Channel"})).toHaveCount(0); -// }); -// -// test("Edit and test a valid Discord channel", async ({page}) => { -// await page.goto("/dashboard/notifications/channels"); -// await expect(page.getByRole("heading", {name: "Notification channels"})).toBeVisible(); -// await expect(get(page, validChannelName)).toBeVisible(); -// await testFromEdit(page, validChannelName); -// await expect(page.getByRole("heading", {name: "Edit Notification Channel"})).toBeVisible(); -// await expect(page.getByText("Sent to Discord")).toBeVisible(); -// await cancel(page); -// await expect(page.getByRole("heading", {name: "Edit Notification Channel"})).toHaveCount(0); -// }); -// }); - -test.describe.serial("Invalid channel", () => { - test("Create and test invalid Discord channel", async ({page}) => { - await page.goto("/dashboard/notifications/channels"); - await expect(page.getByRole("heading", {name: "Notification channels"})).toBeVisible(); - await create(page, "Discord", invalidChannelName, async (page) => { - await page.getByLabel(/Discord Webhook URL/).fill("https://discord.com/api/webhooks/123456789012345678/wrong-discord-webhook-token"); - }); - await expect(page.getByRole("heading", {name: "Add Notification Channel"})).toBeVisible(); - await submit(page); - await expect(page.getByText("Notification channel has been successfully created.")).toBeVisible(); - await expect(get(page, invalidChannelName)).toBeVisible(); - - await testFromEdit(page, invalidChannelName); - await expect(page.getByRole("heading", {name: "Edit Notification Channel"})).toBeVisible(); - await expect(page.getByText("An error occurred while testing the notification channel, check your configuration")).toBeVisible(); - await cancel(page); - await expect(page.getByRole("heading", {name: "Edit Notification Channel"})).toHaveCount(0); - }); - - test("Delete invalid Discord channel", async ({page}) => { - await page.goto("/dashboard/notifications/channels"); - await expect(page.getByRole("heading", {name: "Notification channels"})).toBeVisible(); - await expect(get(page, invalidChannelName)).toBeVisible(); - await remove(page, invalidChannelName); - await expect(page.getByText("Notification channel has been successfully removed.")).toBeVisible(); - await expect(page.getByText(invalidChannelName)).toHaveCount(0); - }); -}); diff --git a/e2e/notification/gotify.spec.ts b/e2e/notification/gotify.spec.ts deleted file mode 100644 index 059aa812..00000000 --- a/e2e/notification/gotify.spec.ts +++ /dev/null @@ -1,66 +0,0 @@ -import {expect, test} from "@playwright/test"; -import { - cancel, create, get, remove, submit, testFromEdit, -} from "../helpers/notification"; -import {getEnv} from "../helpers/env"; -import {LOCAL_STORAGE_PATH} from "../helpers/session"; - -test.use({storageState: LOCAL_STORAGE_PATH}); - -const validChannelName = "Gotify E2E Required"; -const invalidChannelName = "Gotify E2E Invalid"; - -// test.describe.serial("Valid channel", () => { -// test("Create and test a valid Gotify channel", async ({page}) => { -// await page.goto("/dashboard/notifications/channels"); -// await expect(page.getByRole("heading", {name: "Notification channels"})).toBeVisible(); -// await create(page, "Gotify", validChannelName, async (page) => { -// await page.getByLabel(/Gotify Server URL/).fill(getEnv("E2E_NOTIFICATION_GOTIFY_SERVER_URL")); -// await page.getByLabel(/Application Token/).fill(getEnv("E2E_NOTIFICATION_GOTIFY_APP_TOKEN")); -// }); -// await expect(page.getByRole("heading", {name: "Add Notification Channel"})).toBeVisible(); -// await submit(page); -// await expect(page.getByText("Notification channel has been successfully created.")).toBeVisible(); -// await expect(get(page, validChannelName)).toBeVisible(); -// -// await testFromEdit(page, validChannelName); -// await expect(page.getByText("Sent to Gotify")).toBeVisible(); -// await cancel(page); -// }); -// -// test("Edit and test a valid Gotify channel", async ({page}) => { -// await page.goto("/dashboard/notifications/channels"); -// await expect(page.getByRole("heading", {name: "Notification channels"})).toBeVisible(); -// await expect(get(page, validChannelName)).toBeVisible(); -// await testFromEdit(page, validChannelName); -// await expect(page.getByText("Sent to Gotify")).toBeVisible(); -// await cancel(page); -// }); -// }); - -test.describe.serial("Invalid channel", () => { - test("Create and test invalid Gotify channel", async ({page}) => { - await page.goto("/dashboard/notifications/channels"); - await expect(page.getByRole("heading", {name: "Notification channels"})).toBeVisible(); - await create(page, "Gotify", invalidChannelName, async (page) => { - await page.getByLabel(/Gotify Server URL/).fill(getEnv("E2E_NOTIFICATION_GOTIFY_SERVER_URL")); - await page.getByLabel(/Application Token/).fill("wrong-gotify-app-token"); - }); - await expect(page.getByRole("heading", {name: "Add Notification Channel"})).toBeVisible(); - await submit(page); - await expect(page.getByText("Notification channel has been successfully created.")).toBeVisible(); - await expect(get(page, invalidChannelName)).toBeVisible(); - await testFromEdit(page, invalidChannelName); - await expect(page.getByText("An error occurred while testing the notification channel, check your configuration")).toBeVisible(); - await cancel(page); - }); - - test("Delete invalid Gotify channel", async ({page}) => { - await page.goto("/dashboard/notifications/channels"); - await expect(page.getByRole("heading", {name: "Notification channels"})).toBeVisible(); - await expect(get(page, invalidChannelName)).toBeVisible(); - await remove(page, invalidChannelName); - await expect(page.getByText("Notification channel has been successfully removed.")).toBeVisible(); - await expect(page.getByText(invalidChannelName)).toHaveCount(0); - }); -}); diff --git a/e2e/notification/ntfy.spec.ts b/e2e/notification/ntfy.spec.ts deleted file mode 100644 index cbf8178f..00000000 --- a/e2e/notification/ntfy.spec.ts +++ /dev/null @@ -1,73 +0,0 @@ -import {expect, test} from "@playwright/test"; -import { - cancel, create, get, remove, submit, testFromEdit, -} from "../helpers/notification"; -import {getEnv} from "../helpers/env"; -import {LOCAL_STORAGE_PATH} from "../helpers/session"; - -test.use({storageState: LOCAL_STORAGE_PATH}); - -const requiredChannelName = "Ntfy E2E Required"; -const optionalChannelName = "Ntfy E2E Optional"; -const invalidChannelName = "Ntfy E2E Invalid"; - -// test.describe.serial("Valid channels", () => { -// test("Create and test a valid Ntfy channel with only required fields", async ({page}) => { -// await page.goto("/dashboard/notifications/channels"); -// await expect(page.getByRole("heading", {name: "Notification channels"})).toBeVisible(); -// await create(page, "ntfy.sh", requiredChannelName, async (page) => { -// await page.getByLabel(/Topic Name/).fill(getEnv("E2E_NOTIFICATION_NTFY_TOPIC")); -// }); -// await submit(page); -// await expect(page.getByText("Notification channel has been successfully created.")).toBeVisible(); -// await expect(get(page, requiredChannelName)).toBeVisible(); -// await testFromEdit(page, requiredChannelName); -// await expect(page.getByText("Sent to ntfy")).toBeVisible(); -// await cancel(page); -// }); -// -// test("Create and test a valid Ntfy channel with optional fields", async ({page}) => { -// await page.goto("/dashboard/notifications/channels"); -// await expect(page.getByRole("heading", {name: "Notification channels"})).toBeVisible(); -// await create(page, "ntfy.sh", optionalChannelName, async (page) => { -// await page.getByLabel(/Topic Name/).fill(getEnv("E2E_NOTIFICATION_NTFY_TOPIC")); -// await page.getByLabel(/^Server URL$/).fill(getEnv("E2E_NOTIFICATION_NTFY_SERVER_URL")); -// await page.getByLabel(/^Access Token$/).fill(getEnv("E2E_NOTIFICATION_NTFY_TOKEN")); -// await page.getByLabel(/^Basic Auth Username$/).fill(getEnv("E2E_NOTIFICATION_NTFY_USERNAME")); -// await page.getByLabel(/^Basic Auth Password$/).fill(getEnv("E2E_NOTIFICATION_NTFY_PASSWORD")); -// }); -// await submit(page); -// await expect(page.getByText("Notification channel has been successfully created.")).toBeVisible(); -// await expect(get(page, optionalChannelName)).toBeVisible(); -// await testFromEdit(page, optionalChannelName); -// await expect(page.getByText("Sent to ntfy")).toBeVisible(); -// await cancel(page); -// }); -// }); - -test.describe.serial("Invalid channel", () => { - test("Create and test invalid Ntfy channel", async ({page}) => { - await page.goto("/dashboard/notifications/channels"); - await expect(page.getByRole("heading", {name: "Notification channels"})).toBeVisible(); - await create(page, "ntfy.sh", invalidChannelName, async (page) => { - await page.getByLabel(/Topic Name/).fill("wrong-ntfy-topic"); - await page.getByLabel(/^Server URL$/).fill(getEnv("E2E_NOTIFICATION_NTFY_SERVER_URL")); - await page.getByLabel(/^Access Token$/).fill("wrong-ntfy-token"); - }); - await submit(page); - await expect(page.getByText("Notification channel has been successfully created.")).toBeVisible(); - await expect(get(page, invalidChannelName)).toBeVisible(); - await testFromEdit(page, invalidChannelName); - await expect(page.getByText("An error occurred while testing the notification channel, check your configuration")).toBeVisible(); - await cancel(page); - }); - - test("Delete invalid Ntfy channel", async ({page}) => { - await page.goto("/dashboard/notifications/channels"); - await expect(page.getByRole("heading", {name: "Notification channels"})).toBeVisible(); - await expect(get(page, invalidChannelName)).toBeVisible(); - await remove(page, invalidChannelName); - await expect(page.getByText("Notification channel has been successfully removed.")).toBeVisible(); - await expect(page.getByText(invalidChannelName)).toHaveCount(0); - }); -}); diff --git a/e2e/notification/slack.spec.ts b/e2e/notification/slack.spec.ts deleted file mode 100644 index aba1d0f7..00000000 --- a/e2e/notification/slack.spec.ts +++ /dev/null @@ -1,61 +0,0 @@ -import {expect, test} from "@playwright/test"; -import { - cancel, create, get, remove, submit, testFromEdit, -} from "../helpers/notification"; -import {getEnv} from "../helpers/env"; -import {LOCAL_STORAGE_PATH} from "../helpers/session"; - -test.use({storageState: LOCAL_STORAGE_PATH}); - -const validChannelName = "Slack E2E Required"; -const invalidChannelName = "Slack E2E Invalid"; - -// test.describe.serial("Valid channel", () => { -// test("Create and test a valid Slack channel", async ({page}) => { -// await page.goto("/dashboard/notifications/channels"); -// await expect(page.getByRole("heading", {name: "Notification channels"})).toBeVisible(); -// await create(page, "Slack", validChannelName, async (page) => { -// await page.getByLabel(/Slack Webhook URL/).fill(getEnv("E2E_NOTIFICATION_SLACK_WEBHOOK")); -// }); -// await submit(page); -// await expect(page.getByText("Notification channel has been successfully created.")).toBeVisible(); -// await expect(get(page, validChannelName)).toBeVisible(); -// await testFromEdit(page, validChannelName); -// await expect(page.getByText("Sent to Slack")).toBeVisible(); -// await cancel(page); -// }); -// -// test("Edit and test a valid Slack E2E channel", async ({page}) => { -// await page.goto("/dashboard/notifications/channels"); -// await expect(page.getByRole("heading", {name: "Notification channels"})).toBeVisible(); -// await expect(get(page, validChannelName)).toBeVisible(); -// await testFromEdit(page, validChannelName); -// await expect(page.getByText("Sent to Slack")).toBeVisible(); -// await cancel(page); -// }); -// }); - -test.describe.serial("Invalid channel", () => { - test("Create and test invalid Slack channel", async ({page}) => { - await page.goto("/dashboard/notifications/channels"); - await expect(page.getByRole("heading", {name: "Notification channels"})).toBeVisible(); - await create(page, "Slack", invalidChannelName, async (page) => { - await page.getByLabel(/Slack Webhook URL/).fill("https://WRONG_SLACK_WEBHOOK"); - }); - await submit(page); - await expect(page.getByText("Notification channel has been successfully created.")).toBeVisible(); - await expect(get(page, invalidChannelName)).toBeVisible(); - await testFromEdit(page, invalidChannelName); - await expect(page.getByText("An error occurred while testing the notification channel, check your configuration")).toBeVisible(); - await cancel(page); - }); - - test("Delete invalid Slack channel", async ({page}) => { - await page.goto("/dashboard/notifications/channels"); - await expect(page.getByRole("heading", {name: "Notification channels"})).toBeVisible(); - await expect(get(page, invalidChannelName)).toBeVisible(); - await remove(page, invalidChannelName); - await expect(page.getByText("Notification channel has been successfully removed.")).toBeVisible(); - await expect(page.getByText(invalidChannelName)).toHaveCount(0); - }); -}); diff --git a/e2e/notification/smtp.spec.ts b/e2e/notification/smtp.spec.ts deleted file mode 100644 index 093b5954..00000000 --- a/e2e/notification/smtp.spec.ts +++ /dev/null @@ -1,72 +0,0 @@ -import {expect, test} from "@playwright/test"; -import {getEnv} from "../helpers/env"; -import { - cancel, create, get, remove, submit, testFromEdit, -} from "../helpers/notification"; -import {LOCAL_STORAGE_PATH} from "../helpers/session"; - -test.use({storageState: LOCAL_STORAGE_PATH}); - -const validChannelName = "SMTP E2E Required"; -const invalidChannelName = "SMTP E2E Invalid"; -const successMessage = `Email sent: ${getEnv("E2E_NOTIFICATION_SMTP_TO")}`; - -// test.describe.serial("Valid channel", () => { -// test("Create and test a valid SMTP channel", async ({page}) => { -// await page.goto("/dashboard/notifications/channels"); -// await expect(page.getByRole("heading", {name: "Notification channels"})).toBeVisible(); -// await create(page, "Email", validChannelName, async (page) => { -// await page.getByLabel(/SMTP Host/).fill(getEnv("E2E_NOTIFICATION_SMTP_HOST")); -// await page.getByLabel(/SMTP Port/).fill(getEnv("E2E_NOTIFICATION_SMTP_PORT")); -// await page.getByLabel(/Username/).fill(getEnv("E2E_NOTIFICATION_SMTP_USER")); -// await page.getByLabel(/Password/).fill(getEnv("E2E_NOTIFICATION_SMTP_PASSWORD")); -// await page.getByLabel(/From Email/).fill(getEnv("E2E_NOTIFICATION_SMTP_FROM")); -// await page.getByLabel(/To Email/).fill(getEnv("E2E_NOTIFICATION_SMTP_TO")); -// }); -// await submit(page); -// await expect(page.getByText("Notification channel has been successfully created.")).toBeVisible(); -// await expect(get(page, validChannelName)).toBeVisible(); -// await testFromEdit(page, validChannelName); -// await expect(page.getByText(successMessage)).toBeVisible(); -// await cancel(page); -// }); -// -// test("Edit and test a valid SMTP channel", async ({page}) => { -// await page.goto("/dashboard/notifications/channels"); -// await expect(page.getByRole("heading", {name: "Notification channels"})).toBeVisible(); -// await expect(get(page, validChannelName)).toBeVisible(); -// await testFromEdit(page, validChannelName); -// await expect(page.getByText(successMessage)).toBeVisible(); -// await cancel(page); -// }); -// }); - -test.describe.serial("Invalid channel", () => { - test("Create and test invalid SMTP E2E channel", async ({page}) => { - await page.goto("/dashboard/notifications/channels"); - await expect(page.getByRole("heading", {name: "Notification channels"})).toBeVisible(); - await create(page, "Email", invalidChannelName, async (page) => { - await page.getByLabel(/SMTP Host/).fill("smtp.invalid"); - await page.getByLabel(/SMTP Port/).fill(getEnv("E2E_NOTIFICATION_SMTP_PORT")); - await page.getByLabel(/Username/).fill(getEnv("E2E_NOTIFICATION_SMTP_USER")); - await page.getByLabel(/Password/).fill("wrong-password"); - await page.getByLabel(/From Email/).fill(getEnv("E2E_NOTIFICATION_SMTP_FROM")); - await page.getByLabel(/To Email/).fill(getEnv("E2E_NOTIFICATION_SMTP_TO")); - }); - await submit(page); - await expect(page.getByText("Notification channel has been successfully created.")).toBeVisible(); - await expect(get(page, invalidChannelName)).toBeVisible(); - await testFromEdit(page, invalidChannelName); - await expect(page.getByText("An error occurred while testing the notification channel, check your configuration")).toBeVisible(); - await cancel(page); - }); - - test("Delete invalid SMTP channel", async ({page}) => { - await page.goto("/dashboard/notifications/channels"); - await expect(page.getByRole("heading", {name: "Notification channels"})).toBeVisible(); - await expect(get(page, invalidChannelName)).toBeVisible(); - await remove(page, invalidChannelName); - await expect(page.getByText("Notification channel has been successfully removed.")).toBeVisible(); - await expect(page.getByText(invalidChannelName)).toHaveCount(0); - }); -}); diff --git a/e2e/notification/teams.spec.ts b/e2e/notification/teams.spec.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/e2e/notification/telegram.spec.ts b/e2e/notification/telegram.spec.ts deleted file mode 100644 index 85963d71..00000000 --- a/e2e/notification/telegram.spec.ts +++ /dev/null @@ -1,72 +0,0 @@ -import {expect, test} from "@playwright/test"; -import { - cancel, create, get, remove, submit, testFromEdit, -} from "../helpers/notification"; -import {getEnv} from "../helpers/env"; -import {LOCAL_STORAGE_PATH} from "../helpers/session"; - -test.use({storageState: LOCAL_STORAGE_PATH}); - -const requiredChannelName = "Telegram E2E Required"; -const optionalChannelName = "Telegram E2E Optional"; -const invalidChannelName = "Telegram E2E Invalid"; - -// test.describe.serial("Valid channels", () => { -// test("Create and test a valid Telegram channel", async ({page}) => { -// await page.goto("/dashboard/notifications/channels"); -// await expect(page.getByRole("heading", {name: "Notification channels"})).toBeVisible(); -// await create(page, "Telegram", requiredChannelName, async (page) => { -// await page.getByLabel(/Telegram Bot Token/).fill(getEnv("E2E_NOTIFICATION_TELEGRAM_BOT_TOKEN")); -// await page.getByLabel(/Telegram Chat ID/).fill(getEnv("E2E_NOTIFICATION_TELEGRAM_CHAT_ID")); -// }); -// await submit(page); -// await expect(page.getByText("Notification channel has been successfully created.")).toBeVisible(); -// await expect(get(page, requiredChannelName)).toBeVisible(); -// await testFromEdit(page, requiredChannelName); -// await expect(page.getByText("Sent to Telegram")).toBeVisible(); -// await cancel(page); -// }); -// -// test("Create and test a valid Telegram channel with Topic ID", async ({page}) => { -// await page.goto("/dashboard/notifications/channels"); -// await expect(page.getByRole("heading", {name: "Notification channels"})).toBeVisible(); -// await create(page, "Telegram", optionalChannelName, async (page) => { -// await page.getByLabel(/Telegram Bot Token/).fill(getEnv("E2E_NOTIFICATION_TELEGRAM_BOT_TOKEN")); -// await page.getByLabel(/Telegram Chat ID/).fill(getEnv("E2E_NOTIFICATION_TELEGRAM_CHAT_ID")); -// await page.getByLabel(/Telegram Topic ID/).fill(getEnv("E2E_NOTIFICATION_TELEGRAM_TOPIC_ID")); -// }); -// await submit(page); -// await expect(page.getByText("Notification channel has been successfully created.")).toBeVisible(); -// await expect(get(page, optionalChannelName)).toBeVisible(); -// await testFromEdit(page, optionalChannelName); -// await expect(page.getByText("Sent to Telegram")).toBeVisible(); -// await cancel(page); -// }); -// }); - -test.describe.serial("Invalid channel", () => { - test("Create and test invalid Telegram channel", async ({page}) => { - await page.goto("/dashboard/notifications/channels"); - await expect(page.getByRole("heading", {name: "Notification channels"})).toBeVisible(); - await create(page, "Telegram", invalidChannelName, async (page) => { - await page.getByLabel(/Telegram Bot Token/).fill("wrong-telegram-bot-token"); - await page.getByLabel(/Telegram Chat ID/).fill(getEnv("E2E_NOTIFICATION_TELEGRAM_CHAT_ID")); - await page.getByLabel(/Telegram Topic ID/).fill(getEnv("E2E_NOTIFICATION_TELEGRAM_TOPIC_ID")); - }); - await submit(page); - await expect(page.getByText("Notification channel has been successfully created.")).toBeVisible(); - await expect(get(page, invalidChannelName)).toBeVisible(); - await testFromEdit(page, invalidChannelName); - await expect(page.getByText("An error occurred while testing the notification channel, check your configuration")).toBeVisible(); - await cancel(page); - }); - - test("Delete invalid Telegram channel", async ({page}) => { - await page.goto("/dashboard/notifications/channels"); - await expect(page.getByRole("heading", {name: "Notification channels"})).toBeVisible(); - await expect(get(page, invalidChannelName)).toBeVisible(); - await remove(page, invalidChannelName); - await expect(page.getByText("Notification channel has been successfully removed.")).toBeVisible(); - await expect(page.getByText(invalidChannelName)).toHaveCount(0); - }); -}); diff --git a/e2e/notification/webhook.spec.ts b/e2e/notification/webhook.spec.ts deleted file mode 100644 index 3eeb6427..00000000 --- a/e2e/notification/webhook.spec.ts +++ /dev/null @@ -1,73 +0,0 @@ -import {expect, test} from "@playwright/test"; -import { - cancel, create, get, remove, submit, testFromEdit, -} from "../helpers/notification"; -import {getEnv} from "../helpers/env"; -import {LOCAL_STORAGE_PATH} from "../helpers/session"; - -test.use({storageState: LOCAL_STORAGE_PATH}); - -const requiredChannelName = "Webhook E2E Required"; -const optionalChannelName = "Webhook E2E Optional"; -const invalidChannelName = "Webhook E2E Invalid"; - -// test.describe.serial("Valid channels", () => { -// test("Create and test a valid Webhook channel", async ({page}) => { -// await page.goto("/dashboard/notifications/channels"); -// await expect(page.getByRole("heading", {name: "Notification channels"})).toBeVisible(); -// await create(page, "Webhook", requiredChannelName, async (page) => { -// await page.getByLabel(/Webhook URL/).fill(getEnv("E2E_NOTIFICATION_WEBHOOK_URL")); -// }); -// await submit(page); -// await expect(page.getByText("Notification channel has been successfully created.")).toBeVisible(); -// await expect(get(page, requiredChannelName)).toBeVisible(); -// await testFromEdit(page, requiredChannelName); -// await expect(page.getByText("Sent to Webhook")).toBeVisible(); -// await cancel(page); -// }); -// -// test("Create and test a valid Webhook channel with optional header", async ({page}) => { -// await page.goto("/dashboard/notifications/channels"); -// await expect(page.getByRole("heading", {name: "Notification channels"})).toBeVisible(); -// await create(page, "Webhook", optionalChannelName, async (page) => { -// await page.getByLabel(/Webhook URL/).fill(getEnv("E2E_NOTIFICATION_WEBHOOK_URL")); -// await page.getByRole("button", { name: "Add Header" }).click(); -// await page.getByLabel(/^Header Name$/).fill(getEnv("E2E_NOTIFICATION_WEBHOOK_SECRET_HEADER")); -// await page.getByLabel(/^Header Value$/).fill(getEnv("E2E_NOTIFICATION_WEBHOOK_SECRET")); -// }); -// await submit(page); -// await expect(page.getByText("Notification channel has been successfully created.")).toBeVisible(); -// await expect(get(page, optionalChannelName)).toBeVisible(); -// await testFromEdit(page, optionalChannelName); -// await expect(page.getByText("Sent to Webhook")).toBeVisible(); -// await cancel(page); -// }); -// }); - -test.describe.serial("Invalid channel", () => { - test("Create and test invalid Webhook channel", async ({page}) => { - await page.goto("/dashboard/notifications/channels"); - await expect(page.getByRole("heading", {name: "Notification channels"})).toBeVisible(); - await create(page, "Webhook", invalidChannelName, async (page) => { - await page.getByLabel(/Webhook URL/).fill("https://webhook.example.com/api/wrong-webhook"); - await page.getByRole("button", { name: "Add Header" }).click(); - await page.getByLabel(/^Header Name$/).fill(getEnv("E2E_NOTIFICATION_WEBHOOK_SECRET_HEADER")); - await page.getByLabel(/^Header Value$/).fill("wrong-webhook-secret"); - }); - await submit(page); - await expect(page.getByText("Notification channel has been successfully created.")).toBeVisible(); - await expect(get(page, invalidChannelName)).toBeVisible(); - await testFromEdit(page, invalidChannelName); - await expect(page.getByText("An error occurred while testing the notification channel, check your configuration")).toBeVisible(); - await cancel(page); - }); - - test("Delete invalid Webhook E2E channel", async ({page}) => { - await page.goto("/dashboard/notifications/channels"); - await expect(page.getByRole("heading", {name: "Notification channels"})).toBeVisible(); - await expect(get(page, invalidChannelName)).toBeVisible(); - await remove(page, invalidChannelName); - await expect(page.getByText("Notification channel has been successfully removed.")).toBeVisible(); - await expect(page.getByText(invalidChannelName)).toHaveCount(0); - }); -}); diff --git a/e2e/project.spec.ts b/e2e/project.spec.ts deleted file mode 100644 index 7685ccdf..00000000 --- a/e2e/project.spec.ts +++ /dev/null @@ -1,59 +0,0 @@ -import {expect, test} from "@playwright/test"; -import {create, edit, get, remove} from "./helpers/project"; -import {LOCAL_STORAGE_PATH} from "./helpers/session"; - -test.use({storageState: LOCAL_STORAGE_PATH}); - -const project = { - emptyStateName: "Project A", - buttonName: "Project B", - updatedButtonName: "Project B Updated", -}; - -test.describe.serial(() => { - test("Create a project from empty state", async ({page}) => { - await page.goto("/dashboard/projects"); - await expect(page.getByRole("heading", {name: "Projects"})).toBeVisible(); - await expect(page.getByText("Create new Project", {exact: true})).toBeVisible(); - - await create(page, "emptyState", project.emptyStateName); - - await expect(page.getByText("Project has been successfully created.")).toBeVisible(); - await expect(get(page, project.emptyStateName)).toBeVisible(); - await expect(page.getByText("Create new Project", {exact: true})).toHaveCount(0); - }); - - test("Create a project from classic button", async ({page}) => { - await page.goto("/dashboard/projects"); - await expect(page.getByRole("heading", {name: "Projects"})).toBeVisible(); - await expect(get(page, project.emptyStateName)).toBeVisible(); - - await create(page, "button", project.buttonName); - - await expect(page.getByText("Project has been successfully created.")).toBeVisible(); - await expect(get(page, project.buttonName)).toBeVisible(); - }); - - test("Edit the second created project", async ({page}) => { - await page.goto("/dashboard/projects"); - await expect(page.getByRole("heading", {name: "Projects"})).toBeVisible(); - await expect(get(page, project.buttonName)).toBeVisible(); - - await edit(page, project.buttonName, project.updatedButtonName); - - await expect(page.getByText("Project has been successfully updated.")).toBeVisible(); - await expect(page.getByText(project.updatedButtonName, {exact: true})).toBeVisible(); - }); - - test("Delete the second created project", async ({page}) => { - await page.goto("/dashboard/projects"); - await expect(page.getByRole("heading", {name: "Projects"})).toBeVisible(); - await expect(get(page, project.updatedButtonName)).toBeVisible(); - - await remove(page, project.updatedButtonName); - - await expect(page).toHaveURL("/dashboard/projects"); - await expect(page.getByText("Projects has been successfully archived.")).toBeVisible(); - await expect(page.getByText(project.updatedButtonName)).toHaveCount(0); - }); -}); diff --git a/e2e/setup.spec.ts b/e2e/setup.spec.ts deleted file mode 100644 index 3bcc3baa..00000000 --- a/e2e/setup.spec.ts +++ /dev/null @@ -1,13 +0,0 @@ -import {test} from "@playwright/test"; -import fs from "fs"; -import {LOCAL_STORAGE_PATH} from "./helpers/session"; - -test.describe(() => { - test.beforeAll(async () => { - if (fs.existsSync(LOCAL_STORAGE_PATH)) fs.unlinkSync(LOCAL_STORAGE_PATH); - fs.writeFileSync(LOCAL_STORAGE_PATH, JSON.stringify({})); - }); - - test("Prepare shared storage state", async () => { - }); -}); diff --git a/e2e/storage/azure.spec.ts b/e2e/storage/azure.spec.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/e2e/storage/gcs.spec.ts b/e2e/storage/gcs.spec.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/e2e/storage/google-drive.spec.ts b/e2e/storage/google-drive.spec.ts deleted file mode 100644 index aa050e20..00000000 --- a/e2e/storage/google-drive.spec.ts +++ /dev/null @@ -1,60 +0,0 @@ -import {expect, test} from "@playwright/test"; -import {getEnv} from "../helpers/env"; -import {LOCAL_STORAGE_PATH} from "../helpers/session"; -import { - cancel, create, get, remove, submit, testConnection, testFromEdit, -} from "../helpers/storage"; - -test.use({storageState: LOCAL_STORAGE_PATH}); - -const requiredChannelName = "Google Drive E2E Required"; -const invalidChannelName = "Google Drive E2E Invalid"; - -// test.describe.serial("Valid channel", () => { -// test("Create and test a valid Google Drive channel", async ({page}) => { -// await page.goto("/dashboard/storages/channels"); -// await expect(page.getByRole("heading", {name: "Storage channels"})).toBeVisible(); -// await create(page, "Google Drive", requiredChannelName, async (page) => { -// await page.getByLabel(/Client ID/).fill(getEnv("E2E_STORAGE_GOOGLE_DRIVE_CLIENT_ID")); -// await page.getByLabel(/Client Secret/).fill(getEnv("E2E_STORAGE_GOOGLE_DRIVE_CLIENT_SECRET")); -// await page.getByLabel(/Folder ID/).fill(getEnv("E2E_STORAGE_GOOGLE_DRIVE_FOLDER_ID")); -// }); -// await expect(page.getByRole("heading", {name: "Add Storage Channel"})).toBeVisible(); -// await testConnection(page); -// await expect(page.getByText("Successfully connected to storage channel")).toBeVisible(); -// await submit(page); -// await expect(page.getByText("Storage channel has been successfully created.")).toBeVisible(); -// await expect(get(page, requiredChannelName)).toBeVisible(); -// -// await testFromEdit(page, requiredChannelName); -// await expect(page.getByRole("heading", {name: "Edit Storage Channel"})).toBeVisible(); -// await expect(page.getByText("Successfully connected to storage channel")).toBeVisible(); -// await cancel(page); -// }); -// }); - -test.describe.serial("Invalid channel", () => { - test("Create and test invalid Google Drive channel", async ({page}) => { - await page.goto("/dashboard/storages/channels"); - await expect(page.getByRole("heading", {name: "Storage channels"})).toBeVisible(); - await create(page, "Google Drive", invalidChannelName, async (page) => { - await page.getByLabel(/Client ID/).fill(getEnv("E2E_STORAGE_GOOGLE_DRIVE_CLIENT_ID")); - await page.getByLabel(/Client Secret/).fill("wrong-google-drive-client-secret"); - await page.getByLabel(/Folder ID/).fill(getEnv("E2E_STORAGE_GOOGLE_DRIVE_FOLDER_ID")); - }); - await testConnection(page); - await expect(page.getByText("An error occurred while testing the storage channel")).toBeVisible(); - await submit(page); - await expect(page.getByText("Storage channel has been successfully created.")).toBeVisible(); - await expect(get(page, invalidChannelName)).toBeVisible(); - }); - - test("Delete invalid Google Drive E2E channel", async ({page}) => { - await page.goto("/dashboard/storages/channels"); - await expect(page.getByRole("heading", {name: "Storage channels"})).toBeVisible(); - await expect(get(page, invalidChannelName)).toBeVisible(); - await remove(page, invalidChannelName); - await expect(page.getByText("Storage channel has been successfully removed.")).toBeVisible(); - await expect(page.getByText(invalidChannelName)).toHaveCount(0); - }); -}); diff --git a/e2e/storage/s3.spec.ts b/e2e/storage/s3.spec.ts deleted file mode 100644 index 8993e72e..00000000 --- a/e2e/storage/s3.spec.ts +++ /dev/null @@ -1,121 +0,0 @@ -import {expect, test} from "@playwright/test"; -import {getEnv} from "../helpers/env"; -import {LOCAL_STORAGE_PATH} from "../helpers/session"; -import { - cancel, create, get, remove, submit, testConnection, testFromEdit, -} from "../helpers/storage"; - -test.use({storageState: LOCAL_STORAGE_PATH}); - -const providers = [ - { - title: "AWS S3", - requiredChannelName: "AWS S3 E2E Required", - optionalChannelName: "AWS S3 E2E Optional", - invalidChannelName: "AWS S3 E2E Invalid", - endpointUrl: getEnv("E2E_STORAGE_AWS_S3_ENDPOINT_URL"), - region: getEnv("E2E_STORAGE_AWS_S3_REGION"), - accessKey: getEnv("E2E_STORAGE_AWS_S3_ACCESS_KEY"), - secretKey: getEnv("E2E_STORAGE_AWS_S3_SECRET_KEY"), - bucketName: getEnv("E2E_STORAGE_AWS_S3_BUCKET_NAME"), - port: getEnv("E2E_STORAGE_AWS_S3_PORT"), - invalidAccessKey: "wrong-aws-access-key", - invalidSecretKey: "wrong-aws-secret-key", - }, - { - title: "Cloudflare R2", - requiredChannelName: "Cloudflare R2 E2E Required", - optionalChannelName: "Cloudflare R2 E2E Optional", - invalidChannelName: "Cloudflare R2 E2E Invalid", - endpointUrl: getEnv("E2E_STORAGE_R2_ENDPOINT_URL"), - region: getEnv("E2E_STORAGE_R2_REGION"), - accessKey: getEnv("E2E_STORAGE_R2_ACCESS_KEY"), - secretKey: getEnv("E2E_STORAGE_R2_SECRET_KEY"), - bucketName: getEnv("E2E_STORAGE_R2_BUCKET_NAME"), - port: getEnv("E2E_STORAGE_R2_PORT"), - invalidAccessKey: "wrong-r2-access-key", - invalidSecretKey: "wrong-r2-secret-key", - }, -] as const; - -for (const provider of providers) { - test.describe(provider.title, () => { - // test.describe.serial("Valid channels", () => { - // test(`Create and test a valid ${provider.title} channel`, async ({page}) => { - // await page.goto("/dashboard/storages/channels"); - // await expect(page.getByRole("heading", {name: "Storage channels"})).toBeVisible(); - // await create(page, "S3", provider.requiredChannelName, async (page) => { - // await page.getByLabel(/Endpoint URL/).fill(provider.endpointUrl); - // await page.getByLabel(/Access Key/).fill(provider.accessKey); - // await page.getByLabel(/Secret Key/).fill(provider.secretKey); - // await page.getByLabel(/Bucket name/).fill(provider.bucketName); - // }); - // await expect(page.getByRole("heading", {name: "Add Storage Channel"})).toBeVisible(); - // await testConnection(page); - // await expect(page.getByText("Successfully connected to storage channel")).toBeVisible(); - // await submit(page); - // await expect(page.getByText("Storage channel has been successfully created.")).toBeVisible(); - // await expect(get(page, provider.requiredChannelName)).toBeVisible(); - // - // await testFromEdit(page, provider.requiredChannelName); - // await expect(page.getByRole("heading", {name: "Edit Storage Channel"})).toBeVisible(); - // await expect(page.getByText("Successfully connected to storage channel")).toBeVisible(); - // await cancel(page); - // }); - // - // test(`Create and test a valid ${provider.title} channel with optional region and port`, async ({page}) => { - // await page.goto("/dashboard/storages/channels"); - // await expect(page.getByRole("heading", {name: "Storage channels"})).toBeVisible(); - // await create(page, "S3", provider.optionalChannelName, async (page) => { - // await page.getByLabel(/Endpoint URL/).fill(provider.endpointUrl); - // await page.getByLabel(/^Region$/).fill(provider.region); - // await page.getByLabel(/Access Key/).fill(provider.accessKey); - // await page.getByLabel(/Secret Key/).fill(provider.secretKey); - // await page.getByLabel(/Bucket name/).fill(provider.bucketName); - // await page.getByLabel(/^Port$/).fill(provider.port); - // }); - // await expect(page.getByRole("heading", {name: "Add Storage Channel"})).toBeVisible(); - // await testConnection(page); - // await expect(page.getByText("Successfully connected to storage channel")).toBeVisible(); - // await submit(page); - // await expect(page.getByText("Storage channel has been successfully created.")).toBeVisible(); - // await expect(get(page, provider.optionalChannelName)).toBeVisible(); - // - // await testFromEdit(page, provider.optionalChannelName); - // await expect(page.getByRole("heading", {name: "Edit Storage Channel"})).toBeVisible(); - // await expect(page.getByText("Successfully connected to storage channel")).toBeVisible(); - // await cancel(page); - // }); - // }); - - test.describe.serial("Invalid channel", () => { - test(`Create and test invalid ${provider.title} channel`, async ({page}) => { - await page.goto("/dashboard/storages/channels"); - await expect(page.getByRole("heading", {name: "Storage channels"})).toBeVisible(); - await create(page, "S3", provider.invalidChannelName, async (page) => { - await page.getByLabel(/Endpoint URL/).fill(provider.endpointUrl); - await page.getByLabel(/^Region$/).fill(provider.region); - await page.getByLabel(/Access Key/).fill(provider.invalidAccessKey); - await page.getByLabel(/Secret Key/).fill(provider.invalidSecretKey); - await page.getByLabel(/Bucket name/).fill(provider.bucketName); - await page.getByLabel(/^Port$/).fill(provider.port); - }); - await expect(page.getByRole("heading", {name: "Add Storage Channel"})).toBeVisible(); - await testConnection(page); - await expect(page.getByText("An error occurred while testing the storage channel")).toBeVisible(); - await submit(page); - await expect(page.getByText("Storage channel has been successfully created.")).toBeVisible(); - await expect(get(page, provider.invalidChannelName)).toBeVisible(); - }); - - test(`Delete invalid ${provider.title} channel`, async ({page}) => { - await page.goto("/dashboard/storages/channels"); - await expect(page.getByRole("heading", {name: "Storage channels"})).toBeVisible(); - await expect(get(page, provider.invalidChannelName)).toBeVisible(); - await remove(page, provider.invalidChannelName); - await expect(page.getByText("Storage channel has been successfully removed.")).toBeVisible(); - await expect(page.getByText(provider.invalidChannelName)).toHaveCount(0); - }); - }); - }); -} diff --git a/package.json b/package.json index 8484817c..e20053fa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "portabase", - "version": "1.18.2", + "version": "1.20.0", "private": true, "scripts": { "dev": "next dev --turbopack -p 8887", @@ -16,6 +16,7 @@ }, "dependencies": { "@asteasolutions/zod-to-openapi": "^8.5.0", + "@azure/storage-blob": "^12.32.0", "@better-auth/api-key": "1.6.11", "@better-auth/core": "1.6.11", "@better-auth/passkey": "1.6.11", @@ -111,7 +112,6 @@ }, "devDependencies": { "@iconify/react": "^6.0.2", - "@playwright/test": "1.58.2", "@react-email/preview-server": "4.3.2", "@react-email/render": "^2.0.8", "@release-it/bumper": "^7.0.5", @@ -142,5 +142,5 @@ "typescript": "^5.9.3", "zenstack": "2.14.2" }, - "packageManager": "pnpm@11.5.2+sha512.71c631e382066efc25625d5cf029075de07b61b37f6e27350fbd84b1bda5864c8c1967adc280776b45c30a715c0359a3be08fef42d5bb09e2b99029979692916" + "packageManager": "pnpm@11.8.0+sha512.c1f5e7c4cb241c8f174b743851d82f42b802324afc8b0f116b96adb15aa06664948dde36960a3ba1079ba5b4b29dd0140135b94b5b5f5263592249d68e555f26" } diff --git a/playwright.config.ts b/playwright.config.ts deleted file mode 100644 index cb09219d..00000000 --- a/playwright.config.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { defineConfig, devices } from '@playwright/test'; -import dotenv from 'dotenv'; -import path from 'path'; - -/** - * Read environment variables from file. - * https://github.com/motdotla/dotenv - */ -dotenv.config({ path: path.resolve(__dirname, '.env') }); - -/** - * See https://playwright.dev/docs/test-configuration. - */ -export default defineConfig({ - testDir: './e2e', - /* Run tests in files in parallel */ - fullyParallel: true, - /* Fail the build on CI if you accidentally left test.only in the source code. */ - forbidOnly: !!process.env.CI, - /* Retry on CI only */ - retries: process.env.CI ? 2 : 0, - /* Opt out of parallel tests on CI. */ - workers: process.env.CI ? 1 : undefined, - /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: 'html', - /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ - use: { - ...devices['Desktop Chrome'], - baseURL: process.env.PROJECT_URL, - - /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ - trace: 'on-first-retry', - }, - - /* Configure projects and dependency order */ - projects: [ - { - name: 'setup', - testMatch: '**/setup.spec.ts', - }, - { - name: 'auth', - testMatch: '**/auth.spec.ts', - dependencies: ['setup'], - }, - { - name: 'access-management', - testMatch: '**/access-management.spec.ts', - dependencies: ['auth'], - }, - { - name: 'notification', - testMatch: '**/notification/**/*.spec.ts', - dependencies: ['access-management'], - }, - { - name: 'storage', - testMatch: '**/storage/**/*.spec.ts', - dependencies: ['access-management'], - }, - { - name: 'agent', - testMatch: '**/agent.spec.ts', - dependencies: ['access-management'], - }, - - { - name: 'project', - testMatch: '**/project.spec.ts', - dependencies: ['access-management'], - }, - { - name: 'cleanup', - testMatch: '**/cleanup.spec.ts', - dependencies: ['agent', 'storage', 'notification', 'project'], - }, - ] - -}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0cd5ad8b..9b5c8166 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@asteasolutions/zod-to-openapi': specifier: ^8.5.0 version: 8.5.0(zod@4.3.6) + '@azure/storage-blob': + specifier: ^12.32.0 + version: 12.32.0 '@better-auth/api-key': specifier: 1.6.11 version: 1.6.11(@better-auth/core@1.6.11(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-call@1.3.5(zod@4.3.6))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.0)(better-auth@1.6.11(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.21.0)(prisma@6.7.0(typescript@5.9.3)))(next@16.2.3(@babel/core@7.29.7)(@playwright/test@1.58.2)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.21.0)(prisma@6.7.0(typescript@5.9.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)) @@ -291,9 +294,6 @@ importers: '@iconify/react': specifier: ^6.0.2 version: 6.0.2(react@19.2.7) - '@playwright/test': - specifier: 1.58.2 - version: 1.58.2 '@react-email/preview-server': specifier: 4.3.2 version: 4.3.2(@playwright/test@1.58.2)(postcss@8.5.10) @@ -401,6 +401,61 @@ packages: resolution: {integrity: sha512-cTlrKttbrRHEw3W+0/I609A2Matj5JQaRvfLtEIGZvlN0RaPi+3ANsMeqAyCAVlH/lUIW2tmtBlSMni74lcXeg==} engines: {node: '>=12'} + '@azure/abort-controller@2.1.2': + resolution: {integrity: sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==} + engines: {node: '>=18.0.0'} + + '@azure/core-auth@1.10.1': + resolution: {integrity: sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg==} + engines: {node: '>=20.0.0'} + + '@azure/core-client@1.10.2': + resolution: {integrity: sha512-1D2LpsU7y9xrqKjdIbsB7PlrRePw0xsVV8p+AKTlzITrWmscajryfJCdDJB/oGwvDI5HmRo04eMMADB67uwAwQ==} + engines: {node: '>=20.0.0'} + + '@azure/core-http-compat@2.4.0': + resolution: {integrity: sha512-f1P96IB399YiN2ARYHP7EpZi3Bf3wH4SN2lGzrw7JVwm7bbsVYtf2iKSBwTywD2P62NOPZGHFSZi+6jjb75JuA==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@azure/core-client': ^1.10.0 + '@azure/core-rest-pipeline': ^1.22.0 + + '@azure/core-lro@2.7.2': + resolution: {integrity: sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw==} + engines: {node: '>=18.0.0'} + + '@azure/core-paging@1.6.2': + resolution: {integrity: sha512-YKWi9YuCU04B55h25cnOYZHxXYtEvQEbKST5vqRga7hWY9ydd3FZHdeQF8pyh+acWZvppw13M/LMGx0LABUVMA==} + engines: {node: '>=18.0.0'} + + '@azure/core-rest-pipeline@1.24.0': + resolution: {integrity: sha512-PpLsoDQ3AMmKZ0VU+0GrmqMxgp/sExjlVm4R+nLWngeoEGAzOIPVifaxKGU5gMv+nWELUoHfvrolWD+ZS/nFJg==} + engines: {node: '>=20.0.0'} + + '@azure/core-tracing@1.3.1': + resolution: {integrity: sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ==} + engines: {node: '>=20.0.0'} + + '@azure/core-util@1.13.1': + resolution: {integrity: sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A==} + engines: {node: '>=20.0.0'} + + '@azure/core-xml@1.5.1': + resolution: {integrity: sha512-xcNRHqCoSp4AunOALEae6A8f3qATb83gSrm31Iqb01OzblvC3/W/bfXozcq78EzIdzZzuH1bZ2NvRR0TdX709w==} + engines: {node: '>=20.0.0'} + + '@azure/logger@1.3.0': + resolution: {integrity: sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA==} + engines: {node: '>=20.0.0'} + + '@azure/storage-blob@12.32.0': + resolution: {integrity: sha512-80LzSNnFQye2LCCBFghAJS6jJQJ7N4bfgZ6qDMgVGRtugZ7TLDKQZ2hczMigmZH3jAcMRdma/IygsC5+0gT7Tw==} + engines: {node: '>=20.0.0'} + + '@azure/storage-common@12.4.0': + resolution: {integrity: sha512-kNhJKMxQb374KOVt63CZnGIpDcrKNzJeyANLJymxE9mCJSdRGzb+Iv9oSIiCj6tNMLypr530b9ObOiA/5OvwOg==} + engines: {node: '>=20.0.0'} + '@babel/code-frame@7.29.7': resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} engines: {node: '>=6.9.0'} @@ -3973,6 +4028,10 @@ packages: resolution: {integrity: sha512-EbGRQg4FhrmwLodl+t3JNAnXHWVr9Vp+Zl1QBZVPY4ByfkzIT8cX3K6QWODHtkIZqqJVEWvhHSx3v5PDHsaQag==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typespec/ts-http-runtime@0.3.6': + resolution: {integrity: sha512-jIXhD0eWQ1JA6ln/5Dltyx22UxWNrw0hZmhy2rlv6m6KgF7kplHx3g0fzi09lNmTJQRR91OlemYp3xFnvDK9og==} + engines: {node: '>=20.0.0'} + '@unrs/resolver-binding-android-arm-eabi@1.12.2': resolution: {integrity: sha512-g5T90pqg1bo/7mytQx6F4iBNC0Wsh9cu+z9veDbFjc7HjpesJFWD7QMS0NGStXM075+7dJPPVvBbpZlnrdpi/w==} cpu: [arm] @@ -8601,6 +8660,119 @@ snapshots: escape-html: 1.0.3 xpath: 0.0.32 + '@azure/abort-controller@2.1.2': + dependencies: + tslib: 2.8.1 + + '@azure/core-auth@1.10.1': + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-util': 1.13.1 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/core-client@1.10.2': + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-auth': 1.10.1 + '@azure/core-rest-pipeline': 1.24.0 + '@azure/core-tracing': 1.3.1 + '@azure/core-util': 1.13.1 + '@azure/logger': 1.3.0 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/core-http-compat@2.4.0(@azure/core-client@1.10.2)(@azure/core-rest-pipeline@1.24.0)': + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-client': 1.10.2 + '@azure/core-rest-pipeline': 1.24.0 + + '@azure/core-lro@2.7.2': + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-util': 1.13.1 + '@azure/logger': 1.3.0 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/core-paging@1.6.2': + dependencies: + tslib: 2.8.1 + + '@azure/core-rest-pipeline@1.24.0': + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-auth': 1.10.1 + '@azure/core-tracing': 1.3.1 + '@azure/core-util': 1.13.1 + '@azure/logger': 1.3.0 + '@typespec/ts-http-runtime': 0.3.6 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/core-tracing@1.3.1': + dependencies: + tslib: 2.8.1 + + '@azure/core-util@1.13.1': + dependencies: + '@azure/abort-controller': 2.1.2 + '@typespec/ts-http-runtime': 0.3.6 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/core-xml@1.5.1': + dependencies: + fast-xml-parser: 5.8.0 + tslib: 2.8.1 + + '@azure/logger@1.3.0': + dependencies: + '@typespec/ts-http-runtime': 0.3.6 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/storage-blob@12.32.0': + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-auth': 1.10.1 + '@azure/core-client': 1.10.2 + '@azure/core-http-compat': 2.4.0(@azure/core-client@1.10.2)(@azure/core-rest-pipeline@1.24.0) + '@azure/core-lro': 2.7.2 + '@azure/core-paging': 1.6.2 + '@azure/core-rest-pipeline': 1.24.0 + '@azure/core-tracing': 1.3.1 + '@azure/core-util': 1.13.1 + '@azure/core-xml': 1.5.1 + '@azure/logger': 1.3.0 + '@azure/storage-common': 12.4.0(@azure/core-client@1.10.2) + events: 3.3.0 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/storage-common@12.4.0(@azure/core-client@1.10.2)': + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-auth': 1.10.1 + '@azure/core-http-compat': 2.4.0(@azure/core-client@1.10.2)(@azure/core-rest-pipeline@1.24.0) + '@azure/core-rest-pipeline': 1.24.0 + '@azure/core-tracing': 1.3.1 + '@azure/core-util': 1.13.1 + '@azure/logger': 1.3.0 + events: 3.3.0 + tslib: 2.8.1 + transitivePeerDependencies: + - '@azure/core-client' + - supports-color + '@babel/code-frame@7.29.7': dependencies: '@babel/helper-validator-identifier': 7.29.7 @@ -10053,6 +10225,7 @@ snapshots: '@playwright/test@1.58.2': dependencies: playwright: 1.58.2 + optional: true '@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3)': optionalDependencies: @@ -11919,6 +12092,14 @@ snapshots: '@typescript-eslint/types': 8.60.1 eslint-visitor-keys: 5.0.1 + '@typespec/ts-http-runtime@0.3.6': + dependencies: + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + '@unrs/resolver-binding-android-arm-eabi@1.12.2': optional: true @@ -15429,13 +15610,15 @@ snapshots: exsolve: 1.0.8 pathe: 2.0.3 - playwright-core@1.58.2: {} + playwright-core@1.58.2: + optional: true playwright@1.58.2: dependencies: playwright-core: 1.58.2 optionalDependencies: fsevents: 2.3.2 + optional: true pluralize@8.0.0: {} diff --git a/seeds/azurite/azurite-list.sh b/seeds/azurite/azurite-list.sh new file mode 100755 index 00000000..456c999c --- /dev/null +++ b/seeds/azurite/azurite-list.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +# +# azurite-list.sh — enumerate every container and blob in a local Azurite emulator. +# +# Usage: +# ./azurite-list.sh # list containers + blobs +# ./azurite-list.sh -v # also print blob size, last-modified, content-type +# + +set -euo pipefail + +export AZURE_STORAGE_CONNECTION_STRING="DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;QueueEndpoint=http://127.0.0.1:10001/devstoreaccount1;TableEndpoint=http://127.0.0.1:10002/devstoreaccount1;" + +VERBOSE=0 +[[ "${1:-}" == "-v" || "${1:-}" == "--verbose" ]] && VERBOSE=1 + +command -v az >/dev/null 2>&1 || { echo "ERROR: 'az' CLI not found in PATH." >&2; exit 1; } +command -v jq >/dev/null 2>&1 || { echo "ERROR: 'jq' not found in PATH." >&2; exit 1; } + +if ! az storage container list --num-results 1 >/dev/null 2>&1; then + echo "ERROR: Cannot reach Azurite at 127.0.0.1:10000. Is the container running?" >&2 + echo " Start it with: docker start azurite" >&2 + exit 1 +fi + +containers=$(az storage container list --query "[].name" -o tsv) + +if [[ -z "$containers" ]]; then + echo "(no containers found in this emulator)" + exit 0 +fi + +while IFS= read -r container; do + echo "📦 container: $container" + + if [[ "$VERBOSE" -eq 1 ]]; then + az storage blob list --container-name "$container" \ + --query "[].{name:name, bytes:properties.contentLength, modified:properties.lastModified, type:properties.contentSettings.contentType}" \ + -o json \ + | jq -r '.[] | " • \(.name) [\(.bytes) bytes] \(.type // "?") \(.modified)"' + else + az storage blob list --container-name "$container" \ + --query "[].{name:name, bytes:properties.contentLength}" \ + -o json \ + | jq -r '.[] | " • \(.name) [\(.bytes) bytes]"' + fi + + count=$(az storage blob list --container-name "$container" --query "length(@)" -o tsv) + [[ "$count" -eq 0 ]] && echo " (empty)" + echo +done <<< "$containers" \ No newline at end of file diff --git a/seeds/azurite/azurite-seed.sh b/seeds/azurite/azurite-seed.sh new file mode 100755 index 00000000..c3b80595 --- /dev/null +++ b/seeds/azurite/azurite-seed.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# +# azurite-seed.sh — seed a local Azurite emulator with a container + sample blob. +# +# Usage: +# ./azurite-seed.sh # create container "portabase" + sample blob +# ./azurite-seed.sh my-container # override container name +# + +set -euo pipefail + +export AZURE_STORAGE_CONNECTION_STRING="DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;QueueEndpoint=http://127.0.0.1:10001/devstoreaccount1;TableEndpoint=http://127.0.0.1:10002/devstoreaccount1;" + +CONTAINER="${1:-portabase}" +BLOB_NAME="hello.txt" + +command -v az >/dev/null 2>&1 || { echo "ERROR: 'az' CLI not found in PATH." >&2; exit 1; } + +# Reachability check — surface the real az error instead of guessing "container down". +if ! err=$(az storage container list --num-results 1 2>&1 >/dev/null); then + echo "ERROR: Azurite query failed:" >&2 + printf '%s\n' "$err" | sed 's/^/ /' >&2 + echo " Is Azurite up? Start it with: docker compose up -d azurite" >&2 + exit 1 +fi + +# Create container (idempotent — az returns created:false if it already exists). +az storage container create --name "$CONTAINER" -o none +echo "📦 container ready: $CONTAINER" + +# Upload a sample blob from a temp file. +tmp=$(mktemp) +trap 'rm -f "$tmp"' EXIT +printf 'hello from azurite seed — %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$tmp" + +az storage blob upload \ + --container-name "$CONTAINER" \ + --name "$BLOB_NAME" \ + --file "$tmp" \ + --overwrite true \ + --no-progress \ + -o none +echo " • uploaded blob: $BLOB_NAME" + +echo +echo "Seed complete. Verify with: make list-blob" diff --git a/src/db/migrations/0062_futuristic_dragon_lord.sql b/src/db/migrations/0062_futuristic_dragon_lord.sql new file mode 100644 index 00000000..0319af5b --- /dev/null +++ b/src/db/migrations/0062_futuristic_dragon_lord.sql @@ -0,0 +1 @@ +ALTER TYPE "public"."provider_kind" ADD VALUE 'teams'; \ No newline at end of file diff --git a/src/db/migrations/0063_open_proudstar.sql b/src/db/migrations/0063_open_proudstar.sql new file mode 100644 index 00000000..95f376ce --- /dev/null +++ b/src/db/migrations/0063_open_proudstar.sql @@ -0,0 +1 @@ +ALTER TYPE "public"."provider_kind" ADD VALUE 'pushover'; \ No newline at end of file diff --git a/src/db/migrations/0064_orange_richard_fisk.sql b/src/db/migrations/0064_orange_richard_fisk.sql new file mode 100644 index 00000000..a0961476 --- /dev/null +++ b/src/db/migrations/0064_orange_richard_fisk.sql @@ -0,0 +1 @@ +ALTER TYPE "public"."provider_storage_kind" ADD VALUE 'blob'; \ No newline at end of file diff --git a/src/db/migrations/meta/0062_snapshot.json b/src/db/migrations/meta/0062_snapshot.json new file mode 100644 index 00000000..15f1c3d6 --- /dev/null +++ b/src/db/migrations/meta/0062_snapshot.json @@ -0,0 +1,2968 @@ +{ + "id": "679bcdd7-cf48-43d5-965d-0814b49196e1", + "prevId": "e6d15bce-96b6-468c-8853-08efe5a18ecd", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "smtp_password": { + "name": "smtp_password", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "smtp_from": { + "name": "smtp_from", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "smtp_host": { + "name": "smtp_host", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "smtp_port": { + "name": "smtp_port", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "smtp_user": { + "name": "smtp_user", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "smtp_secure": { + "name": "smtp_secure", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "default_notification_channel_id": { + "name": "default_notification_channel_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "default_storage_channel_id": { + "name": "default_storage_channel_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "encryption": { + "name": "encryption", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "settings_default_notification_channel_id_notification_channel_id_fk": { + "name": "settings_default_notification_channel_id_notification_channel_id_fk", + "tableFrom": "settings", + "tableTo": "notification_channel", + "columnsFrom": [ + "default_notification_channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "settings_default_storage_channel_id_storage_channel_id_fk": { + "name": "settings_default_storage_channel_id_storage_channel_id_fk", + "tableFrom": "settings", + "tableTo": "storage_channel", + "columnsFrom": [ + "default_storage_channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "settings_name_unique": { + "name": "settings_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.passkey": { + "name": "passkey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "publicKey": { + "name": "publicKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "credentialID": { + "name": "credentialID", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "deviceType": { + "name": "deviceType", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "backedUp": { + "name": "backedUp", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "aaguid": { + "name": "aaguid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "passkey_user_id_user_id_fk": { + "name": "passkey_user_id_user_id_fk", + "tableFrom": "passkey", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sso_provider": { + "name": "sso_provider", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oidc_config": { + "name": "oidc_config", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "saml_config": { + "name": "saml_config", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "sso_provider_user_id_user_id_fk": { + "name": "sso_provider_user_id_user_id_fk", + "tableFrom": "sso_provider", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sso_provider_provider_id_unique": { + "name": "sso_provider_provider_id_unique", + "nullsNotDistinct": false, + "columns": [ + "provider_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.two_factor": { + "name": "two_factor", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "backup_codes": { + "name": "backup_codes", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "verified": { + "name": "verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "two_factor_user_id_user_id_fk": { + "name": "two_factor_user_id_user_id_fk", + "tableFrom": "two_factor", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "theme": { + "name": "theme", + "type": "user_themes", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'light'" + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "lastConnectedAt": { + "name": "lastConnectedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "lastChangedPasswordAt": { + "name": "lastChangedPasswordAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "two_factor_enabled": { + "name": "two_factor_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organization_slug_unique": { + "name": "organization_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": [ + "inviter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.projects": { + "name": "projects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_archived": { + "name": "is_archived", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "projects_organization_id_organization_id_fk": { + "name": "projects_organization_id_organization_id_fk", + "tableFrom": "projects", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "projects_slug_unique": { + "name": "projects_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.backups": { + "name": "backups", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "status": { + "name": "status", + "type": "status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'waiting'" + }, + "file": { + "name": "file", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "file_size": { + "name": "file_size", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "database_id": { + "name": "database_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "imported": { + "name": "imported", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "migrated": { + "name": "migrated", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "backups_database_id_databases_id_fk": { + "name": "backups_database_id_databases_id_fk", + "tableFrom": "backups", + "tableTo": "databases", + "columnsFrom": [ + "database_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.databases": { + "name": "databases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "agent_database_id": { + "name": "agent_database_id", + "type": "uuid", + "primaryKey": false, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dbms": { + "name": "dbms", + "type": "dbms_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "backup_policy": { + "name": "backup_policy", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_waiting_for_backup": { + "name": "is_waiting_for_backup", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "backup_to_restore": { + "name": "backup_to_restore", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "health_error_count": { + "name": "health_error_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "last_contact": { + "name": "last_contact", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "databases_agent_id_agents_id_fk": { + "name": "databases_agent_id_agents_id_fk", + "tableFrom": "databases", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "databases_project_id_projects_id_fk": { + "name": "databases_project_id_projects_id_fk", + "tableFrom": "databases", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.restorations": { + "name": "restorations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "status": { + "name": "status", + "type": "status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'waiting'" + }, + "duration_ms": { + "name": "duration_ms", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "backup_storage_id": { + "name": "backup_storage_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "backup_id": { + "name": "backup_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "database_id": { + "name": "database_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "restorations_backup_storage_id_backup_storage_id_fk": { + "name": "restorations_backup_storage_id_backup_storage_id_fk", + "tableFrom": "restorations", + "tableTo": "backup_storage", + "columnsFrom": [ + "backup_storage_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "restorations_backup_id_backups_id_fk": { + "name": "restorations_backup_id_backups_id_fk", + "tableFrom": "restorations", + "tableTo": "backups", + "columnsFrom": [ + "backup_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "restorations_database_id_databases_id_fk": { + "name": "restorations_database_id_databases_id_fk", + "tableFrom": "restorations", + "tableTo": "databases", + "columnsFrom": [ + "database_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.retention_policies": { + "name": "retention_policies", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "database_id": { + "name": "database_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "retention_policy_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 7 + }, + "days": { + "name": "days", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 30 + }, + "gfs_daily": { + "name": "gfs_daily", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 7 + }, + "gfs_weekly": { + "name": "gfs_weekly", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 4 + }, + "gfs_monthly": { + "name": "gfs_monthly", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 12 + }, + "gfs_yearly": { + "name": "gfs_yearly", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3 + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "retention_policies_database_id_databases_id_fk": { + "name": "retention_policies_database_id_databases_id_fk", + "tableFrom": "retention_policies", + "tableTo": "databases", + "columnsFrom": [ + "database_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agents": { + "name": "agents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "health_error_count": { + "name": "health_error_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_archived": { + "name": "is_archived", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "last_contact": { + "name": "last_contact", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "agents_organization_id_organization_id_fk": { + "name": "agents_organization_id_organization_id_fk", + "tableFrom": "agents", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "agents_slug_unique": { + "name": "agents_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_agents": { + "name": "organization_agents", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "organization_agents_organization_id_organization_id_fk": { + "name": "organization_agents_organization_id_organization_id_fk", + "tableFrom": "organization_agents", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_agents_agent_id_agents_id_fk": { + "name": "organization_agents_agent_id_agents_id_fk", + "tableFrom": "organization_agents", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organization_agents_organization_id_agent_id_unique": { + "name": "organization_agents_organization_id_agent_id_unique", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "agent_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification_channel": { + "name": "notification_channel", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "provider": { + "name": "provider", + "type": "provider_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "notification_channel_organization_id_organization_id_fk": { + "name": "notification_channel_organization_id_organization_id_fk", + "tableFrom": "notification_channel", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_notification_channels": { + "name": "organization_notification_channels", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "notification_channel_id": { + "name": "notification_channel_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "organization_notification_channels_organization_id_organization_id_fk": { + "name": "organization_notification_channels_organization_id_organization_id_fk", + "tableFrom": "organization_notification_channels", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_notification_channels_notification_channel_id_notification_channel_id_fk": { + "name": "organization_notification_channels_notification_channel_id_notification_channel_id_fk", + "tableFrom": "organization_notification_channels", + "tableTo": "notification_channel", + "columnsFrom": [ + "notification_channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organization_notification_channels_organization_id_notification_channel_id_unique": { + "name": "organization_notification_channels_organization_id_notification_channel_id_unique", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "notification_channel_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.alert_policy": { + "name": "alert_policy", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "notification_channel_id": { + "name": "notification_channel_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "event_kind": { + "name": "event_kind", + "type": "event_kind[]", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "database_id": { + "name": "database_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "alert_policy_notification_channel_id_notification_channel_id_fk": { + "name": "alert_policy_notification_channel_id_notification_channel_id_fk", + "tableFrom": "alert_policy", + "tableTo": "notification_channel", + "columnsFrom": [ + "notification_channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_policy_database_id_databases_id_fk": { + "name": "alert_policy_database_id_databases_id_fk", + "tableFrom": "alert_policy", + "tableTo": "databases", + "columnsFrom": [ + "database_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification_log": { + "name": "notification_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "channel_id": { + "name": "channel_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "policy_id": { + "name": "policy_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "event": { + "name": "event", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_name": { + "name": "provider_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "level", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "success": { + "name": "success", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_response": { + "name": "provider_response", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_storage_channels": { + "name": "organization_storage_channels", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "storage_channel_id": { + "name": "storage_channel_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "organization_storage_channels_organization_id_organization_id_fk": { + "name": "organization_storage_channels_organization_id_organization_id_fk", + "tableFrom": "organization_storage_channels", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_storage_channels_storage_channel_id_storage_channel_id_fk": { + "name": "organization_storage_channels_storage_channel_id_storage_channel_id_fk", + "tableFrom": "organization_storage_channels", + "tableTo": "storage_channel", + "columnsFrom": [ + "storage_channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organization_storage_channels_organization_id_storage_channel_id_unique": { + "name": "organization_storage_channels_organization_id_storage_channel_id_unique", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "storage_channel_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.storage_channel": { + "name": "storage_channel", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "provider_storage_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "storage_channel_organization_id_organization_id_fk": { + "name": "storage_channel_organization_id_organization_id_fk", + "tableFrom": "storage_channel", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.storage_policy": { + "name": "storage_policy", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "storage_channel_id": { + "name": "storage_channel_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "database_id": { + "name": "database_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "storage_policy_storage_channel_id_storage_channel_id_fk": { + "name": "storage_policy_storage_channel_id_storage_channel_id_fk", + "tableFrom": "storage_policy", + "tableTo": "storage_channel", + "columnsFrom": [ + "storage_channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "storage_policy_database_id_databases_id_fk": { + "name": "storage_policy_database_id_databases_id_fk", + "tableFrom": "storage_policy", + "tableTo": "databases", + "columnsFrom": [ + "database_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.backup_storage": { + "name": "backup_storage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "backup_id": { + "name": "backup_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "storage_channel_id": { + "name": "storage_channel_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "backup_storage_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "size": { + "name": "size", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "checksum": { + "name": "checksum", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "backup_storage_backup_id_backups_id_fk": { + "name": "backup_storage_backup_id_backups_id_fk", + "tableFrom": "backup_storage", + "tableTo": "backups", + "columnsFrom": [ + "backup_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_storage_storage_channel_id_storage_channel_id_fk": { + "name": "backup_storage_storage_channel_id_storage_channel_id_fk", + "tableFrom": "backup_storage", + "tableTo": "storage_channel", + "columnsFrom": [ + "storage_channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.healthcheck_log": { + "name": "healthcheck_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "kind": { + "name": "kind", + "type": "healthcheck_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "date": { + "name": "date", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "healthcheck_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "object_id": { + "name": "object_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.apikey": { + "name": "apikey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "config_id": { + "name": "config_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "start": { + "name": "start", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refill_interval": { + "name": "refill_interval", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "refill_amount": { + "name": "refill_amount", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "rate_limit_enabled": { + "name": "rate_limit_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "rate_limit_time_window": { + "name": "rate_limit_time_window", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rate_limit_max": { + "name": "rate_limit_max", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "request_count": { + "name": "request_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "remaining": { + "name": "remaining", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_request": { + "name": "last_request", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.job_log": { + "name": "job_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "backup_id": { + "name": "backup_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "restoration_id": { + "name": "restoration_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "logged_at": { + "name": "logged_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "entry_type": { + "name": "entry_type", + "type": "job_log_entry_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "job_log_level", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "output": { + "name": "output", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "exit_code": { + "name": "exit_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "job_log_backup_id_backups_id_fk": { + "name": "job_log_backup_id_backups_id_fk", + "tableFrom": "job_log", + "tableTo": "backups", + "columnsFrom": [ + "backup_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "job_log_restoration_id_restorations_id_fk": { + "name": "job_log_restoration_id_restorations_id_fk", + "tableFrom": "job_log", + "tableTo": "restorations", + "columnsFrom": [ + "restoration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.user_themes": { + "name": "user_themes", + "schema": "public", + "values": [ + "light", + "dark", + "system" + ] + }, + "public.retention_policy_type": { + "name": "retention_policy_type", + "schema": "public", + "values": [ + "count", + "days", + "gfs" + ] + }, + "public.provider_kind": { + "name": "provider_kind", + "schema": "public", + "values": [ + "slack", + "smtp", + "discord", + "telegram", + "gotify", + "ntfy", + "webhook", + "nextcloud", + "teams" + ] + }, + "public.event_kind": { + "name": "event_kind", + "schema": "public", + "values": [ + "error_backup", + "error_restore", + "success_restore", + "success_backup", + "weekly_report", + "error_health_agent", + "error_health_database" + ] + }, + "public.level": { + "name": "level", + "schema": "public", + "values": [ + "critical", + "warning", + "info" + ] + }, + "public.provider_storage_kind": { + "name": "provider_storage_kind", + "schema": "public", + "values": [ + "local", + "s3", + "google-drive" + ] + }, + "public.backup_storage_status": { + "name": "backup_storage_status", + "schema": "public", + "values": [ + "pending", + "success", + "failed" + ] + }, + "public.healthcheck_status": { + "name": "healthcheck_status", + "schema": "public", + "values": [ + "success", + "failed" + ] + }, + "public.healthcheck_kind": { + "name": "healthcheck_kind", + "schema": "public", + "values": [ + "database", + "agent" + ] + }, + "public.job_log_entry_type": { + "name": "job_log_entry_type", + "schema": "public", + "values": [ + "log", + "command" + ] + }, + "public.job_log_level": { + "name": "job_log_level", + "schema": "public", + "values": [ + "debug", + "info", + "warn", + "error" + ] + }, + "public.dbms_status": { + "name": "dbms_status", + "schema": "public", + "values": [ + "postgresql", + "mysql", + "mariadb", + "mongodb", + "sqlite", + "redis", + "valkey", + "firebird", + "mssql" + ] + }, + "public.status": { + "name": "status", + "schema": "public", + "values": [ + "waiting", + "ongoing", + "failed", + "success" + ] + }, + "public.type_storage": { + "name": "type_storage", + "schema": "public", + "values": [ + "local", + "s3" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/src/db/migrations/meta/0063_snapshot.json b/src/db/migrations/meta/0063_snapshot.json new file mode 100644 index 00000000..22e578c6 --- /dev/null +++ b/src/db/migrations/meta/0063_snapshot.json @@ -0,0 +1,2969 @@ +{ + "id": "93d81732-8e94-4c2e-be9d-79c7bacc398e", + "prevId": "679bcdd7-cf48-43d5-965d-0814b49196e1", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "smtp_password": { + "name": "smtp_password", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "smtp_from": { + "name": "smtp_from", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "smtp_host": { + "name": "smtp_host", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "smtp_port": { + "name": "smtp_port", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "smtp_user": { + "name": "smtp_user", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "smtp_secure": { + "name": "smtp_secure", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "default_notification_channel_id": { + "name": "default_notification_channel_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "default_storage_channel_id": { + "name": "default_storage_channel_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "encryption": { + "name": "encryption", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "settings_default_notification_channel_id_notification_channel_id_fk": { + "name": "settings_default_notification_channel_id_notification_channel_id_fk", + "tableFrom": "settings", + "tableTo": "notification_channel", + "columnsFrom": [ + "default_notification_channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "settings_default_storage_channel_id_storage_channel_id_fk": { + "name": "settings_default_storage_channel_id_storage_channel_id_fk", + "tableFrom": "settings", + "tableTo": "storage_channel", + "columnsFrom": [ + "default_storage_channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "settings_name_unique": { + "name": "settings_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.passkey": { + "name": "passkey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "publicKey": { + "name": "publicKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "credentialID": { + "name": "credentialID", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "deviceType": { + "name": "deviceType", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "backedUp": { + "name": "backedUp", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "aaguid": { + "name": "aaguid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "passkey_user_id_user_id_fk": { + "name": "passkey_user_id_user_id_fk", + "tableFrom": "passkey", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sso_provider": { + "name": "sso_provider", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oidc_config": { + "name": "oidc_config", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "saml_config": { + "name": "saml_config", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "sso_provider_user_id_user_id_fk": { + "name": "sso_provider_user_id_user_id_fk", + "tableFrom": "sso_provider", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sso_provider_provider_id_unique": { + "name": "sso_provider_provider_id_unique", + "nullsNotDistinct": false, + "columns": [ + "provider_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.two_factor": { + "name": "two_factor", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "backup_codes": { + "name": "backup_codes", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "verified": { + "name": "verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "two_factor_user_id_user_id_fk": { + "name": "two_factor_user_id_user_id_fk", + "tableFrom": "two_factor", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "theme": { + "name": "theme", + "type": "user_themes", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'light'" + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "lastConnectedAt": { + "name": "lastConnectedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "lastChangedPasswordAt": { + "name": "lastChangedPasswordAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "two_factor_enabled": { + "name": "two_factor_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organization_slug_unique": { + "name": "organization_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": [ + "inviter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.projects": { + "name": "projects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_archived": { + "name": "is_archived", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "projects_organization_id_organization_id_fk": { + "name": "projects_organization_id_organization_id_fk", + "tableFrom": "projects", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "projects_slug_unique": { + "name": "projects_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.backups": { + "name": "backups", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "status": { + "name": "status", + "type": "status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'waiting'" + }, + "file": { + "name": "file", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "file_size": { + "name": "file_size", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "database_id": { + "name": "database_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "imported": { + "name": "imported", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "migrated": { + "name": "migrated", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "backups_database_id_databases_id_fk": { + "name": "backups_database_id_databases_id_fk", + "tableFrom": "backups", + "tableTo": "databases", + "columnsFrom": [ + "database_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.databases": { + "name": "databases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "agent_database_id": { + "name": "agent_database_id", + "type": "uuid", + "primaryKey": false, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dbms": { + "name": "dbms", + "type": "dbms_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "backup_policy": { + "name": "backup_policy", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_waiting_for_backup": { + "name": "is_waiting_for_backup", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "backup_to_restore": { + "name": "backup_to_restore", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "health_error_count": { + "name": "health_error_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "last_contact": { + "name": "last_contact", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "databases_agent_id_agents_id_fk": { + "name": "databases_agent_id_agents_id_fk", + "tableFrom": "databases", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "databases_project_id_projects_id_fk": { + "name": "databases_project_id_projects_id_fk", + "tableFrom": "databases", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.restorations": { + "name": "restorations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "status": { + "name": "status", + "type": "status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'waiting'" + }, + "duration_ms": { + "name": "duration_ms", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "backup_storage_id": { + "name": "backup_storage_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "backup_id": { + "name": "backup_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "database_id": { + "name": "database_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "restorations_backup_storage_id_backup_storage_id_fk": { + "name": "restorations_backup_storage_id_backup_storage_id_fk", + "tableFrom": "restorations", + "tableTo": "backup_storage", + "columnsFrom": [ + "backup_storage_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "restorations_backup_id_backups_id_fk": { + "name": "restorations_backup_id_backups_id_fk", + "tableFrom": "restorations", + "tableTo": "backups", + "columnsFrom": [ + "backup_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "restorations_database_id_databases_id_fk": { + "name": "restorations_database_id_databases_id_fk", + "tableFrom": "restorations", + "tableTo": "databases", + "columnsFrom": [ + "database_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.retention_policies": { + "name": "retention_policies", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "database_id": { + "name": "database_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "retention_policy_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 7 + }, + "days": { + "name": "days", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 30 + }, + "gfs_daily": { + "name": "gfs_daily", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 7 + }, + "gfs_weekly": { + "name": "gfs_weekly", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 4 + }, + "gfs_monthly": { + "name": "gfs_monthly", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 12 + }, + "gfs_yearly": { + "name": "gfs_yearly", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3 + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "retention_policies_database_id_databases_id_fk": { + "name": "retention_policies_database_id_databases_id_fk", + "tableFrom": "retention_policies", + "tableTo": "databases", + "columnsFrom": [ + "database_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agents": { + "name": "agents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "health_error_count": { + "name": "health_error_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_archived": { + "name": "is_archived", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "last_contact": { + "name": "last_contact", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "agents_organization_id_organization_id_fk": { + "name": "agents_organization_id_organization_id_fk", + "tableFrom": "agents", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "agents_slug_unique": { + "name": "agents_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_agents": { + "name": "organization_agents", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "organization_agents_organization_id_organization_id_fk": { + "name": "organization_agents_organization_id_organization_id_fk", + "tableFrom": "organization_agents", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_agents_agent_id_agents_id_fk": { + "name": "organization_agents_agent_id_agents_id_fk", + "tableFrom": "organization_agents", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organization_agents_organization_id_agent_id_unique": { + "name": "organization_agents_organization_id_agent_id_unique", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "agent_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification_channel": { + "name": "notification_channel", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "provider": { + "name": "provider", + "type": "provider_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "notification_channel_organization_id_organization_id_fk": { + "name": "notification_channel_organization_id_organization_id_fk", + "tableFrom": "notification_channel", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_notification_channels": { + "name": "organization_notification_channels", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "notification_channel_id": { + "name": "notification_channel_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "organization_notification_channels_organization_id_organization_id_fk": { + "name": "organization_notification_channels_organization_id_organization_id_fk", + "tableFrom": "organization_notification_channels", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_notification_channels_notification_channel_id_notification_channel_id_fk": { + "name": "organization_notification_channels_notification_channel_id_notification_channel_id_fk", + "tableFrom": "organization_notification_channels", + "tableTo": "notification_channel", + "columnsFrom": [ + "notification_channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organization_notification_channels_organization_id_notification_channel_id_unique": { + "name": "organization_notification_channels_organization_id_notification_channel_id_unique", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "notification_channel_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.alert_policy": { + "name": "alert_policy", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "notification_channel_id": { + "name": "notification_channel_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "event_kind": { + "name": "event_kind", + "type": "event_kind[]", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "database_id": { + "name": "database_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "alert_policy_notification_channel_id_notification_channel_id_fk": { + "name": "alert_policy_notification_channel_id_notification_channel_id_fk", + "tableFrom": "alert_policy", + "tableTo": "notification_channel", + "columnsFrom": [ + "notification_channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_policy_database_id_databases_id_fk": { + "name": "alert_policy_database_id_databases_id_fk", + "tableFrom": "alert_policy", + "tableTo": "databases", + "columnsFrom": [ + "database_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification_log": { + "name": "notification_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "channel_id": { + "name": "channel_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "policy_id": { + "name": "policy_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "event": { + "name": "event", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_name": { + "name": "provider_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "level", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "success": { + "name": "success", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_response": { + "name": "provider_response", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_storage_channels": { + "name": "organization_storage_channels", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "storage_channel_id": { + "name": "storage_channel_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "organization_storage_channels_organization_id_organization_id_fk": { + "name": "organization_storage_channels_organization_id_organization_id_fk", + "tableFrom": "organization_storage_channels", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_storage_channels_storage_channel_id_storage_channel_id_fk": { + "name": "organization_storage_channels_storage_channel_id_storage_channel_id_fk", + "tableFrom": "organization_storage_channels", + "tableTo": "storage_channel", + "columnsFrom": [ + "storage_channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organization_storage_channels_organization_id_storage_channel_id_unique": { + "name": "organization_storage_channels_organization_id_storage_channel_id_unique", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "storage_channel_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.storage_channel": { + "name": "storage_channel", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "provider_storage_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "storage_channel_organization_id_organization_id_fk": { + "name": "storage_channel_organization_id_organization_id_fk", + "tableFrom": "storage_channel", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.storage_policy": { + "name": "storage_policy", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "storage_channel_id": { + "name": "storage_channel_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "database_id": { + "name": "database_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "storage_policy_storage_channel_id_storage_channel_id_fk": { + "name": "storage_policy_storage_channel_id_storage_channel_id_fk", + "tableFrom": "storage_policy", + "tableTo": "storage_channel", + "columnsFrom": [ + "storage_channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "storage_policy_database_id_databases_id_fk": { + "name": "storage_policy_database_id_databases_id_fk", + "tableFrom": "storage_policy", + "tableTo": "databases", + "columnsFrom": [ + "database_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.backup_storage": { + "name": "backup_storage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "backup_id": { + "name": "backup_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "storage_channel_id": { + "name": "storage_channel_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "backup_storage_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "size": { + "name": "size", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "checksum": { + "name": "checksum", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "backup_storage_backup_id_backups_id_fk": { + "name": "backup_storage_backup_id_backups_id_fk", + "tableFrom": "backup_storage", + "tableTo": "backups", + "columnsFrom": [ + "backup_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_storage_storage_channel_id_storage_channel_id_fk": { + "name": "backup_storage_storage_channel_id_storage_channel_id_fk", + "tableFrom": "backup_storage", + "tableTo": "storage_channel", + "columnsFrom": [ + "storage_channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.healthcheck_log": { + "name": "healthcheck_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "kind": { + "name": "kind", + "type": "healthcheck_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "date": { + "name": "date", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "healthcheck_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "object_id": { + "name": "object_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.apikey": { + "name": "apikey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "config_id": { + "name": "config_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "start": { + "name": "start", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refill_interval": { + "name": "refill_interval", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "refill_amount": { + "name": "refill_amount", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "rate_limit_enabled": { + "name": "rate_limit_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "rate_limit_time_window": { + "name": "rate_limit_time_window", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rate_limit_max": { + "name": "rate_limit_max", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "request_count": { + "name": "request_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "remaining": { + "name": "remaining", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_request": { + "name": "last_request", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.job_log": { + "name": "job_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "backup_id": { + "name": "backup_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "restoration_id": { + "name": "restoration_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "logged_at": { + "name": "logged_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "entry_type": { + "name": "entry_type", + "type": "job_log_entry_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "job_log_level", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "output": { + "name": "output", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "exit_code": { + "name": "exit_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "job_log_backup_id_backups_id_fk": { + "name": "job_log_backup_id_backups_id_fk", + "tableFrom": "job_log", + "tableTo": "backups", + "columnsFrom": [ + "backup_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "job_log_restoration_id_restorations_id_fk": { + "name": "job_log_restoration_id_restorations_id_fk", + "tableFrom": "job_log", + "tableTo": "restorations", + "columnsFrom": [ + "restoration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.user_themes": { + "name": "user_themes", + "schema": "public", + "values": [ + "light", + "dark", + "system" + ] + }, + "public.retention_policy_type": { + "name": "retention_policy_type", + "schema": "public", + "values": [ + "count", + "days", + "gfs" + ] + }, + "public.provider_kind": { + "name": "provider_kind", + "schema": "public", + "values": [ + "slack", + "smtp", + "discord", + "telegram", + "gotify", + "ntfy", + "webhook", + "nextcloud", + "teams", + "pushover" + ] + }, + "public.event_kind": { + "name": "event_kind", + "schema": "public", + "values": [ + "error_backup", + "error_restore", + "success_restore", + "success_backup", + "weekly_report", + "error_health_agent", + "error_health_database" + ] + }, + "public.level": { + "name": "level", + "schema": "public", + "values": [ + "critical", + "warning", + "info" + ] + }, + "public.provider_storage_kind": { + "name": "provider_storage_kind", + "schema": "public", + "values": [ + "local", + "s3", + "google-drive" + ] + }, + "public.backup_storage_status": { + "name": "backup_storage_status", + "schema": "public", + "values": [ + "pending", + "success", + "failed" + ] + }, + "public.healthcheck_status": { + "name": "healthcheck_status", + "schema": "public", + "values": [ + "success", + "failed" + ] + }, + "public.healthcheck_kind": { + "name": "healthcheck_kind", + "schema": "public", + "values": [ + "database", + "agent" + ] + }, + "public.job_log_entry_type": { + "name": "job_log_entry_type", + "schema": "public", + "values": [ + "log", + "command" + ] + }, + "public.job_log_level": { + "name": "job_log_level", + "schema": "public", + "values": [ + "debug", + "info", + "warn", + "error" + ] + }, + "public.dbms_status": { + "name": "dbms_status", + "schema": "public", + "values": [ + "postgresql", + "mysql", + "mariadb", + "mongodb", + "sqlite", + "redis", + "valkey", + "firebird", + "mssql" + ] + }, + "public.status": { + "name": "status", + "schema": "public", + "values": [ + "waiting", + "ongoing", + "failed", + "success" + ] + }, + "public.type_storage": { + "name": "type_storage", + "schema": "public", + "values": [ + "local", + "s3" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/src/db/migrations/meta/0064_snapshot.json b/src/db/migrations/meta/0064_snapshot.json new file mode 100644 index 00000000..cfe5b623 --- /dev/null +++ b/src/db/migrations/meta/0064_snapshot.json @@ -0,0 +1,2970 @@ +{ + "id": "53c89cd2-a05d-4dd5-b954-790eabf5eff7", + "prevId": "93d81732-8e94-4c2e-be9d-79c7bacc398e", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "smtp_password": { + "name": "smtp_password", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "smtp_from": { + "name": "smtp_from", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "smtp_host": { + "name": "smtp_host", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "smtp_port": { + "name": "smtp_port", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "smtp_user": { + "name": "smtp_user", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "smtp_secure": { + "name": "smtp_secure", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "default_notification_channel_id": { + "name": "default_notification_channel_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "default_storage_channel_id": { + "name": "default_storage_channel_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "encryption": { + "name": "encryption", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "settings_default_notification_channel_id_notification_channel_id_fk": { + "name": "settings_default_notification_channel_id_notification_channel_id_fk", + "tableFrom": "settings", + "tableTo": "notification_channel", + "columnsFrom": [ + "default_notification_channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "settings_default_storage_channel_id_storage_channel_id_fk": { + "name": "settings_default_storage_channel_id_storage_channel_id_fk", + "tableFrom": "settings", + "tableTo": "storage_channel", + "columnsFrom": [ + "default_storage_channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "settings_name_unique": { + "name": "settings_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.passkey": { + "name": "passkey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "publicKey": { + "name": "publicKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "credentialID": { + "name": "credentialID", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "deviceType": { + "name": "deviceType", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "backedUp": { + "name": "backedUp", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "aaguid": { + "name": "aaguid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "passkey_user_id_user_id_fk": { + "name": "passkey_user_id_user_id_fk", + "tableFrom": "passkey", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sso_provider": { + "name": "sso_provider", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oidc_config": { + "name": "oidc_config", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "saml_config": { + "name": "saml_config", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "sso_provider_user_id_user_id_fk": { + "name": "sso_provider_user_id_user_id_fk", + "tableFrom": "sso_provider", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sso_provider_provider_id_unique": { + "name": "sso_provider_provider_id_unique", + "nullsNotDistinct": false, + "columns": [ + "provider_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.two_factor": { + "name": "two_factor", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "backup_codes": { + "name": "backup_codes", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "verified": { + "name": "verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "two_factor_user_id_user_id_fk": { + "name": "two_factor_user_id_user_id_fk", + "tableFrom": "two_factor", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "theme": { + "name": "theme", + "type": "user_themes", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'light'" + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "lastConnectedAt": { + "name": "lastConnectedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "lastChangedPasswordAt": { + "name": "lastChangedPasswordAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "two_factor_enabled": { + "name": "two_factor_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organization_slug_unique": { + "name": "organization_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": [ + "inviter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.projects": { + "name": "projects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_archived": { + "name": "is_archived", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "projects_organization_id_organization_id_fk": { + "name": "projects_organization_id_organization_id_fk", + "tableFrom": "projects", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "projects_slug_unique": { + "name": "projects_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.backups": { + "name": "backups", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "status": { + "name": "status", + "type": "status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'waiting'" + }, + "file": { + "name": "file", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "file_size": { + "name": "file_size", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "database_id": { + "name": "database_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "imported": { + "name": "imported", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "migrated": { + "name": "migrated", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "backups_database_id_databases_id_fk": { + "name": "backups_database_id_databases_id_fk", + "tableFrom": "backups", + "tableTo": "databases", + "columnsFrom": [ + "database_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.databases": { + "name": "databases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "agent_database_id": { + "name": "agent_database_id", + "type": "uuid", + "primaryKey": false, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dbms": { + "name": "dbms", + "type": "dbms_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "backup_policy": { + "name": "backup_policy", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_waiting_for_backup": { + "name": "is_waiting_for_backup", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "backup_to_restore": { + "name": "backup_to_restore", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "health_error_count": { + "name": "health_error_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "last_contact": { + "name": "last_contact", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "databases_agent_id_agents_id_fk": { + "name": "databases_agent_id_agents_id_fk", + "tableFrom": "databases", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "databases_project_id_projects_id_fk": { + "name": "databases_project_id_projects_id_fk", + "tableFrom": "databases", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.restorations": { + "name": "restorations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "status": { + "name": "status", + "type": "status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'waiting'" + }, + "duration_ms": { + "name": "duration_ms", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "backup_storage_id": { + "name": "backup_storage_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "backup_id": { + "name": "backup_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "database_id": { + "name": "database_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "restorations_backup_storage_id_backup_storage_id_fk": { + "name": "restorations_backup_storage_id_backup_storage_id_fk", + "tableFrom": "restorations", + "tableTo": "backup_storage", + "columnsFrom": [ + "backup_storage_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "restorations_backup_id_backups_id_fk": { + "name": "restorations_backup_id_backups_id_fk", + "tableFrom": "restorations", + "tableTo": "backups", + "columnsFrom": [ + "backup_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "restorations_database_id_databases_id_fk": { + "name": "restorations_database_id_databases_id_fk", + "tableFrom": "restorations", + "tableTo": "databases", + "columnsFrom": [ + "database_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.retention_policies": { + "name": "retention_policies", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "database_id": { + "name": "database_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "retention_policy_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 7 + }, + "days": { + "name": "days", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 30 + }, + "gfs_daily": { + "name": "gfs_daily", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 7 + }, + "gfs_weekly": { + "name": "gfs_weekly", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 4 + }, + "gfs_monthly": { + "name": "gfs_monthly", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 12 + }, + "gfs_yearly": { + "name": "gfs_yearly", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3 + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "retention_policies_database_id_databases_id_fk": { + "name": "retention_policies_database_id_databases_id_fk", + "tableFrom": "retention_policies", + "tableTo": "databases", + "columnsFrom": [ + "database_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agents": { + "name": "agents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "health_error_count": { + "name": "health_error_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_archived": { + "name": "is_archived", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "last_contact": { + "name": "last_contact", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "agents_organization_id_organization_id_fk": { + "name": "agents_organization_id_organization_id_fk", + "tableFrom": "agents", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "agents_slug_unique": { + "name": "agents_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_agents": { + "name": "organization_agents", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "organization_agents_organization_id_organization_id_fk": { + "name": "organization_agents_organization_id_organization_id_fk", + "tableFrom": "organization_agents", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_agents_agent_id_agents_id_fk": { + "name": "organization_agents_agent_id_agents_id_fk", + "tableFrom": "organization_agents", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organization_agents_organization_id_agent_id_unique": { + "name": "organization_agents_organization_id_agent_id_unique", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "agent_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification_channel": { + "name": "notification_channel", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "provider": { + "name": "provider", + "type": "provider_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "notification_channel_organization_id_organization_id_fk": { + "name": "notification_channel_organization_id_organization_id_fk", + "tableFrom": "notification_channel", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_notification_channels": { + "name": "organization_notification_channels", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "notification_channel_id": { + "name": "notification_channel_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "organization_notification_channels_organization_id_organization_id_fk": { + "name": "organization_notification_channels_organization_id_organization_id_fk", + "tableFrom": "organization_notification_channels", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_notification_channels_notification_channel_id_notification_channel_id_fk": { + "name": "organization_notification_channels_notification_channel_id_notification_channel_id_fk", + "tableFrom": "organization_notification_channels", + "tableTo": "notification_channel", + "columnsFrom": [ + "notification_channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organization_notification_channels_organization_id_notification_channel_id_unique": { + "name": "organization_notification_channels_organization_id_notification_channel_id_unique", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "notification_channel_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.alert_policy": { + "name": "alert_policy", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "notification_channel_id": { + "name": "notification_channel_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "event_kind": { + "name": "event_kind", + "type": "event_kind[]", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "database_id": { + "name": "database_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "alert_policy_notification_channel_id_notification_channel_id_fk": { + "name": "alert_policy_notification_channel_id_notification_channel_id_fk", + "tableFrom": "alert_policy", + "tableTo": "notification_channel", + "columnsFrom": [ + "notification_channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "alert_policy_database_id_databases_id_fk": { + "name": "alert_policy_database_id_databases_id_fk", + "tableFrom": "alert_policy", + "tableTo": "databases", + "columnsFrom": [ + "database_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification_log": { + "name": "notification_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "channel_id": { + "name": "channel_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "policy_id": { + "name": "policy_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "event": { + "name": "event", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_name": { + "name": "provider_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "level", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "success": { + "name": "success", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_response": { + "name": "provider_response", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_storage_channels": { + "name": "organization_storage_channels", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "storage_channel_id": { + "name": "storage_channel_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "organization_storage_channels_organization_id_organization_id_fk": { + "name": "organization_storage_channels_organization_id_organization_id_fk", + "tableFrom": "organization_storage_channels", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_storage_channels_storage_channel_id_storage_channel_id_fk": { + "name": "organization_storage_channels_storage_channel_id_storage_channel_id_fk", + "tableFrom": "organization_storage_channels", + "tableTo": "storage_channel", + "columnsFrom": [ + "storage_channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organization_storage_channels_organization_id_storage_channel_id_unique": { + "name": "organization_storage_channels_organization_id_storage_channel_id_unique", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "storage_channel_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.storage_channel": { + "name": "storage_channel", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "provider_storage_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "storage_channel_organization_id_organization_id_fk": { + "name": "storage_channel_organization_id_organization_id_fk", + "tableFrom": "storage_channel", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.storage_policy": { + "name": "storage_policy", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "storage_channel_id": { + "name": "storage_channel_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "database_id": { + "name": "database_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "storage_policy_storage_channel_id_storage_channel_id_fk": { + "name": "storage_policy_storage_channel_id_storage_channel_id_fk", + "tableFrom": "storage_policy", + "tableTo": "storage_channel", + "columnsFrom": [ + "storage_channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "storage_policy_database_id_databases_id_fk": { + "name": "storage_policy_database_id_databases_id_fk", + "tableFrom": "storage_policy", + "tableTo": "databases", + "columnsFrom": [ + "database_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.backup_storage": { + "name": "backup_storage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "backup_id": { + "name": "backup_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "storage_channel_id": { + "name": "storage_channel_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "backup_storage_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "size": { + "name": "size", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "checksum": { + "name": "checksum", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "backup_storage_backup_id_backups_id_fk": { + "name": "backup_storage_backup_id_backups_id_fk", + "tableFrom": "backup_storage", + "tableTo": "backups", + "columnsFrom": [ + "backup_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_storage_storage_channel_id_storage_channel_id_fk": { + "name": "backup_storage_storage_channel_id_storage_channel_id_fk", + "tableFrom": "backup_storage", + "tableTo": "storage_channel", + "columnsFrom": [ + "storage_channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.healthcheck_log": { + "name": "healthcheck_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "kind": { + "name": "kind", + "type": "healthcheck_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "date": { + "name": "date", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "healthcheck_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "object_id": { + "name": "object_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.apikey": { + "name": "apikey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "config_id": { + "name": "config_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "start": { + "name": "start", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refill_interval": { + "name": "refill_interval", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "refill_amount": { + "name": "refill_amount", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "rate_limit_enabled": { + "name": "rate_limit_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "rate_limit_time_window": { + "name": "rate_limit_time_window", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rate_limit_max": { + "name": "rate_limit_max", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "request_count": { + "name": "request_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "remaining": { + "name": "remaining", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_request": { + "name": "last_request", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.job_log": { + "name": "job_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "backup_id": { + "name": "backup_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "restoration_id": { + "name": "restoration_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "logged_at": { + "name": "logged_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "entry_type": { + "name": "entry_type", + "type": "job_log_entry_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "job_log_level", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "output": { + "name": "output", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "exit_code": { + "name": "exit_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "job_log_backup_id_backups_id_fk": { + "name": "job_log_backup_id_backups_id_fk", + "tableFrom": "job_log", + "tableTo": "backups", + "columnsFrom": [ + "backup_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "job_log_restoration_id_restorations_id_fk": { + "name": "job_log_restoration_id_restorations_id_fk", + "tableFrom": "job_log", + "tableTo": "restorations", + "columnsFrom": [ + "restoration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.user_themes": { + "name": "user_themes", + "schema": "public", + "values": [ + "light", + "dark", + "system" + ] + }, + "public.retention_policy_type": { + "name": "retention_policy_type", + "schema": "public", + "values": [ + "count", + "days", + "gfs" + ] + }, + "public.provider_kind": { + "name": "provider_kind", + "schema": "public", + "values": [ + "slack", + "smtp", + "discord", + "telegram", + "gotify", + "ntfy", + "webhook", + "nextcloud", + "teams", + "pushover" + ] + }, + "public.event_kind": { + "name": "event_kind", + "schema": "public", + "values": [ + "error_backup", + "error_restore", + "success_restore", + "success_backup", + "weekly_report", + "error_health_agent", + "error_health_database" + ] + }, + "public.level": { + "name": "level", + "schema": "public", + "values": [ + "critical", + "warning", + "info" + ] + }, + "public.provider_storage_kind": { + "name": "provider_storage_kind", + "schema": "public", + "values": [ + "local", + "s3", + "google-drive", + "blob" + ] + }, + "public.backup_storage_status": { + "name": "backup_storage_status", + "schema": "public", + "values": [ + "pending", + "success", + "failed" + ] + }, + "public.healthcheck_status": { + "name": "healthcheck_status", + "schema": "public", + "values": [ + "success", + "failed" + ] + }, + "public.healthcheck_kind": { + "name": "healthcheck_kind", + "schema": "public", + "values": [ + "database", + "agent" + ] + }, + "public.job_log_entry_type": { + "name": "job_log_entry_type", + "schema": "public", + "values": [ + "log", + "command" + ] + }, + "public.job_log_level": { + "name": "job_log_level", + "schema": "public", + "values": [ + "debug", + "info", + "warn", + "error" + ] + }, + "public.dbms_status": { + "name": "dbms_status", + "schema": "public", + "values": [ + "postgresql", + "mysql", + "mariadb", + "mongodb", + "sqlite", + "redis", + "valkey", + "firebird", + "mssql" + ] + }, + "public.status": { + "name": "status", + "schema": "public", + "values": [ + "waiting", + "ongoing", + "failed", + "success" + ] + }, + "public.type_storage": { + "name": "type_storage", + "schema": "public", + "values": [ + "local", + "s3" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/src/db/migrations/meta/_journal.json b/src/db/migrations/meta/_journal.json index 7bc8f931..806f6262 100644 --- a/src/db/migrations/meta/_journal.json +++ b/src/db/migrations/meta/_journal.json @@ -435,6 +435,27 @@ "when": 1780822414802, "tag": "0061_illegal_mole_man", "breakpoints": true + }, + { + "idx": 62, + "version": "7", + "when": 1780934519843, + "tag": "0062_futuristic_dragon_lord", + "breakpoints": true + }, + { + "idx": 63, + "version": "7", + "when": 1781854323074, + "tag": "0063_open_proudstar", + "breakpoints": true + }, + { + "idx": 64, + "version": "7", + "when": 1781960184847, + "tag": "0064_orange_richard_fisk", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/db/schema/09_notification-channel.ts b/src/db/schema/09_notification-channel.ts index b8281bac..5ddde86a 100644 --- a/src/db/schema/09_notification-channel.ts +++ b/src/db/schema/09_notification-channel.ts @@ -7,7 +7,7 @@ import {z} from "zod"; import {OrganizationInvitation} from "@/db/schema/05_invitation"; -export const providerKindEnum = pgEnum('provider_kind', ['slack', 'smtp', 'discord', 'telegram', 'gotify', 'ntfy', 'webhook', 'nextcloud']); +export const providerKindEnum = pgEnum('provider_kind', ['slack', 'smtp', 'discord', 'telegram', 'gotify', 'ntfy', 'webhook', 'nextcloud', 'teams', 'pushover']); export const notificationChannel = pgTable('notification_channel', { id: uuid("id").defaultRandom().primaryKey(), diff --git a/src/db/schema/12_storage-channel.ts b/src/db/schema/12_storage-channel.ts index 9cc865ae..dc404d5d 100644 --- a/src/db/schema/12_storage-channel.ts +++ b/src/db/schema/12_storage-channel.ts @@ -5,7 +5,7 @@ import {relations} from "drizzle-orm"; import {createSelectSchema} from "drizzle-zod"; import {z} from "zod"; -export const providerStorageKindEnum = pgEnum('provider_storage_kind', ['local', 's3', 'google-drive']); +export const providerStorageKindEnum = pgEnum('provider_storage_kind', ['local', 's3', 'google-drive', 'blob']); export const storageChannel = pgTable('storage_channel', { id: uuid("id").defaultRandom().primaryKey(), diff --git a/src/features/channel/channel-form.schema.ts b/src/features/channel/channel-form.schema.ts index 1f7bfa7b..1fe5d6fe 100644 --- a/src/features/channel/channel-form.schema.ts +++ b/src/features/channel/channel-form.schema.ts @@ -7,9 +7,12 @@ import {GotifyChannelConfigSchema} from "@/features/channel/notifications/gotify import {NtfyChannelConfigSchema} from "@/features/channel/notifications/ntfy.schema"; import {WebhookChannelConfigSchema} from "@/features/channel/notifications/webhook.schema"; import {NextcloudChannelConfigSchema} from "@/features/channel/notifications/nextcloud.schema"; +import {PushoverChannelConfigSchema} from "@/features/channel/notifications/pushover.schema"; import {S3ChannelConfigSchema} from "@/features/channel/storages/s3.schema"; import {GoogleDriveChannelConfigSchema} from "@/features/channel/storages/google-drive/google-drive.schema"; import {LocalChannelConfigSchema} from "@/features/channel/storages/local.schema"; +import {TeamsChannelConfigSchema} from "@/features/channel/notifications/teams.schema"; +import {BlobChannelConfigSchema} from "@/features/channel/storages/az-blob.schema"; const BaseChannelFormSchema = z.object({ @@ -53,6 +56,14 @@ export const NotificationChannelFormSchema = z.discriminatedUnion("provider", [ provider: z.literal("nextcloud"), config: NextcloudChannelConfigSchema, }), + BaseChannelFormSchema.extend({ + provider: z.literal("teams"), + config: TeamsChannelConfigSchema, + }), + BaseChannelFormSchema.extend({ + provider: z.literal("pushover"), + config: PushoverChannelConfigSchema, + }), ]); export const StorageChannelFormSchema = z.discriminatedUnion("provider", [ @@ -64,6 +75,10 @@ export const StorageChannelFormSchema = z.discriminatedUnion("provider", [ provider: z.literal("google-drive"), config: GoogleDriveChannelConfigSchema, }), + BaseChannelFormSchema.extend({ + provider: z.literal("blob"), + config: BlobChannelConfigSchema, + }), BaseChannelFormSchema.extend({ provider: z.literal("local"), config: LocalChannelConfigSchema diff --git a/src/features/channel/channels-helpers.tsx b/src/features/channel/channels-helpers.tsx index e6917dfa..5d1f6b6b 100644 --- a/src/features/channel/channels-helpers.tsx +++ b/src/features/channel/channels-helpers.tsx @@ -23,9 +23,15 @@ import { import { NotifierNextcloudForm } from "@/features/channel/notifications/nextcloud.form"; +import { + NotifierPushoverForm +} from "@/features/channel/notifications/pushover.form"; import { notificationProviders, } from "@/features/channel/channels-notification-helper"; +import { + NotifierTeamsForm +} from "@/features/channel/notifications/teams.form"; import {storageProviders} from "@/features/channel/channels-storage-helper"; import {ForwardRefExoticComponent, JSX, RefAttributes, SVGProps} from "react"; import {LucideProps} from "lucide-react"; @@ -35,6 +41,9 @@ import { import { StorageGoogleDriveForm } from "@/features/channel/storages/google-drive/google-drive.form"; +import { + StorageBlobForm +} from "@/features/channel/storages/az-blob.form"; export type ChannelKind = "notification" | "storage"; @@ -92,10 +101,16 @@ export const renderChannelForm = (provider: string | undefined, form: UseFormRet return ; case "nextcloud": return ; + case "teams": + return ; + case "pushover": + return ; case "s3": return case "google-drive": return + case "blob": + return case "local": return <> default: diff --git a/src/features/channel/channels-notification-helper.tsx b/src/features/channel/channels-notification-helper.tsx index fbe58882..d903a604 100644 --- a/src/features/channel/channels-notification-helper.tsx +++ b/src/features/channel/channels-notification-helper.tsx @@ -14,7 +14,8 @@ export const notificationProviders: ProviderIconTypes[] = [ {value: "ntfy", label: "ntfy.sh", icon: NtfyIcon}, {value: "webhook", label: "Webhook", icon: WebhookIcon}, {value: "nextcloud", label: "Nextcloud Talk", icon: NextcloudIcon}, - {value: "microsoft-teams", label: "Microsoft Teams", icon: MSTeamsIcon, preview: true} + {value: "teams", label: "Microsoft Teams", icon: MSTeamsIcon}, + {value: "pushover", label: "Pushover", icon: PushoverIcon}, ] @@ -555,4 +556,21 @@ export function NextcloudIcon(props: SVGProps) { /> ); +} + +export function PushoverIcon(props: SVGProps) { + return ( + + + + + + ); } \ No newline at end of file diff --git a/src/features/channel/channels-storage-helper.tsx b/src/features/channel/channels-storage-helper.tsx index 833d0a6d..0c151f24 100644 --- a/src/features/channel/channels-storage-helper.tsx +++ b/src/features/channel/channels-storage-helper.tsx @@ -6,7 +6,7 @@ export const storageProviders: ProviderIconTypes[] = [ {value: "local", label: "Local", icon: Server}, {value: "s3", label: "S3", icon: S3Icon}, {value: "google-drive", label: "Google Drive", icon: GoogleDriveIcon}, - {value: "blob", label: "Azure Blob Storage", icon: BlobIcon, preview: true}, + {value: "blob", label: "Azure Blob Storage", icon: BlobIcon}, {value: "gcs", label: "Google Cloud Storage", icon: GCSIcon, preview: true}, ] diff --git a/src/features/channel/notifications/index.ts b/src/features/channel/notifications/index.ts index ed2d7f6e..08e67961 100644 --- a/src/features/channel/notifications/index.ts +++ b/src/features/channel/notifications/index.ts @@ -8,6 +8,8 @@ import {sendGotify} from "@/features/channel/notifications/gotify"; import {sendNtfy} from "@/features/channel/notifications/ntfy"; import {sendWebhook} from "@/features/channel/notifications/webhook"; import {sendNextcloud} from "@/features/channel/notifications/nextcloud"; +import {sendPushover} from "@/features/channel/notifications/pushover"; +import {sendTeams} from "@/features/channel/notifications/teams" const handlers: Record< ProviderKind, @@ -21,6 +23,8 @@ const handlers: Record< ntfy: sendNtfy, webhook: sendWebhook, nextcloud: sendNextcloud, + teams: sendTeams, + pushover: sendPushover, }; export async function dispatchViaProvider( diff --git a/src/features/channel/notifications/pushover.form.tsx b/src/features/channel/notifications/pushover.form.tsx new file mode 100644 index 00000000..36327652 --- /dev/null +++ b/src/features/channel/notifications/pushover.form.tsx @@ -0,0 +1,100 @@ +import {UseFormReturn} from "react-hook-form"; +import {FormControl, FormField, FormItem, FormLabel, FormMessage} from "@/components/ui/form"; +import {Input} from "@/components/ui/input"; +import {Separator} from "@/components/ui/separator"; +import {PasswordInput} from "@/components/ui/password-input"; +import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from "@/components/ui/select"; +import {PUSHOVER_PRIORITIES} from "@/features/channel/notifications/pushover.schema"; + +type NotifierPushoverFormProps = { + form: UseFormReturn; +}; + +export const NotifierPushoverForm = ({form}: NotifierPushoverFormProps) => { + return ( + <> + + + ( + + User Key * + + + +

+ Found at the top of your Pushover dashboard at pushover.net. +

+ +
+ )} + /> + + ( + + App API Token * + + + +

+ Create an application at pushover.net/apps to get an API token. +

+ +
+ )} + /> + + + + ( + + Message Priority + +

+ Emergency priority repeats every 60 seconds until acknowledged (max 1 hour). +

+ +
+ )} + /> + + ( + + Device Name + + + +

+ Target a specific registered device. Leave empty to send to all devices. +

+ +
+ )} + /> + + ); +}; diff --git a/src/features/channel/notifications/pushover.schema.ts b/src/features/channel/notifications/pushover.schema.ts new file mode 100644 index 00000000..6214dcf6 --- /dev/null +++ b/src/features/channel/notifications/pushover.schema.ts @@ -0,0 +1,16 @@ +import {z} from "zod"; + +export const PUSHOVER_PRIORITIES = [ + {value: "-2", label: "Lowest - no sound, no vibration, no alert"}, + {value: "-1", label: "Low - quiet notification, no sound"}, + {value: "0", label: "Normal - default sound and alert"}, + {value: "1", label: "High - bypass quiet hours"}, + {value: "2", label: "Emergency - repeat until acknowledged"}, +] as const; + +export const PushoverChannelConfigSchema = z.object({ + pushoverUserKey: z.string().min(30, "User key must be 30 characters").max(30, "User key must be 30 characters"), + pushoverApiToken: z.string().min(30, "API token must be 30 characters").max(30, "API token must be 30 characters"), + pushoverPriority: z.enum(["-2", "-1", "0", "1", "2"]).default("0"), + pushoverDevice: z.string().max(25, "Device name must be at most 25 characters").optional().or(z.literal("")), +}); diff --git a/src/features/channel/notifications/pushover.ts b/src/features/channel/notifications/pushover.ts new file mode 100644 index 00000000..42dbd6d5 --- /dev/null +++ b/src/features/channel/notifications/pushover.ts @@ -0,0 +1,65 @@ +import type {EventPayload, DispatchResult} from '@/features/notifications/notifications.types'; + +export async function sendPushover( + config: { + pushoverUserKey: string; + pushoverApiToken: string; + pushoverPriority: string; + pushoverDevice?: string; + }, + payload: EventPayload +): Promise { + const {pushoverUserKey, pushoverApiToken, pushoverDevice} = config; + const priority = parseInt(config.pushoverPriority ?? "0", 10); + + const rawTitle = `[${payload.level.toUpperCase()}] ${payload.title}`; + const rawMessage = payload.data + ? `${payload.message}\n\nData:\n${JSON.stringify(payload.data)}` + : payload.message; + + const body: Record = { + token: pushoverApiToken, + user: pushoverUserKey, + title: rawTitle.length > 250 ? rawTitle.slice(0, 249) + "…" : rawTitle, + message: rawMessage.length > 1024 ? rawMessage.slice(0, 1023) + "…" : rawMessage, + priority, + }; + + if (pushoverDevice) { + body.device = pushoverDevice; + } + + // Emergency priority requires retry + expire + if (priority === 2) { + body.retry = 60; + body.expire = 3600; + } + + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 10_000); + let res: Response; + try { + res = await fetch("https://api.pushover.net/1/messages.json", { + method: "POST", + headers: {"Content-Type": "application/json"}, + body: JSON.stringify(body), + signal: controller.signal, + }); + } finally { + clearTimeout(timeout); + } + + const json = await res.json().catch(() => null); + + if (!res.ok) { + const errors = json?.errors?.join(", ") ?? res.statusText; + throw new Error(`Pushover error ${res.status}: ${errors}`); + } + + return { + success: true, + provider: "pushover", + message: "Sent via Pushover", + response: json, + }; +} diff --git a/src/features/channel/notifications/teams.form.tsx b/src/features/channel/notifications/teams.form.tsx new file mode 100644 index 00000000..9c46f6d2 --- /dev/null +++ b/src/features/channel/notifications/teams.form.tsx @@ -0,0 +1,38 @@ +import { UseFormReturn } from "react-hook-form"; +import { + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Separator } from "@/components/ui/separator"; +import { PasswordInput } from "@/components/ui/password-input"; + +type NotifierTeamsFormProps = { + form: UseFormReturn; +}; + +export const NotifierTeamsForm = ({ form }: NotifierTeamsFormProps) => { + return ( + <> + + ( + + Teams Webhook URL * + + + + + + )} + /> + + ); +}; diff --git a/src/features/channel/notifications/teams.schema.ts b/src/features/channel/notifications/teams.schema.ts new file mode 100644 index 00000000..a6ccb013 --- /dev/null +++ b/src/features/channel/notifications/teams.schema.ts @@ -0,0 +1,5 @@ +import {z} from "zod"; + +export const TeamsChannelConfigSchema = z.object({ + teamsWebhook: z.string().url("Must be a valid URL"), +}); \ No newline at end of file diff --git a/src/features/channel/notifications/teams.ts b/src/features/channel/notifications/teams.ts new file mode 100644 index 00000000..f1106678 --- /dev/null +++ b/src/features/channel/notifications/teams.ts @@ -0,0 +1,93 @@ +import type { EventPayload, DispatchResult } from '@/features/notifications/notifications.types'; + + + +export async function sendTeams( + config: { teamsWebhook: string }, + payload: EventPayload +): Promise { + const { teamsWebhook: webhookUrl } = config; + const levelColor: Record = { + critical: 'attention', + warning: 'warning', + info: 'good', + }; + const accentColor = levelColor[payload.level] ?? 'default'; + const bodyItems: object[] = [ + { + type: 'TextBlock', + text: `**[${payload.level.toUpperCase()}] ${payload.title}**`, + wrap: true, + color: accentColor, + size: 'Medium', + weight: 'Bolder', + }, + { + type: 'TextBlock', + text: payload.message, + wrap: true, + }, + ]; + + if (payload.data) { + bodyItems.push( + { + type: 'TextBlock', + text: '**Data:**', + wrap: true, + weight: 'Bolder', + }, + { + type: 'TextBlock', + text: JSON.stringify(payload.data, null, 2).substring(0, 1000), + wrap: true, + fontType: 'Monospace', + } + ); + } + + const adaptiveCard = { + type: 'AdaptiveCard', + $schema: 'http://adaptivecards.io/schemas/adaptive-card.json', + version: '1.2', + body: bodyItems, + }; + + const body = { + type: 'message', + attachments: [ + { + contentType: 'application/vnd.microsoft.card.adaptive', + contentUrl: null, + content: adaptiveCard, + }, + ], + }; + + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 10_000); + let res: Response; + try { + res = await fetch(webhookUrl, { + method: 'POST', + body: JSON.stringify(body), + headers: { 'Content-Type': 'application/json' }, + signal: controller.signal, + }); + } finally { + clearTimeout(timeout); + } + + if (!res.ok) { + const err = await res.text(); + throw new Error(`Teams error: ${res.status} ${err}`); + } + + return { + success: true, + provider: 'teams', + message: 'Sent to Microsoft Teams', + response: res.statusText, + }; +} + diff --git a/src/features/channel/storages/az-blob.form.tsx b/src/features/channel/storages/az-blob.form.tsx new file mode 100644 index 00000000..2aed8201 --- /dev/null +++ b/src/features/channel/storages/az-blob.form.tsx @@ -0,0 +1,87 @@ +import { UseFormReturn } from "react-hook-form"; +import { + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { Separator } from "@/components/ui/separator"; +import { PasswordInput } from "@/components/ui/password-input"; +type StorageBlobFormProps = { + form: UseFormReturn; +}; + +export const StorageBlobForm = ({ form }: StorageBlobFormProps) => { + return ( + <> + + ( + + Account Name * + + + + + + )} + /> + ( + + Account Key + + + + + + )} + /> + ( + + Connection String + + + + + + )} + /> + ( + + Container Name * + + + + + + )} + /> + ( + + Endpoint URL + + + + + + )} + /> + + ); +}; diff --git a/src/features/channel/storages/az-blob.schema.ts b/src/features/channel/storages/az-blob.schema.ts new file mode 100644 index 00000000..dc9aa75e --- /dev/null +++ b/src/features/channel/storages/az-blob.schema.ts @@ -0,0 +1,15 @@ +import {z} from "zod"; + +export const BlobChannelConfigSchema = z.object({ + accountName: z.string().min(1, "Account name is required"), + accountKey: z.string().optional(), + connectionString: z.string().optional(), + containerName: z.string().min(1, "Container name is required"), + endpointUrl: z.preprocess( + (v) => (v === "" ? undefined : v), + z.string().url("Endpoint URL must be a valid URL").optional(), + ), +}).refine( + (data) => data.accountKey || data.connectionString, + {message: "Either account key or connection string is required"} +); diff --git a/src/features/channel/storages/az-blob.ts b/src/features/channel/storages/az-blob.ts new file mode 100644 index 00000000..0ed81ee6 --- /dev/null +++ b/src/features/channel/storages/az-blob.ts @@ -0,0 +1,185 @@ +import {BlobSASPermissions, BlobServiceClient, StorageSharedKeyCredential} from "@azure/storage-blob"; +import { + StorageCopyInput, + StorageDeleteInput, + StorageGetInput, + StorageMetaData, + StorageResult, + StorageUploadInput +} from '@/features/storages/storages.types'; +import {Readable} from "node:stream"; + +type BlobConfig = { + accountName: string; + accountKey?: string; + connectionString?: string; + containerName: string; + endpointUrl?: string; +}; + +async function getBlobClient(config: BlobConfig) { + if (config.connectionString) { + return BlobServiceClient.fromConnectionString(config.connectionString); + } + + const url = config.endpointUrl ?? `https://${config.accountName}.blob.core.windows.net`; + const credential = new StorageSharedKeyCredential(config.accountName, config.accountKey!); + return new BlobServiceClient(url, credential); +} + +const BASE_DIR = ""; + + +async function ensureContainer(config: BlobConfig) { + const client = await getBlobClient(config); + const containerClient = client.getContainerClient(config.containerName); + await containerClient.createIfNotExists(); +} + +export async function uploadBlob( + config: BlobConfig, + input: { data: StorageUploadInput, metadata?: StorageMetaData } +): Promise { + const client = await getBlobClient(config); + await ensureContainer(config); + + const key = `${BASE_DIR}${input.data.path}`; + const file = input.data.file; + + let uploadStream: Readable; + if (Buffer.isBuffer(file) || file instanceof Uint8Array) { + uploadStream = Readable.from(file); + } else if ((file as any).pipe) { + uploadStream = file; + } else { + return {success: false, provider: "blob", error: "Unsupported file type for streaming upload"}; + } + + try { + const containerClient = client.getContainerClient(config.containerName); + const blockBlobClient = containerClient.getBlockBlobClient(key); + await blockBlobClient.uploadStream(uploadStream); + } catch (err: any) { + return {success: false, provider: "blob", error: err.message}; + } + + return {success: true, provider: "blob"}; +} + + +export async function getBlob( + config: BlobConfig, + input: { data: StorageGetInput, metadata: StorageMetaData } +): Promise { + const client = await getBlobClient(config); + const key = `${BASE_DIR}${input.data.path}`; + + const containerClient = client.getContainerClient(config.containerName); + const blockBlobClient = containerClient.getBlockBlobClient(key); + + if (!(await blockBlobClient.exists())) { + return {success: false, provider: "blob", error: "File not found"}; + } + + const downloadResponse = await blockBlobClient.download(); + const fileStream = downloadResponse.readableStreamBody as unknown as Readable; + + let presignedUrl: string | undefined; + if (input.data.signedUrl) { + presignedUrl = await blockBlobClient.generateSasUrl({ + expiresOn: new Date(Date.now() + (input.data.expiresInSeconds ?? 60) * 1000), + permissions: BlobSASPermissions.parse("r"), + }); + } + + return { + success: true, + provider: "blob", + file: fileStream, + url: presignedUrl, + }; +} + + +export async function deleteBlob(config: BlobConfig, input: { + data: StorageDeleteInput, + metadata?: StorageMetaData +}): Promise { + const client = await getBlobClient(config); + const key = `${BASE_DIR}${input.data.path}`; + + try { + const containerClient = client.getContainerClient(config.containerName); + const blockBlobClient = containerClient.getBlockBlobClient(key); + await blockBlobClient.delete(); + return {success: true, provider: "blob"}; + } catch (err: any) { + return {success: false, provider: "blob", error: err.message}; + } +} + +export async function pingBlob(config: BlobConfig): Promise { + try { + const client = await getBlobClient(config); + const containerClient = client.getContainerClient(config.containerName); + const exists = await containerClient.exists(); + if (!exists) return { + success: false, + provider: "blob", + response: "Container does not exist" + }; + const key = `${BASE_DIR}ping-${Date.now()}-${Math.random().toString(36).slice(2)}.txt`; + const blockBlobClient = containerClient.getBlockBlobClient(key); + try { + await blockBlobClient.upload(Buffer.from("ping"), 4); + await blockBlobClient.download(); + } finally { + await blockBlobClient.delete().catch(() => undefined); + } + + return { + success: true, + provider: "blob", + response: "Blob storage OK" + }; + } catch (err: any) { + return { + success: false, + provider: "blob", + response: err.message + }; + } +} + + +export async function copyBlob( + config: BlobConfig, + input: { + data: StorageCopyInput, + }, +): Promise { + const client = await getBlobClient(config); + await ensureContainer(config); + + const sourceKey = `${BASE_DIR}${input.data.from}`; + const destinationKey = `${BASE_DIR}${input.data.to}`; + + try { + const containerClient = client.getContainerClient(config.containerName); + const sourceBlob = containerClient.getBlockBlobClient(sourceKey); + const destBlob = containerClient.getBlockBlobClient(destinationKey); + const poller = await destBlob.beginCopyFromURL(sourceBlob.url); + await poller.pollUntilDone(); + + return { + success: true, + provider: "blob", + }; + } catch (err: any) { + return { + success: false, + provider: "blob", + error: err.message, + }; + } +} \ No newline at end of file diff --git a/src/features/channel/storages/index.ts b/src/features/channel/storages/index.ts index be40622d..851b6bf2 100644 --- a/src/features/channel/storages/index.ts +++ b/src/features/channel/storages/index.ts @@ -6,6 +6,7 @@ import { import {uploadLocal, getLocal, deleteLocal, pingLocal, copyLocal} from './local'; import {copyS3, deleteS3, getS3, pingS3, uploadS3} from "@/features/channel/storages/s3"; +import {copyBlob, deleteBlob, getBlob, pingBlob, uploadBlob} from "@/features/channel/storages/az-blob"; import { copyGoogleDrive, deleteGoogleDrive, @@ -43,6 +44,13 @@ const handlers: Record = { delete: deleteGoogleDrive, ping: pingGoogleDrive, copy: copyGoogleDrive, + }, + blob: { + upload: uploadBlob, + get: getBlob, + delete: deleteBlob, + ping: pingBlob, + copy: copyBlob, } }; diff --git a/src/features/notifications/notification-log-modal.tsx b/src/features/notifications/notification-log-modal.tsx index d2a517a2..d6ce2d4e 100644 --- a/src/features/notifications/notification-log-modal.tsx +++ b/src/features/notifications/notification-log-modal.tsx @@ -79,7 +79,8 @@ export const NotificationLogModal = ({ // @ts-expect-error — payload type is not fully typed yet const payloadError = notificationLog.payload?.error; - const troubleshooting = getTroubleshootingForError(payloadError); + const dispatchError = !notificationLog.success ? notificationLog.error : null; + const troubleshooting = getTroubleshootingForError(payloadError || dispatchError); return ( @@ -131,6 +132,24 @@ export const NotificationLogModal = ({ )} + {dispatchError && ( + <> +
+
+
+
+
+ +
+ Dispatch Error +
+
+ {dispatchError} +
+
+ + )} +
{notificationLog.payload && diff --git a/src/features/notifications/notifications.types.ts b/src/features/notifications/notifications.types.ts index 72bade1f..1992fa5d 100644 --- a/src/features/notifications/notifications.types.ts +++ b/src/features/notifications/notifications.types.ts @@ -1,4 +1,4 @@ -export type ProviderKind = 'slack' | 'smtp' | 'discord' | 'telegram' | 'gotify' | 'ntfy' | 'webhook' | 'nextcloud'; +export type ProviderKind = 'slack' | 'smtp' | 'discord' | 'telegram' | 'gotify' | 'ntfy' | 'webhook' | 'nextcloud' | 'teams' | 'pushover'; export interface DispatchResult { success: boolean; diff --git a/src/features/storages/storages.types.ts b/src/features/storages/storages.types.ts index 5dfc5815..d98bfc0c 100644 --- a/src/features/storages/storages.types.ts +++ b/src/features/storages/storages.types.ts @@ -4,6 +4,7 @@ export type StorageProviderKind = | 'local' | 's3' | 'google-drive' + | 'blob' ; export type StorageAction =