Enhance CDN Manager: Add Cloudflare API token support, update documentation, and introduce new routes for managing nodes, aliases, and topology. Improve UI components and status badges for better user experience.
quality / commitlint (push) Skipped
quality / changes (push) Successful in 5s
quality / docker-check (push) Skipped
CD / update-wiki (push) Successful in 6s
quality / web (push) Successful in 53s
quality / api (push) Successful in 49s
CD / quality (push) Successful in 1m57s
CD / publish (push) Successful in 27s
quality / commitlint (push) Skipped
quality / changes (push) Successful in 5s
quality / docker-check (push) Skipped
CD / update-wiki (push) Successful in 6s
quality / web (push) Successful in 53s
quality / api (push) Successful in 49s
CD / quality (push) Successful in 1m57s
CD / publish (push) Successful in 27s
This commit is contained in:
Vendored
+2790
-1
File diff suppressed because it is too large
Load Diff
Vendored
+565
-7
@@ -1,14 +1,126 @@
|
||||
// src/schema.ts
|
||||
import { sql } from "drizzle-orm";
|
||||
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
||||
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
|
||||
appSettings,
|
||||
zones,
|
||||
locations,
|
||||
nodes,
|
||||
nodeAddresses,
|
||||
aliases,
|
||||
syncJobs,
|
||||
syncEvents,
|
||||
ignoredOrphans
|
||||
};
|
||||
|
||||
// src/client.ts
|
||||
@@ -50,8 +162,8 @@ function runMigrations(sqlite) {
|
||||
for (const file of files) {
|
||||
const applied = sqlite.prepare("SELECT 1 FROM _migrations WHERE name = ?").get(file);
|
||||
if (applied) continue;
|
||||
const sql2 = readFileSync(join(migrationsDir, file), "utf-8");
|
||||
sqlite.exec(sql2);
|
||||
const sql3 = readFileSync(join(migrationsDir, file), "utf-8");
|
||||
sqlite.exec(sql3);
|
||||
sqlite.prepare("INSERT INTO _migrations (name) VALUES (?)").run(file);
|
||||
}
|
||||
}
|
||||
@@ -79,7 +191,10 @@ var SETTINGS_ID = "settings-main";
|
||||
function toDto(row) {
|
||||
return {
|
||||
id: row.id,
|
||||
showQuickActions: row.show_quick_actions == null ? true : Boolean(row.show_quick_actions)
|
||||
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) {
|
||||
@@ -87,7 +202,10 @@ function ensureRow(db) {
|
||||
if (existing) return existing;
|
||||
db.insert(appSettings).values({
|
||||
id: SETTINGS_ID,
|
||||
show_quick_actions: true
|
||||
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();
|
||||
}
|
||||
@@ -102,19 +220,459 @@ function updateAppSettings(db, patch) {
|
||||
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";
|
||||
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 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,
|
||||
getAlias,
|
||||
getAppSettings,
|
||||
getLocation,
|
||||
getNode,
|
||||
getSyncJob,
|
||||
getZone,
|
||||
healthCheck,
|
||||
ignoreOrphan,
|
||||
ignoredOrphans,
|
||||
listAliases,
|
||||
listIgnoredOrphans,
|
||||
listLocations,
|
||||
listNodes,
|
||||
listSyncJobs,
|
||||
listZones,
|
||||
locations,
|
||||
nodeAddresses,
|
||||
nodes,
|
||||
resolveDatabasePath,
|
||||
runMigrations,
|
||||
schema,
|
||||
updateAppSettings
|
||||
syncEvents,
|
||||
syncJobs,
|
||||
unignoreOrphan,
|
||||
updateAlias,
|
||||
updateAppSettings,
|
||||
updateNode,
|
||||
updateSyncJob,
|
||||
updateZone,
|
||||
zones
|
||||
};
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
-- Fleet DNS: zones, locations, nodes, aliases, sync
|
||||
|
||||
CREATE TABLE IF NOT EXISTS zones (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
cf_zone_id TEXT,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
role TEXT NOT NULL DEFAULT 'routing',
|
||||
naming_template TEXT NOT NULL DEFAULT '{loc}-{role}{nn}.{zone}',
|
||||
default_ttl INTEGER NOT NULL DEFAULT 300,
|
||||
last_sync_at TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS locations (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
code TEXT NOT NULL UNIQUE,
|
||||
name TEXT NOT NULL,
|
||||
country TEXT,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS nodes (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
zone_id TEXT NOT NULL REFERENCES zones(id) ON DELETE CASCADE,
|
||||
location_id TEXT NOT NULL REFERENCES locations(id),
|
||||
hostname TEXT NOT NULL,
|
||||
role TEXT NOT NULL,
|
||||
index_num INTEGER NOT NULL DEFAULT 1,
|
||||
provider_tag TEXT,
|
||||
notes TEXT,
|
||||
sync_status TEXT NOT NULL DEFAULT 'pending',
|
||||
cf_a_record_id TEXT,
|
||||
cf_aaaa_record_id TEXT,
|
||||
last_error TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
UNIQUE (zone_id, hostname)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_nodes_zone ON nodes(zone_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_nodes_location ON nodes(location_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_nodes_sync ON nodes(sync_status);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS node_addresses (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
node_id TEXT NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
|
||||
family TEXT NOT NULL,
|
||||
ip TEXT NOT NULL,
|
||||
UNIQUE (node_id, family)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS aliases (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
zone_id TEXT NOT NULL REFERENCES zones(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
purpose TEXT NOT NULL DEFAULT 'geo',
|
||||
mode TEXT NOT NULL DEFAULT 'primary',
|
||||
target_node_id TEXT NOT NULL REFERENCES nodes(id) ON DELETE RESTRICT,
|
||||
sync_status TEXT NOT NULL DEFAULT 'pending',
|
||||
cf_record_id TEXT,
|
||||
last_error TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
UNIQUE (zone_id, name)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_aliases_zone ON aliases(zone_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_aliases_target ON aliases(target_node_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_aliases_sync ON aliases(sync_status);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sync_jobs (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
zone_id TEXT NOT NULL REFERENCES zones(id) ON DELETE CASCADE,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
diff_json TEXT,
|
||||
error TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
finished_at TEXT
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_sync_jobs_zone ON sync_jobs(zone_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sync_events (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
job_id TEXT NOT NULL REFERENCES sync_jobs(id) ON DELETE CASCADE,
|
||||
kind TEXT NOT NULL,
|
||||
record_name TEXT,
|
||||
record_type TEXT,
|
||||
detail TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_sync_events_job ON sync_events(job_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ignored_orphans (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
zone_id TEXT NOT NULL REFERENCES zones(id) ON DELETE CASCADE,
|
||||
record_name TEXT NOT NULL,
|
||||
record_type TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
UNIQUE (zone_id, record_name, record_type)
|
||||
);
|
||||
|
||||
-- Seed locations (IATA / routing codes from naming doc)
|
||||
INSERT OR IGNORE INTO locations (id, code, name, country, sort_order) VALUES
|
||||
('loc-msk', 'msk', 'Москва', 'RU', 10),
|
||||
('loc-fra', 'fra', 'Франкфурт', 'DE', 20),
|
||||
('loc-ams', 'ams', 'Амстердам', 'NL', 30),
|
||||
('loc-hel', 'hel', 'Хельсинки', 'FI', 40),
|
||||
('loc-par', 'par', 'Париж', 'FR', 50);
|
||||
|
||||
-- Extend settings defaults for naming / TTL
|
||||
ALTER TABLE app_settings ADD COLUMN default_ttl INTEGER NOT NULL DEFAULT 300;
|
||||
ALTER TABLE app_settings ADD COLUMN naming_template TEXT NOT NULL DEFAULT '{loc}-{role}{nn}.{zone}';
|
||||
ALTER TABLE app_settings ADD COLUMN proxied_lock INTEGER NOT NULL DEFAULT 1;
|
||||
@@ -0,0 +1,734 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { and, asc, count, eq, sql } from "drizzle-orm";
|
||||
import type { Db } from "./client.js";
|
||||
import { NotFoundError, ConflictError } from "./errors.js";
|
||||
import {
|
||||
aliases,
|
||||
ignoredOrphans,
|
||||
locations,
|
||||
nodeAddresses,
|
||||
nodes,
|
||||
syncEvents,
|
||||
syncJobs,
|
||||
zones,
|
||||
} from "./schema.js";
|
||||
|
||||
function now() {
|
||||
return new Date().toISOString().replace("T", " ").slice(0, 19);
|
||||
}
|
||||
|
||||
function id(prefix: string) {
|
||||
return `${prefix}-${randomUUID().slice(0, 8)}`;
|
||||
}
|
||||
|
||||
export type LocationRow = {
|
||||
id: string;
|
||||
code: string;
|
||||
name: string;
|
||||
country: string | null;
|
||||
sortOrder: number;
|
||||
};
|
||||
|
||||
export type ZoneRow = {
|
||||
id: string;
|
||||
cfZoneId: string | null;
|
||||
name: string;
|
||||
role: string;
|
||||
namingTemplate: string;
|
||||
defaultTtl: number;
|
||||
lastSyncAt: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type NodeAddressRow = {
|
||||
id: string;
|
||||
family: "v4" | "v6";
|
||||
ip: string;
|
||||
};
|
||||
|
||||
export type NodeRow = {
|
||||
id: string;
|
||||
zoneId: string;
|
||||
locationId: string;
|
||||
locationCode?: string;
|
||||
locationName?: string;
|
||||
hostname: string;
|
||||
role: string;
|
||||
indexNum: number;
|
||||
providerTag: string | null;
|
||||
notes: string | null;
|
||||
syncStatus: string;
|
||||
cfARecordId: string | null;
|
||||
cfAaaaRecordId: string | null;
|
||||
lastError: string | null;
|
||||
addresses: NodeAddressRow[];
|
||||
aliasCount?: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type AliasRow = {
|
||||
id: string;
|
||||
zoneId: string;
|
||||
name: string;
|
||||
purpose: string;
|
||||
mode: string;
|
||||
targetNodeId: string;
|
||||
targetHostname?: string;
|
||||
syncStatus: string;
|
||||
cfRecordId: string | null;
|
||||
lastError: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
function mapLocation(row: typeof locations.$inferSelect): LocationRow {
|
||||
return {
|
||||
id: row.id,
|
||||
code: row.code,
|
||||
name: row.name,
|
||||
country: row.country,
|
||||
sortOrder: row.sort_order,
|
||||
};
|
||||
}
|
||||
|
||||
function mapZone(row: typeof zones.$inferSelect): ZoneRow {
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
export function listLocations(db: Db): LocationRow[] {
|
||||
return db
|
||||
.select()
|
||||
.from(locations)
|
||||
.orderBy(asc(locations.sort_order), asc(locations.code))
|
||||
.all()
|
||||
.map(mapLocation);
|
||||
}
|
||||
|
||||
export function getLocation(db: Db, locationId: string): LocationRow {
|
||||
const row = db
|
||||
.select()
|
||||
.from(locations)
|
||||
.where(eq(locations.id, locationId))
|
||||
.get();
|
||||
if (!row) throw new NotFoundError(`location ${locationId}`);
|
||||
return mapLocation(row);
|
||||
}
|
||||
|
||||
export function listZones(db: Db): ZoneRow[] {
|
||||
return db
|
||||
.select()
|
||||
.from(zones)
|
||||
.orderBy(asc(zones.name))
|
||||
.all()
|
||||
.map(mapZone);
|
||||
}
|
||||
|
||||
export function getZone(db: Db, zoneId: string): ZoneRow {
|
||||
const row = db.select().from(zones).where(eq(zones.id, zoneId)).get();
|
||||
if (!row) throw new NotFoundError(`zone ${zoneId}`);
|
||||
return mapZone(row);
|
||||
}
|
||||
|
||||
export function createZone(
|
||||
db: Db,
|
||||
input: {
|
||||
name: string;
|
||||
cfZoneId?: string | null;
|
||||
role?: string;
|
||||
namingTemplate?: string;
|
||||
defaultTtl?: number;
|
||||
},
|
||||
): ZoneRow {
|
||||
const existing = db
|
||||
.select()
|
||||
.from(zones)
|
||||
.where(eq(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);
|
||||
}
|
||||
|
||||
export function updateZone(
|
||||
db: Db,
|
||||
zoneId: string,
|
||||
patch: {
|
||||
name?: string;
|
||||
cfZoneId?: string | null;
|
||||
role?: string;
|
||||
namingTemplate?: string;
|
||||
defaultTtl?: number;
|
||||
lastSyncAt?: string | null;
|
||||
},
|
||||
): ZoneRow {
|
||||
getZone(db, zoneId);
|
||||
const updates: Partial<typeof zones.$inferInsert> = {
|
||||
updated_at: now(),
|
||||
};
|
||||
if (patch.name !== undefined) updates.name = patch.name;
|
||||
if (patch.cfZoneId !== undefined) updates.cf_zone_id = patch.cfZoneId;
|
||||
if (patch.role !== undefined) updates.role = patch.role;
|
||||
if (patch.namingTemplate !== undefined)
|
||||
updates.naming_template = patch.namingTemplate;
|
||||
if (patch.defaultTtl !== undefined) updates.default_ttl = patch.defaultTtl;
|
||||
if (patch.lastSyncAt !== undefined) updates.last_sync_at = patch.lastSyncAt;
|
||||
db.update(zones).set(updates).where(eq(zones.id, zoneId)).run();
|
||||
return getZone(db, zoneId);
|
||||
}
|
||||
|
||||
export function deleteZone(db: Db, zoneId: string): void {
|
||||
getZone(db, zoneId);
|
||||
db.delete(zones).where(eq(zones.id, zoneId)).run();
|
||||
}
|
||||
|
||||
function loadAddresses(db: Db, nodeId: string): NodeAddressRow[] {
|
||||
return db
|
||||
.select()
|
||||
.from(nodeAddresses)
|
||||
.where(eq(nodeAddresses.node_id, nodeId))
|
||||
.all()
|
||||
.map((r) => ({
|
||||
id: r.id,
|
||||
family: r.family as "v4" | "v6",
|
||||
ip: r.ip,
|
||||
}));
|
||||
}
|
||||
|
||||
function mapNode(
|
||||
db: Db,
|
||||
row: typeof nodes.$inferSelect,
|
||||
loc?: { code: string; name: string },
|
||||
): NodeRow {
|
||||
const aliasCount = db
|
||||
.select({ c: count() })
|
||||
.from(aliases)
|
||||
.where(eq(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,
|
||||
};
|
||||
}
|
||||
|
||||
export function listNodes(
|
||||
db: Db,
|
||||
filter: {
|
||||
zoneId?: string;
|
||||
locationId?: string;
|
||||
role?: string;
|
||||
syncStatus?: string;
|
||||
q?: string;
|
||||
} = {},
|
||||
): NodeRow[] {
|
||||
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));
|
||||
}
|
||||
|
||||
export function getNode(db: Db, nodeId: string): NodeRow {
|
||||
const row = db.select().from(nodes).where(eq(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: Db,
|
||||
nodeId: string,
|
||||
ipv4: string,
|
||||
ipv6?: string | null,
|
||||
) {
|
||||
db.delete(nodeAddresses).where(eq(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();
|
||||
}
|
||||
}
|
||||
|
||||
export function createNode(
|
||||
db: Db,
|
||||
input: {
|
||||
zoneId: string;
|
||||
locationId: string;
|
||||
hostname: string;
|
||||
role: string;
|
||||
indexNum: number;
|
||||
providerTag?: string | null;
|
||||
notes?: string | null;
|
||||
ipv4: string;
|
||||
ipv6?: string | null;
|
||||
},
|
||||
): NodeRow {
|
||||
getZone(db, input.zoneId);
|
||||
getLocation(db, input.locationId);
|
||||
const dup = db
|
||||
.select()
|
||||
.from(nodes)
|
||||
.where(
|
||||
and(eq(nodes.zone_id, input.zoneId), eq(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);
|
||||
}
|
||||
|
||||
export function updateNode(
|
||||
db: Db,
|
||||
nodeId: string,
|
||||
patch: {
|
||||
locationId?: string;
|
||||
hostname?: string;
|
||||
role?: string;
|
||||
indexNum?: number;
|
||||
providerTag?: string | null;
|
||||
notes?: string | null;
|
||||
ipv4?: string;
|
||||
ipv6?: string | null;
|
||||
syncStatus?: string;
|
||||
cfARecordId?: string | null;
|
||||
cfAaaaRecordId?: string | null;
|
||||
lastError?: string | null;
|
||||
},
|
||||
): NodeRow {
|
||||
const current = getNode(db, nodeId);
|
||||
if (patch.locationId) getLocation(db, patch.locationId);
|
||||
const updates: Partial<typeof nodes.$inferInsert> = {
|
||||
updated_at: now(),
|
||||
};
|
||||
if (patch.locationId !== undefined) updates.location_id = patch.locationId;
|
||||
if (patch.hostname !== undefined) updates.hostname = patch.hostname;
|
||||
if (patch.role !== undefined) updates.role = patch.role;
|
||||
if (patch.indexNum !== undefined) updates.index_num = patch.indexNum;
|
||||
if (patch.providerTag !== undefined) updates.provider_tag = patch.providerTag;
|
||||
if (patch.notes !== undefined) updates.notes = patch.notes;
|
||||
if (patch.syncStatus !== undefined) updates.sync_status = patch.syncStatus;
|
||||
if (patch.cfARecordId !== undefined)
|
||||
updates.cf_a_record_id = patch.cfARecordId;
|
||||
if (patch.cfAaaaRecordId !== undefined)
|
||||
updates.cf_aaaa_record_id = patch.cfAaaaRecordId;
|
||||
if (patch.lastError !== undefined) updates.last_error = patch.lastError;
|
||||
|
||||
db.update(nodes).set(updates).where(eq(nodes.id, nodeId)).run();
|
||||
|
||||
if (patch.ipv4 !== undefined) {
|
||||
const v6 =
|
||||
patch.ipv6 !== undefined
|
||||
? patch.ipv6
|
||||
: (current.addresses.find((a) => a.family === "v6")?.ip ?? null);
|
||||
setAddresses(db, nodeId, patch.ipv4, v6);
|
||||
} else if (patch.ipv6 !== undefined) {
|
||||
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);
|
||||
}
|
||||
|
||||
export function deleteNode(db: Db, nodeId: string): void {
|
||||
getNode(db, nodeId);
|
||||
const linked = db
|
||||
.select({ c: count() })
|
||||
.from(aliases)
|
||||
.where(eq(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(eq(nodes.id, nodeId)).run();
|
||||
}
|
||||
|
||||
function mapAlias(
|
||||
row: typeof aliases.$inferSelect,
|
||||
targetHostname?: string,
|
||||
): AliasRow {
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
export function listAliases(
|
||||
db: Db,
|
||||
filter: {
|
||||
zoneId?: string;
|
||||
purpose?: string;
|
||||
syncStatus?: string;
|
||||
q?: string;
|
||||
} = {},
|
||||
): AliasRow[] {
|
||||
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));
|
||||
}
|
||||
|
||||
export function getAlias(db: Db, aliasId: string): AliasRow {
|
||||
const row = db.select().from(aliases).where(eq(aliases.id, aliasId)).get();
|
||||
if (!row) throw new NotFoundError(`alias ${aliasId}`);
|
||||
const target = db
|
||||
.select()
|
||||
.from(nodes)
|
||||
.where(eq(nodes.id, row.target_node_id))
|
||||
.get();
|
||||
return mapAlias(row, target?.hostname);
|
||||
}
|
||||
|
||||
export function createAlias(
|
||||
db: Db,
|
||||
input: {
|
||||
zoneId: string;
|
||||
name: string;
|
||||
purpose: string;
|
||||
mode: string;
|
||||
targetNodeId: string;
|
||||
},
|
||||
): AliasRow {
|
||||
getZone(db, input.zoneId);
|
||||
getNode(db, input.targetNodeId);
|
||||
const dup = db
|
||||
.select()
|
||||
.from(aliases)
|
||||
.where(and(eq(aliases.zone_id, input.zoneId), eq(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);
|
||||
}
|
||||
|
||||
export function updateAlias(
|
||||
db: Db,
|
||||
aliasId: string,
|
||||
patch: {
|
||||
name?: string;
|
||||
purpose?: string;
|
||||
mode?: string;
|
||||
targetNodeId?: string;
|
||||
syncStatus?: string;
|
||||
cfRecordId?: string | null;
|
||||
lastError?: string | null;
|
||||
},
|
||||
): AliasRow {
|
||||
getAlias(db, aliasId);
|
||||
if (patch.targetNodeId) getNode(db, patch.targetNodeId);
|
||||
const updates: Partial<typeof aliases.$inferInsert> = {
|
||||
updated_at: now(),
|
||||
};
|
||||
if (patch.name !== undefined) updates.name = patch.name;
|
||||
if (patch.purpose !== undefined) updates.purpose = patch.purpose;
|
||||
if (patch.mode !== undefined) updates.mode = patch.mode;
|
||||
if (patch.targetNodeId !== undefined)
|
||||
updates.target_node_id = patch.targetNodeId;
|
||||
if (patch.syncStatus !== undefined) updates.sync_status = patch.syncStatus;
|
||||
if (patch.cfRecordId !== undefined) updates.cf_record_id = patch.cfRecordId;
|
||||
if (patch.lastError !== undefined) updates.last_error = patch.lastError;
|
||||
db.update(aliases).set(updates).where(eq(aliases.id, aliasId)).run();
|
||||
return getAlias(db, aliasId);
|
||||
}
|
||||
|
||||
export function deleteAlias(db: Db, aliasId: string): void {
|
||||
getAlias(db, aliasId);
|
||||
db.delete(aliases).where(eq(aliases.id, aliasId)).run();
|
||||
}
|
||||
|
||||
export function createSyncJob(db: Db, zoneId: string): string {
|
||||
getZone(db, zoneId);
|
||||
const jobId = id("sync");
|
||||
db.insert(syncJobs)
|
||||
.values({
|
||||
id: jobId,
|
||||
zone_id: zoneId,
|
||||
status: "pending",
|
||||
created_at: now(),
|
||||
})
|
||||
.run();
|
||||
return jobId;
|
||||
}
|
||||
|
||||
export function updateSyncJob(
|
||||
db: Db,
|
||||
jobId: string,
|
||||
patch: {
|
||||
status?: string;
|
||||
diffJson?: string | null;
|
||||
error?: string | null;
|
||||
finishedAt?: string | null;
|
||||
},
|
||||
) {
|
||||
const updates: Partial<typeof syncJobs.$inferInsert> = {};
|
||||
if (patch.status !== undefined) updates.status = patch.status;
|
||||
if (patch.diffJson !== undefined) updates.diff_json = patch.diffJson;
|
||||
if (patch.error !== undefined) updates.error = patch.error;
|
||||
if (patch.finishedAt !== undefined) updates.finished_at = patch.finishedAt;
|
||||
db.update(syncJobs).set(updates).where(eq(syncJobs.id, jobId)).run();
|
||||
}
|
||||
|
||||
export function getSyncJob(db: Db, jobId: string) {
|
||||
const row = db.select().from(syncJobs).where(eq(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,
|
||||
};
|
||||
}
|
||||
|
||||
export function listSyncJobs(db: Db, zoneId: string) {
|
||||
return db
|
||||
.select()
|
||||
.from(syncJobs)
|
||||
.where(eq(syncJobs.zone_id, zoneId))
|
||||
.orderBy(sql`${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,
|
||||
}));
|
||||
}
|
||||
|
||||
export function addSyncEvent(
|
||||
db: Db,
|
||||
jobId: string,
|
||||
event: {
|
||||
kind: string;
|
||||
recordName?: string;
|
||||
recordType?: string;
|
||||
detail?: string;
|
||||
},
|
||||
) {
|
||||
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();
|
||||
}
|
||||
|
||||
export function listIgnoredOrphans(db: Db, zoneId: string) {
|
||||
return db
|
||||
.select()
|
||||
.from(ignoredOrphans)
|
||||
.where(eq(ignoredOrphans.zone_id, zoneId))
|
||||
.all()
|
||||
.map((r) => ({
|
||||
id: r.id,
|
||||
recordName: r.record_name,
|
||||
recordType: r.record_type,
|
||||
}));
|
||||
}
|
||||
|
||||
export function ignoreOrphan(
|
||||
db: Db,
|
||||
zoneId: string,
|
||||
recordName: string,
|
||||
recordType: string,
|
||||
) {
|
||||
getZone(db, zoneId);
|
||||
db.insert(ignoredOrphans)
|
||||
.values({
|
||||
id: id("ign"),
|
||||
zone_id: zoneId,
|
||||
record_name: recordName,
|
||||
record_type: recordType,
|
||||
created_at: now(),
|
||||
})
|
||||
.onConflictDoNothing()
|
||||
.run();
|
||||
}
|
||||
|
||||
export function unignoreOrphan(
|
||||
db: Db,
|
||||
zoneId: string,
|
||||
recordName: string,
|
||||
recordType: string,
|
||||
) {
|
||||
db.delete(ignoredOrphans)
|
||||
.where(
|
||||
and(
|
||||
eq(ignoredOrphans.zone_id, zoneId),
|
||||
eq(ignoredOrphans.record_name, recordName),
|
||||
eq(ignoredOrphans.record_type, recordType),
|
||||
),
|
||||
)
|
||||
.run();
|
||||
}
|
||||
|
||||
export function dashboardCounts(db: 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,
|
||||
};
|
||||
}
|
||||
@@ -2,3 +2,4 @@ export * from "./schema.js";
|
||||
export * from "./client.js";
|
||||
export * from "./errors.js";
|
||||
export * from "./settings-repo.js";
|
||||
export * from "./fleet-repo.js";
|
||||
|
||||
+163
-1
@@ -1,11 +1,18 @@
|
||||
import { sql } from "drizzle-orm";
|
||||
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
||||
import { integer, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core";
|
||||
|
||||
export const 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')`),
|
||||
@@ -14,6 +21,161 @@ export const appSettings = sqliteTable("app_settings", {
|
||||
.default(sql`datetime('now')`),
|
||||
});
|
||||
|
||||
export const 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')`),
|
||||
});
|
||||
|
||||
export const 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')`),
|
||||
});
|
||||
|
||||
export const 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)],
|
||||
);
|
||||
|
||||
export const 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)],
|
||||
);
|
||||
|
||||
export const 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)],
|
||||
);
|
||||
|
||||
export const 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"),
|
||||
});
|
||||
|
||||
export const 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')`),
|
||||
});
|
||||
|
||||
export const 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,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
export const schema = {
|
||||
appSettings,
|
||||
zones,
|
||||
locations,
|
||||
nodes,
|
||||
nodeAddresses,
|
||||
aliases,
|
||||
syncJobs,
|
||||
syncEvents,
|
||||
ignoredOrphans,
|
||||
};
|
||||
|
||||
@@ -7,10 +7,16 @@ const SETTINGS_ID = "settings-main";
|
||||
export type AppSettingsDto = {
|
||||
id: string;
|
||||
showQuickActions: boolean;
|
||||
defaultTtl: number;
|
||||
namingTemplate: string;
|
||||
proxiedLock: boolean;
|
||||
};
|
||||
|
||||
export type AppSettingsPatch = {
|
||||
showQuickActions?: boolean;
|
||||
defaultTtl?: number;
|
||||
namingTemplate?: string;
|
||||
proxiedLock?: boolean;
|
||||
};
|
||||
|
||||
function toDto(row: typeof appSettings.$inferSelect): AppSettingsDto {
|
||||
@@ -18,6 +24,9 @@ function toDto(row: typeof appSettings.$inferSelect): AppSettingsDto {
|
||||
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),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -33,6 +42,9 @@ function ensureRow(db: Db): typeof appSettings.$inferSelect {
|
||||
.values({
|
||||
id: SETTINGS_ID,
|
||||
show_quick_actions: true,
|
||||
default_ttl: 300,
|
||||
naming_template: "{loc}-{role}{nn}.{zone}",
|
||||
proxied_lock: true,
|
||||
})
|
||||
.run();
|
||||
|
||||
@@ -58,6 +70,15 @@ export function updateAppSettings(
|
||||
if (patch.showQuickActions !== undefined) {
|
||||
updates.show_quick_actions = patch.showQuickActions;
|
||||
}
|
||||
if (patch.defaultTtl !== undefined) {
|
||||
updates.default_ttl = patch.defaultTtl;
|
||||
}
|
||||
if (patch.namingTemplate !== undefined) {
|
||||
updates.naming_template = patch.namingTemplate;
|
||||
}
|
||||
if (patch.proxiedLock !== undefined) {
|
||||
updates.proxied_lock = patch.proxiedLock;
|
||||
}
|
||||
db.update(appSettings)
|
||||
.set(updates)
|
||||
.where(eq(appSettings.id, SETTINGS_ID))
|
||||
|
||||
Vendored
+423
-1
@@ -66,12 +66,434 @@ type LoginResponse = {
|
||||
};
|
||||
declare const appSettingsPatchSchema: z.ZodObject<{
|
||||
showQuickActions: z.ZodOptional<z.ZodBoolean>;
|
||||
defaultTtl: z.ZodOptional<z.ZodNumber>;
|
||||
namingTemplate: z.ZodOptional<z.ZodString>;
|
||||
proxiedLock: z.ZodOptional<z.ZodBoolean>;
|
||||
}, z.core.$strip>;
|
||||
type AppSettingsPatch = z.infer<typeof appSettingsPatchSchema>;
|
||||
declare const appSettingsSchema: z.ZodObject<{
|
||||
id: z.ZodString;
|
||||
showQuickActions: z.ZodBoolean;
|
||||
defaultTtl: z.ZodNumber;
|
||||
namingTemplate: z.ZodString;
|
||||
proxiedLock: z.ZodBoolean;
|
||||
cloudflareConfigured: z.ZodOptional<z.ZodBoolean>;
|
||||
}, z.core.$strip>;
|
||||
type AppSettings = z.infer<typeof appSettingsSchema>;
|
||||
|
||||
export { type AppSettings, type AppSettingsPatch, type AppSwitcherConfig, type AppSwitcherEntry, type JwtClaims, type LoginInput, type LoginRequest, type LoginResponse, appSettingsPatchSchema, appSettingsSchema, appSwitcherConfigSchema, appSwitcherEntrySchema, appSwitcherIconSchema, loginSchema };
|
||||
declare const nodeRoleSchema: z.ZodEnum<{
|
||||
hub: "hub";
|
||||
gw: "gw";
|
||||
edge: "edge";
|
||||
ix: "ix";
|
||||
}>;
|
||||
type NodeRole = z.infer<typeof nodeRoleSchema>;
|
||||
declare const aliasPurposeSchema: z.ZodEnum<{
|
||||
custom: "custom";
|
||||
ix: "ix";
|
||||
geo: "geo";
|
||||
backup: "backup";
|
||||
admin: "admin";
|
||||
}>;
|
||||
type AliasPurpose = z.infer<typeof aliasPurposeSchema>;
|
||||
declare const aliasModeSchema: z.ZodEnum<{
|
||||
primary: "primary";
|
||||
pair: "pair";
|
||||
}>;
|
||||
type AliasMode = z.infer<typeof aliasModeSchema>;
|
||||
declare const syncStatusSchema: z.ZodEnum<{
|
||||
error: "error";
|
||||
pending: "pending";
|
||||
ok: "ok";
|
||||
drift: "drift";
|
||||
missing: "missing";
|
||||
}>;
|
||||
type SyncStatus = z.infer<typeof syncStatusSchema>;
|
||||
declare const addressFamilySchema: z.ZodEnum<{
|
||||
v4: "v4";
|
||||
v6: "v6";
|
||||
}>;
|
||||
declare const cfZoneSchema: z.ZodObject<{
|
||||
id: z.ZodString;
|
||||
name: z.ZodString;
|
||||
status: z.ZodOptional<z.ZodString>;
|
||||
}, z.core.$strip>;
|
||||
type CfZone = z.infer<typeof cfZoneSchema>;
|
||||
declare const cfDnsRecordSchema: z.ZodObject<{
|
||||
id: z.ZodOptional<z.ZodString>;
|
||||
type: z.ZodString;
|
||||
name: z.ZodString;
|
||||
content: z.ZodString;
|
||||
ttl: z.ZodNumber;
|
||||
proxied: z.ZodOptional<z.ZodBoolean>;
|
||||
priority: z.ZodOptional<z.ZodNumber>;
|
||||
}, z.core.$strip>;
|
||||
type CfDnsRecord = z.infer<typeof cfDnsRecordSchema>;
|
||||
declare const createDnsRecordPayloadSchema: z.ZodObject<{
|
||||
type: z.ZodString;
|
||||
name: z.ZodString;
|
||||
content: z.ZodString;
|
||||
ttl: z.ZodNumber;
|
||||
proxied: z.ZodOptional<z.ZodBoolean>;
|
||||
priority: z.ZodOptional<z.ZodNumber>;
|
||||
}, z.core.$strip>;
|
||||
type CreateDnsRecordPayload = z.infer<typeof createDnsRecordPayloadSchema>;
|
||||
declare const patchDnsRecordPayloadSchema: z.ZodObject<{
|
||||
type: z.ZodOptional<z.ZodString>;
|
||||
name: z.ZodOptional<z.ZodString>;
|
||||
content: z.ZodOptional<z.ZodString>;
|
||||
ttl: z.ZodOptional<z.ZodNumber>;
|
||||
proxied: z.ZodOptional<z.ZodOptional<z.ZodBoolean>>;
|
||||
priority: z.ZodOptional<z.ZodOptional<z.ZodNumber>>;
|
||||
}, z.core.$strip>;
|
||||
type PatchDnsRecordPayload = z.infer<typeof patchDnsRecordPayloadSchema>;
|
||||
declare const locationSchema: z.ZodObject<{
|
||||
id: z.ZodString;
|
||||
code: z.ZodString;
|
||||
name: z.ZodString;
|
||||
country: z.ZodNullable<z.ZodString>;
|
||||
sortOrder: z.ZodNumber;
|
||||
}, z.core.$strip>;
|
||||
type Location = z.infer<typeof locationSchema>;
|
||||
declare const zoneSchema: z.ZodObject<{
|
||||
id: z.ZodString;
|
||||
cfZoneId: z.ZodNullable<z.ZodString>;
|
||||
name: z.ZodString;
|
||||
role: z.ZodString;
|
||||
namingTemplate: z.ZodString;
|
||||
defaultTtl: z.ZodNumber;
|
||||
lastSyncAt: z.ZodNullable<z.ZodString>;
|
||||
createdAt: z.ZodString;
|
||||
updatedAt: z.ZodString;
|
||||
}, z.core.$strip>;
|
||||
type Zone = z.infer<typeof zoneSchema>;
|
||||
declare const zoneCreateSchema: z.ZodObject<{
|
||||
name: z.ZodString;
|
||||
cfZoneId: z.ZodNullable<z.ZodOptional<z.ZodString>>;
|
||||
role: z.ZodDefault<z.ZodString>;
|
||||
namingTemplate: z.ZodOptional<z.ZodString>;
|
||||
defaultTtl: z.ZodOptional<z.ZodNumber>;
|
||||
}, z.core.$strip>;
|
||||
type ZoneCreate = z.infer<typeof zoneCreateSchema>;
|
||||
declare const zonePatchSchema: z.ZodObject<{
|
||||
name: z.ZodOptional<z.ZodString>;
|
||||
cfZoneId: z.ZodOptional<z.ZodNullable<z.ZodOptional<z.ZodString>>>;
|
||||
role: z.ZodOptional<z.ZodDefault<z.ZodString>>;
|
||||
namingTemplate: z.ZodOptional<z.ZodOptional<z.ZodString>>;
|
||||
defaultTtl: z.ZodOptional<z.ZodOptional<z.ZodNumber>>;
|
||||
}, z.core.$strip>;
|
||||
type ZonePatch = z.infer<typeof zonePatchSchema>;
|
||||
declare const nodeAddressSchema: z.ZodObject<{
|
||||
id: z.ZodString;
|
||||
family: z.ZodEnum<{
|
||||
v4: "v4";
|
||||
v6: "v6";
|
||||
}>;
|
||||
ip: z.ZodString;
|
||||
}, z.core.$strip>;
|
||||
type NodeAddress = z.infer<typeof nodeAddressSchema>;
|
||||
declare const nodeSchema: z.ZodObject<{
|
||||
id: z.ZodString;
|
||||
zoneId: z.ZodString;
|
||||
locationId: z.ZodString;
|
||||
locationCode: z.ZodOptional<z.ZodString>;
|
||||
locationName: z.ZodOptional<z.ZodString>;
|
||||
hostname: z.ZodString;
|
||||
role: z.ZodEnum<{
|
||||
hub: "hub";
|
||||
gw: "gw";
|
||||
edge: "edge";
|
||||
ix: "ix";
|
||||
}>;
|
||||
indexNum: z.ZodNumber;
|
||||
providerTag: z.ZodNullable<z.ZodString>;
|
||||
notes: z.ZodNullable<z.ZodString>;
|
||||
syncStatus: z.ZodEnum<{
|
||||
error: "error";
|
||||
pending: "pending";
|
||||
ok: "ok";
|
||||
drift: "drift";
|
||||
missing: "missing";
|
||||
}>;
|
||||
cfARecordId: z.ZodNullable<z.ZodString>;
|
||||
cfAaaaRecordId: z.ZodNullable<z.ZodString>;
|
||||
lastError: z.ZodNullable<z.ZodString>;
|
||||
addresses: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
||||
id: z.ZodString;
|
||||
family: z.ZodEnum<{
|
||||
v4: "v4";
|
||||
v6: "v6";
|
||||
}>;
|
||||
ip: z.ZodString;
|
||||
}, z.core.$strip>>>;
|
||||
aliasCount: z.ZodOptional<z.ZodNumber>;
|
||||
createdAt: z.ZodString;
|
||||
updatedAt: z.ZodString;
|
||||
}, z.core.$strip>;
|
||||
type Node = z.infer<typeof nodeSchema>;
|
||||
declare const nodeCreateSchema: z.ZodObject<{
|
||||
zoneId: z.ZodString;
|
||||
locationId: z.ZodString;
|
||||
role: z.ZodEnum<{
|
||||
hub: "hub";
|
||||
gw: "gw";
|
||||
edge: "edge";
|
||||
ix: "ix";
|
||||
}>;
|
||||
indexNum: z.ZodDefault<z.ZodNumber>;
|
||||
hostname: z.ZodOptional<z.ZodString>;
|
||||
providerTag: z.ZodNullable<z.ZodOptional<z.ZodString>>;
|
||||
notes: z.ZodNullable<z.ZodOptional<z.ZodString>>;
|
||||
ipv4: z.ZodString;
|
||||
ipv6: z.ZodNullable<z.ZodOptional<z.ZodString>>;
|
||||
}, z.core.$strip>;
|
||||
type NodeCreate = z.infer<typeof nodeCreateSchema>;
|
||||
declare const nodePatchSchema: z.ZodObject<{
|
||||
locationId: z.ZodOptional<z.ZodString>;
|
||||
role: z.ZodOptional<z.ZodEnum<{
|
||||
hub: "hub";
|
||||
gw: "gw";
|
||||
edge: "edge";
|
||||
ix: "ix";
|
||||
}>>;
|
||||
indexNum: z.ZodOptional<z.ZodNumber>;
|
||||
hostname: z.ZodOptional<z.ZodString>;
|
||||
providerTag: z.ZodNullable<z.ZodOptional<z.ZodString>>;
|
||||
notes: z.ZodNullable<z.ZodOptional<z.ZodString>>;
|
||||
ipv4: z.ZodOptional<z.ZodString>;
|
||||
ipv6: z.ZodNullable<z.ZodOptional<z.ZodString>>;
|
||||
}, z.core.$strip>;
|
||||
type NodePatch = z.infer<typeof nodePatchSchema>;
|
||||
declare const aliasSchema: z.ZodObject<{
|
||||
id: z.ZodString;
|
||||
zoneId: z.ZodString;
|
||||
name: z.ZodString;
|
||||
purpose: z.ZodEnum<{
|
||||
custom: "custom";
|
||||
ix: "ix";
|
||||
geo: "geo";
|
||||
backup: "backup";
|
||||
admin: "admin";
|
||||
}>;
|
||||
mode: z.ZodEnum<{
|
||||
primary: "primary";
|
||||
pair: "pair";
|
||||
}>;
|
||||
targetNodeId: z.ZodString;
|
||||
targetHostname: z.ZodOptional<z.ZodString>;
|
||||
syncStatus: z.ZodEnum<{
|
||||
error: "error";
|
||||
pending: "pending";
|
||||
ok: "ok";
|
||||
drift: "drift";
|
||||
missing: "missing";
|
||||
}>;
|
||||
cfRecordId: z.ZodNullable<z.ZodString>;
|
||||
lastError: z.ZodNullable<z.ZodString>;
|
||||
createdAt: z.ZodString;
|
||||
updatedAt: z.ZodString;
|
||||
}, z.core.$strip>;
|
||||
type Alias = z.infer<typeof aliasSchema>;
|
||||
declare const aliasCreateSchema: z.ZodObject<{
|
||||
zoneId: z.ZodString;
|
||||
name: z.ZodString;
|
||||
purpose: z.ZodDefault<z.ZodEnum<{
|
||||
custom: "custom";
|
||||
ix: "ix";
|
||||
geo: "geo";
|
||||
backup: "backup";
|
||||
admin: "admin";
|
||||
}>>;
|
||||
mode: z.ZodDefault<z.ZodEnum<{
|
||||
primary: "primary";
|
||||
pair: "pair";
|
||||
}>>;
|
||||
targetNodeId: z.ZodString;
|
||||
}, z.core.$strip>;
|
||||
type AliasCreate = z.infer<typeof aliasCreateSchema>;
|
||||
declare const aliasPatchSchema: z.ZodObject<{
|
||||
name: z.ZodOptional<z.ZodString>;
|
||||
purpose: z.ZodOptional<z.ZodEnum<{
|
||||
custom: "custom";
|
||||
ix: "ix";
|
||||
geo: "geo";
|
||||
backup: "backup";
|
||||
admin: "admin";
|
||||
}>>;
|
||||
mode: z.ZodOptional<z.ZodEnum<{
|
||||
primary: "primary";
|
||||
pair: "pair";
|
||||
}>>;
|
||||
targetNodeId: z.ZodOptional<z.ZodString>;
|
||||
}, z.core.$strip>;
|
||||
type AliasPatch = z.infer<typeof aliasPatchSchema>;
|
||||
declare const aliasRetargetSchema: z.ZodObject<{
|
||||
targetNodeId: z.ZodString;
|
||||
}, z.core.$strip>;
|
||||
type AliasRetarget = z.infer<typeof aliasRetargetSchema>;
|
||||
declare const syncDiffOpSchema: z.ZodObject<{
|
||||
id: z.ZodString;
|
||||
kind: z.ZodEnum<{
|
||||
create: "create";
|
||||
update: "update";
|
||||
delete: "delete";
|
||||
orphan: "orphan";
|
||||
proxy_violation: "proxy_violation";
|
||||
noop: "noop";
|
||||
}>;
|
||||
entityType: z.ZodEnum<{
|
||||
orphan: "orphan";
|
||||
node_a: "node_a";
|
||||
node_aaaa: "node_aaaa";
|
||||
alias: "alias";
|
||||
}>;
|
||||
entityId: z.ZodNullable<z.ZodString>;
|
||||
recordName: z.ZodString;
|
||||
recordType: z.ZodString;
|
||||
desired: z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
||||
observed: z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
||||
detail: z.ZodOptional<z.ZodString>;
|
||||
}, z.core.$strip>;
|
||||
type SyncDiffOp = z.infer<typeof syncDiffOpSchema>;
|
||||
declare const syncJobSchema: z.ZodObject<{
|
||||
id: z.ZodString;
|
||||
zoneId: z.ZodString;
|
||||
status: z.ZodEnum<{
|
||||
pending: "pending";
|
||||
running: "running";
|
||||
done: "done";
|
||||
failed: "failed";
|
||||
}>;
|
||||
diff: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
||||
id: z.ZodString;
|
||||
kind: z.ZodEnum<{
|
||||
create: "create";
|
||||
update: "update";
|
||||
delete: "delete";
|
||||
orphan: "orphan";
|
||||
proxy_violation: "proxy_violation";
|
||||
noop: "noop";
|
||||
}>;
|
||||
entityType: z.ZodEnum<{
|
||||
orphan: "orphan";
|
||||
node_a: "node_a";
|
||||
node_aaaa: "node_aaaa";
|
||||
alias: "alias";
|
||||
}>;
|
||||
entityId: z.ZodNullable<z.ZodString>;
|
||||
recordName: z.ZodString;
|
||||
recordType: z.ZodString;
|
||||
desired: z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
||||
observed: z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
||||
detail: z.ZodOptional<z.ZodString>;
|
||||
}, z.core.$strip>>>;
|
||||
error: z.ZodNullable<z.ZodString>;
|
||||
createdAt: z.ZodString;
|
||||
finishedAt: z.ZodNullable<z.ZodString>;
|
||||
}, z.core.$strip>;
|
||||
type SyncJob = z.infer<typeof syncJobSchema>;
|
||||
declare const syncApplySchema: z.ZodObject<{
|
||||
opIds: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
||||
}, z.core.$strip>;
|
||||
type SyncApply = z.infer<typeof syncApplySchema>;
|
||||
declare const dashboardStatsSchema: z.ZodObject<{
|
||||
nodes: z.ZodNumber;
|
||||
aliases: z.ZodNumber;
|
||||
syncOk: z.ZodNumber;
|
||||
drift: z.ZodNumber;
|
||||
proxyViolations: z.ZodNumber;
|
||||
orphans: z.ZodNumber;
|
||||
lastSyncAt: z.ZodNullable<z.ZodString>;
|
||||
nodesWithoutIp: z.ZodNumber;
|
||||
brokenAliases: z.ZodNumber;
|
||||
}, z.core.$strip>;
|
||||
type DashboardStats = z.infer<typeof dashboardStatsSchema>;
|
||||
declare const topologyNodeSchema: z.ZodObject<{
|
||||
id: z.ZodString;
|
||||
hostname: z.ZodString;
|
||||
role: z.ZodEnum<{
|
||||
hub: "hub";
|
||||
gw: "gw";
|
||||
edge: "edge";
|
||||
ix: "ix";
|
||||
}>;
|
||||
locationCode: z.ZodString;
|
||||
ipv4: z.ZodNullable<z.ZodString>;
|
||||
syncStatus: z.ZodEnum<{
|
||||
error: "error";
|
||||
pending: "pending";
|
||||
ok: "ok";
|
||||
drift: "drift";
|
||||
missing: "missing";
|
||||
}>;
|
||||
}, z.core.$strip>;
|
||||
declare const topologyEdgeSchema: z.ZodObject<{
|
||||
id: z.ZodString;
|
||||
aliasName: z.ZodString;
|
||||
purpose: z.ZodEnum<{
|
||||
custom: "custom";
|
||||
ix: "ix";
|
||||
geo: "geo";
|
||||
backup: "backup";
|
||||
admin: "admin";
|
||||
}>;
|
||||
fromNodeId: z.ZodString;
|
||||
toHostname: z.ZodString;
|
||||
}, z.core.$strip>;
|
||||
declare const topologySchema: z.ZodObject<{
|
||||
nodes: z.ZodArray<z.ZodObject<{
|
||||
id: z.ZodString;
|
||||
hostname: z.ZodString;
|
||||
role: z.ZodEnum<{
|
||||
hub: "hub";
|
||||
gw: "gw";
|
||||
edge: "edge";
|
||||
ix: "ix";
|
||||
}>;
|
||||
locationCode: z.ZodString;
|
||||
ipv4: z.ZodNullable<z.ZodString>;
|
||||
syncStatus: z.ZodEnum<{
|
||||
error: "error";
|
||||
pending: "pending";
|
||||
ok: "ok";
|
||||
drift: "drift";
|
||||
missing: "missing";
|
||||
}>;
|
||||
}, z.core.$strip>>;
|
||||
edges: z.ZodArray<z.ZodObject<{
|
||||
id: z.ZodString;
|
||||
aliasName: z.ZodString;
|
||||
purpose: z.ZodEnum<{
|
||||
custom: "custom";
|
||||
ix: "ix";
|
||||
geo: "geo";
|
||||
backup: "backup";
|
||||
admin: "admin";
|
||||
}>;
|
||||
fromNodeId: z.ZodString;
|
||||
toHostname: z.ZodString;
|
||||
}, z.core.$strip>>;
|
||||
locations: z.ZodArray<z.ZodObject<{
|
||||
id: z.ZodString;
|
||||
code: z.ZodString;
|
||||
name: z.ZodString;
|
||||
country: z.ZodNullable<z.ZodString>;
|
||||
sortOrder: z.ZodNumber;
|
||||
}, z.core.$strip>>;
|
||||
}, z.core.$strip>;
|
||||
type Topology = z.infer<typeof topologySchema>;
|
||||
declare const bindExportSchema: z.ZodObject<{
|
||||
zoneName: z.ZodString;
|
||||
content: z.ZodString;
|
||||
}, z.core.$strip>;
|
||||
type BindExport = z.infer<typeof bindExportSchema>;
|
||||
declare const orphanIgnoreSchema: z.ZodObject<{
|
||||
recordName: z.ZodString;
|
||||
recordType: z.ZodString;
|
||||
}, z.core.$strip>;
|
||||
|
||||
declare class ValidationError extends Error {
|
||||
constructor(message: string);
|
||||
}
|
||||
|
||||
export { type Alias, type AliasCreate, type AliasMode, type AliasPatch, type AliasPurpose, type AliasRetarget, type AppSettings, type AppSettingsPatch, type AppSwitcherConfig, type AppSwitcherEntry, type BindExport, type CfDnsRecord, type CfZone, type CreateDnsRecordPayload, type DashboardStats, type JwtClaims, type Location, type LoginInput, type LoginRequest, type LoginResponse, type Node, type NodeAddress, type NodeCreate, type NodePatch, type NodeRole, type PatchDnsRecordPayload, type SyncApply, type SyncDiffOp, type SyncJob, type SyncStatus, type Topology, ValidationError, type Zone, type ZoneCreate, type ZonePatch, addressFamilySchema, aliasCreateSchema, aliasModeSchema, aliasPatchSchema, aliasPurposeSchema, aliasRetargetSchema, aliasSchema, appSettingsPatchSchema, appSettingsSchema, appSwitcherConfigSchema, appSwitcherEntrySchema, appSwitcherIconSchema, bindExportSchema, cfDnsRecordSchema, cfZoneSchema, createDnsRecordPayloadSchema, dashboardStatsSchema, locationSchema, loginSchema, nodeAddressSchema, nodeCreateSchema, nodePatchSchema, nodeRoleSchema, nodeSchema, orphanIgnoreSchema, patchDnsRecordPayloadSchema, syncApplySchema, syncDiffOpSchema, syncJobSchema, syncStatusSchema, topologyEdgeSchema, topologyNodeSchema, topologySchema, zoneCreateSchema, zonePatchSchema, zoneSchema };
|
||||
|
||||
Vendored
+263
-3
@@ -29,17 +29,277 @@ var loginSchema = z2.object({
|
||||
password: z2.string().min(1)
|
||||
});
|
||||
var appSettingsPatchSchema = z2.object({
|
||||
showQuickActions: z2.boolean().optional()
|
||||
showQuickActions: z2.boolean().optional(),
|
||||
defaultTtl: z2.number().int().min(60).max(86400).optional(),
|
||||
namingTemplate: z2.string().min(1).optional(),
|
||||
proxiedLock: z2.boolean().optional()
|
||||
});
|
||||
var appSettingsSchema = z2.object({
|
||||
id: z2.string(),
|
||||
showQuickActions: z2.boolean()
|
||||
showQuickActions: z2.boolean(),
|
||||
defaultTtl: z2.number(),
|
||||
namingTemplate: z2.string(),
|
||||
proxiedLock: z2.boolean(),
|
||||
cloudflareConfigured: z2.boolean().optional()
|
||||
});
|
||||
|
||||
// src/fleet.ts
|
||||
import { z as z3 } from "zod";
|
||||
var nodeRoleSchema = z3.enum(["hub", "gw", "edge", "ix"]);
|
||||
var aliasPurposeSchema = z3.enum([
|
||||
"geo",
|
||||
"ix",
|
||||
"backup",
|
||||
"admin",
|
||||
"custom"
|
||||
]);
|
||||
var aliasModeSchema = z3.enum(["primary", "pair"]);
|
||||
var syncStatusSchema = z3.enum([
|
||||
"pending",
|
||||
"ok",
|
||||
"drift",
|
||||
"missing",
|
||||
"error"
|
||||
]);
|
||||
var addressFamilySchema = z3.enum(["v4", "v6"]);
|
||||
var cfZoneSchema = z3.object({
|
||||
id: z3.string(),
|
||||
name: z3.string(),
|
||||
status: z3.string().optional()
|
||||
});
|
||||
var cfDnsRecordSchema = z3.object({
|
||||
id: z3.string().optional(),
|
||||
type: z3.string(),
|
||||
name: z3.string(),
|
||||
content: z3.string(),
|
||||
ttl: z3.number(),
|
||||
proxied: z3.boolean().optional(),
|
||||
priority: z3.number().optional()
|
||||
});
|
||||
var createDnsRecordPayloadSchema = z3.object({
|
||||
type: z3.string(),
|
||||
name: z3.string(),
|
||||
content: z3.string(),
|
||||
ttl: z3.number(),
|
||||
proxied: z3.boolean().optional(),
|
||||
priority: z3.number().optional()
|
||||
});
|
||||
var patchDnsRecordPayloadSchema = createDnsRecordPayloadSchema.partial();
|
||||
var locationSchema = z3.object({
|
||||
id: z3.string(),
|
||||
code: z3.string(),
|
||||
name: z3.string(),
|
||||
country: z3.string().nullable(),
|
||||
sortOrder: z3.number()
|
||||
});
|
||||
var zoneSchema = z3.object({
|
||||
id: z3.string(),
|
||||
cfZoneId: z3.string().nullable(),
|
||||
name: z3.string(),
|
||||
role: z3.string(),
|
||||
namingTemplate: z3.string(),
|
||||
defaultTtl: z3.number(),
|
||||
lastSyncAt: z3.string().nullable(),
|
||||
createdAt: z3.string(),
|
||||
updatedAt: z3.string()
|
||||
});
|
||||
var zoneCreateSchema = z3.object({
|
||||
name: z3.string().min(1),
|
||||
cfZoneId: z3.string().optional().nullable(),
|
||||
role: z3.string().default("routing"),
|
||||
namingTemplate: z3.string().optional(),
|
||||
defaultTtl: z3.number().int().min(60).max(86400).optional()
|
||||
});
|
||||
var zonePatchSchema = zoneCreateSchema.partial();
|
||||
var nodeAddressSchema = z3.object({
|
||||
id: z3.string(),
|
||||
family: addressFamilySchema,
|
||||
ip: z3.string()
|
||||
});
|
||||
var nodeSchema = z3.object({
|
||||
id: z3.string(),
|
||||
zoneId: z3.string(),
|
||||
locationId: z3.string(),
|
||||
locationCode: z3.string().optional(),
|
||||
locationName: z3.string().optional(),
|
||||
hostname: z3.string(),
|
||||
role: nodeRoleSchema,
|
||||
indexNum: z3.number(),
|
||||
providerTag: z3.string().nullable(),
|
||||
notes: z3.string().nullable(),
|
||||
syncStatus: syncStatusSchema,
|
||||
cfARecordId: z3.string().nullable(),
|
||||
cfAaaaRecordId: z3.string().nullable(),
|
||||
lastError: z3.string().nullable(),
|
||||
addresses: z3.array(nodeAddressSchema).default([]),
|
||||
aliasCount: z3.number().optional(),
|
||||
createdAt: z3.string(),
|
||||
updatedAt: z3.string()
|
||||
});
|
||||
var nodeCreateSchema = z3.object({
|
||||
zoneId: z3.string().min(1),
|
||||
locationId: z3.string().min(1),
|
||||
role: nodeRoleSchema,
|
||||
indexNum: z3.number().int().min(1).max(99).default(1),
|
||||
hostname: z3.string().min(1).optional(),
|
||||
providerTag: z3.string().optional().nullable(),
|
||||
notes: z3.string().optional().nullable(),
|
||||
ipv4: z3.string().min(1),
|
||||
ipv6: z3.string().optional().nullable()
|
||||
});
|
||||
var nodePatchSchema = z3.object({
|
||||
locationId: z3.string().optional(),
|
||||
role: nodeRoleSchema.optional(),
|
||||
indexNum: z3.number().int().min(1).max(99).optional(),
|
||||
hostname: z3.string().min(1).optional(),
|
||||
providerTag: z3.string().optional().nullable(),
|
||||
notes: z3.string().optional().nullable(),
|
||||
ipv4: z3.string().min(1).optional(),
|
||||
ipv6: z3.string().optional().nullable()
|
||||
});
|
||||
var aliasSchema = z3.object({
|
||||
id: z3.string(),
|
||||
zoneId: z3.string(),
|
||||
name: z3.string(),
|
||||
purpose: aliasPurposeSchema,
|
||||
mode: aliasModeSchema,
|
||||
targetNodeId: z3.string(),
|
||||
targetHostname: z3.string().optional(),
|
||||
syncStatus: syncStatusSchema,
|
||||
cfRecordId: z3.string().nullable(),
|
||||
lastError: z3.string().nullable(),
|
||||
createdAt: z3.string(),
|
||||
updatedAt: z3.string()
|
||||
});
|
||||
var aliasCreateSchema = z3.object({
|
||||
zoneId: z3.string().min(1),
|
||||
name: z3.string().min(1),
|
||||
purpose: aliasPurposeSchema.default("geo"),
|
||||
mode: aliasModeSchema.default("primary"),
|
||||
targetNodeId: z3.string().min(1)
|
||||
});
|
||||
var aliasPatchSchema = z3.object({
|
||||
name: z3.string().min(1).optional(),
|
||||
purpose: aliasPurposeSchema.optional(),
|
||||
mode: aliasModeSchema.optional(),
|
||||
targetNodeId: z3.string().min(1).optional()
|
||||
});
|
||||
var aliasRetargetSchema = z3.object({
|
||||
targetNodeId: z3.string().min(1)
|
||||
});
|
||||
var syncDiffOpSchema = z3.object({
|
||||
id: z3.string(),
|
||||
kind: z3.enum([
|
||||
"create",
|
||||
"update",
|
||||
"delete",
|
||||
"orphan",
|
||||
"proxy_violation",
|
||||
"noop"
|
||||
]),
|
||||
entityType: z3.enum(["node_a", "node_aaaa", "alias", "orphan"]),
|
||||
entityId: z3.string().nullable(),
|
||||
recordName: z3.string(),
|
||||
recordType: z3.string(),
|
||||
desired: z3.record(z3.string(), z3.unknown()).nullable(),
|
||||
observed: z3.record(z3.string(), z3.unknown()).nullable(),
|
||||
detail: z3.string().optional()
|
||||
});
|
||||
var syncJobSchema = z3.object({
|
||||
id: z3.string(),
|
||||
zoneId: z3.string(),
|
||||
status: z3.enum(["pending", "running", "done", "failed"]),
|
||||
diff: z3.array(syncDiffOpSchema).optional(),
|
||||
error: z3.string().nullable(),
|
||||
createdAt: z3.string(),
|
||||
finishedAt: z3.string().nullable()
|
||||
});
|
||||
var syncApplySchema = z3.object({
|
||||
opIds: z3.array(z3.string()).optional()
|
||||
});
|
||||
var dashboardStatsSchema = z3.object({
|
||||
nodes: z3.number(),
|
||||
aliases: z3.number(),
|
||||
syncOk: z3.number(),
|
||||
drift: z3.number(),
|
||||
proxyViolations: z3.number(),
|
||||
orphans: z3.number(),
|
||||
lastSyncAt: z3.string().nullable(),
|
||||
nodesWithoutIp: z3.number(),
|
||||
brokenAliases: z3.number()
|
||||
});
|
||||
var topologyNodeSchema = z3.object({
|
||||
id: z3.string(),
|
||||
hostname: z3.string(),
|
||||
role: nodeRoleSchema,
|
||||
locationCode: z3.string(),
|
||||
ipv4: z3.string().nullable(),
|
||||
syncStatus: syncStatusSchema
|
||||
});
|
||||
var topologyEdgeSchema = z3.object({
|
||||
id: z3.string(),
|
||||
aliasName: z3.string(),
|
||||
purpose: aliasPurposeSchema,
|
||||
fromNodeId: z3.string(),
|
||||
toHostname: z3.string()
|
||||
});
|
||||
var topologySchema = z3.object({
|
||||
nodes: z3.array(topologyNodeSchema),
|
||||
edges: z3.array(topologyEdgeSchema),
|
||||
locations: z3.array(locationSchema)
|
||||
});
|
||||
var bindExportSchema = z3.object({
|
||||
zoneName: z3.string(),
|
||||
content: z3.string()
|
||||
});
|
||||
var orphanIgnoreSchema = z3.object({
|
||||
recordName: z3.string().min(1),
|
||||
recordType: z3.string().min(1)
|
||||
});
|
||||
|
||||
// src/index.ts
|
||||
var ValidationError = class extends Error {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.name = "ValidationError";
|
||||
}
|
||||
};
|
||||
export {
|
||||
ValidationError,
|
||||
addressFamilySchema,
|
||||
aliasCreateSchema,
|
||||
aliasModeSchema,
|
||||
aliasPatchSchema,
|
||||
aliasPurposeSchema,
|
||||
aliasRetargetSchema,
|
||||
aliasSchema,
|
||||
appSettingsPatchSchema,
|
||||
appSettingsSchema,
|
||||
appSwitcherConfigSchema,
|
||||
appSwitcherEntrySchema,
|
||||
appSwitcherIconSchema,
|
||||
loginSchema
|
||||
bindExportSchema,
|
||||
cfDnsRecordSchema,
|
||||
cfZoneSchema,
|
||||
createDnsRecordPayloadSchema,
|
||||
dashboardStatsSchema,
|
||||
locationSchema,
|
||||
loginSchema,
|
||||
nodeAddressSchema,
|
||||
nodeCreateSchema,
|
||||
nodePatchSchema,
|
||||
nodeRoleSchema,
|
||||
nodeSchema,
|
||||
orphanIgnoreSchema,
|
||||
patchDnsRecordPayloadSchema,
|
||||
syncApplySchema,
|
||||
syncDiffOpSchema,
|
||||
syncJobSchema,
|
||||
syncStatusSchema,
|
||||
topologyEdgeSchema,
|
||||
topologyNodeSchema,
|
||||
topologySchema,
|
||||
zoneCreateSchema,
|
||||
zonePatchSchema,
|
||||
zoneSchema
|
||||
};
|
||||
|
||||
@@ -0,0 +1,268 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const nodeRoleSchema = z.enum(["hub", "gw", "edge", "ix"]);
|
||||
export type NodeRole = z.infer<typeof nodeRoleSchema>;
|
||||
|
||||
export const aliasPurposeSchema = z.enum([
|
||||
"geo",
|
||||
"ix",
|
||||
"backup",
|
||||
"admin",
|
||||
"custom",
|
||||
]);
|
||||
export type AliasPurpose = z.infer<typeof aliasPurposeSchema>;
|
||||
|
||||
export const aliasModeSchema = z.enum(["primary", "pair"]);
|
||||
export type AliasMode = z.infer<typeof aliasModeSchema>;
|
||||
|
||||
export const syncStatusSchema = z.enum([
|
||||
"pending",
|
||||
"ok",
|
||||
"drift",
|
||||
"missing",
|
||||
"error",
|
||||
]);
|
||||
export type SyncStatus = z.infer<typeof syncStatusSchema>;
|
||||
|
||||
export const addressFamilySchema = z.enum(["v4", "v6"]);
|
||||
|
||||
export const cfZoneSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
status: z.string().optional(),
|
||||
});
|
||||
export type CfZone = z.infer<typeof cfZoneSchema>;
|
||||
|
||||
export const cfDnsRecordSchema = z.object({
|
||||
id: z.string().optional(),
|
||||
type: z.string(),
|
||||
name: z.string(),
|
||||
content: z.string(),
|
||||
ttl: z.number(),
|
||||
proxied: z.boolean().optional(),
|
||||
priority: z.number().optional(),
|
||||
});
|
||||
export type CfDnsRecord = z.infer<typeof cfDnsRecordSchema>;
|
||||
|
||||
export const createDnsRecordPayloadSchema = z.object({
|
||||
type: z.string(),
|
||||
name: z.string(),
|
||||
content: z.string(),
|
||||
ttl: z.number(),
|
||||
proxied: z.boolean().optional(),
|
||||
priority: z.number().optional(),
|
||||
});
|
||||
export type CreateDnsRecordPayload = z.infer<typeof createDnsRecordPayloadSchema>;
|
||||
|
||||
export const patchDnsRecordPayloadSchema = createDnsRecordPayloadSchema.partial();
|
||||
export type PatchDnsRecordPayload = z.infer<typeof patchDnsRecordPayloadSchema>;
|
||||
|
||||
export const locationSchema = z.object({
|
||||
id: z.string(),
|
||||
code: z.string(),
|
||||
name: z.string(),
|
||||
country: z.string().nullable(),
|
||||
sortOrder: z.number(),
|
||||
});
|
||||
export type Location = z.infer<typeof locationSchema>;
|
||||
|
||||
export const zoneSchema = z.object({
|
||||
id: z.string(),
|
||||
cfZoneId: z.string().nullable(),
|
||||
name: z.string(),
|
||||
role: z.string(),
|
||||
namingTemplate: z.string(),
|
||||
defaultTtl: z.number(),
|
||||
lastSyncAt: z.string().nullable(),
|
||||
createdAt: z.string(),
|
||||
updatedAt: z.string(),
|
||||
});
|
||||
export type Zone = z.infer<typeof zoneSchema>;
|
||||
|
||||
export const zoneCreateSchema = z.object({
|
||||
name: z.string().min(1),
|
||||
cfZoneId: z.string().optional().nullable(),
|
||||
role: z.string().default("routing"),
|
||||
namingTemplate: z.string().optional(),
|
||||
defaultTtl: z.number().int().min(60).max(86400).optional(),
|
||||
});
|
||||
export type ZoneCreate = z.infer<typeof zoneCreateSchema>;
|
||||
|
||||
export const zonePatchSchema = zoneCreateSchema.partial();
|
||||
export type ZonePatch = z.infer<typeof zonePatchSchema>;
|
||||
|
||||
export const nodeAddressSchema = z.object({
|
||||
id: z.string(),
|
||||
family: addressFamilySchema,
|
||||
ip: z.string(),
|
||||
});
|
||||
export type NodeAddress = z.infer<typeof nodeAddressSchema>;
|
||||
|
||||
export const nodeSchema = z.object({
|
||||
id: z.string(),
|
||||
zoneId: z.string(),
|
||||
locationId: z.string(),
|
||||
locationCode: z.string().optional(),
|
||||
locationName: z.string().optional(),
|
||||
hostname: z.string(),
|
||||
role: nodeRoleSchema,
|
||||
indexNum: z.number(),
|
||||
providerTag: z.string().nullable(),
|
||||
notes: z.string().nullable(),
|
||||
syncStatus: syncStatusSchema,
|
||||
cfARecordId: z.string().nullable(),
|
||||
cfAaaaRecordId: z.string().nullable(),
|
||||
lastError: z.string().nullable(),
|
||||
addresses: z.array(nodeAddressSchema).default([]),
|
||||
aliasCount: z.number().optional(),
|
||||
createdAt: z.string(),
|
||||
updatedAt: z.string(),
|
||||
});
|
||||
export type Node = z.infer<typeof nodeSchema>;
|
||||
|
||||
export const nodeCreateSchema = z.object({
|
||||
zoneId: z.string().min(1),
|
||||
locationId: z.string().min(1),
|
||||
role: nodeRoleSchema,
|
||||
indexNum: z.number().int().min(1).max(99).default(1),
|
||||
hostname: z.string().min(1).optional(),
|
||||
providerTag: z.string().optional().nullable(),
|
||||
notes: z.string().optional().nullable(),
|
||||
ipv4: z.string().min(1),
|
||||
ipv6: z.string().optional().nullable(),
|
||||
});
|
||||
export type NodeCreate = z.infer<typeof nodeCreateSchema>;
|
||||
|
||||
export const nodePatchSchema = z.object({
|
||||
locationId: z.string().optional(),
|
||||
role: nodeRoleSchema.optional(),
|
||||
indexNum: z.number().int().min(1).max(99).optional(),
|
||||
hostname: z.string().min(1).optional(),
|
||||
providerTag: z.string().optional().nullable(),
|
||||
notes: z.string().optional().nullable(),
|
||||
ipv4: z.string().min(1).optional(),
|
||||
ipv6: z.string().optional().nullable(),
|
||||
});
|
||||
export type NodePatch = z.infer<typeof nodePatchSchema>;
|
||||
|
||||
export const aliasSchema = z.object({
|
||||
id: z.string(),
|
||||
zoneId: z.string(),
|
||||
name: z.string(),
|
||||
purpose: aliasPurposeSchema,
|
||||
mode: aliasModeSchema,
|
||||
targetNodeId: z.string(),
|
||||
targetHostname: z.string().optional(),
|
||||
syncStatus: syncStatusSchema,
|
||||
cfRecordId: z.string().nullable(),
|
||||
lastError: z.string().nullable(),
|
||||
createdAt: z.string(),
|
||||
updatedAt: z.string(),
|
||||
});
|
||||
export type Alias = z.infer<typeof aliasSchema>;
|
||||
|
||||
export const aliasCreateSchema = z.object({
|
||||
zoneId: z.string().min(1),
|
||||
name: z.string().min(1),
|
||||
purpose: aliasPurposeSchema.default("geo"),
|
||||
mode: aliasModeSchema.default("primary"),
|
||||
targetNodeId: z.string().min(1),
|
||||
});
|
||||
export type AliasCreate = z.infer<typeof aliasCreateSchema>;
|
||||
|
||||
export const aliasPatchSchema = z.object({
|
||||
name: z.string().min(1).optional(),
|
||||
purpose: aliasPurposeSchema.optional(),
|
||||
mode: aliasModeSchema.optional(),
|
||||
targetNodeId: z.string().min(1).optional(),
|
||||
});
|
||||
export type AliasPatch = z.infer<typeof aliasPatchSchema>;
|
||||
|
||||
export const aliasRetargetSchema = z.object({
|
||||
targetNodeId: z.string().min(1),
|
||||
});
|
||||
export type AliasRetarget = z.infer<typeof aliasRetargetSchema>;
|
||||
|
||||
export const syncDiffOpSchema = z.object({
|
||||
id: z.string(),
|
||||
kind: z.enum([
|
||||
"create",
|
||||
"update",
|
||||
"delete",
|
||||
"orphan",
|
||||
"proxy_violation",
|
||||
"noop",
|
||||
]),
|
||||
entityType: z.enum(["node_a", "node_aaaa", "alias", "orphan"]),
|
||||
entityId: z.string().nullable(),
|
||||
recordName: z.string(),
|
||||
recordType: z.string(),
|
||||
desired: z.record(z.string(), z.unknown()).nullable(),
|
||||
observed: z.record(z.string(), z.unknown()).nullable(),
|
||||
detail: z.string().optional(),
|
||||
});
|
||||
export type SyncDiffOp = z.infer<typeof syncDiffOpSchema>;
|
||||
|
||||
export const syncJobSchema = z.object({
|
||||
id: z.string(),
|
||||
zoneId: z.string(),
|
||||
status: z.enum(["pending", "running", "done", "failed"]),
|
||||
diff: z.array(syncDiffOpSchema).optional(),
|
||||
error: z.string().nullable(),
|
||||
createdAt: z.string(),
|
||||
finishedAt: z.string().nullable(),
|
||||
});
|
||||
export type SyncJob = z.infer<typeof syncJobSchema>;
|
||||
|
||||
export const syncApplySchema = z.object({
|
||||
opIds: z.array(z.string()).optional(),
|
||||
});
|
||||
export type SyncApply = z.infer<typeof syncApplySchema>;
|
||||
|
||||
export const dashboardStatsSchema = z.object({
|
||||
nodes: z.number(),
|
||||
aliases: z.number(),
|
||||
syncOk: z.number(),
|
||||
drift: z.number(),
|
||||
proxyViolations: z.number(),
|
||||
orphans: z.number(),
|
||||
lastSyncAt: z.string().nullable(),
|
||||
nodesWithoutIp: z.number(),
|
||||
brokenAliases: z.number(),
|
||||
});
|
||||
export type DashboardStats = z.infer<typeof dashboardStatsSchema>;
|
||||
|
||||
export const topologyNodeSchema = z.object({
|
||||
id: z.string(),
|
||||
hostname: z.string(),
|
||||
role: nodeRoleSchema,
|
||||
locationCode: z.string(),
|
||||
ipv4: z.string().nullable(),
|
||||
syncStatus: syncStatusSchema,
|
||||
});
|
||||
|
||||
export const topologyEdgeSchema = z.object({
|
||||
id: z.string(),
|
||||
aliasName: z.string(),
|
||||
purpose: aliasPurposeSchema,
|
||||
fromNodeId: z.string(),
|
||||
toHostname: z.string(),
|
||||
});
|
||||
|
||||
export const topologySchema = z.object({
|
||||
nodes: z.array(topologyNodeSchema),
|
||||
edges: z.array(topologyEdgeSchema),
|
||||
locations: z.array(locationSchema),
|
||||
});
|
||||
export type Topology = z.infer<typeof topologySchema>;
|
||||
|
||||
export const bindExportSchema = z.object({
|
||||
zoneName: z.string(),
|
||||
content: z.string(),
|
||||
});
|
||||
export type BindExport = z.infer<typeof bindExportSchema>;
|
||||
|
||||
export const orphanIgnoreSchema = z.object({
|
||||
recordName: z.string().min(1),
|
||||
recordType: z.string().min(1),
|
||||
});
|
||||
@@ -1,2 +1,10 @@
|
||||
export * from "./app-switcher.js";
|
||||
export * from "./settings.js";
|
||||
export * from "./fleet.js";
|
||||
|
||||
export class ValidationError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = "ValidationError";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,9 @@ export type LoginResponse = {
|
||||
|
||||
export const appSettingsPatchSchema = z.object({
|
||||
showQuickActions: z.boolean().optional(),
|
||||
defaultTtl: z.number().int().min(60).max(86400).optional(),
|
||||
namingTemplate: z.string().min(1).optional(),
|
||||
proxiedLock: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export type AppSettingsPatch = z.infer<typeof appSettingsPatchSchema>;
|
||||
@@ -32,6 +35,10 @@ export type AppSettingsPatch = z.infer<typeof appSettingsPatchSchema>;
|
||||
export const appSettingsSchema = z.object({
|
||||
id: z.string(),
|
||||
showQuickActions: z.boolean(),
|
||||
defaultTtl: z.number(),
|
||||
namingTemplate: z.string(),
|
||||
proxiedLock: z.boolean(),
|
||||
cloudflareConfigured: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export type AppSettings = z.infer<typeof appSettingsSchema>;
|
||||
|
||||
Reference in New Issue
Block a user