Introduced a new `verify_tls` boolean option in health check configurations across various services, allowing users to specify whether to validate TLS certificates during health checks. Updated related components and services to accommodate this new option, ensuring proper handling in both the backend and frontend. Enhanced tests to validate the new functionality and ensure correct behavior with different configurations.
1698 lines
64 KiB
JavaScript
1698 lines
64 KiB
JavaScript
var __defProp = Object.defineProperty;
|
|
var __export = (target, all) => {
|
|
for (var name in all)
|
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
};
|
|
|
|
// src/schema.ts
|
|
import { sql } from "drizzle-orm";
|
|
import {
|
|
integer,
|
|
primaryKey,
|
|
sqliteTable,
|
|
text
|
|
} from "drizzle-orm/sqlite-core";
|
|
var groups = sqliteTable("groups", {
|
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
name: text("name").notNull(),
|
|
slug: text("slug").notNull().unique(),
|
|
created_at: text("created_at").notNull().default(sql`datetime('now')`),
|
|
updated_at: text("updated_at").notNull().default(sql`datetime('now')`)
|
|
});
|
|
var services = sqliteTable("services", {
|
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
name: text("name").notNull(),
|
|
slug: text("slug").notNull().unique(),
|
|
service_group_id: integer("service_group_id").references(
|
|
() => serviceGroups.id,
|
|
{ onDelete: "set null" }
|
|
),
|
|
subdomain: text("subdomain"),
|
|
enabled: integer("enabled", { mode: "boolean" }).notNull().default(false),
|
|
sort_order: integer("sort_order").notNull().default(0),
|
|
lb_weight: integer("lb_weight").notNull().default(1),
|
|
lb_priority: integer("lb_priority").notNull().default(1),
|
|
created_at: text("created_at").notNull().default(sql`datetime('now')`),
|
|
updated_at: text("updated_at").notNull().default(sql`datetime('now')`)
|
|
});
|
|
var serviceGroups = sqliteTable("service_groups", {
|
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
name: text("name").notNull(),
|
|
type: text("type").notNull().default("custom"),
|
|
icon: text("icon"),
|
|
domain: text("domain"),
|
|
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
|
|
lb_mode: text("lb_mode").notNull().default("round_robin"),
|
|
health_check_enabled: integer("health_check_enabled", { mode: "boolean" }).notNull().default(false),
|
|
health_check_type: text("health_check_type").notNull().default("tcp"),
|
|
health_check_port: integer("health_check_port"),
|
|
health_check_path: text("health_check_path"),
|
|
health_check_expected_status: integer("health_check_expected_status"),
|
|
health_check_interval_sec: integer("health_check_interval_sec").notNull().default(30),
|
|
health_check_timeout_ms: integer("health_check_timeout_ms").notNull().default(3e3),
|
|
health_check_verify_tls: integer("health_check_verify_tls", { mode: "boolean" }).notNull().default(false),
|
|
created_at: text("created_at").notNull().default(sql`datetime('now')`),
|
|
updated_at: text("updated_at").notNull().default(sql`datetime('now')`)
|
|
});
|
|
var domains = sqliteTable("domains", {
|
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
group_id: integer("group_id").references(() => groups.id, {
|
|
onDelete: "set null"
|
|
}),
|
|
zone_name: text("zone_name").notNull().unique(),
|
|
cf_zone_id: text("cf_zone_id").notNull(),
|
|
status: text("status").notNull().default("active"),
|
|
cert_monitoring: text("cert_monitoring").notNull().default("auto"),
|
|
environment: text("environment"),
|
|
last_synced_at: text("last_synced_at"),
|
|
created_at: text("created_at").notNull().default(sql`datetime('now')`),
|
|
updated_at: text("updated_at").notNull().default(sql`datetime('now')`)
|
|
});
|
|
var subdomains = sqliteTable("subdomains", {
|
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
domain_id: integer("domain_id").notNull().references(() => domains.id, { onDelete: "cascade" }),
|
|
name: text("name").notNull(),
|
|
fqdn: text("fqdn").notNull(),
|
|
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
|
|
cert_monitoring: text("cert_monitoring").notNull().default("auto"),
|
|
created_at: text("created_at").notNull().default(sql`datetime('now')`),
|
|
updated_at: text("updated_at").notNull().default(sql`datetime('now')`)
|
|
});
|
|
var dnsRecords = sqliteTable("dns_records", {
|
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
domain_id: integer("domain_id").notNull().references(() => domains.id, { onDelete: "cascade" }),
|
|
cf_record_id: text("cf_record_id"),
|
|
record_type: text("record_type").notNull(),
|
|
name: text("name").notNull(),
|
|
content: text("content").notNull(),
|
|
ttl: integer("ttl").notNull().default(1),
|
|
proxied: integer("proxied", { mode: "boolean" }).notNull().default(false),
|
|
priority: integer("priority"),
|
|
sync_status: text("sync_status").notNull().default("pending_push"),
|
|
origin: text("origin").notNull().default("local"),
|
|
last_error: text("last_error"),
|
|
created_at: text("created_at").notNull().default(sql`datetime('now')`),
|
|
updated_at: text("updated_at").notNull().default(sql`datetime('now')`)
|
|
});
|
|
var serviceBindings = sqliteTable("service_bindings", {
|
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
domain_id: integer("domain_id").notNull().references(() => domains.id, { onDelete: "cascade" }),
|
|
service_id: integer("service_id").notNull().references(() => services.id, { onDelete: "cascade" }),
|
|
hostname: text("hostname").notNull().default("@"),
|
|
cname_target: text("cname_target"),
|
|
dns_record_id: integer("dns_record_id").references(() => dnsRecords.id, {
|
|
onDelete: "set null"
|
|
}),
|
|
lb_mode: text("lb_mode").notNull().default("round_robin"),
|
|
health_check_enabled: integer("health_check_enabled", { mode: "boolean" }).notNull().default(false),
|
|
health_check_type: text("health_check_type").notNull().default("tcp"),
|
|
health_check_port: integer("health_check_port"),
|
|
health_check_path: text("health_check_path"),
|
|
health_check_expected_status: integer("health_check_expected_status"),
|
|
health_check_interval_sec: integer("health_check_interval_sec").notNull().default(30),
|
|
health_check_timeout_ms: integer("health_check_timeout_ms").notNull().default(3e3),
|
|
health_check_verify_tls: integer("health_check_verify_tls", { mode: "boolean" }).notNull().default(false),
|
|
created_at: text("created_at").notNull().default(sql`datetime('now')`),
|
|
updated_at: text("updated_at").notNull().default(sql`datetime('now')`)
|
|
});
|
|
var serviceIps = sqliteTable("service_ips", {
|
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
service_id: integer("service_id").notNull().references(() => services.id, { onDelete: "cascade" }),
|
|
ip: text("ip").notNull(),
|
|
created_at: text("created_at").notNull().default(sql`datetime('now')`)
|
|
});
|
|
var serviceBindingRecords = sqliteTable(
|
|
"service_binding_records",
|
|
{
|
|
binding_id: integer("binding_id").notNull().references(() => serviceBindings.id, { onDelete: "cascade" }),
|
|
dns_record_id: integer("dns_record_id").notNull().references(() => dnsRecords.id, { onDelete: "cascade" })
|
|
},
|
|
(t) => [primaryKey({ columns: [t.binding_id, t.dns_record_id] })]
|
|
);
|
|
var serviceBindingIps = sqliteTable(
|
|
"service_binding_ips",
|
|
{
|
|
binding_id: integer("binding_id").notNull().references(() => serviceBindings.id, { onDelete: "cascade" }),
|
|
ip: text("ip").notNull(),
|
|
weight: integer("weight").notNull().default(1),
|
|
priority: integer("priority").notNull().default(1)
|
|
},
|
|
(t) => [primaryKey({ columns: [t.binding_id, t.ip] })]
|
|
);
|
|
var serviceGroupDnsRecords = sqliteTable(
|
|
"service_group_dns_records",
|
|
{
|
|
group_id: integer("group_id").notNull().references(() => serviceGroups.id, { onDelete: "cascade" }),
|
|
dns_record_id: integer("dns_record_id").notNull().references(() => dnsRecords.id, { onDelete: "cascade" })
|
|
},
|
|
(t) => [primaryKey({ columns: [t.group_id, t.dns_record_id] })]
|
|
);
|
|
var certificates = sqliteTable("certificates", {
|
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
domain_id: integer("domain_id").notNull().references(() => domains.id, { onDelete: "cascade" }),
|
|
subdomain_id: integer("subdomain_id").references(() => subdomains.id, {
|
|
onDelete: "set null"
|
|
}),
|
|
hostname: text("hostname").notNull().unique(),
|
|
expires_at: text("expires_at"),
|
|
last_checked_at: text("last_checked_at"),
|
|
last_error: text("last_error"),
|
|
status: text("status").notNull().default("unknown"),
|
|
created_at: text("created_at").notNull().default(sql`datetime('now')`),
|
|
updated_at: text("updated_at").notNull().default(sql`datetime('now')`)
|
|
});
|
|
var syncJobs = sqliteTable("sync_jobs", {
|
|
id: text("id").primaryKey(),
|
|
status: text("status").notNull().default("pending"),
|
|
domain_id: integer("domain_id").references(() => domains.id, {
|
|
onDelete: "set null"
|
|
}),
|
|
message: text("message"),
|
|
created_at: text("created_at").notNull().default(sql`datetime('now')`),
|
|
finished_at: text("finished_at")
|
|
});
|
|
var ipHealthStatus = sqliteTable(
|
|
"ip_health_status",
|
|
{
|
|
scope: text("scope").notNull(),
|
|
ref_id: integer("ref_id").notNull(),
|
|
ip: text("ip").notNull(),
|
|
status: text("status").notNull().default("unknown"),
|
|
latency_ms: integer("latency_ms"),
|
|
consecutive_failures: integer("consecutive_failures").notNull().default(0),
|
|
last_checked_at: text("last_checked_at"),
|
|
last_error: text("last_error"),
|
|
created_at: text("created_at").notNull().default(sql`datetime('now')`),
|
|
updated_at: text("updated_at").notNull().default(sql`datetime('now')`)
|
|
},
|
|
(t) => [primaryKey({ columns: [t.scope, t.ref_id, t.ip] })]
|
|
);
|
|
var appSettings = sqliteTable("app_settings", {
|
|
id: text("id").primaryKey(),
|
|
app_switcher_json: text("app_switcher_json"),
|
|
// deprecated: portal is SoT; column kept for migrate compat
|
|
vps_tracker_url: text("vps_tracker_url"),
|
|
vps_tracker_integration_token: text("vps_tracker_integration_token"),
|
|
vps_tracker_sync_enabled: integer("vps_tracker_sync_enabled", {
|
|
mode: "boolean"
|
|
}).notNull().default(false),
|
|
vps_tracker_last_sync_at: text("vps_tracker_last_sync_at"),
|
|
show_quick_actions: integer("show_quick_actions", {
|
|
mode: "boolean"
|
|
}).notNull().default(true),
|
|
created_at: text("created_at").notNull().default(sql`datetime('now')`),
|
|
updated_at: text("updated_at").notNull().default(sql`datetime('now')`)
|
|
});
|
|
var domainTags = sqliteTable(
|
|
"domain_tags",
|
|
{
|
|
domain_id: integer("domain_id").notNull().references(() => domains.id, { onDelete: "cascade" }),
|
|
tag: text("tag").notNull()
|
|
},
|
|
(t) => [primaryKey({ columns: [t.domain_id, t.tag] })]
|
|
);
|
|
var domainMonitors = sqliteTable("domain_monitors", {
|
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
domain_id: integer("domain_id").notNull().references(() => domains.id, { onDelete: "cascade" }),
|
|
hostname: text("hostname").notNull(),
|
|
type: text("type").notNull().default("http"),
|
|
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
|
|
interval_sec: integer("interval_sec").notNull().default(60),
|
|
timeout_ms: integer("timeout_ms").notNull().default(5e3),
|
|
path: text("path"),
|
|
expected_status: integer("expected_status"),
|
|
last_status: text("last_status").notNull().default("unknown"),
|
|
last_latency_ms: integer("last_latency_ms"),
|
|
last_checked_at: text("last_checked_at"),
|
|
last_error: text("last_error"),
|
|
created_at: text("created_at").notNull().default(sql`datetime('now')`),
|
|
updated_at: text("updated_at").notNull().default(sql`datetime('now')`)
|
|
});
|
|
var domainMonitorResults = sqliteTable("domain_monitor_results", {
|
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
monitor_id: integer("monitor_id").notNull().references(() => domainMonitors.id, { onDelete: "cascade" }),
|
|
status: text("status").notNull(),
|
|
latency_ms: integer("latency_ms"),
|
|
error: text("error"),
|
|
checked_at: text("checked_at").notNull().default(sql`datetime('now')`)
|
|
});
|
|
var notificationLog = sqliteTable("notification_log", {
|
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
kind: text("kind").notNull(),
|
|
ref_type: text("ref_type").notNull(),
|
|
ref_id: integer("ref_id"),
|
|
title: text("title").notNull(),
|
|
message: text("message").notNull(),
|
|
created_at: text("created_at").notNull().default(sql`datetime('now')`)
|
|
});
|
|
var schema = {
|
|
groups,
|
|
services,
|
|
serviceGroups,
|
|
domains,
|
|
subdomains,
|
|
dnsRecords,
|
|
serviceBindings,
|
|
serviceIps,
|
|
serviceBindingRecords,
|
|
serviceBindingIps,
|
|
serviceGroupDnsRecords,
|
|
certificates,
|
|
syncJobs,
|
|
ipHealthStatus,
|
|
appSettings,
|
|
domainTags,
|
|
domainMonitors,
|
|
domainMonitorResults,
|
|
notificationLog
|
|
};
|
|
|
|
// src/client.ts
|
|
import { dirname, join } from "path";
|
|
import { fileURLToPath } from "url";
|
|
import { readFileSync, readdirSync } from "fs";
|
|
import Database from "better-sqlite3";
|
|
import { drizzle } from "drizzle-orm/better-sqlite3";
|
|
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
function resolveDatabasePath(databaseUrl) {
|
|
const url = databaseUrl.startsWith("sqlite:") ? databaseUrl.slice("sqlite:".length) : databaseUrl;
|
|
return url;
|
|
}
|
|
function createDb(databaseUrl) {
|
|
const path = resolveDatabasePath(databaseUrl);
|
|
const sqlite = new Database(path);
|
|
sqlite.pragma("journal_mode = WAL");
|
|
sqlite.pragma("synchronous = NORMAL");
|
|
sqlite.pragma("foreign_keys = ON");
|
|
const db = drizzle(sqlite, { schema });
|
|
return { db, sqlite };
|
|
}
|
|
function createMemoryDb() {
|
|
const sqlite = new Database(":memory:");
|
|
sqlite.pragma("foreign_keys = ON");
|
|
const db = drizzle(sqlite, { schema });
|
|
return { db, sqlite };
|
|
}
|
|
function runMigrations(sqlite) {
|
|
const migrationsDir = join(__dirname, "..", "migrations");
|
|
const files = readdirSync(migrationsDir).filter((f) => f.endsWith(".sql")).sort();
|
|
sqlite.exec(
|
|
`CREATE TABLE IF NOT EXISTS _migrations (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL UNIQUE,
|
|
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
)`
|
|
);
|
|
for (const file of files) {
|
|
const applied = sqlite.prepare("SELECT 1 FROM _migrations WHERE name = ?").get(file);
|
|
if (applied) continue;
|
|
const sql3 = readFileSync(join(migrationsDir, file), "utf-8");
|
|
sqlite.exec(sql3);
|
|
sqlite.prepare("INSERT INTO _migrations (name) VALUES (?)").run(file);
|
|
}
|
|
}
|
|
function healthCheck(sqlite) {
|
|
sqlite.prepare("SELECT 1").get();
|
|
}
|
|
|
|
// src/errors.ts
|
|
var NotFoundError = class extends Error {
|
|
constructor(message) {
|
|
super(message);
|
|
this.name = "NotFoundError";
|
|
}
|
|
};
|
|
var ConflictError = class extends Error {
|
|
constructor(message) {
|
|
super(message);
|
|
this.name = "ConflictError";
|
|
}
|
|
};
|
|
|
|
// src/settings-repo.ts
|
|
import { eq } from "drizzle-orm";
|
|
var SETTINGS_ID = "settings-main";
|
|
function toDto(row) {
|
|
return {
|
|
id: row.id,
|
|
vpsTrackerUrl: row.vps_tracker_url?.trim() ?? "",
|
|
vpsTrackerIntegrationTokenSet: Boolean(
|
|
row.vps_tracker_integration_token?.trim()
|
|
),
|
|
vpsTrackerSyncEnabled: Boolean(row.vps_tracker_sync_enabled),
|
|
vpsTrackerLastSyncAt: row.vps_tracker_last_sync_at,
|
|
showQuickActions: row.show_quick_actions == null ? true : Boolean(row.show_quick_actions)
|
|
};
|
|
}
|
|
function getAppSettings(db) {
|
|
const row = db.select().from(appSettings).where(eq(appSettings.id, SETTINGS_ID)).get();
|
|
if (!row) {
|
|
db.insert(appSettings).values({ id: SETTINGS_ID }).run();
|
|
return toDto(
|
|
db.select().from(appSettings).where(eq(appSettings.id, SETTINGS_ID)).get()
|
|
);
|
|
}
|
|
return toDto(row);
|
|
}
|
|
function getAppSettingsSecrets(db) {
|
|
const row = db.select().from(appSettings).where(eq(appSettings.id, SETTINGS_ID)).get();
|
|
return {
|
|
vpsTrackerUrl: row?.vps_tracker_url?.trim() ?? "",
|
|
vpsTrackerIntegrationToken: row?.vps_tracker_integration_token?.trim() ?? "",
|
|
vpsTrackerSyncEnabled: Boolean(row?.vps_tracker_sync_enabled)
|
|
};
|
|
}
|
|
function updateAppSettings(db, patch) {
|
|
const existing = db.select().from(appSettings).where(eq(appSettings.id, SETTINGS_ID)).get();
|
|
if (!existing) {
|
|
db.insert(appSettings).values({ id: SETTINGS_ID }).run();
|
|
}
|
|
const current = db.select().from(appSettings).where(eq(appSettings.id, SETTINGS_ID)).get();
|
|
db.update(appSettings).set({
|
|
// app_switcher_json: deprecated — source of truth is auth-portal
|
|
vps_tracker_url: patch.vpsTrackerUrl !== void 0 ? patch.vpsTrackerUrl : current.vps_tracker_url,
|
|
vps_tracker_integration_token: patch.vpsTrackerIntegrationToken !== void 0 && patch.vpsTrackerIntegrationToken.trim() !== "" ? patch.vpsTrackerIntegrationToken : current.vps_tracker_integration_token,
|
|
vps_tracker_sync_enabled: patch.vpsTrackerSyncEnabled !== void 0 ? patch.vpsTrackerSyncEnabled : current.vps_tracker_sync_enabled,
|
|
show_quick_actions: patch.showQuickActions !== void 0 ? patch.showQuickActions : current.show_quick_actions,
|
|
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
}).where(eq(appSettings.id, SETTINGS_ID)).run();
|
|
return getAppSettings(db);
|
|
}
|
|
function touchVpsTrackerSync(db) {
|
|
db.update(appSettings).set({
|
|
vps_tracker_last_sync_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
}).where(eq(appSettings.id, SETTINGS_ID)).run();
|
|
}
|
|
|
|
// src/repos.ts
|
|
var repos_exports = {};
|
|
__export(repos_exports, {
|
|
addDomainTags: () => addDomainTags,
|
|
aggregateIpHealthByRefs: () => aggregateIpHealthByRefs,
|
|
aggregateIpHealthByServiceIds: () => aggregateIpHealthByServiceIds,
|
|
bindingsToRemove: () => bindingsToRemove,
|
|
countCertificatesByStatus: () => countCertificatesByStatus,
|
|
createDomain: () => createDomain,
|
|
createDomainMonitor: () => createDomainMonitor,
|
|
createGroup: () => createGroup,
|
|
createService: () => createService,
|
|
createServiceGroup: () => createServiceGroup,
|
|
createSubdomain: () => createSubdomain,
|
|
createSyncJob: () => createSyncJob,
|
|
deleteBinding: () => deleteBinding,
|
|
deleteBindingsExcept: () => deleteBindingsExcept,
|
|
deleteCertificatesNotIn: () => deleteCertificatesNotIn,
|
|
deleteDnsRecord: () => deleteDnsRecord,
|
|
deleteDomain: () => deleteDomain,
|
|
deleteDomainMonitor: () => deleteDomainMonitor,
|
|
deleteGroup: () => deleteGroup,
|
|
deleteIpHealthStatusForIp: () => deleteIpHealthStatusForIp,
|
|
deleteIpHealthStatusForRef: () => deleteIpHealthStatusForRef,
|
|
deleteService: () => deleteService,
|
|
deleteServiceGroup: () => deleteServiceGroup,
|
|
deleteSubdomain: () => deleteSubdomain,
|
|
findBinding: () => findBinding,
|
|
findDnsByCfId: () => findDnsByCfId,
|
|
findDomainByZoneName: () => findDomainByZoneName,
|
|
findSubdomainByDomainAndName: () => findSubdomainByDomainAndName,
|
|
finishSyncJob: () => finishSyncJob,
|
|
getBinding: () => getBinding,
|
|
getBindingView: () => getBindingView,
|
|
getCertificate: () => getCertificate,
|
|
getDnsRecord: () => getDnsRecord,
|
|
getDomain: () => getDomain,
|
|
getDomainMonitor: () => getDomainMonitor,
|
|
getGroup: () => getGroup,
|
|
getGroupWithStats: () => getGroupWithStats,
|
|
getIpHealthStatusRow: () => getIpHealthStatusRow,
|
|
getService: () => getService,
|
|
getServiceGroup: () => getServiceGroup,
|
|
getSubdomain: () => getSubdomain,
|
|
getSyncJob: () => getSyncJob,
|
|
insertBinding: () => insertBinding,
|
|
insertDnsRecord: () => insertDnsRecord,
|
|
insertNotificationLog: () => insertNotificationLog,
|
|
linkBindingRecord: () => linkBindingRecord,
|
|
linkGroupDnsRecord: () => linkGroupDnsRecord,
|
|
listAllBindings: () => listAllBindings,
|
|
listAllDomains: () => listAllDomains,
|
|
listAllSubdomains: () => listAllSubdomains,
|
|
listBindingIps: () => listBindingIps,
|
|
listBindingIpsWithMeta: () => listBindingIpsWithMeta,
|
|
listBindingsByDomain: () => listBindingsByDomain,
|
|
listBindingsByService: () => listBindingsByService,
|
|
listCertificates: () => listCertificates,
|
|
listDnsByDomain: () => listDnsByDomain,
|
|
listDnsRecords: () => listDnsRecords,
|
|
listDomainMonitorResults: () => listDomainMonitorResults,
|
|
listDomainMonitorResultsForDomain: () => listDomainMonitorResultsForDomain,
|
|
listDomainMonitors: () => listDomainMonitors,
|
|
listDomainTags: () => listDomainTags,
|
|
listDomains: () => listDomains,
|
|
listDomainsEnriched: () => listDomainsEnriched,
|
|
listEnabledDomainMonitors: () => listEnabledDomainMonitors,
|
|
listGroupDnsRecords: () => listGroupDnsRecords,
|
|
listGroups: () => listGroups,
|
|
listHealthCheckTargets: () => listHealthCheckTargets,
|
|
listIpHealthStatus: () => listIpHealthStatus,
|
|
listNotificationLog: () => listNotificationLog,
|
|
listRecordsForBinding: () => listRecordsForBinding,
|
|
listServiceGroups: () => listServiceGroups,
|
|
listServiceIps: () => listServiceIps,
|
|
listServices: () => listServices,
|
|
listServicesByGroup: () => listServicesByGroup,
|
|
listSubdomainsByDomain: () => listSubdomainsByDomain,
|
|
listUngroupedServices: () => listUngroupedServices,
|
|
markDnsPendingDelete: () => markDnsPendingDelete,
|
|
mergeHealthAggregates: () => mergeHealthAggregates,
|
|
pruneStaleIpHealthStatus: () => pruneStaleIpHealthStatus,
|
|
reorderServices: () => reorderServices,
|
|
replaceBindingIps: () => replaceBindingIps,
|
|
replaceBindingIpsWithMeta: () => replaceBindingIpsWithMeta,
|
|
replaceServiceIps: () => replaceServiceIps,
|
|
setBindingCnameTarget: () => setBindingCnameTarget,
|
|
setBindingDnsRecordId: () => setBindingDnsRecordId,
|
|
setDnsSyncStatus: () => setDnsSyncStatus,
|
|
setDomainLastSynced: () => setDomainLastSynced,
|
|
setDomainTags: () => setDomainTags,
|
|
setServiceEnabled: () => setServiceEnabled,
|
|
setServiceGroup: () => setServiceGroup,
|
|
setServiceGroupEnabled: () => setServiceGroupEnabled,
|
|
setServiceLb: () => setServiceLb,
|
|
unlinkBindingRecord: () => unlinkBindingRecord,
|
|
unlinkGroupDnsRecord: () => unlinkGroupDnsRecord,
|
|
updateBindingFields: () => updateBindingFields,
|
|
updateBindingLbConfig: () => updateBindingLbConfig,
|
|
updateDnsFields: () => updateDnsFields,
|
|
updateDomain: () => updateDomain,
|
|
updateDomainMonitorResult: () => updateDomainMonitorResult,
|
|
updateGroup: () => updateGroup,
|
|
updateService: () => updateService,
|
|
updateServiceGroup: () => updateServiceGroup,
|
|
updateSubdomain: () => updateSubdomain,
|
|
upsertCertificateCheck: () => upsertCertificateCheck,
|
|
upsertIpHealthStatus: () => upsertIpHealthStatus,
|
|
upsertSubdomain: () => upsertSubdomain
|
|
});
|
|
import { dnsRecordNamesMatch } from "@cfdm/shared";
|
|
import { and, asc, count, eq as eq2, isNull, like, notInArray, or, sql as sql2 } from "drizzle-orm";
|
|
function listGroups(db) {
|
|
return db.select().from(groups).orderBy(asc(groups.name)).all();
|
|
}
|
|
function getGroup(db, id) {
|
|
const row = db.select().from(groups).where(eq2(groups.id, id)).get();
|
|
if (!row) throw new NotFoundError(`group ${id}`);
|
|
return row;
|
|
}
|
|
function getGroupWithStats(db, id) {
|
|
const result = db.all(sql2`
|
|
SELECT g.id, g.name, g.slug, g.created_at, g.updated_at,
|
|
(SELECT COUNT(*) FROM domains d WHERE d.group_id = g.id) AS domain_count
|
|
FROM groups g WHERE g.id = ${id}
|
|
`);
|
|
if (!result[0]) throw new NotFoundError(`group ${id}`);
|
|
return result[0];
|
|
}
|
|
function createGroup(db, name, slug) {
|
|
const id = db.insert(groups).values({ name, slug }).returning({ id: groups.id }).get().id;
|
|
return getGroup(db, id);
|
|
}
|
|
function updateGroup(db, id, name, slug) {
|
|
const result = db.update(groups).set({ name, slug, updated_at: sql2`datetime('now')` }).where(eq2(groups.id, id)).run();
|
|
if (result.changes === 0) throw new NotFoundError(`group ${id}`);
|
|
return getGroup(db, id);
|
|
}
|
|
function deleteGroup(db, id) {
|
|
const result = db.delete(groups).where(eq2(groups.id, id)).run();
|
|
if (result.changes === 0) throw new NotFoundError(`group ${id}`);
|
|
}
|
|
function listDomains(db, groupId) {
|
|
if (groupId != null) {
|
|
return db.select().from(domains).where(eq2(domains.group_id, groupId)).orderBy(asc(domains.zone_name)).all();
|
|
}
|
|
return db.select().from(domains).orderBy(asc(domains.zone_name)).all();
|
|
}
|
|
function listDomainsEnriched(db, groupId) {
|
|
const base = groupId != null ? sql2`WHERE d.group_id = ${groupId}` : sql2``;
|
|
const rows = db.all(sql2`
|
|
SELECT d.*, g.name AS group_name,
|
|
(SELECT COUNT(*) FROM service_bindings sb WHERE sb.domain_id = d.id) AS service_count,
|
|
COALESCE((
|
|
SELECT CASE
|
|
WHEN MAX(CASE
|
|
WHEN ihs.status = 'down' THEN 3
|
|
WHEN ihs.status = 'degraded' THEN 2
|
|
WHEN ihs.status = 'up' THEN 1
|
|
ELSE 0
|
|
END) = 3 THEN 'down'
|
|
WHEN MAX(CASE
|
|
WHEN ihs.status = 'down' THEN 3
|
|
WHEN ihs.status = 'degraded' THEN 2
|
|
WHEN ihs.status = 'up' THEN 1
|
|
ELSE 0
|
|
END) = 2 THEN 'degraded'
|
|
WHEN MAX(CASE
|
|
WHEN ihs.status = 'down' THEN 3
|
|
WHEN ihs.status = 'degraded' THEN 2
|
|
WHEN ihs.status = 'up' THEN 1
|
|
ELSE 0
|
|
END) = 1 THEN 'up'
|
|
ELSE 'unknown'
|
|
END
|
|
FROM ip_health_status ihs
|
|
INNER JOIN service_bindings sb ON ihs.scope = 'binding' AND ihs.ref_id = sb.id
|
|
WHERE sb.domain_id = d.id
|
|
), 'unknown') AS health_status,
|
|
(
|
|
SELECT MAX(ihs.latency_ms)
|
|
FROM ip_health_status ihs
|
|
INNER JOIN service_bindings sb ON ihs.scope = 'binding' AND ihs.ref_id = sb.id
|
|
WHERE sb.domain_id = d.id
|
|
) AS health_latency_ms,
|
|
(
|
|
SELECT GROUP_CONCAT(dt.tag, ',')
|
|
FROM domain_tags dt
|
|
WHERE dt.domain_id = d.id
|
|
) AS tags_json
|
|
FROM domains d
|
|
LEFT JOIN groups g ON g.id = d.group_id
|
|
${base}
|
|
ORDER BY d.zone_name ASC
|
|
`);
|
|
return rows.map((row) => {
|
|
const { tags_json, ...rest } = row;
|
|
return {
|
|
...rest,
|
|
environment: rest.environment ?? null,
|
|
health_status: rest.health_status ?? "unknown",
|
|
health_latency_ms: rest.health_latency_ms ?? null,
|
|
tags: tags_json ? tags_json.split(",").map((t) => t.trim()).filter(Boolean) : []
|
|
};
|
|
});
|
|
}
|
|
function findDomainByZoneName(db, zoneName) {
|
|
const rows = db.all(sql2`
|
|
SELECT * FROM domains WHERE LOWER(zone_name) = LOWER(${zoneName}) LIMIT 1
|
|
`);
|
|
return rows[0] ?? null;
|
|
}
|
|
function getDomain(db, id) {
|
|
const row = db.select().from(domains).where(eq2(domains.id, id)).get();
|
|
if (!row) throw new NotFoundError(`domain ${id}`);
|
|
return row;
|
|
}
|
|
function createDomain(db, groupId, zoneName, cfZoneId) {
|
|
const id = db.insert(domains).values({
|
|
group_id: groupId,
|
|
zone_name: zoneName,
|
|
cf_zone_id: cfZoneId
|
|
}).returning({ id: domains.id }).get().id;
|
|
return getDomain(db, id);
|
|
}
|
|
function updateDomain(db, id, patch) {
|
|
const existing = getDomain(db, id);
|
|
const updates = {
|
|
group_id: patch.group_id !== void 0 ? patch.group_id : existing.group_id,
|
|
status: patch.status !== void 0 ? patch.status : existing.status,
|
|
updated_at: sql2`datetime('now')`
|
|
};
|
|
if (patch.cert_monitoring !== void 0) {
|
|
updates.cert_monitoring = patch.cert_monitoring;
|
|
}
|
|
if (patch.environment !== void 0) {
|
|
updates.environment = patch.environment;
|
|
}
|
|
const result = db.update(domains).set(updates).where(eq2(domains.id, id)).run();
|
|
if (result.changes === 0) throw new NotFoundError(`domain ${id}`);
|
|
return getDomain(db, id);
|
|
}
|
|
function deleteDomain(db, id) {
|
|
const result = db.delete(domains).where(eq2(domains.id, id)).run();
|
|
if (result.changes === 0) throw new NotFoundError(`domain ${id}`);
|
|
}
|
|
function setDomainLastSynced(db, id) {
|
|
db.update(domains).set({
|
|
last_synced_at: sql2`datetime('now')`,
|
|
updated_at: sql2`datetime('now')`
|
|
}).where(eq2(domains.id, id)).run();
|
|
}
|
|
function listAllDomains(db) {
|
|
return listDomains(db);
|
|
}
|
|
function listSubdomainsByDomain(db, domainId) {
|
|
return db.select().from(subdomains).where(eq2(subdomains.domain_id, domainId)).orderBy(asc(subdomains.name)).all();
|
|
}
|
|
function getSubdomain(db, id) {
|
|
const row = db.select().from(subdomains).where(eq2(subdomains.id, id)).get();
|
|
if (!row) throw new NotFoundError(`subdomain ${id}`);
|
|
return row;
|
|
}
|
|
function findSubdomainByDomainAndName(db, domainId, name) {
|
|
const row = db.select().from(subdomains).where(and(eq2(subdomains.domain_id, domainId), eq2(subdomains.name, name))).get();
|
|
return row ?? null;
|
|
}
|
|
function upsertSubdomain(db, domainId, name, fqdn) {
|
|
db.run(sql2`
|
|
INSERT INTO subdomains (domain_id, name, fqdn)
|
|
VALUES (${domainId}, ${name}, ${fqdn})
|
|
ON CONFLICT(domain_id, name) DO UPDATE SET
|
|
fqdn = excluded.fqdn,
|
|
updated_at = datetime('now')
|
|
`);
|
|
}
|
|
function createSubdomain(db, domainId, name, fqdn) {
|
|
const id = db.insert(subdomains).values({ domain_id: domainId, name, fqdn }).returning({ id: subdomains.id }).get().id;
|
|
return getSubdomain(db, id);
|
|
}
|
|
function updateSubdomain(db, id, patch) {
|
|
const updates = { updated_at: sql2`datetime('now')` };
|
|
if (patch.name !== void 0) updates.name = patch.name;
|
|
if (patch.fqdn !== void 0) updates.fqdn = patch.fqdn;
|
|
if (patch.enabled !== void 0) updates.enabled = patch.enabled;
|
|
if (patch.cert_monitoring !== void 0) {
|
|
updates.cert_monitoring = patch.cert_monitoring;
|
|
}
|
|
const result = db.update(subdomains).set(updates).where(eq2(subdomains.id, id)).run();
|
|
if (result.changes === 0) throw new NotFoundError(`subdomain ${id}`);
|
|
return getSubdomain(db, id);
|
|
}
|
|
function deleteSubdomain(db, id) {
|
|
const result = db.delete(subdomains).where(eq2(subdomains.id, id)).run();
|
|
if (result.changes === 0) throw new NotFoundError(`subdomain ${id}`);
|
|
}
|
|
function listAllSubdomains(db) {
|
|
return db.select().from(subdomains).orderBy(asc(subdomains.fqdn)).all();
|
|
}
|
|
function mapDnsRecord(row) {
|
|
return row;
|
|
}
|
|
function listDnsRecords(db, domainId, filter = {}) {
|
|
const conditions = [eq2(dnsRecords.domain_id, domainId)];
|
|
if (filter.record_type) {
|
|
conditions.push(eq2(dnsRecords.record_type, filter.record_type.toUpperCase()));
|
|
}
|
|
if (filter.name) {
|
|
conditions.push(like(dnsRecords.name, `%${filter.name}%`));
|
|
}
|
|
if (filter.content) {
|
|
conditions.push(like(dnsRecords.content, `%${filter.content}%`));
|
|
}
|
|
if (filter.proxied != null) {
|
|
conditions.push(eq2(dnsRecords.proxied, filter.proxied));
|
|
}
|
|
if (filter.sync_status) {
|
|
conditions.push(eq2(dnsRecords.sync_status, filter.sync_status));
|
|
}
|
|
if (filter.q) {
|
|
const pat = `%${filter.q}%`;
|
|
conditions.push(
|
|
or(
|
|
like(dnsRecords.name, pat),
|
|
like(dnsRecords.content, pat),
|
|
like(dnsRecords.record_type, pat)
|
|
)
|
|
);
|
|
}
|
|
const sortCol = filter.sort === "type" ? dnsRecords.record_type : filter.sort === "updated_at" ? dnsRecords.updated_at : dnsRecords.name;
|
|
const page = Math.max(1, filter.page ?? 1);
|
|
const limit = Math.min(200, Math.max(1, filter.limit ?? 50));
|
|
const offset = (page - 1) * limit;
|
|
return db.select().from(dnsRecords).where(and(...conditions)).orderBy(asc(sortCol)).limit(limit).offset(offset).all().map(mapDnsRecord);
|
|
}
|
|
function getDnsRecord(db, domainId, id) {
|
|
const row = db.select().from(dnsRecords).where(and(eq2(dnsRecords.id, id), eq2(dnsRecords.domain_id, domainId))).get();
|
|
if (!row) throw new NotFoundError(`dns record ${id}`);
|
|
return mapDnsRecord(row);
|
|
}
|
|
function insertDnsRecord(db, domainId, recordType, name, content, ttl, proxied, priority, syncStatus, origin, cfRecordId) {
|
|
const id = db.insert(dnsRecords).values({
|
|
domain_id: domainId,
|
|
cf_record_id: cfRecordId,
|
|
record_type: recordType.toUpperCase(),
|
|
name,
|
|
content,
|
|
ttl,
|
|
proxied,
|
|
priority,
|
|
sync_status: syncStatus,
|
|
origin
|
|
}).returning({ id: dnsRecords.id }).get().id;
|
|
return getDnsRecord(db, domainId, id);
|
|
}
|
|
function updateDnsFields(db, id, recordType, name, content, ttl, proxied, priority, syncStatus, cfRecordId, lastError) {
|
|
db.update(dnsRecords).set({
|
|
cf_record_id: cfRecordId,
|
|
record_type: recordType.toUpperCase(),
|
|
name,
|
|
content,
|
|
ttl,
|
|
proxied,
|
|
priority,
|
|
sync_status: syncStatus,
|
|
last_error: lastError,
|
|
updated_at: sql2`datetime('now')`
|
|
}).where(eq2(dnsRecords.id, id)).run();
|
|
}
|
|
function setDnsSyncStatus(db, id, syncStatus, cfRecordId, lastError) {
|
|
db.update(dnsRecords).set({
|
|
sync_status: syncStatus,
|
|
cf_record_id: cfRecordId,
|
|
last_error: lastError,
|
|
updated_at: sql2`datetime('now')`
|
|
}).where(eq2(dnsRecords.id, id)).run();
|
|
}
|
|
function deleteDnsRecord(db, id) {
|
|
db.delete(dnsRecords).where(eq2(dnsRecords.id, id)).run();
|
|
}
|
|
function listDnsByDomain(db, domainId) {
|
|
return db.select().from(dnsRecords).where(eq2(dnsRecords.domain_id, domainId)).all().map(mapDnsRecord);
|
|
}
|
|
function findDnsByCfId(db, domainId, cfRecordId) {
|
|
const row = db.select().from(dnsRecords).where(
|
|
and(
|
|
eq2(dnsRecords.domain_id, domainId),
|
|
eq2(dnsRecords.cf_record_id, cfRecordId)
|
|
)
|
|
).get();
|
|
return row ? mapDnsRecord(row) : null;
|
|
}
|
|
function markDnsPendingDelete(db, id) {
|
|
setDnsSyncStatus(db, id, "pending_delete", null, null);
|
|
}
|
|
function maxSortOrderInGroup(db, groupId) {
|
|
const condition = groupId === null ? isNull(services.service_group_id) : eq2(services.service_group_id, groupId);
|
|
const row = db.select({ maxOrder: sql2`coalesce(max(${services.sort_order}), -1)` }).from(services).where(condition).get();
|
|
return row?.maxOrder ?? -1;
|
|
}
|
|
function listServices(db) {
|
|
return db.select().from(services).orderBy(asc(services.sort_order), asc(services.name)).all();
|
|
}
|
|
function listServicesByGroup(db, groupId) {
|
|
return db.select().from(services).where(eq2(services.service_group_id, groupId)).orderBy(asc(services.sort_order), asc(services.name)).all();
|
|
}
|
|
function listUngroupedServices(db) {
|
|
return db.select().from(services).where(isNull(services.service_group_id)).orderBy(asc(services.sort_order), asc(services.name)).all();
|
|
}
|
|
function getService(db, id) {
|
|
const row = db.select().from(services).where(eq2(services.id, id)).get();
|
|
if (!row) throw new NotFoundError(`service ${id}`);
|
|
return row;
|
|
}
|
|
function createService(db, name, slug) {
|
|
const sortOrder = maxSortOrderInGroup(db, null) + 1;
|
|
const id = db.insert(services).values({ name, slug, subdomain: slug, sort_order: sortOrder }).returning({ id: services.id }).get().id;
|
|
return getService(db, id);
|
|
}
|
|
function updateService(db, id, name, slug) {
|
|
db.update(services).set({
|
|
name,
|
|
slug,
|
|
subdomain: slug,
|
|
updated_at: sql2`datetime('now')`
|
|
}).where(eq2(services.id, id)).run();
|
|
return getService(db, id);
|
|
}
|
|
function setServiceEnabled(db, id, enabled) {
|
|
db.update(services).set({ enabled, updated_at: sql2`datetime('now')` }).where(eq2(services.id, id)).run();
|
|
return getService(db, id);
|
|
}
|
|
function setServiceLb(db, id, weight, priority) {
|
|
db.update(services).set({
|
|
lb_weight: weight,
|
|
lb_priority: priority,
|
|
updated_at: sql2`datetime('now')`
|
|
}).where(eq2(services.id, id)).run();
|
|
}
|
|
function setServiceGroup(db, id, groupId) {
|
|
const sortOrder = maxSortOrderInGroup(db, groupId) + 1;
|
|
db.update(services).set({
|
|
service_group_id: groupId,
|
|
sort_order: sortOrder,
|
|
updated_at: sql2`datetime('now')`
|
|
}).where(eq2(services.id, id)).run();
|
|
}
|
|
function reorderServices(db, groupId, orderedIds) {
|
|
const uniqueIds = new Set(orderedIds);
|
|
if (uniqueIds.size !== orderedIds.length) {
|
|
throw new Error("duplicate service ids in reorder request");
|
|
}
|
|
const condition = groupId === null ? isNull(services.service_group_id) : eq2(services.service_group_id, groupId);
|
|
const existing = db.select({ id: services.id }).from(services).where(condition).all().map((row) => row.id);
|
|
const existingSet = new Set(existing);
|
|
for (const serviceId of orderedIds) {
|
|
if (!existingSet.has(serviceId)) {
|
|
throw new NotFoundError(`service ${serviceId} not in group`);
|
|
}
|
|
}
|
|
db.transaction((tx) => {
|
|
for (let index = 0; index < orderedIds.length; index++) {
|
|
tx.update(services).set({ sort_order: index, updated_at: sql2`datetime('now')` }).where(eq2(services.id, orderedIds[index])).run();
|
|
}
|
|
});
|
|
}
|
|
function deleteService(db, id) {
|
|
const result = db.delete(services).where(eq2(services.id, id)).run();
|
|
if (result.changes === 0) throw new NotFoundError(`service ${id}`);
|
|
}
|
|
function mapServiceGroup(row) {
|
|
return {
|
|
id: row.id,
|
|
name: row.name,
|
|
type: row.type,
|
|
icon: row.icon,
|
|
domain: row.domain,
|
|
enabled: row.enabled,
|
|
lb_mode: row.lb_mode,
|
|
health_check_enabled: row.health_check_enabled,
|
|
health_check_type: row.health_check_type,
|
|
health_check_port: row.health_check_port,
|
|
health_check_path: row.health_check_path,
|
|
health_check_expected_status: row.health_check_expected_status,
|
|
health_check_interval_sec: row.health_check_interval_sec,
|
|
health_check_timeout_ms: row.health_check_timeout_ms,
|
|
health_check_verify_tls: row.health_check_verify_tls,
|
|
created_at: row.created_at,
|
|
updated_at: row.updated_at
|
|
};
|
|
}
|
|
function listServiceGroups(db) {
|
|
return db.select().from(serviceGroups).orderBy(asc(serviceGroups.name)).all().map(mapServiceGroup);
|
|
}
|
|
function getServiceGroup(db, id) {
|
|
const row = db.select().from(serviceGroups).where(eq2(serviceGroups.id, id)).get();
|
|
if (!row) throw new NotFoundError(`service group ${id}`);
|
|
return mapServiceGroup(row);
|
|
}
|
|
function createServiceGroup(db, name, groupType, icon, domain, lbPatch) {
|
|
const id = db.insert(serviceGroups).values({
|
|
name,
|
|
type: groupType,
|
|
icon,
|
|
domain,
|
|
lb_mode: lbPatch?.lb_mode ?? "round_robin",
|
|
health_check_enabled: lbPatch?.health_check_enabled ?? false,
|
|
health_check_type: lbPatch?.health_check_type ?? "tcp",
|
|
health_check_port: lbPatch?.health_check_port ?? null,
|
|
health_check_path: lbPatch?.health_check_path ?? null,
|
|
health_check_expected_status: lbPatch?.health_check_expected_status ?? null,
|
|
health_check_interval_sec: lbPatch?.health_check_interval_sec ?? 30,
|
|
health_check_timeout_ms: lbPatch?.health_check_timeout_ms ?? 3e3,
|
|
health_check_verify_tls: lbPatch?.health_check_verify_tls ?? false
|
|
}).returning({ id: serviceGroups.id }).get().id;
|
|
return getServiceGroup(db, id);
|
|
}
|
|
function updateServiceGroup(db, id, name, groupType, icon, domain, lbPatch) {
|
|
const update = {
|
|
name,
|
|
type: groupType,
|
|
icon,
|
|
domain,
|
|
updated_at: sql2`datetime('now')`
|
|
};
|
|
if (lbPatch) {
|
|
if (lbPatch.lb_mode !== void 0) update.lb_mode = lbPatch.lb_mode;
|
|
if (lbPatch.health_check_enabled !== void 0)
|
|
update.health_check_enabled = lbPatch.health_check_enabled;
|
|
if (lbPatch.health_check_type !== void 0)
|
|
update.health_check_type = lbPatch.health_check_type;
|
|
if (lbPatch.health_check_port !== void 0)
|
|
update.health_check_port = lbPatch.health_check_port;
|
|
if (lbPatch.health_check_path !== void 0)
|
|
update.health_check_path = lbPatch.health_check_path;
|
|
if (lbPatch.health_check_expected_status !== void 0)
|
|
update.health_check_expected_status = lbPatch.health_check_expected_status;
|
|
if (lbPatch.health_check_interval_sec !== void 0)
|
|
update.health_check_interval_sec = lbPatch.health_check_interval_sec;
|
|
if (lbPatch.health_check_timeout_ms !== void 0)
|
|
update.health_check_timeout_ms = lbPatch.health_check_timeout_ms;
|
|
if (lbPatch.health_check_verify_tls !== void 0)
|
|
update.health_check_verify_tls = lbPatch.health_check_verify_tls;
|
|
}
|
|
const result = db.update(serviceGroups).set(update).where(eq2(serviceGroups.id, id)).run();
|
|
if (result.changes === 0) throw new NotFoundError(`service group ${id}`);
|
|
return getServiceGroup(db, id);
|
|
}
|
|
function setServiceGroupEnabled(db, id, enabled) {
|
|
const result = db.update(serviceGroups).set({ enabled, updated_at: sql2`datetime('now')` }).where(eq2(serviceGroups.id, id)).run();
|
|
if (result.changes === 0) throw new NotFoundError(`service group ${id}`);
|
|
return getServiceGroup(db, id);
|
|
}
|
|
function deleteServiceGroup(db, id) {
|
|
const result = db.delete(serviceGroups).where(eq2(serviceGroups.id, id)).run();
|
|
if (result.changes === 0) throw new NotFoundError(`service group ${id}`);
|
|
}
|
|
function listServiceIps(db, serviceId) {
|
|
return db.select({ ip: serviceIps.ip }).from(serviceIps).where(eq2(serviceIps.service_id, serviceId)).all().map((r) => r.ip);
|
|
}
|
|
function replaceServiceIps(db, serviceId, ips) {
|
|
db.delete(serviceIps).where(eq2(serviceIps.service_id, serviceId)).run();
|
|
for (const ip of ips) {
|
|
db.insert(serviceIps).values({ service_id: serviceId, ip }).run();
|
|
}
|
|
}
|
|
function listBindingIps(db, bindingId) {
|
|
return db.select({ ip: serviceBindingIps.ip }).from(serviceBindingIps).where(eq2(serviceBindingIps.binding_id, bindingId)).all().map((r) => r.ip);
|
|
}
|
|
function listBindingIpsWithMeta(db, bindingId) {
|
|
return db.select({
|
|
ip: serviceBindingIps.ip,
|
|
weight: serviceBindingIps.weight,
|
|
priority: serviceBindingIps.priority
|
|
}).from(serviceBindingIps).where(eq2(serviceBindingIps.binding_id, bindingId)).all();
|
|
}
|
|
function replaceBindingIps(db, bindingId, ips) {
|
|
replaceBindingIpsWithMeta(
|
|
db,
|
|
bindingId,
|
|
ips.map((ip) => ({ ip, weight: 1, priority: 1 }))
|
|
);
|
|
}
|
|
function replaceBindingIpsWithMeta(db, bindingId, entries) {
|
|
db.delete(serviceBindingIps).where(eq2(serviceBindingIps.binding_id, bindingId)).run();
|
|
for (const entry of entries) {
|
|
db.insert(serviceBindingIps).values({
|
|
binding_id: bindingId,
|
|
ip: entry.ip,
|
|
weight: entry.weight,
|
|
priority: entry.priority
|
|
}).run();
|
|
}
|
|
}
|
|
function updateBindingLbConfig(db, bindingId, patch) {
|
|
const update = {
|
|
updated_at: sql2`datetime('now')`
|
|
};
|
|
if (patch.lb_mode !== void 0) update.lb_mode = patch.lb_mode;
|
|
if (patch.health_check_enabled !== void 0)
|
|
update.health_check_enabled = patch.health_check_enabled;
|
|
if (patch.health_check_type !== void 0)
|
|
update.health_check_type = patch.health_check_type;
|
|
if (patch.health_check_port !== void 0)
|
|
update.health_check_port = patch.health_check_port;
|
|
if (patch.health_check_path !== void 0)
|
|
update.health_check_path = patch.health_check_path;
|
|
if (patch.health_check_expected_status !== void 0)
|
|
update.health_check_expected_status = patch.health_check_expected_status;
|
|
if (patch.health_check_interval_sec !== void 0)
|
|
update.health_check_interval_sec = patch.health_check_interval_sec;
|
|
if (patch.health_check_timeout_ms !== void 0)
|
|
update.health_check_timeout_ms = patch.health_check_timeout_ms;
|
|
if (patch.health_check_verify_tls !== void 0)
|
|
update.health_check_verify_tls = patch.health_check_verify_tls;
|
|
db.update(serviceBindings).set(update).where(eq2(serviceBindings.id, bindingId)).run();
|
|
}
|
|
function setBindingCnameTarget(db, bindingId, target) {
|
|
db.update(serviceBindings).set({
|
|
cname_target: target,
|
|
updated_at: sql2`datetime('now')`
|
|
}).where(eq2(serviceBindings.id, bindingId)).run();
|
|
}
|
|
function listRecordsForBinding(db, bindingId) {
|
|
return db.all(sql2`
|
|
SELECT dr.* FROM dns_records dr
|
|
INNER JOIN service_binding_records sbr ON sbr.dns_record_id = dr.id
|
|
WHERE sbr.binding_id = ${bindingId}
|
|
`);
|
|
}
|
|
function linkBindingRecord(db, bindingId, dnsRecordId) {
|
|
db.run(sql2`
|
|
INSERT INTO service_binding_records (binding_id, dns_record_id)
|
|
VALUES (${bindingId}, ${dnsRecordId})
|
|
ON CONFLICT(binding_id, dns_record_id) DO NOTHING
|
|
`);
|
|
}
|
|
function unlinkBindingRecord(db, bindingId, dnsRecordId) {
|
|
db.delete(serviceBindingRecords).where(
|
|
and(
|
|
eq2(serviceBindingRecords.binding_id, bindingId),
|
|
eq2(serviceBindingRecords.dns_record_id, dnsRecordId)
|
|
)
|
|
).run();
|
|
}
|
|
function listGroupDnsRecords(db, groupId) {
|
|
return db.all(sql2`
|
|
SELECT dr.* FROM dns_records dr
|
|
INNER JOIN service_group_dns_records sgdr ON sgdr.dns_record_id = dr.id
|
|
WHERE sgdr.group_id = ${groupId}
|
|
`);
|
|
}
|
|
function linkGroupDnsRecord(db, groupId, dnsRecordId) {
|
|
db.run(sql2`
|
|
INSERT INTO service_group_dns_records (group_id, dns_record_id)
|
|
VALUES (${groupId}, ${dnsRecordId})
|
|
ON CONFLICT(group_id, dns_record_id) DO NOTHING
|
|
`);
|
|
}
|
|
function unlinkGroupDnsRecord(db, groupId, dnsRecordId) {
|
|
db.delete(serviceGroupDnsRecords).where(
|
|
and(
|
|
eq2(serviceGroupDnsRecords.group_id, groupId),
|
|
eq2(serviceGroupDnsRecords.dns_record_id, dnsRecordId)
|
|
)
|
|
).run();
|
|
}
|
|
function dnsRecordMatchesHostname(recordName, hostname, zoneName) {
|
|
return dnsRecordNamesMatch(recordName, hostname, zoneName);
|
|
}
|
|
var SERVICE_BINDING_SELECT_COLUMNS = `sb.id, sb.domain_id, sb.service_id, sb.hostname, sb.dns_record_id,
|
|
sb.lb_mode, sb.health_check_enabled, sb.health_check_type, sb.health_check_port,
|
|
sb.health_check_path, sb.health_check_expected_status, sb.health_check_interval_sec,
|
|
sb.health_check_timeout_ms, sb.health_check_verify_tls, sb.cname_target,
|
|
d.zone_name, d.group_id, g.name AS group_name,
|
|
s.name AS service_name, s.slug AS service_slug,
|
|
dr.content AS target_ip, dr.sync_status,
|
|
sb.created_at, sb.updated_at`;
|
|
function enrichServiceBindingView(db, row) {
|
|
const configured = listBindingIpsWithMeta(db, row.id);
|
|
const configuredIps = configured.map((c) => c.ip);
|
|
const linkedRecords = listRecordsForBinding(db, row.id);
|
|
const linkedIps = linkedRecords.filter((record) => record.record_type.toUpperCase() === "A").map((record) => record.content);
|
|
const target_ips = [
|
|
.../* @__PURE__ */ new Set([
|
|
...configuredIps,
|
|
...linkedIps,
|
|
...row.target_ip ? [row.target_ip] : []
|
|
])
|
|
].sort();
|
|
if (target_ips.length === 0) {
|
|
for (const record of listDnsByDomain(db, row.domain_id)) {
|
|
if (record.record_type.toUpperCase() !== "A") continue;
|
|
if (!dnsRecordMatchesHostname(record.name, row.hostname, row.zone_name)) {
|
|
continue;
|
|
}
|
|
if (!target_ips.includes(record.content)) {
|
|
target_ips.push(record.content);
|
|
}
|
|
}
|
|
target_ips.sort();
|
|
}
|
|
const target_ip_weights = {};
|
|
const target_ip_priorities = {};
|
|
for (const entry of configured) {
|
|
target_ip_weights[entry.ip] = entry.weight;
|
|
target_ip_priorities[entry.ip] = entry.priority;
|
|
}
|
|
for (const ip of target_ips) {
|
|
if (target_ip_weights[ip] === void 0) target_ip_weights[ip] = 1;
|
|
if (target_ip_priorities[ip] === void 0) target_ip_priorities[ip] = 1;
|
|
}
|
|
const sync_status = row.sync_status ?? linkedRecords.find((record) => record.sync_status)?.sync_status ?? null;
|
|
return {
|
|
...row,
|
|
target_ips,
|
|
target_ip: target_ips[0] ?? null,
|
|
target_ip_weights,
|
|
target_ip_priorities,
|
|
sync_status
|
|
};
|
|
}
|
|
function listAllBindings(db) {
|
|
return db.all(sql2`
|
|
SELECT ${sql2.raw(SERVICE_BINDING_SELECT_COLUMNS)}
|
|
FROM service_bindings sb
|
|
JOIN domains d ON d.id = sb.domain_id
|
|
LEFT JOIN groups g ON g.id = d.group_id
|
|
JOIN services s ON s.id = sb.service_id
|
|
LEFT JOIN dns_records dr ON dr.id = sb.dns_record_id
|
|
ORDER BY d.zone_name, s.name
|
|
`).map((row) => enrichServiceBindingView(db, row));
|
|
}
|
|
function listBindingsByDomain(db, domainId) {
|
|
return db.all(sql2`
|
|
SELECT ${sql2.raw(SERVICE_BINDING_SELECT_COLUMNS)}
|
|
FROM service_bindings sb
|
|
JOIN domains d ON d.id = sb.domain_id
|
|
LEFT JOIN groups g ON g.id = d.group_id
|
|
JOIN services s ON s.id = sb.service_id
|
|
LEFT JOIN dns_records dr ON dr.id = sb.dns_record_id
|
|
WHERE sb.domain_id = ${domainId}
|
|
ORDER BY s.name
|
|
`).map((row) => enrichServiceBindingView(db, row));
|
|
}
|
|
function listBindingsByService(db, serviceId) {
|
|
return db.all(sql2`
|
|
SELECT sb.*, d.zone_name FROM service_bindings sb
|
|
JOIN domains d ON d.id = sb.domain_id
|
|
WHERE sb.service_id = ${serviceId}
|
|
`);
|
|
}
|
|
function getBinding(db, id) {
|
|
const row = db.select().from(serviceBindings).where(eq2(serviceBindings.id, id)).get();
|
|
if (!row) throw new NotFoundError(`service binding ${id}`);
|
|
return row;
|
|
}
|
|
function getBindingView(db, id) {
|
|
const rows = db.all(sql2`
|
|
SELECT ${sql2.raw(SERVICE_BINDING_SELECT_COLUMNS)}
|
|
FROM service_bindings sb
|
|
JOIN domains d ON d.id = sb.domain_id
|
|
LEFT JOIN groups g ON g.id = d.group_id
|
|
JOIN services s ON s.id = sb.service_id
|
|
LEFT JOIN dns_records dr ON dr.id = sb.dns_record_id
|
|
WHERE sb.id = ${id}
|
|
`);
|
|
if (!rows[0]) throw new NotFoundError(`service binding ${id}`);
|
|
return enrichServiceBindingView(db, rows[0]);
|
|
}
|
|
function findBinding(db, serviceId, domainId, hostname) {
|
|
const row = db.select().from(serviceBindings).where(
|
|
and(
|
|
eq2(serviceBindings.service_id, serviceId),
|
|
eq2(serviceBindings.domain_id, domainId),
|
|
eq2(serviceBindings.hostname, hostname)
|
|
)
|
|
).get();
|
|
return row ?? null;
|
|
}
|
|
function insertBinding(db, domainId, serviceId, hostname, dnsRecordId) {
|
|
const id = db.insert(serviceBindings).values({
|
|
domain_id: domainId,
|
|
service_id: serviceId,
|
|
hostname,
|
|
dns_record_id: dnsRecordId
|
|
}).returning({ id: serviceBindings.id }).get().id;
|
|
return getBinding(db, id);
|
|
}
|
|
function updateBindingFields(db, id, serviceId, hostname, dnsRecordId) {
|
|
db.update(serviceBindings).set({
|
|
service_id: serviceId,
|
|
hostname,
|
|
dns_record_id: dnsRecordId,
|
|
updated_at: sql2`datetime('now')`
|
|
}).where(eq2(serviceBindings.id, id)).run();
|
|
}
|
|
function setBindingDnsRecordId(db, bindingId, dnsRecordId) {
|
|
db.update(serviceBindings).set({
|
|
dns_record_id: dnsRecordId,
|
|
updated_at: sql2`datetime('now')`
|
|
}).where(eq2(serviceBindings.id, bindingId)).run();
|
|
}
|
|
function bindingsToRemove(db, serviceId, keepIds) {
|
|
const all = db.select().from(serviceBindings).where(eq2(serviceBindings.service_id, serviceId)).all();
|
|
return all.filter((b) => !keepIds.includes(b.id));
|
|
}
|
|
function deleteBindingsExcept(db, serviceId, keepIds) {
|
|
const all = db.select().from(serviceBindings).where(eq2(serviceBindings.service_id, serviceId)).all();
|
|
for (const binding of all) {
|
|
if (!keepIds.includes(binding.id)) {
|
|
db.delete(serviceBindings).where(eq2(serviceBindings.id, binding.id)).run();
|
|
}
|
|
}
|
|
}
|
|
function deleteBinding(db, id) {
|
|
const result = db.delete(serviceBindings).where(eq2(serviceBindings.id, id)).run();
|
|
if (result.changes === 0) throw new NotFoundError(`service binding ${id}`);
|
|
}
|
|
function listCertificates(db, status) {
|
|
if (status) {
|
|
return db.select().from(certificates).where(eq2(certificates.status, status)).orderBy(asc(certificates.expires_at)).all();
|
|
}
|
|
return db.select().from(certificates).orderBy(asc(certificates.expires_at)).all();
|
|
}
|
|
function getCertificate(db, id) {
|
|
const row = db.select().from(certificates).where(eq2(certificates.id, id)).get();
|
|
if (!row) throw new NotFoundError(`certificate ${id}`);
|
|
return row;
|
|
}
|
|
function upsertCertificateCheck(db, domainId, subdomainId, hostname, expiresAt, status, lastError) {
|
|
const existing = db.select().from(certificates).where(eq2(certificates.hostname, hostname)).get();
|
|
if (existing) {
|
|
db.update(certificates).set({
|
|
domain_id: domainId,
|
|
subdomain_id: subdomainId,
|
|
expires_at: expiresAt,
|
|
last_checked_at: sql2`datetime('now')`,
|
|
last_error: lastError,
|
|
status,
|
|
updated_at: sql2`datetime('now')`
|
|
}).where(eq2(certificates.id, existing.id)).run();
|
|
return getCertificate(db, existing.id);
|
|
}
|
|
const id = db.insert(certificates).values({
|
|
domain_id: domainId,
|
|
subdomain_id: subdomainId,
|
|
hostname,
|
|
expires_at: expiresAt,
|
|
last_checked_at: sql2`datetime('now')`,
|
|
last_error: lastError,
|
|
status
|
|
}).returning({ id: certificates.id }).get().id;
|
|
return getCertificate(db, id);
|
|
}
|
|
function countCertificatesByStatus(db) {
|
|
const rows = db.select({
|
|
status: certificates.status,
|
|
cnt: count()
|
|
}).from(certificates).groupBy(certificates.status).all();
|
|
return rows.map((r) => [r.status, r.cnt]);
|
|
}
|
|
function deleteCertificatesNotIn(db, hostnames) {
|
|
if (hostnames.length === 0) {
|
|
const result2 = db.delete(certificates).run();
|
|
return result2.changes;
|
|
}
|
|
const result = db.delete(certificates).where(notInArray(certificates.hostname, hostnames)).run();
|
|
return result.changes;
|
|
}
|
|
function createSyncJob(db, id, domainId) {
|
|
db.insert(syncJobs).values({ id, domain_id: domainId, status: "pending" }).run();
|
|
}
|
|
function getSyncJob(db, id) {
|
|
const row = db.select().from(syncJobs).where(eq2(syncJobs.id, id)).get();
|
|
if (!row) throw new NotFoundError(`sync job ${id}`);
|
|
return row;
|
|
}
|
|
function finishSyncJob(db, id, status, message) {
|
|
db.update(syncJobs).set({
|
|
status,
|
|
message,
|
|
finished_at: sql2`datetime('now')`
|
|
}).where(eq2(syncJobs.id, id)).run();
|
|
}
|
|
function listIpHealthStatus(db, scope, refId) {
|
|
return db.all(sql2`
|
|
SELECT scope, ref_id, ip, status, latency_ms, consecutive_failures,
|
|
last_checked_at, last_error
|
|
FROM ip_health_status
|
|
WHERE scope = ${scope} AND ref_id = ${refId}
|
|
`);
|
|
}
|
|
var UNKNOWN_HEALTH = {
|
|
health_status: "unknown",
|
|
health_latency_ms: null
|
|
};
|
|
function parseHealthAggregateRow(row) {
|
|
const status = row.health_status;
|
|
if (status === "up" || status === "down" || status === "degraded" || status === "unknown") {
|
|
return {
|
|
health_status: status,
|
|
health_latency_ms: row.health_latency_ms ?? null
|
|
};
|
|
}
|
|
return UNKNOWN_HEALTH;
|
|
}
|
|
var WORST_HEALTH_SQL = sql2.raw(`CASE
|
|
WHEN MAX(CASE
|
|
WHEN status = 'down' THEN 3
|
|
WHEN status = 'degraded' THEN 2
|
|
WHEN status = 'up' THEN 1
|
|
ELSE 0
|
|
END) = 3 THEN 'down'
|
|
WHEN MAX(CASE
|
|
WHEN status = 'down' THEN 3
|
|
WHEN status = 'degraded' THEN 2
|
|
WHEN status = 'up' THEN 1
|
|
ELSE 0
|
|
END) = 2 THEN 'degraded'
|
|
WHEN MAX(CASE
|
|
WHEN status = 'down' THEN 3
|
|
WHEN status = 'degraded' THEN 2
|
|
WHEN status = 'up' THEN 1
|
|
ELSE 0
|
|
END) = 1 THEN 'up'
|
|
ELSE 'unknown'
|
|
END`);
|
|
function aggregateIpHealthByRefs(db, scope, refIds) {
|
|
const result = /* @__PURE__ */ new Map();
|
|
if (refIds.length === 0) return result;
|
|
const idList = sql2.join(
|
|
refIds.map((id) => sql2`${id}`),
|
|
sql2`, `
|
|
);
|
|
const rows = db.all(sql2`
|
|
SELECT ref_id,
|
|
${WORST_HEALTH_SQL} AS health_status,
|
|
MAX(latency_ms) AS health_latency_ms
|
|
FROM ip_health_status
|
|
WHERE scope = ${scope} AND ref_id IN (${idList})
|
|
GROUP BY ref_id
|
|
`);
|
|
for (const row of rows) {
|
|
result.set(row.ref_id, parseHealthAggregateRow(row));
|
|
}
|
|
return result;
|
|
}
|
|
function aggregateIpHealthByServiceIds(db, serviceIds) {
|
|
const result = /* @__PURE__ */ new Map();
|
|
if (serviceIds.length === 0) return result;
|
|
const idList = sql2.join(
|
|
serviceIds.map((id) => sql2`${id}`),
|
|
sql2`, `
|
|
);
|
|
const rows = db.all(sql2`
|
|
SELECT sb.service_id AS service_id,
|
|
${WORST_HEALTH_SQL} AS health_status,
|
|
MAX(ihs.latency_ms) AS health_latency_ms
|
|
FROM ip_health_status ihs
|
|
INNER JOIN service_bindings sb
|
|
ON ihs.scope = 'binding' AND ihs.ref_id = sb.id
|
|
WHERE sb.service_id IN (${idList})
|
|
GROUP BY sb.service_id
|
|
`);
|
|
for (const row of rows) {
|
|
result.set(row.service_id, parseHealthAggregateRow(row));
|
|
}
|
|
return result;
|
|
}
|
|
function mergeHealthAggregates(parts) {
|
|
const rank = {
|
|
unknown: 0,
|
|
up: 1,
|
|
degraded: 2,
|
|
down: 3
|
|
};
|
|
let worst = UNKNOWN_HEALTH;
|
|
let hasAny = false;
|
|
for (const part of parts) {
|
|
if (!part) continue;
|
|
hasAny = true;
|
|
if (rank[part.health_status] > rank[worst.health_status]) {
|
|
worst = {
|
|
health_status: part.health_status,
|
|
health_latency_ms: part.health_latency_ms
|
|
};
|
|
} else if (part.health_status === worst.health_status && part.health_latency_ms != null && (worst.health_latency_ms == null || part.health_latency_ms > worst.health_latency_ms)) {
|
|
worst = {
|
|
health_status: worst.health_status,
|
|
health_latency_ms: part.health_latency_ms
|
|
};
|
|
}
|
|
}
|
|
return hasAny ? worst : UNKNOWN_HEALTH;
|
|
}
|
|
function getIpHealthStatusRow(db, scope, refId, ip) {
|
|
const rows = db.all(sql2`
|
|
SELECT scope, ref_id, ip, status, latency_ms, consecutive_failures,
|
|
last_checked_at, last_error
|
|
FROM ip_health_status
|
|
WHERE scope = ${scope} AND ref_id = ${refId} AND ip = ${ip}
|
|
LIMIT 1
|
|
`);
|
|
return rows[0] ?? null;
|
|
}
|
|
function upsertIpHealthStatus(db, scope, refId, ip, status, latencyMs, consecutiveFailures, lastError) {
|
|
db.run(sql2`
|
|
INSERT INTO ip_health_status
|
|
(scope, ref_id, ip, status, latency_ms, consecutive_failures,
|
|
last_checked_at, last_error, created_at, updated_at)
|
|
VALUES (${scope}, ${refId}, ${ip}, ${status}, ${latencyMs}, ${consecutiveFailures},
|
|
datetime('now'), ${lastError}, datetime('now'), datetime('now'))
|
|
ON CONFLICT(scope, ref_id, ip) DO UPDATE SET
|
|
status = excluded.status,
|
|
latency_ms = excluded.latency_ms,
|
|
consecutive_failures = excluded.consecutive_failures,
|
|
last_checked_at = excluded.last_checked_at,
|
|
last_error = excluded.last_error,
|
|
updated_at = datetime('now')
|
|
`);
|
|
}
|
|
function deleteIpHealthStatusForRef(db, scope, refId) {
|
|
db.delete(ipHealthStatus).where(
|
|
and(
|
|
eq2(ipHealthStatus.scope, scope),
|
|
eq2(ipHealthStatus.ref_id, refId)
|
|
)
|
|
).run();
|
|
}
|
|
function deleteIpHealthStatusForIp(db, scope, refId, ip) {
|
|
db.delete(ipHealthStatus).where(
|
|
and(
|
|
eq2(ipHealthStatus.scope, scope),
|
|
eq2(ipHealthStatus.ref_id, refId),
|
|
eq2(ipHealthStatus.ip, ip)
|
|
)
|
|
).run();
|
|
}
|
|
function pruneStaleIpHealthStatus(db, activeTargets) {
|
|
const byRef = /* @__PURE__ */ new Map();
|
|
for (const t of activeTargets) {
|
|
const key = `${t.scope}:${t.ref_id}`;
|
|
let ips = byRef.get(key);
|
|
if (!ips) {
|
|
ips = /* @__PURE__ */ new Set();
|
|
byRef.set(key, ips);
|
|
}
|
|
ips.add(t.ip);
|
|
}
|
|
let deleted = 0;
|
|
for (const [key, ips] of byRef) {
|
|
const sep = key.indexOf(":");
|
|
const scope = key.slice(0, sep);
|
|
const refId = Number(key.slice(sep + 1));
|
|
if (!Number.isFinite(refId)) continue;
|
|
for (const row of listIpHealthStatus(db, scope, refId)) {
|
|
if (!ips.has(row.ip)) {
|
|
deleteIpHealthStatusForIp(db, scope, refId, row.ip);
|
|
deleted += 1;
|
|
}
|
|
}
|
|
}
|
|
return deleted;
|
|
}
|
|
function listHealthCheckTargets(db) {
|
|
const fqdnExpr = sql2`CASE WHEN sb.hostname = '@' OR sb.hostname IS NULL THEN d.zone_name ELSE sb.hostname || '.' || d.zone_name END`;
|
|
const bindingTargets = db.all(sql2`
|
|
SELECT 'binding' AS scope, sb.id AS ref_id, sbi.ip,
|
|
${fqdnExpr} AS hostname,
|
|
sb.health_check_type AS type,
|
|
sb.health_check_port AS port,
|
|
sb.health_check_path AS path,
|
|
sb.health_check_expected_status AS expected_status,
|
|
sb.health_check_timeout_ms AS timeout_ms,
|
|
sb.health_check_verify_tls AS verify_tls
|
|
FROM service_binding_ips sbi
|
|
JOIN service_bindings sb ON sb.id = sbi.binding_id
|
|
JOIN domains d ON d.id = sb.domain_id
|
|
WHERE sb.health_check_enabled = 1
|
|
`);
|
|
const groupTargets = db.all(sql2`
|
|
SELECT 'group' AS scope, sg.id AS ref_id, sip.ip,
|
|
sg.domain AS hostname,
|
|
sg.health_check_type AS type,
|
|
sg.health_check_port AS port,
|
|
sg.health_check_path AS path,
|
|
sg.health_check_expected_status AS expected_status,
|
|
sg.health_check_timeout_ms AS timeout_ms,
|
|
sg.health_check_verify_tls AS verify_tls
|
|
FROM services s
|
|
JOIN service_ips sip ON sip.service_id = s.id
|
|
JOIN service_groups sg ON sg.id = s.service_group_id
|
|
WHERE sg.health_check_enabled = 1
|
|
AND sg.domain IS NOT NULL
|
|
AND s.enabled = 1
|
|
AND (sg.enabled = 1)
|
|
`);
|
|
const groupInheritedBindingTargets = db.all(sql2`
|
|
SELECT 'binding' AS scope, sb.id AS ref_id, sbi.ip,
|
|
${fqdnExpr} AS hostname,
|
|
sg.health_check_type AS type,
|
|
sg.health_check_port AS port,
|
|
sg.health_check_path AS path,
|
|
sg.health_check_expected_status AS expected_status,
|
|
sg.health_check_timeout_ms AS timeout_ms,
|
|
sg.health_check_verify_tls AS verify_tls
|
|
FROM service_binding_ips sbi
|
|
JOIN service_bindings sb ON sb.id = sbi.binding_id
|
|
JOIN domains d ON d.id = sb.domain_id
|
|
JOIN services s ON s.id = sb.service_id
|
|
JOIN service_groups sg ON sg.id = s.service_group_id
|
|
WHERE sg.health_check_enabled = 1
|
|
AND sg.domain IS NOT NULL
|
|
AND s.enabled = 1
|
|
AND sg.enabled = 1
|
|
AND sb.health_check_enabled = 0
|
|
`);
|
|
const cnameBindingTargets = db.all(sql2`
|
|
SELECT 'binding' AS scope, sb.id AS ref_id, sb.cname_target AS ip,
|
|
${fqdnExpr} AS hostname,
|
|
sb.health_check_type AS type,
|
|
sb.health_check_port AS port,
|
|
sb.health_check_path AS path,
|
|
sb.health_check_expected_status AS expected_status,
|
|
sb.health_check_timeout_ms AS timeout_ms,
|
|
sb.health_check_verify_tls AS verify_tls
|
|
FROM service_bindings sb
|
|
JOIN domains d ON d.id = sb.domain_id
|
|
JOIN services s ON s.id = sb.service_id
|
|
WHERE sb.health_check_enabled = 1
|
|
AND sb.cname_target IS NOT NULL
|
|
AND sb.cname_target <> ''
|
|
AND s.enabled = 1
|
|
`);
|
|
const groupInheritedCnameBindingTargets = db.all(sql2`
|
|
SELECT 'binding' AS scope, sb.id AS ref_id, sb.cname_target AS ip,
|
|
${fqdnExpr} AS hostname,
|
|
sg.health_check_type AS type,
|
|
sg.health_check_port AS port,
|
|
sg.health_check_path AS path,
|
|
sg.health_check_expected_status AS expected_status,
|
|
sg.health_check_timeout_ms AS timeout_ms,
|
|
sg.health_check_verify_tls AS verify_tls
|
|
FROM service_bindings sb
|
|
JOIN domains d ON d.id = sb.domain_id
|
|
JOIN services s ON s.id = sb.service_id
|
|
JOIN service_groups sg ON sg.id = s.service_group_id
|
|
WHERE sg.health_check_enabled = 1
|
|
AND sg.domain IS NOT NULL
|
|
AND s.enabled = 1
|
|
AND sg.enabled = 1
|
|
AND sb.health_check_enabled = 0
|
|
AND sb.cname_target IS NOT NULL
|
|
AND sb.cname_target <> ''
|
|
`);
|
|
return [
|
|
...bindingTargets,
|
|
...groupTargets,
|
|
...groupInheritedBindingTargets,
|
|
...cnameBindingTargets,
|
|
...groupInheritedCnameBindingTargets
|
|
].map((t) => ({
|
|
...t,
|
|
verify_tls: Boolean(t.verify_tls)
|
|
}));
|
|
}
|
|
function listDomainTags(db, domainId) {
|
|
return db.select({ tag: domainTags.tag }).from(domainTags).where(eq2(domainTags.domain_id, domainId)).all().map((r) => r.tag);
|
|
}
|
|
function setDomainTags(db, domainId, tags) {
|
|
db.delete(domainTags).where(eq2(domainTags.domain_id, domainId)).run();
|
|
const unique = [...new Set(tags.map((t) => t.trim()).filter(Boolean))];
|
|
for (const tag of unique) {
|
|
db.insert(domainTags).values({ domain_id: domainId, tag }).run();
|
|
}
|
|
}
|
|
function addDomainTags(db, domainId, tags) {
|
|
const unique = [...new Set(tags.map((t) => t.trim()).filter(Boolean))];
|
|
for (const tag of unique) {
|
|
db.run(sql2`
|
|
INSERT INTO domain_tags (domain_id, tag)
|
|
VALUES (${domainId}, ${tag})
|
|
ON CONFLICT(domain_id, tag) DO NOTHING
|
|
`);
|
|
}
|
|
}
|
|
function listDomainMonitors(db, domainId) {
|
|
return db.select().from(domainMonitors).where(eq2(domainMonitors.domain_id, domainId)).orderBy(asc(domainMonitors.id)).all();
|
|
}
|
|
function listEnabledDomainMonitors(db) {
|
|
return db.select().from(domainMonitors).where(eq2(domainMonitors.enabled, true)).all();
|
|
}
|
|
function getDomainMonitor(db, id) {
|
|
const row = db.select().from(domainMonitors).where(eq2(domainMonitors.id, id)).get();
|
|
if (!row) throw new NotFoundError(`domain_monitor ${id}`);
|
|
return row;
|
|
}
|
|
function createDomainMonitor(db, domainId, input) {
|
|
const id = db.insert(domainMonitors).values({
|
|
domain_id: domainId,
|
|
hostname: input.hostname.trim(),
|
|
type: input.type,
|
|
enabled: input.enabled ?? true,
|
|
interval_sec: input.interval_sec ?? 60,
|
|
timeout_ms: input.timeout_ms ?? 5e3,
|
|
path: input.path ?? null,
|
|
expected_status: input.expected_status ?? null
|
|
}).returning({ id: domainMonitors.id }).get().id;
|
|
return getDomainMonitor(db, id);
|
|
}
|
|
function deleteDomainMonitor(db, id) {
|
|
const result = db.delete(domainMonitors).where(eq2(domainMonitors.id, id)).run();
|
|
if (result.changes === 0) throw new NotFoundError(`domain_monitor ${id}`);
|
|
}
|
|
function updateDomainMonitorResult(db, monitorId, status, latencyMs, error) {
|
|
db.update(domainMonitors).set({
|
|
last_status: status,
|
|
last_latency_ms: latencyMs,
|
|
last_checked_at: sql2`datetime('now')`,
|
|
last_error: error,
|
|
updated_at: sql2`datetime('now')`
|
|
}).where(eq2(domainMonitors.id, monitorId)).run();
|
|
db.insert(domainMonitorResults).values({
|
|
monitor_id: monitorId,
|
|
status,
|
|
latency_ms: latencyMs,
|
|
error
|
|
}).run();
|
|
db.run(sql2`
|
|
DELETE FROM domain_monitor_results
|
|
WHERE monitor_id = ${monitorId}
|
|
AND id NOT IN (
|
|
SELECT id FROM domain_monitor_results
|
|
WHERE monitor_id = ${monitorId}
|
|
ORDER BY checked_at DESC, id DESC
|
|
LIMIT 100
|
|
)
|
|
`);
|
|
}
|
|
function listDomainMonitorResults(db, monitorId, limit = 50) {
|
|
return db.all(sql2`
|
|
SELECT id, monitor_id, status, latency_ms, error, checked_at
|
|
FROM domain_monitor_results
|
|
WHERE monitor_id = ${monitorId}
|
|
ORDER BY checked_at DESC, id DESC
|
|
LIMIT ${limit}
|
|
`);
|
|
}
|
|
function listDomainMonitorResultsForDomain(db, domainId, limit = 50) {
|
|
return db.all(sql2`
|
|
SELECT r.id, r.monitor_id, m.hostname, m.type, r.status, r.latency_ms, r.error, r.checked_at
|
|
FROM domain_monitor_results r
|
|
JOIN domain_monitors m ON m.id = r.monitor_id
|
|
WHERE m.domain_id = ${domainId}
|
|
ORDER BY r.checked_at DESC, r.id DESC
|
|
LIMIT ${limit}
|
|
`);
|
|
}
|
|
function insertNotificationLog(db, kind, refType, refId, title, message) {
|
|
db.insert(notificationLog).values({
|
|
kind,
|
|
ref_type: refType,
|
|
ref_id: refId,
|
|
title,
|
|
message
|
|
}).run();
|
|
}
|
|
function listNotificationLog(db, limit = 50) {
|
|
return db.all(sql2`
|
|
SELECT id, kind, ref_type, ref_id, title, message, created_at
|
|
FROM notification_log
|
|
ORDER BY created_at DESC, id DESC
|
|
LIMIT ${limit}
|
|
`);
|
|
}
|
|
export {
|
|
ConflictError,
|
|
NotFoundError,
|
|
appSettings,
|
|
certificates,
|
|
createDb,
|
|
createMemoryDb,
|
|
dnsRecords,
|
|
domainMonitorResults,
|
|
domainMonitors,
|
|
domainTags,
|
|
domains,
|
|
getAppSettings,
|
|
getAppSettingsSecrets,
|
|
groups,
|
|
healthCheck,
|
|
ipHealthStatus,
|
|
notificationLog,
|
|
repos_exports as repos,
|
|
resolveDatabasePath,
|
|
runMigrations,
|
|
schema,
|
|
serviceBindingIps,
|
|
serviceBindingRecords,
|
|
serviceBindings,
|
|
serviceGroupDnsRecords,
|
|
serviceGroups,
|
|
serviceIps,
|
|
services,
|
|
subdomains,
|
|
syncJobs,
|
|
touchVpsTrackerSync,
|
|
updateAppSettings
|
|
};
|