Files
cloudflare-domain-manager/packages/db/dist/index.js
T
DenozordecandCursor 1f710bbe8b
Build, Test, and Push CFDM Docker Image / test (push) Successful in 10m17s
Build, Test, and Push CFDM Docker Image / build-and-push (push) Successful in 1m48s
Build, Test, and Push CFDM Docker Image / update-wiki (push) Successful in 6s
Build, Test, and Push CFDM Docker Image / create-release (push) Has been skipped
feat(api, web): integrate health status management for services and domains
- Added health status and latency fields to service and domain schemas, enhancing monitoring capabilities.
- Implemented health status aggregation for services in the API, allowing for improved health checks and reporting.
- Updated web components to display health status using HealthCheckBadge, improving user visibility of service health.
- Refactored service and domain management components to incorporate health status in various views, enhancing overall functionality.

Co-authored-by: Cursor <[email protected]>
2026-07-17 02:55:49 +07:00

1684 lines
63 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),
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),
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"),
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"),
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";
import { appSwitcherConfigSchema } from "@cfdm/shared";
var SETTINGS_ID = "settings-main";
var DEFAULT_APP_SWITCHER = {
menuLabel: "\u041F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u044F",
apps: [
{
id: "vps-tracker",
name: "VPS Tracker",
subtitle: "\u0423\u0447\u0451\u0442 \u0432\u0438\u0440\u0442\u0443\u0430\u043B\u044C\u043D\u044B\u0445 \u0441\u0435\u0440\u0432\u0435\u0440\u043E\u0432",
url: "http://192.168.100.67:3001",
icon: "server",
shortcut: "\u23181"
},
{
id: "cfdm",
name: "CF Domain Manager",
subtitle: "\u0423\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u0438\u0435 \u0434\u043E\u043C\u0435\u043D\u0430\u043C\u0438",
url: "http://192.168.100.67:6363",
icon: "cloud",
shortcut: "\u23182"
}
]
};
function parseAppSwitcher(raw) {
if (!raw?.trim()) return DEFAULT_APP_SWITCHER;
try {
return appSwitcherConfigSchema.parse(JSON.parse(raw));
} catch {
return DEFAULT_APP_SWITCHER;
}
}
function toDto(row) {
return {
id: row.id,
appSwitcher: parseAppSwitcher(row.app_switcher_json),
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
};
}
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: patch.appSwitcher !== void 0 ? JSON.stringify(patch.appSwitcher) : current.app_switcher_json,
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,
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();
}
function getAppSwitcher(db) {
return getAppSettings(db).appSwitcher;
}
// 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,
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,
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
}).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;
}
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;
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.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 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
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
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
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
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
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
];
}
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,
getAppSwitcher,
groups,
healthCheck,
ipHealthStatus,
notificationLog,
repos_exports as repos,
resolveDatabasePath,
runMigrations,
schema,
serviceBindingIps,
serviceBindingRecords,
serviceBindings,
serviceGroupDnsRecords,
serviceGroups,
serviceIps,
services,
subdomains,
syncJobs,
touchVpsTrackerSync,
updateAppSettings
};