diff --git a/CITATION.cff b/CITATION.cff index fc01c94a..a371edb0 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -33,5 +33,5 @@ keywords: - web-ui - agent license: Apache-2.0 -version: 1.20.1 +version: 1.20.2 date-released: '2026-03-02' diff --git a/app/api/agent/[agentId]/status/helpers.ts b/app/api/agent/[agentId]/status/helpers.ts index 83d9e051..794bdee2 100644 --- a/app/api/agent/[agentId]/status/helpers.ts +++ b/app/api/agent/[agentId]/status/helpers.ts @@ -1,328 +1,278 @@ -import { NextResponse } from "next/server"; -import { Body } from "./route"; -import { Agent } from "@/db/schema/08_agent"; -import { DatabaseWith } from "@/db/schema/07_database"; +import {NextResponse} from "next/server"; +import {Body} from "./route"; +import {isUuidv4} from "@/utils/verify-uuid"; +import {Agent} from "@/db/schema/08_agent"; +import {DatabaseWith} from "@/db/schema/07_database"; import * as drizzleDb from "@/db"; -import { db, db as dbClient } from "@/db"; -import { and, eq, inArray } from "drizzle-orm"; -import { dbmsEnumSchema, EDbmsSchema } from "@/db/schema/types"; -import { withUpdatedAt } from "@/db/utils"; -import type { StorageInput } from "@/features/storages/types"; -import { dispatchStorage } from "@/features/storages/utils/storages.dispatch"; -import { Setting } from "@/db/schema/01_setting"; -import { logger } from "@/lib/logger"; -import { isUUID } from "@/utils/text"; +import {db, db as dbClient} from "@/db"; +import {and, eq, inArray} from "drizzle-orm"; +import {dbmsEnumSchema, EDbmsSchema} from "@/db/schema/types"; +import {withUpdatedAt} from "@/db/utils"; +import type {StorageInput} from "@/features/storages/storages.types"; +import {dispatchStorage} from "@/features/storages/storages.dispatch"; +import {Setting} from "@/db/schema/01_setting"; +import {logger} from "@/lib/logger"; -const log = logger.child({ module: "api/agent/status/helpers" }); +const log = logger.child({module: "api/agent/status/helpers"}); -export async function handleDatabases( - body: Body, - agent: Agent, - lastContact: Date, - settings: Setting, -) { - const databasesResponse = []; +export async function handleDatabases(body: Body, agent: Agent, lastContact: Date, settings: Setting) { + const databasesResponse = []; - const formatDatabase = ( - database: DatabaseWith, - backupAction: boolean, - restoreAction: boolean, - UrlBackup: string | null, - storages: PingDatabaseStorageChannels[], - urlMeta: string | null, - ) => ({ - generatedId: database.agentDatabaseId, - dbms: database.dbms, - storages: storages, - encrypt: settings.encryption, - data: { - backup: { - action: backupAction, - cron: database.backupPolicy, - }, - restore: { - action: restoreAction, - file: UrlBackup, - metaFile: urlMeta, - }, - }, - }); - - for (const db of body.databases) { - const existingDatabase = await dbClient.query.database.findFirst({ - where: eq(drizzleDb.schemas.database.agentDatabaseId, db.generatedId), - with: { - project: true, - }, + const formatDatabase = (database: DatabaseWith, backupAction: boolean, restoreAction: boolean, UrlBackup: string | null, storages: PingDatabaseStorageChannels[], urlMeta: string | null, backupSize: number | null) => ({ + generatedId: database.agentDatabaseId, + dbms: database.dbms, + storages: storages, + encrypt: settings.encryption, + data: { + backup: { + action: backupAction, + cron: database.backupPolicy, + }, + restore: { + action: restoreAction, + file: UrlBackup, + metaFile: urlMeta, + size: backupSize + }, + }, }); - let backupAction: boolean = false; - let restoreAction: boolean = false; - let urlBackup: string | null = null; - let urlMeta: string | null = null; + for (const db of body.databases) { - if (!existingDatabase) { - if (!isUUID(db.generatedId)) { - return NextResponse.json( - { error: "generatedId is not a valid uuid" }, - { status: 500 }, - ); - } - - if (!dbmsEnumSchema.safeParse(db.dbms).success) { - log.error( - { name: "handleDatabases" }, - `Database type not available: ${db.dbms}`, - ); - continue; - } - - const [databaseCreated] = await dbClient - .insert(drizzleDb.schemas.database) - .values({ - agentId: agent.id, - name: db.name, - dbms: db.dbms as EDbmsSchema, - agentDatabaseId: db.generatedId, - lastContact: db.pingStatus ? lastContact : null, - healthErrorCount: null, - }) - .returning(); - - if (databaseCreated) { - await dbClient.insert(drizzleDb.schemas.healthcheckLog).values({ - kind: "database", - status: db.pingStatus ? "success" : "failed", - objectId: databaseCreated.id, - date: lastContact, + const existingDatabase = await dbClient.query.database.findFirst({ + where: eq(drizzleDb.schemas.database.agentDatabaseId, db.generatedId), + with: { + project: true + } }); - const storages = await getDatabaseStorageChannels(databaseCreated.id); + let backupAction: boolean = false + let restoreAction: boolean = false + let urlBackup: string | null = null; + let urlMeta: string | null = null + let backupSize: number | null = null - databasesResponse.push( - formatDatabase( - databaseCreated, - backupAction, - restoreAction, - urlBackup, - storages, - null, - ), - ); - } - } else { - const [databaseUpdated] = await dbClient - .update(drizzleDb.schemas.database) - .set( - withUpdatedAt({ - name: db.name, - agentId: agent.id, - dbms: db.dbms as EDbmsSchema, - lastContact: db.pingStatus - ? lastContact - : existingDatabase.lastContact, - healthErrorCount: db.pingStatus - ? null - : existingDatabase.healthErrorCount, - }), - ) - .where(eq(drizzleDb.schemas.database.id, existingDatabase.id)) - .returning(); + if (!existingDatabase) { + if (!isUuidv4(db.generatedId)) { + return NextResponse.json( + {error: "generatedId is not a valid uuid"}, + {status: 500} + ); + } - await dbClient.insert(drizzleDb.schemas.healthcheckLog).values({ - kind: "database", - status: db.pingStatus ? "success" : "failed", - objectId: databaseUpdated.id, - date: lastContact, - }); + if (!dbmsEnumSchema.safeParse(db.dbms).success) { + log.error({name: "handleDatabases"},`Database type not available: ${db.dbms}`); + continue; + } - const activeBackup = await dbClient.query.backup.findFirst({ - where: and( - eq(drizzleDb.schemas.backup.databaseId, databaseUpdated.id), - inArray(drizzleDb.schemas.backup.status, ["waiting", "ongoing"]), - ), - }); + const [databaseCreated] = await dbClient + .insert(drizzleDb.schemas.database) + .values({ + agentId: agent.id, + name: db.name, + dbms: db.dbms as EDbmsSchema, + agentDatabaseId: db.generatedId, + lastContact: db.pingStatus ? lastContact : null, + healthErrorCount: null + }) + .returning(); - const restoration = await dbClient.query.restoration.findFirst({ - where: and( - eq(drizzleDb.schemas.restoration.databaseId, databaseUpdated.id), - eq(drizzleDb.schemas.restoration.status, "waiting"), - ), - with: { - backupStorage: true, - }, - }); - if (activeBackup && activeBackup.status == "waiting") { - backupAction = true; + if (databaseCreated) { - await dbClient - .update(drizzleDb.schemas.backup) - .set(withUpdatedAt({ status: "ongoing" })) - .where(eq(drizzleDb.schemas.backup.id, activeBackup.id)); - } - if (restoration) { - restoreAction = true; + await dbClient + .insert(drizzleDb.schemas.healthcheckLog) + .values({ + kind: "database", + status: db.pingStatus ? "success" : "failed", + objectId: databaseCreated.id, + date: lastContact + }) - if ( - !restoration.backupStorage || - restoration.backupStorage.status != "success" || - !restoration.backupStorage.path - ) { - restoreAction = false; - continue; - } + const storages = await getDatabaseStorageChannels(databaseCreated.id) - const input: StorageInput = { - action: "get", - data: { - path: restoration.backupStorage.path, - signedUrl: true, - }, - metadata: { - storageId: restoration.backupStorage.storageChannelId, - fileKind: "backups", - }, - }; + databasesResponse.push(formatDatabase(databaseCreated, backupAction, restoreAction, urlBackup, storages, null, null)); + } + } else { - const inputMeta: StorageInput = { - action: "get", - data: { - path: `${restoration.backupStorage.path}.meta`, - signedUrl: true, - }, - metadata: { - storageId: restoration.backupStorage.storageChannelId, - fileKind: "backups", - }, - }; + const [databaseUpdated] = await dbClient + .update(drizzleDb.schemas.database) + .set(withUpdatedAt({ + name: db.name, + agentId: agent.id, + dbms: db.dbms as EDbmsSchema, + lastContact: db.pingStatus ? lastContact : existingDatabase.lastContact, + healthErrorCount: db.pingStatus ? null : existingDatabase.healthErrorCount, + })) + .where(eq(drizzleDb.schemas.database.id, existingDatabase.id)) + .returning(); - try { - const result = await dispatchStorage( - input, - undefined, - restoration.backupStorage.storageChannelId, - ); - const resultMeta = await dispatchStorage( - inputMeta, - undefined, - restoration.backupStorage.storageChannelId, - ); - if (result.success) { - urlBackup = result.url ?? null; - urlMeta = resultMeta.url ?? null; - } else { await dbClient - .update(drizzleDb.schemas.restoration) - .set(withUpdatedAt({ status: "failed" })) - .where(eq(drizzleDb.schemas.restoration.id, restoration.id)); + .insert(drizzleDb.schemas.healthcheckLog) + .values({ + kind: "database", + status: db.pingStatus ? "success" : "failed", + objectId: databaseUpdated.id, + date: lastContact + }) - const errorMessage = "Failed to get backup URL"; - log.error( - { error: errorMessage, name: "handleDatabases" }, - "Restoration failed", - ); - continue; - } - } catch (err) { - log.error( - { error: err, name: "handleDatabases" }, - "Restoration crashed unexpectedly", - ); - await dbClient - .update(drizzleDb.schemas.restoration) - .set(withUpdatedAt({ status: "failed" })) - .where(eq(drizzleDb.schemas.restoration.id, restoration.id)); - continue; + + const activeBackup = await dbClient.query.backup.findFirst({ + where: and( + eq(drizzleDb.schemas.backup.databaseId, databaseUpdated.id), + inArray(drizzleDb.schemas.backup.status, ["waiting", "ongoing"]) + ) + }) + + const restoration = await dbClient.query.restoration.findFirst({ + where: and(eq(drizzleDb.schemas.restoration.databaseId, databaseUpdated.id), eq(drizzleDb.schemas.restoration.status, "waiting")), + with: { + backupStorage: true + } + }) + + if (activeBackup && activeBackup.status == "waiting") { + backupAction = true + + await dbClient + .update(drizzleDb.schemas.backup) + .set(withUpdatedAt({status: "ongoing"})) + .where(eq(drizzleDb.schemas.backup.id, activeBackup.id)); + } + + if (restoration) { + restoreAction = true + + if (!restoration.backupStorage || restoration.backupStorage.status != "success" || !restoration.backupStorage.path) { + restoreAction = false + continue; + } + + const input: StorageInput = { + action: "get", + data: { + path: restoration.backupStorage.path, + signedUrl: true, + }, + metadata: { + storageId: restoration.backupStorage.storageChannelId, + fileKind: "backups" + } + }; + + const inputMeta: StorageInput = { + action: "get", + data: { + path: `${restoration.backupStorage.path}.meta`, + signedUrl: true, + }, + metadata: { + storageId: restoration.backupStorage.storageChannelId, + fileKind: "backups" + } + }; + + + try { + const result = await dispatchStorage(input, undefined, restoration.backupStorage.storageChannelId); + const resultMeta = await dispatchStorage(inputMeta, undefined, restoration.backupStorage.storageChannelId); + + if (result.success) { + urlBackup = result.url ?? null; + urlMeta = resultMeta.url ?? null + backupSize = restoration.backupStorage.size + } else { + await dbClient + .update(drizzleDb.schemas.restoration) + .set(withUpdatedAt({status: "failed"})) + .where(eq(drizzleDb.schemas.restoration.id, restoration.id)); + + const errorMessage = "Failed to get backup URL"; + log.error({error: errorMessage, name: "handleDatabases"}, "Restoration failed"); + continue; + } + } catch (err) { + log.error({error: err, name: "handleDatabases"}, "Restoration crashed unexpectedly"); + await dbClient + .update(drizzleDb.schemas.restoration) + .set(withUpdatedAt({status: "failed"})) + .where(eq(drizzleDb.schemas.restoration.id, restoration.id)); + continue; + } + + await dbClient + .update(drizzleDb.schemas.restoration) + .set(withUpdatedAt({status: "ongoing"})) + .where(eq(drizzleDb.schemas.restoration.id, restoration.id)); + } + const storages = await getDatabaseStorageChannels(databaseUpdated.id) + databasesResponse.push(formatDatabase(databaseUpdated, backupAction, restoreAction, urlBackup, storages, urlMeta, backupSize)); } - - await dbClient - .update(drizzleDb.schemas.restoration) - .set(withUpdatedAt({ status: "ongoing" })) - .where(eq(drizzleDb.schemas.restoration.id, restoration.id)); - } - const storages = await getDatabaseStorageChannels(databaseUpdated.id); - databasesResponse.push( - formatDatabase( - databaseUpdated, - backupAction, - restoreAction, - urlBackup, - storages, - urlMeta, - ), - ); } - } - return databasesResponse; + return databasesResponse; } + type PingDatabaseStorageChannels = { - id: string; - config: any; - provider: string; -}; + id: string; + config: any + provider: string +} -async function getDatabaseStorageChannels( - databaseId: string, -): Promise { - const database = await db.query.database.findFirst({ - where: eq(drizzleDb.schemas.database.id, databaseId), - with: { - project: true, - retentionPolicy: true, - alertPolicies: true, - storagePolicies: true, - }, - }); +async function getDatabaseStorageChannels(databaseId: string): Promise { - if (!database) { - return []; - } + const database = await db.query.database.findFirst({ + where: eq(drizzleDb.schemas.database.id, databaseId), + with: { + project: true, + retentionPolicy: true, + alertPolicies: true, + storagePolicies: true + } + }); - const settings = await db.query.setting.findFirst({ - where: eq(drizzleDb.schemas.setting.name, "system"), - with: { storageChannel: true }, - }); + if (!database) { + return [] + } - const defaultStorageChannel: PingDatabaseStorageChannels[] = - settings?.storageChannel - ? [ - { + const settings = await db.query.setting.findFirst({ + where: eq(drizzleDb.schemas.setting.name, "system"), + with: {storageChannel: true}, + }); + + const defaultStorageChannel: PingDatabaseStorageChannels[] = settings?.storageChannel + ? [{ id: settings.storageChannel.id, provider: settings.storageChannel.provider, config: settings.storageChannel.config, - }, - ] - : []; + }] + : []; - const enabledDatabaseStorageChannels = await Promise.all( - (database.storagePolicies ?? []) - .filter((p) => p.enabled) - .map(async (policy) => { - const storageChannel = await db.query.storageChannel.findFirst({ - where: eq( - drizzleDb.schemas.storageChannel.id, - policy.storageChannelId, - ), - }); - if (!storageChannel) return null; + const enabledDatabaseStorageChannels = await Promise.all( + (database.storagePolicies ?? []) + .filter(p => p.enabled) + .map(async policy => { + const storageChannel = await db.query.storageChannel.findFirst({ + where: eq(drizzleDb.schemas.storageChannel.id, policy.storageChannelId), + }); - return { - id: storageChannel.id, - config: storageChannel.config, - provider: storageChannel.provider, - } as PingDatabaseStorageChannels; - }), - ); + if (!storageChannel) return null; - const filteredChannels: PingDatabaseStorageChannels[] = - enabledDatabaseStorageChannels.filter( - (c): c is PingDatabaseStorageChannels => c !== null, + return { + id: storageChannel.id, + config: storageChannel.config, + provider: storageChannel.provider, + } as PingDatabaseStorageChannels; + }) ); - return filteredChannels.length > 0 ? filteredChannels : defaultStorageChannel; + const filteredChannels: PingDatabaseStorageChannels[] = enabledDatabaseStorageChannels.filter( + (c): c is PingDatabaseStorageChannels => c !== null + ); + + return filteredChannels.length > 0 ? filteredChannels : defaultStorageChannel; } + diff --git a/package.json b/package.json index 787fc2d6..46dde80c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "portabase", - "version": "1.20.1", + "version": "1.20.2", "private": true, "scripts": { "dev": "next dev --turbopack -p 8887",