feat:Update pnpm-lock.yaml to link shared package; modify API routes to utilize new schemas for domain and service management; enhance DNS record handling with CNAME support; refactor service and subdomain routes for improved functionality; implement confirm dialog for domain deletion in the frontend; clean up unused components and improve UI consistency.
Build, Test, and Push CFDM Docker Image / test (push) Failing after 51s
Build, Test, and Push CFDM Docker Image / build-and-push (push) Has been skipped
Build, Test, and Push CFDM Docker Image / update-wiki (push) Has been skipped
Build, Test, and Push CFDM Docker Image / create-release (push) Has been skipped

This commit is contained in:
Denozordec committed 2026-06-22 13:52:33 +07:00
1 parent 4a70cf5854
commit 6d1d8ca4f3
99 files changed
+6692 -1483

No files matched your search

+1 -1
View File
@@ -2,4 +2,4 @@ export * from "./schema.js";
export * from "./client.js";
export * from "./errors.js";
export * as repos from "./repos.js";
export type { DnsListFilter } from "./repos.js";
export type { DnsListFilter, UpdateSubdomainPatch } from "./repos.js";
+209 -20
View File
@@ -12,7 +12,8 @@ import type {
Subdomain,
SyncJob,
} from "@cfdm/shared";
import { and, asc, count, eq, isNull, like, or, sql } from "drizzle-orm";
import { dnsRecordNamesMatch } from "@cfdm/shared";
import { and, asc, count, eq, isNull, like, notInArray, or, sql } from "drizzle-orm";
import type { Db } from "./client.js";
import { NotFoundError } from "./errors.js";
import {
@@ -158,14 +159,24 @@ export function updateDomain(
id: number,
groupId: number | null,
status: string,
certMonitoring?: string,
): Domain {
const updates: {
group_id: number | null;
status: string;
cert_monitoring?: string;
updated_at: ReturnType<typeof sql>;
} = {
group_id: groupId,
status,
updated_at: sql`datetime('now')`,
};
if (certMonitoring !== undefined) {
updates.cert_monitoring = certMonitoring;
}
const result = db
.update(domains)
.set({
group_id: groupId,
status,
updated_at: sql`datetime('now')`,
})
.set(updates)
.where(eq(domains.id, id))
.run();
if (result.changes === 0) throw new NotFoundError(`domain ${id}`);
@@ -208,6 +219,19 @@ export function getSubdomain(db: Db, id: number): Subdomain {
return row as Subdomain;
}
export function findSubdomainByDomainAndName(
db: Db,
domainId: number,
name: string,
): Subdomain | null {
const row = db
.select()
.from(subdomains)
.where(and(eq(subdomains.domain_id, domainId), eq(subdomains.name, name)))
.get();
return (row as Subdomain) ?? null;
}
export function upsertSubdomain(
db: Db,
domainId: number,
@@ -237,15 +261,36 @@ export function createSubdomain(
return getSubdomain(db, id);
}
export interface UpdateSubdomainPatch {
name?: string;
fqdn?: string;
enabled?: boolean;
cert_monitoring?: string;
}
export function updateSubdomain(
db: Db,
id: number,
name: string,
fqdn: string,
patch: UpdateSubdomainPatch,
): Subdomain {
const updates: {
name?: string;
fqdn?: string;
enabled?: boolean;
cert_monitoring?: string;
updated_at: ReturnType<typeof sql>;
} = { updated_at: sql`datetime('now')` };
if (patch.name !== undefined) updates.name = patch.name;
if (patch.fqdn !== undefined) updates.fqdn = patch.fqdn;
if (patch.enabled !== undefined) updates.enabled = patch.enabled;
if (patch.cert_monitoring !== undefined) {
updates.cert_monitoring = patch.cert_monitoring;
}
const result = db
.update(subdomains)
.set({ name, fqdn, updated_at: sql`datetime('now')` })
.set(updates)
.where(eq(subdomains.id, id))
.run();
if (result.changes === 0) throw new NotFoundError(`subdomain ${id}`);
@@ -448,8 +493,25 @@ export function markDnsPendingDelete(db: Db, id: number): void {
// --- Services ---
function maxSortOrderInGroup(db: Db, groupId: number | null): number {
const condition =
groupId === null
? isNull(services.service_group_id)
: eq(services.service_group_id, groupId);
const row = db
.select({ maxOrder: sql<number>`coalesce(max(${services.sort_order}), -1)` })
.from(services)
.where(condition)
.get();
return row?.maxOrder ?? -1;
}
export function listServices(db: Db): Service[] {
return db.select().from(services).orderBy(asc(services.name)).all() as Service[];
return db
.select()
.from(services)
.orderBy(asc(services.sort_order), asc(services.name))
.all() as Service[];
}
export function listServicesByGroup(db: Db, groupId: number): Service[] {
@@ -457,7 +519,7 @@ export function listServicesByGroup(db: Db, groupId: number): Service[] {
.select()
.from(services)
.where(eq(services.service_group_id, groupId))
.orderBy(asc(services.name))
.orderBy(asc(services.sort_order), asc(services.name))
.all() as Service[];
}
@@ -466,7 +528,7 @@ export function listUngroupedServices(db: Db): Service[] {
.select()
.from(services)
.where(isNull(services.service_group_id))
.orderBy(asc(services.name))
.orderBy(asc(services.sort_order), asc(services.name))
.all() as Service[];
}
@@ -477,9 +539,10 @@ export function getService(db: Db, id: number): Service {
}
export function createService(db: Db, name: string, slug: string): Service {
const sortOrder = maxSortOrderInGroup(db, null) + 1;
const id = db
.insert(services)
.values({ name, slug, subdomain: slug })
.values({ name, slug, subdomain: slug, sort_order: sortOrder })
.returning({ id: services.id })
.get()!.id;
return getService(db, id);
@@ -520,12 +583,56 @@ export function setServiceGroup(
id: number,
groupId: number | null,
): void {
const sortOrder = maxSortOrderInGroup(db, groupId) + 1;
db.update(services)
.set({ service_group_id: groupId, updated_at: sql`datetime('now')` })
.set({
service_group_id: groupId,
sort_order: sortOrder,
updated_at: sql`datetime('now')`,
})
.where(eq(services.id, id))
.run();
}
export function reorderServices(
db: Db,
groupId: number | null,
orderedIds: number[],
): void {
const uniqueIds = new Set(orderedIds);
if (uniqueIds.size !== orderedIds.length) {
throw new Error("duplicate service ids in reorder request");
}
const condition =
groupId === null
? isNull(services.service_group_id)
: eq(services.service_group_id, groupId);
const existing = db
.select({ id: services.id })
.from(services)
.where(condition)
.all()
.map((row) => row.id);
const existingSet = new Set(existing);
for (const serviceId of orderedIds) {
if (!existingSet.has(serviceId)) {
throw new NotFoundError(`service ${serviceId} not in group`);
}
}
db.transaction((tx) => {
for (let index = 0; index < orderedIds.length; index++) {
tx.update(services)
.set({ sort_order: index, updated_at: sql`datetime('now')` })
.where(eq(services.id, orderedIds[index]!))
.run();
}
});
}
export function deleteService(db: Db, id: number): void {
const result = db.delete(services).where(eq(services.id, id)).run();
if (result.changes === 0) throw new NotFoundError(`service ${id}`);
@@ -668,6 +775,20 @@ export function replaceBindingIps(
}
}
export function setBindingCnameTarget(
db: Db,
bindingId: number,
target: string | null,
): void {
db.update(serviceBindings)
.set({
cname_target: target,
updated_at: sql`datetime('now')`,
})
.where(eq(serviceBindings.id, bindingId))
.run();
}
// --- Service Binding Records ---
export function listRecordsForBinding(db: Db, bindingId: number): DnsRecord[] {
@@ -744,8 +865,61 @@ export function unlinkGroupDnsRecord(
// --- Service Bindings ---
function dnsRecordMatchesHostname(
recordName: string,
hostname: string,
zoneName: string,
): boolean {
return dnsRecordNamesMatch(recordName, hostname, zoneName);
}
function enrichServiceBindingView(
db: Db,
row: Omit<ServiceBindingView, "target_ips"> & { target_ips?: string[] },
): ServiceBindingView {
const configured = listBindingIps(db, row.id);
const linkedRecords = listRecordsForBinding(db, row.id);
const linkedIps = linkedRecords
.filter((record) => record.record_type.toUpperCase() === "A")
.map((record) => record.content);
const target_ips = [
...new Set([
...configured,
...linkedIps,
...(row.target_ip ? [row.target_ip] : []),
]),
].sort();
if (target_ips.length === 0) {
for (const record of listDnsByDomain(db, row.domain_id)) {
if (record.record_type.toUpperCase() !== "A") continue;
if (!dnsRecordMatchesHostname(record.name, row.hostname, row.zone_name)) {
continue;
}
if (!target_ips.includes(record.content)) {
target_ips.push(record.content);
}
}
target_ips.sort();
}
const sync_status =
row.sync_status ??
linkedRecords.find((record) => record.sync_status)?.sync_status ??
null;
return {
...row,
target_ips,
target_ip: target_ips[0] ?? null,
sync_status,
};
}
export function listAllBindings(db: Db): ServiceBindingView[] {
return db.all<ServiceBindingView>(sql`
return db
.all<Omit<ServiceBindingView, "target_ips">>(sql`
SELECT sb.id, sb.domain_id, sb.service_id, sb.hostname, sb.dns_record_id,
d.zone_name, d.group_id, g.name AS group_name,
s.name AS service_name, s.slug AS service_slug,
@@ -757,11 +931,13 @@ export function listAllBindings(db: Db): ServiceBindingView[] {
JOIN services s ON s.id = sb.service_id
LEFT JOIN dns_records dr ON dr.id = sb.dns_record_id
ORDER BY d.zone_name, s.name
`);
`)
.map((row) => enrichServiceBindingView(db, row));
}
export function listBindingsByDomain(db: Db, domainId: number): ServiceBindingView[] {
return db.all<ServiceBindingView>(sql`
return db
.all<Omit<ServiceBindingView, "target_ips">>(sql`
SELECT sb.id, sb.domain_id, sb.service_id, sb.hostname, sb.dns_record_id,
d.zone_name, d.group_id, g.name AS group_name,
s.name AS service_name, s.slug AS service_slug,
@@ -774,7 +950,8 @@ export function listBindingsByDomain(db: Db, domainId: number): ServiceBindingVi
LEFT JOIN dns_records dr ON dr.id = sb.dns_record_id
WHERE sb.domain_id = ${domainId}
ORDER BY s.name
`);
`)
.map((row) => enrichServiceBindingView(db, row));
}
export function listBindingsByService(db: Db, serviceId: number): Array<ServiceBinding & { zone_name: string }> {
@@ -796,7 +973,7 @@ export function getBinding(db: Db, id: number): ServiceBinding {
}
export function getBindingView(db: Db, id: number): ServiceBindingView {
const rows = db.all<ServiceBindingView>(sql`
const rows = db.all<Omit<ServiceBindingView, "target_ips">>(sql`
SELECT sb.id, sb.domain_id, sb.service_id, sb.hostname, sb.dns_record_id,
d.zone_name, d.group_id, g.name AS group_name,
s.name AS service_name, s.slug AS service_slug,
@@ -810,7 +987,7 @@ export function getBindingView(db: Db, id: number): ServiceBindingView {
WHERE sb.id = ${id}
`);
if (!rows[0]) throw new NotFoundError(`service binding ${id}`);
return rows[0];
return enrichServiceBindingView(db, rows[0]);
}
export function findBinding(
@@ -1005,6 +1182,18 @@ export function countCertificatesByStatus(
return rows.map((r) => [r.status, r.cnt]);
}
export function deleteCertificatesNotIn(db: Db, hostnames: string[]): number {
if (hostnames.length === 0) {
const result = db.delete(certificates).run();
return result.changes;
}
const result = db
.delete(certificates)
.where(notInArray(certificates.hostname, hostnames))
.run();
return result.changes;
}
// --- Sync Jobs ---
export function createSyncJob(
+5
View File
@@ -28,6 +28,7 @@ export const services = sqliteTable("services", {
),
subdomain: text("subdomain"),
enabled: integer("enabled", { mode: "boolean" }).notNull().default(false),
sort_order: integer("sort_order").notNull().default(0),
created_at: text("created_at")
.notNull()
.default(sql`datetime('now')`),
@@ -59,6 +60,7 @@ export const domains = sqliteTable("domains", {
zone_name: text("zone_name").notNull().unique(),
cf_zone_id: text("cf_zone_id").notNull(),
status: text("status").notNull().default("active"),
cert_monitoring: text("cert_monitoring").notNull().default("auto"),
last_synced_at: text("last_synced_at"),
created_at: text("created_at")
.notNull()
@@ -75,6 +77,8 @@ export const subdomains = sqliteTable("subdomains", {
.references(() => domains.id, { onDelete: "cascade" }),
name: text("name").notNull(),
fqdn: text("fqdn").notNull(),
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
cert_monitoring: text("cert_monitoring").notNull().default("auto"),
created_at: text("created_at")
.notNull()
.default(sql`datetime('now')`),
@@ -115,6 +119,7 @@ export const serviceBindings = sqliteTable("service_bindings", {
.notNull()
.references(() => services.id, { onDelete: "cascade" }),
hostname: text("hostname").notNull().default("@"),
cname_target: text("cname_target"),
dns_record_id: integer("dns_record_id").references(() => dnsRecords.id, {
onDelete: "set null",
}),