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), 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), 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"), 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(), 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("@"), dns_record_id: integer("dns_record_id").references(() => dnsRecords.id, { onDelete: "set null" }), 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() }, (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 schema = { groups, services, serviceGroups, domains, subdomains, dnsRecords, serviceBindings, serviceIps, serviceBindingRecords, serviceBindingIps, serviceGroupDnsRecords, certificates, syncJobs }; // 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/repos.ts var repos_exports = {}; __export(repos_exports, { bindingsToRemove: () => bindingsToRemove, countCertificatesByStatus: () => countCertificatesByStatus, createDomain: () => createDomain, createGroup: () => createGroup, createService: () => createService, createServiceGroup: () => createServiceGroup, createSubdomain: () => createSubdomain, createSyncJob: () => createSyncJob, deleteBinding: () => deleteBinding, deleteBindingsExcept: () => deleteBindingsExcept, deleteDnsRecord: () => deleteDnsRecord, deleteDomain: () => deleteDomain, deleteGroup: () => deleteGroup, deleteService: () => deleteService, deleteServiceGroup: () => deleteServiceGroup, deleteSubdomain: () => deleteSubdomain, findBinding: () => findBinding, findDnsByCfId: () => findDnsByCfId, findDomainByZoneName: () => findDomainByZoneName, finishSyncJob: () => finishSyncJob, getBinding: () => getBinding, getBindingView: () => getBindingView, getCertificate: () => getCertificate, getDnsRecord: () => getDnsRecord, getDomain: () => getDomain, getGroup: () => getGroup, getGroupWithStats: () => getGroupWithStats, getService: () => getService, getServiceGroup: () => getServiceGroup, getSubdomain: () => getSubdomain, getSyncJob: () => getSyncJob, insertBinding: () => insertBinding, insertDnsRecord: () => insertDnsRecord, linkBindingRecord: () => linkBindingRecord, linkGroupDnsRecord: () => linkGroupDnsRecord, listAllBindings: () => listAllBindings, listAllDomains: () => listAllDomains, listAllSubdomains: () => listAllSubdomains, listBindingIps: () => listBindingIps, listBindingsByDomain: () => listBindingsByDomain, listBindingsByService: () => listBindingsByService, listCertificates: () => listCertificates, listDnsByDomain: () => listDnsByDomain, listDnsRecords: () => listDnsRecords, listDomains: () => listDomains, listDomainsEnriched: () => listDomainsEnriched, listGroupDnsRecords: () => listGroupDnsRecords, listGroups: () => listGroups, listRecordsForBinding: () => listRecordsForBinding, listServiceGroups: () => listServiceGroups, listServiceIps: () => listServiceIps, listServices: () => listServices, listServicesByGroup: () => listServicesByGroup, listSubdomainsByDomain: () => listSubdomainsByDomain, listUngroupedServices: () => listUngroupedServices, markDnsPendingDelete: () => markDnsPendingDelete, replaceBindingIps: () => replaceBindingIps, replaceServiceIps: () => replaceServiceIps, setBindingDnsRecordId: () => setBindingDnsRecordId, setDnsSyncStatus: () => setDnsSyncStatus, setDomainLastSynced: () => setDomainLastSynced, setServiceEnabled: () => setServiceEnabled, setServiceGroup: () => setServiceGroup, setServiceGroupEnabled: () => setServiceGroupEnabled, unlinkBindingRecord: () => unlinkBindingRecord, unlinkGroupDnsRecord: () => unlinkGroupDnsRecord, updateBindingFields: () => updateBindingFields, updateDnsFields: () => updateDnsFields, updateDomain: () => updateDomain, updateGroup: () => updateGroup, updateService: () => updateService, updateServiceGroup: () => updateServiceGroup, updateSubdomain: () => updateSubdomain, upsertCertificateCheck: () => upsertCertificateCheck, upsertSubdomain: () => upsertSubdomain }); import { and, asc, count, eq, isNull, like, 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(eq(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(eq(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(eq(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(eq(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``; return 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 FROM domains d LEFT JOIN groups g ON g.id = d.group_id ${base} ORDER BY d.zone_name ASC `); } 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(eq(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, groupId, status) { const result = db.update(domains).set({ group_id: groupId, status, updated_at: sql2`datetime('now')` }).where(eq(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(eq(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(eq(domains.id, id)).run(); } function listAllDomains(db) { return listDomains(db); } function listSubdomainsByDomain(db, domainId) { return db.select().from(subdomains).where(eq(subdomains.domain_id, domainId)).orderBy(asc(subdomains.name)).all(); } function getSubdomain(db, id) { const row = db.select().from(subdomains).where(eq(subdomains.id, id)).get(); if (!row) throw new NotFoundError(`subdomain ${id}`); return row; } 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, name, fqdn) { const result = db.update(subdomains).set({ name, fqdn, updated_at: sql2`datetime('now')` }).where(eq(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(eq(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 = [eq(dnsRecords.domain_id, domainId)]; if (filter.record_type) { conditions.push(eq(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(eq(dnsRecords.proxied, filter.proxied)); } if (filter.sync_status) { conditions.push(eq(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(eq(dnsRecords.id, id), eq(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(eq(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(eq(dnsRecords.id, id)).run(); } function deleteDnsRecord(db, id) { db.delete(dnsRecords).where(eq(dnsRecords.id, id)).run(); } function listDnsByDomain(db, domainId) { return db.select().from(dnsRecords).where(eq(dnsRecords.domain_id, domainId)).all().map(mapDnsRecord); } function findDnsByCfId(db, domainId, cfRecordId) { const row = db.select().from(dnsRecords).where( and( eq(dnsRecords.domain_id, domainId), eq(dnsRecords.cf_record_id, cfRecordId) ) ).get(); return row ? mapDnsRecord(row) : null; } function markDnsPendingDelete(db, id) { setDnsSyncStatus(db, id, "pending_delete", null, null); } function listServices(db) { return db.select().from(services).orderBy(asc(services.name)).all(); } function listServicesByGroup(db, groupId) { return db.select().from(services).where(eq(services.service_group_id, groupId)).orderBy(asc(services.name)).all(); } function listUngroupedServices(db) { return db.select().from(services).where(isNull(services.service_group_id)).orderBy(asc(services.name)).all(); } function getService(db, id) { const row = db.select().from(services).where(eq(services.id, id)).get(); if (!row) throw new NotFoundError(`service ${id}`); return row; } function createService(db, name, slug) { const id = db.insert(services).values({ name, slug, subdomain: slug }).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(eq(services.id, id)).run(); return getService(db, id); } function setServiceEnabled(db, id, enabled) { db.update(services).set({ enabled, updated_at: sql2`datetime('now')` }).where(eq(services.id, id)).run(); return getService(db, id); } function setServiceGroup(db, id, groupId) { db.update(services).set({ service_group_id: groupId, updated_at: sql2`datetime('now')` }).where(eq(services.id, id)).run(); } function deleteService(db, id) { const result = db.delete(services).where(eq(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, 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(eq(serviceGroups.id, id)).get(); if (!row) throw new NotFoundError(`service group ${id}`); return mapServiceGroup(row); } function createServiceGroup(db, name, groupType, icon, domain) { const id = db.insert(serviceGroups).values({ name, type: groupType, icon, domain }).returning({ id: serviceGroups.id }).get().id; return getServiceGroup(db, id); } function updateServiceGroup(db, id, name, groupType, icon, domain) { const result = db.update(serviceGroups).set({ name, type: groupType, icon, domain, updated_at: sql2`datetime('now')` }).where(eq(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(eq(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(eq(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(eq(serviceIps.service_id, serviceId)).all().map((r) => r.ip); } function replaceServiceIps(db, serviceId, ips) { db.delete(serviceIps).where(eq(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(eq(serviceBindingIps.binding_id, bindingId)).all().map((r) => r.ip); } function replaceBindingIps(db, bindingId, ips) { db.delete(serviceBindingIps).where(eq(serviceBindingIps.binding_id, bindingId)).run(); for (const ip of ips) { db.insert(serviceBindingIps).values({ binding_id: bindingId, ip }).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( eq(serviceBindingRecords.binding_id, bindingId), eq(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( eq(serviceGroupDnsRecords.group_id, groupId), eq(serviceGroupDnsRecords.dns_record_id, dnsRecordId) ) ).run(); } function listAllBindings(db) { return db.all(sql2` SELECT sb.id, sb.domain_id, sb.service_id, sb.hostname, sb.dns_record_id, 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 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 `); } function listBindingsByDomain(db, domainId) { return db.all(sql2` SELECT sb.id, sb.domain_id, sb.service_id, sb.hostname, sb.dns_record_id, 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 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 `); } 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(eq(serviceBindings.id, id)).get(); if (!row) throw new NotFoundError(`service binding ${id}`); return row; } function getBindingView(db, id) { const rows = db.all(sql2` SELECT sb.id, sb.domain_id, sb.service_id, sb.hostname, sb.dns_record_id, 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 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 rows[0]; } function findBinding(db, serviceId, domainId, hostname) { const row = db.select().from(serviceBindings).where( and( eq(serviceBindings.service_id, serviceId), eq(serviceBindings.domain_id, domainId), eq(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(eq(serviceBindings.id, id)).run(); } function setBindingDnsRecordId(db, bindingId, dnsRecordId) { db.update(serviceBindings).set({ dns_record_id: dnsRecordId, updated_at: sql2`datetime('now')` }).where(eq(serviceBindings.id, bindingId)).run(); } function bindingsToRemove(db, serviceId, keepIds) { const all = db.select().from(serviceBindings).where(eq(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(eq(serviceBindings.service_id, serviceId)).all(); for (const binding of all) { if (!keepIds.includes(binding.id)) { db.delete(serviceBindings).where(eq(serviceBindings.id, binding.id)).run(); } } } function deleteBinding(db, id) { const result = db.delete(serviceBindings).where(eq(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(eq(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(eq(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(eq(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(eq(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 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(eq(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(eq(syncJobs.id, id)).run(); } export { ConflictError, NotFoundError, certificates, createDb, createMemoryDb, dnsRecords, domains, groups, healthCheck, repos_exports as repos, resolveDatabasePath, runMigrations, schema, serviceBindingIps, serviceBindingRecords, serviceBindings, serviceGroupDnsRecords, serviceGroups, serviceIps, services, subdomains, syncJobs };