// src/schema.ts import { sql } from "drizzle-orm"; import { integer, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core"; var appSettings = sqliteTable("app_settings", { id: text("id").primaryKey(), show_quick_actions: integer("show_quick_actions", { mode: "boolean" }).notNull().default(true), default_ttl: integer("default_ttl").notNull().default(300), naming_template: text("naming_template").notNull().default("{loc}-{role}{nn}.{zone}"), proxied_lock: integer("proxied_lock", { 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 zones = sqliteTable("zones", { id: text("id").primaryKey(), cf_zone_id: text("cf_zone_id"), name: text("name").notNull().unique(), role: text("role").notNull().default("routing"), naming_template: text("naming_template").notNull().default("{loc}-{role}{nn}.{zone}"), default_ttl: integer("default_ttl").notNull().default(300), last_sync_at: text("last_sync_at"), created_at: text("created_at").notNull().default(sql`datetime('now')`), updated_at: text("updated_at").notNull().default(sql`datetime('now')`) }); var locations = sqliteTable("locations", { id: text("id").primaryKey(), code: text("code").notNull().unique(), name: text("name").notNull(), country: text("country"), sort_order: integer("sort_order").notNull().default(0), created_at: text("created_at").notNull().default(sql`datetime('now')`) }); var nodes = sqliteTable( "nodes", { id: text("id").primaryKey(), zone_id: text("zone_id").notNull().references(() => zones.id, { onDelete: "cascade" }), location_id: text("location_id").notNull().references(() => locations.id), hostname: text("hostname").notNull(), role: text("role").notNull(), index_num: integer("index_num").notNull().default(1), provider_tag: text("provider_tag"), notes: text("notes"), sync_status: text("sync_status").notNull().default("pending"), cf_a_record_id: text("cf_a_record_id"), cf_aaaa_record_id: text("cf_aaaa_record_id"), 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) => [uniqueIndex("nodes_zone_hostname").on(t.zone_id, t.hostname)] ); var nodeAddresses = sqliteTable( "node_addresses", { id: text("id").primaryKey(), node_id: text("node_id").notNull().references(() => nodes.id, { onDelete: "cascade" }), family: text("family").notNull(), ip: text("ip").notNull() }, (t) => [uniqueIndex("node_addresses_node_family").on(t.node_id, t.family)] ); var aliases = sqliteTable( "aliases", { id: text("id").primaryKey(), zone_id: text("zone_id").notNull().references(() => zones.id, { onDelete: "cascade" }), name: text("name").notNull(), purpose: text("purpose").notNull().default("geo"), mode: text("mode").notNull().default("primary"), target_node_id: text("target_node_id").notNull().references(() => nodes.id, { onDelete: "restrict" }), sync_status: text("sync_status").notNull().default("pending"), cf_record_id: text("cf_record_id"), 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) => [uniqueIndex("aliases_zone_name").on(t.zone_id, t.name)] ); var syncJobs = sqliteTable("sync_jobs", { id: text("id").primaryKey(), zone_id: text("zone_id").notNull().references(() => zones.id, { onDelete: "cascade" }), status: text("status").notNull().default("pending"), diff_json: text("diff_json"), error: text("error"), created_at: text("created_at").notNull().default(sql`datetime('now')`), finished_at: text("finished_at") }); var syncEvents = sqliteTable("sync_events", { id: text("id").primaryKey(), job_id: text("job_id").notNull().references(() => syncJobs.id, { onDelete: "cascade" }), kind: text("kind").notNull(), record_name: text("record_name"), record_type: text("record_type"), detail: text("detail"), created_at: text("created_at").notNull().default(sql`datetime('now')`) }); var ignoredOrphans = sqliteTable( "ignored_orphans", { id: text("id").primaryKey(), zone_id: text("zone_id").notNull().references(() => zones.id, { onDelete: "cascade" }), record_name: text("record_name").notNull(), record_type: text("record_type").notNull(), created_at: text("created_at").notNull().default(sql`datetime('now')`) }, (t) => [ uniqueIndex("ignored_orphans_unique").on( t.zone_id, t.record_name, t.record_type ) ] ); var schema = { appSettings, zones, locations, nodes, nodeAddresses, aliases, syncJobs, syncEvents, ignoredOrphans }; // 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, showQuickActions: row.show_quick_actions == null ? true : Boolean(row.show_quick_actions), defaultTtl: row.default_ttl ?? 300, namingTemplate: row.naming_template ?? "{loc}-{role}{nn}.{zone}", proxiedLock: row.proxied_lock == null ? true : Boolean(row.proxied_lock) }; } function ensureRow(db) { const existing = db.select().from(appSettings).where(eq(appSettings.id, SETTINGS_ID)).get(); if (existing) return existing; db.insert(appSettings).values({ id: SETTINGS_ID, show_quick_actions: true, default_ttl: 300, naming_template: "{loc}-{role}{nn}.{zone}", proxied_lock: true }).run(); return db.select().from(appSettings).where(eq(appSettings.id, SETTINGS_ID)).get(); } function getAppSettings(db) { return toDto(ensureRow(db)); } function updateAppSettings(db, patch) { ensureRow(db); const updates = { updated_at: (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19) }; if (patch.showQuickActions !== void 0) { updates.show_quick_actions = patch.showQuickActions; } if (patch.defaultTtl !== void 0) { updates.default_ttl = patch.defaultTtl; } if (patch.namingTemplate !== void 0) { updates.naming_template = patch.namingTemplate; } if (patch.proxiedLock !== void 0) { updates.proxied_lock = patch.proxiedLock; } db.update(appSettings).set(updates).where(eq(appSettings.id, SETTINGS_ID)).run(); return getAppSettings(db); } // src/fleet-repo.ts import { randomUUID } from "crypto"; import { and, asc, count, eq as eq2, sql as sql2 } from "drizzle-orm"; import { catalogCitiesWithLocCodes } from "@cdnmanager/shared"; function now() { return (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19); } function id(prefix) { return `${prefix}-${randomUUID().slice(0, 8)}`; } function mapLocation(row) { return { id: row.id, code: row.code, name: row.name, country: row.country, sortOrder: row.sort_order }; } function mapZone(row) { return { id: row.id, cfZoneId: row.cf_zone_id, name: row.name, role: row.role, namingTemplate: row.naming_template, defaultTtl: row.default_ttl, lastSyncAt: row.last_sync_at, createdAt: row.created_at, updatedAt: row.updated_at }; } function listLocations(db) { return db.select().from(locations).orderBy(asc(locations.sort_order), asc(locations.code)).all().map(mapLocation); } function getLocation(db, locationId) { const row = db.select().from(locations).where(eq2(locations.id, locationId)).get(); if (!row) throw new NotFoundError(`location ${locationId}`); return mapLocation(row); } function ensureCatalogLocations(db) { const existing = db.select().from(locations).all(); const byCode = new Map(existing.map((r) => [r.code.toLowerCase(), r])); const byNameCountry = new Map( existing.map((r) => [ `${(r.name || "").trim().toLowerCase()}|${(r.country || "").toUpperCase()}`, r ]) ); let inserted = 0; let sortOrder = 100; for (const city of catalogCitiesWithLocCodes()) { const nameKey = `${city.name.trim().toLowerCase()}|${city.countryCode}`; if (byNameCountry.has(nameKey)) continue; if (byCode.has(city.locCode.toLowerCase())) { const alt = `${city.locCode}${city.countryCode.toLowerCase()}`; if (byCode.has(alt) || byNameCountry.has(nameKey)) continue; db.insert(locations).values({ id: `loc-${alt}`, code: alt, name: city.name, country: city.countryCode, sort_order: sortOrder++ }).run(); byCode.set(alt, { id: `loc-${alt}` }); byNameCountry.set(nameKey, { id: `loc-${alt}` }); inserted += 1; continue; } db.insert(locations).values({ id: `loc-${city.locCode}`, code: city.locCode, name: city.name, country: city.countryCode, sort_order: sortOrder++ }).run(); byCode.set(city.locCode.toLowerCase(), { id: `loc-${city.locCode}` }); byNameCountry.set(nameKey, { id: `loc-${city.locCode}` }); inserted += 1; } return inserted; } function findLocationByCity(db, cityName, countryCode) { const q = cityName.trim().toLowerCase(); if (!q) return null; const rows = listLocations(db); const match = rows.find((l) => { if (l.name.trim().toLowerCase() !== q) return false; if (countryCode && l.country && l.country.toUpperCase() !== countryCode.toUpperCase()) { return false; } return true; }); return match ?? null; } function listZones(db) { return db.select().from(zones).orderBy(asc(zones.name)).all().map(mapZone); } function getZone(db, zoneId) { const row = db.select().from(zones).where(eq2(zones.id, zoneId)).get(); if (!row) throw new NotFoundError(`zone ${zoneId}`); return mapZone(row); } function createZone(db, input) { const existing = db.select().from(zones).where(eq2(zones.name, input.name)).get(); if (existing) throw new ConflictError(`zone ${input.name} already exists`); const zoneId = id("zone"); const ts = now(); db.insert(zones).values({ id: zoneId, name: input.name, cf_zone_id: input.cfZoneId ?? null, role: input.role ?? "routing", naming_template: input.namingTemplate ?? "{loc}-{role}{nn}.{zone}", default_ttl: input.defaultTtl ?? 300, created_at: ts, updated_at: ts }).run(); return getZone(db, zoneId); } function updateZone(db, zoneId, patch) { getZone(db, zoneId); const updates = { updated_at: now() }; if (patch.name !== void 0) updates.name = patch.name; if (patch.cfZoneId !== void 0) updates.cf_zone_id = patch.cfZoneId; if (patch.role !== void 0) updates.role = patch.role; if (patch.namingTemplate !== void 0) updates.naming_template = patch.namingTemplate; if (patch.defaultTtl !== void 0) updates.default_ttl = patch.defaultTtl; if (patch.lastSyncAt !== void 0) updates.last_sync_at = patch.lastSyncAt; db.update(zones).set(updates).where(eq2(zones.id, zoneId)).run(); return getZone(db, zoneId); } function deleteZone(db, zoneId) { getZone(db, zoneId); db.delete(zones).where(eq2(zones.id, zoneId)).run(); } function loadAddresses(db, nodeId) { return db.select().from(nodeAddresses).where(eq2(nodeAddresses.node_id, nodeId)).all().map((r) => ({ id: r.id, family: r.family, ip: r.ip })); } function mapNode(db, row, loc) { const aliasCount = db.select({ c: count() }).from(aliases).where(eq2(aliases.target_node_id, row.id)).get()?.c; return { id: row.id, zoneId: row.zone_id, locationId: row.location_id, locationCode: loc?.code, locationName: loc?.name, hostname: row.hostname, role: row.role, indexNum: row.index_num, providerTag: row.provider_tag, notes: row.notes, syncStatus: row.sync_status, cfARecordId: row.cf_a_record_id, cfAaaaRecordId: row.cf_aaaa_record_id, lastError: row.last_error, addresses: loadAddresses(db, row.id), aliasCount: Number(aliasCount ?? 0), createdAt: row.created_at, updatedAt: row.updated_at }; } function listNodes(db, filter = {}) { let rows = db.select().from(nodes).all(); if (filter.zoneId) rows = rows.filter((r) => r.zone_id === filter.zoneId); if (filter.locationId) rows = rows.filter((r) => r.location_id === filter.locationId); if (filter.role) rows = rows.filter((r) => r.role === filter.role); if (filter.syncStatus) rows = rows.filter((r) => r.sync_status === filter.syncStatus); if (filter.q) { const q = filter.q.toLowerCase(); rows = rows.filter( (r) => r.hostname.toLowerCase().includes(q) || (r.provider_tag ?? "").toLowerCase().includes(q) ); } const locMap = new Map( listLocations(db).map((l) => [l.id, { code: l.code, name: l.name }]) ); return rows.map((r) => mapNode(db, r, locMap.get(r.location_id))).sort((a, b) => a.hostname.localeCompare(b.hostname)); } function getNode(db, nodeId) { const row = db.select().from(nodes).where(eq2(nodes.id, nodeId)).get(); if (!row) throw new NotFoundError(`node ${nodeId}`); const loc = getLocation(db, row.location_id); return mapNode(db, row, { code: loc.code, name: loc.name }); } function setAddresses(db, nodeId, ipv4, ipv6) { db.delete(nodeAddresses).where(eq2(nodeAddresses.node_id, nodeId)).run(); db.insert(nodeAddresses).values({ id: id("addr"), node_id: nodeId, family: "v4", ip: ipv4 }).run(); if (ipv6) { db.insert(nodeAddresses).values({ id: id("addr"), node_id: nodeId, family: "v6", ip: ipv6 }).run(); } } function createNode(db, input) { getZone(db, input.zoneId); getLocation(db, input.locationId); const dup = db.select().from(nodes).where( and(eq2(nodes.zone_id, input.zoneId), eq2(nodes.hostname, input.hostname)) ).get(); if (dup) throw new ConflictError(`node ${input.hostname} already exists`); const nodeId = id("node"); const ts = now(); db.insert(nodes).values({ id: nodeId, zone_id: input.zoneId, location_id: input.locationId, hostname: input.hostname, role: input.role, index_num: input.indexNum, provider_tag: input.providerTag ?? null, notes: input.notes ?? null, sync_status: "pending", created_at: ts, updated_at: ts }).run(); setAddresses(db, nodeId, input.ipv4, input.ipv6); return getNode(db, nodeId); } function updateNode(db, nodeId, patch) { const current = getNode(db, nodeId); if (patch.locationId) getLocation(db, patch.locationId); const updates = { updated_at: now() }; if (patch.locationId !== void 0) updates.location_id = patch.locationId; if (patch.hostname !== void 0) updates.hostname = patch.hostname; if (patch.role !== void 0) updates.role = patch.role; if (patch.indexNum !== void 0) updates.index_num = patch.indexNum; if (patch.providerTag !== void 0) updates.provider_tag = patch.providerTag; if (patch.notes !== void 0) updates.notes = patch.notes; if (patch.syncStatus !== void 0) updates.sync_status = patch.syncStatus; if (patch.cfARecordId !== void 0) updates.cf_a_record_id = patch.cfARecordId; if (patch.cfAaaaRecordId !== void 0) updates.cf_aaaa_record_id = patch.cfAaaaRecordId; if (patch.lastError !== void 0) updates.last_error = patch.lastError; db.update(nodes).set(updates).where(eq2(nodes.id, nodeId)).run(); if (patch.ipv4 !== void 0) { const v6 = patch.ipv6 !== void 0 ? patch.ipv6 : current.addresses.find((a) => a.family === "v6")?.ip ?? null; setAddresses(db, nodeId, patch.ipv4, v6); } else if (patch.ipv6 !== void 0) { const v4 = current.addresses.find((a) => a.family === "v4")?.ip; if (!v4) throw new ConflictError("node has no IPv4"); setAddresses(db, nodeId, v4, patch.ipv6); } return getNode(db, nodeId); } function deleteNode(db, nodeId) { getNode(db, nodeId); const linked = db.select({ c: count() }).from(aliases).where(eq2(aliases.target_node_id, nodeId)).get()?.c; if (Number(linked ?? 0) > 0) { throw new ConflictError("node has aliases; retarget or delete them first"); } db.delete(nodes).where(eq2(nodes.id, nodeId)).run(); } function mapAlias(row, targetHostname) { return { id: row.id, zoneId: row.zone_id, name: row.name, purpose: row.purpose, mode: row.mode, targetNodeId: row.target_node_id, targetHostname, syncStatus: row.sync_status, cfRecordId: row.cf_record_id, lastError: row.last_error, createdAt: row.created_at, updatedAt: row.updated_at }; } function listAliases(db, filter = {}) { let rows = db.select().from(aliases).all(); if (filter.zoneId) rows = rows.filter((r) => r.zone_id === filter.zoneId); if (filter.purpose) rows = rows.filter((r) => r.purpose === filter.purpose); if (filter.syncStatus) rows = rows.filter((r) => r.sync_status === filter.syncStatus); if (filter.q) { const q = filter.q.toLowerCase(); rows = rows.filter((r) => r.name.toLowerCase().includes(q)); } const nodeHost = new Map( db.select().from(nodes).all().map((n) => [n.id, n.hostname]) ); return rows.map((r) => mapAlias(r, nodeHost.get(r.target_node_id))).sort((a, b) => a.name.localeCompare(b.name)); } function getAlias(db, aliasId) { const row = db.select().from(aliases).where(eq2(aliases.id, aliasId)).get(); if (!row) throw new NotFoundError(`alias ${aliasId}`); const target = db.select().from(nodes).where(eq2(nodes.id, row.target_node_id)).get(); return mapAlias(row, target?.hostname); } function createAlias(db, input) { getZone(db, input.zoneId); getNode(db, input.targetNodeId); const dup = db.select().from(aliases).where(and(eq2(aliases.zone_id, input.zoneId), eq2(aliases.name, input.name))).get(); if (dup) throw new ConflictError(`alias ${input.name} already exists`); const aliasId = id("alias"); const ts = now(); db.insert(aliases).values({ id: aliasId, zone_id: input.zoneId, name: input.name, purpose: input.purpose, mode: input.mode, target_node_id: input.targetNodeId, sync_status: "pending", created_at: ts, updated_at: ts }).run(); return getAlias(db, aliasId); } function updateAlias(db, aliasId, patch) { getAlias(db, aliasId); if (patch.targetNodeId) getNode(db, patch.targetNodeId); const updates = { updated_at: now() }; if (patch.name !== void 0) updates.name = patch.name; if (patch.purpose !== void 0) updates.purpose = patch.purpose; if (patch.mode !== void 0) updates.mode = patch.mode; if (patch.targetNodeId !== void 0) updates.target_node_id = patch.targetNodeId; if (patch.syncStatus !== void 0) updates.sync_status = patch.syncStatus; if (patch.cfRecordId !== void 0) updates.cf_record_id = patch.cfRecordId; if (patch.lastError !== void 0) updates.last_error = patch.lastError; db.update(aliases).set(updates).where(eq2(aliases.id, aliasId)).run(); return getAlias(db, aliasId); } function deleteAlias(db, aliasId) { getAlias(db, aliasId); db.delete(aliases).where(eq2(aliases.id, aliasId)).run(); } function createSyncJob(db, zoneId) { getZone(db, zoneId); const jobId = id("sync"); db.insert(syncJobs).values({ id: jobId, zone_id: zoneId, status: "pending", created_at: now() }).run(); return jobId; } function updateSyncJob(db, jobId, patch) { const updates = {}; if (patch.status !== void 0) updates.status = patch.status; if (patch.diffJson !== void 0) updates.diff_json = patch.diffJson; if (patch.error !== void 0) updates.error = patch.error; if (patch.finishedAt !== void 0) updates.finished_at = patch.finishedAt; db.update(syncJobs).set(updates).where(eq2(syncJobs.id, jobId)).run(); } function getSyncJob(db, jobId) { const row = db.select().from(syncJobs).where(eq2(syncJobs.id, jobId)).get(); if (!row) throw new NotFoundError(`sync job ${jobId}`); return { id: row.id, zoneId: row.zone_id, status: row.status, diff: row.diff_json ? JSON.parse(row.diff_json) : [], error: row.error, createdAt: row.created_at, finishedAt: row.finished_at }; } function listSyncJobs(db, zoneId) { return db.select().from(syncJobs).where(eq2(syncJobs.zone_id, zoneId)).orderBy(sql2`${syncJobs.created_at} DESC`).all().slice(0, 20).map((row) => ({ id: row.id, zoneId: row.zone_id, status: row.status, diff: row.diff_json ? JSON.parse(row.diff_json) : [], error: row.error, createdAt: row.created_at, finishedAt: row.finished_at })); } function addSyncEvent(db, jobId, event) { db.insert(syncEvents).values({ id: id("evt"), job_id: jobId, kind: event.kind, record_name: event.recordName ?? null, record_type: event.recordType ?? null, detail: event.detail ?? null, created_at: now() }).run(); } function listIgnoredOrphans(db, zoneId) { return db.select().from(ignoredOrphans).where(eq2(ignoredOrphans.zone_id, zoneId)).all().map((r) => ({ id: r.id, recordName: r.record_name, recordType: r.record_type })); } function ignoreOrphan(db, zoneId, recordName, recordType) { getZone(db, zoneId); db.insert(ignoredOrphans).values({ id: id("ign"), zone_id: zoneId, record_name: recordName, record_type: recordType, created_at: now() }).onConflictDoNothing().run(); } function unignoreOrphan(db, zoneId, recordName, recordType) { db.delete(ignoredOrphans).where( and( eq2(ignoredOrphans.zone_id, zoneId), eq2(ignoredOrphans.record_name, recordName), eq2(ignoredOrphans.record_type, recordType) ) ).run(); } function dashboardCounts(db) { const nodeRows = db.select().from(nodes).all(); const aliasRows = db.select().from(aliases).all(); const zoneRows = db.select().from(zones).all(); let syncOk = 0; let drift = 0; let nodesWithoutIp = 0; for (const n of nodeRows) { if (n.sync_status === "ok") syncOk += 1; if (n.sync_status === "drift" || n.sync_status === "missing") drift += 1; const addrs = loadAddresses(db, n.id); if (!addrs.some((a) => a.family === "v4")) nodesWithoutIp += 1; } let brokenAliases = 0; for (const a of aliasRows) { if (a.sync_status === "error" || a.sync_status === "missing") brokenAliases += 1; if (a.sync_status === "drift") drift += 1; if (a.sync_status === "ok") syncOk += 1; } const lastSyncAt = zoneRows.map((z) => z.last_sync_at).filter(Boolean).sort().at(-1) ?? null; return { nodes: nodeRows.length, aliases: aliasRows.length, syncOk, drift, nodesWithoutIp, brokenAliases, lastSyncAt }; } export { ConflictError, NotFoundError, addSyncEvent, aliases, appSettings, createAlias, createDb, createMemoryDb, createNode, createSyncJob, createZone, dashboardCounts, deleteAlias, deleteNode, deleteZone, ensureCatalogLocations, findLocationByCity, getAlias, getAppSettings, getLocation, getNode, getSyncJob, getZone, healthCheck, ignoreOrphan, ignoredOrphans, listAliases, listIgnoredOrphans, listLocations, listNodes, listSyncJobs, listZones, locations, nodeAddresses, nodes, resolveDatabasePath, runMigrations, schema, syncEvents, syncJobs, unignoreOrphan, updateAlias, updateAppSettings, updateNode, updateSyncJob, updateZone, zones };