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
2026-06-22 13:52:33 +07:00
parent 4a70cf5854
commit 6d1d8ca4f3
99 changed files with 6692 additions and 1483 deletions
+163 -4
View File
@@ -215,6 +215,23 @@ declare const services: drizzle_orm_sqlite_core.SQLiteTableWithColumns<{
identity: undefined;
generated: undefined;
}, {}, {}>;
sort_order: drizzle_orm_sqlite_core.SQLiteColumn<{
name: "sort_order";
tableName: "services";
dataType: "number";
columnType: "SQLiteInteger";
data: number;
driverParam: number;
notNull: true;
hasDefault: true;
isPrimaryKey: false;
isAutoincrement: false;
hasRuntimeDefault: false;
enumValues: undefined;
baseColumn: never;
identity: undefined;
generated: undefined;
}, {}, {}>;
created_at: drizzle_orm_sqlite_core.SQLiteColumn<{
name: "created_at";
tableName: "services";
@@ -506,6 +523,25 @@ declare const domains: drizzle_orm_sqlite_core.SQLiteTableWithColumns<{
}, {}, {
length: number | undefined;
}>;
cert_monitoring: drizzle_orm_sqlite_core.SQLiteColumn<{
name: "cert_monitoring";
tableName: "domains";
dataType: "string";
columnType: "SQLiteText";
data: string;
driverParam: string;
notNull: true;
hasDefault: true;
isPrimaryKey: false;
isAutoincrement: false;
hasRuntimeDefault: false;
enumValues: [string, ...string[]];
baseColumn: never;
identity: undefined;
generated: undefined;
}, {}, {
length: number | undefined;
}>;
last_synced_at: drizzle_orm_sqlite_core.SQLiteColumn<{
name: "last_synced_at";
tableName: "domains";
@@ -642,6 +678,23 @@ declare const subdomains: drizzle_orm_sqlite_core.SQLiteTableWithColumns<{
}, {}, {
length: number | undefined;
}>;
enabled: drizzle_orm_sqlite_core.SQLiteColumn<{
name: "enabled";
tableName: "subdomains";
dataType: "boolean";
columnType: "SQLiteBoolean";
data: boolean;
driverParam: number;
notNull: true;
hasDefault: true;
isPrimaryKey: false;
isAutoincrement: false;
hasRuntimeDefault: false;
enumValues: undefined;
baseColumn: never;
identity: undefined;
generated: undefined;
}, {}, {}>;
created_at: drizzle_orm_sqlite_core.SQLiteColumn<{
name: "created_at";
tableName: "subdomains";
@@ -1020,6 +1073,25 @@ declare const serviceBindings: drizzle_orm_sqlite_core.SQLiteTableWithColumns<{
}, {}, {
length: number | undefined;
}>;
cname_target: drizzle_orm_sqlite_core.SQLiteColumn<{
name: "cname_target";
tableName: "service_bindings";
dataType: "string";
columnType: "SQLiteText";
data: string;
driverParam: string;
notNull: false;
hasDefault: false;
isPrimaryKey: false;
isAutoincrement: false;
hasRuntimeDefault: false;
enumValues: [string, ...string[]];
baseColumn: never;
identity: undefined;
generated: undefined;
}, {}, {
length: number | undefined;
}>;
dns_record_id: drizzle_orm_sqlite_core.SQLiteColumn<{
name: "dns_record_id";
tableName: "service_bindings";
@@ -1805,6 +1877,23 @@ declare const schema: {
identity: undefined;
generated: undefined;
}, {}, {}>;
sort_order: drizzle_orm_sqlite_core.SQLiteColumn<{
name: "sort_order";
tableName: "services";
dataType: "number";
columnType: "SQLiteInteger";
data: number;
driverParam: number;
notNull: true;
hasDefault: true;
isPrimaryKey: false;
isAutoincrement: false;
hasRuntimeDefault: false;
enumValues: undefined;
baseColumn: never;
identity: undefined;
generated: undefined;
}, {}, {}>;
created_at: drizzle_orm_sqlite_core.SQLiteColumn<{
name: "created_at";
tableName: "services";
@@ -2096,6 +2185,25 @@ declare const schema: {
}, {}, {
length: number | undefined;
}>;
cert_monitoring: drizzle_orm_sqlite_core.SQLiteColumn<{
name: "cert_monitoring";
tableName: "domains";
dataType: "string";
columnType: "SQLiteText";
data: string;
driverParam: string;
notNull: true;
hasDefault: true;
isPrimaryKey: false;
isAutoincrement: false;
hasRuntimeDefault: false;
enumValues: [string, ...string[]];
baseColumn: never;
identity: undefined;
generated: undefined;
}, {}, {
length: number | undefined;
}>;
last_synced_at: drizzle_orm_sqlite_core.SQLiteColumn<{
name: "last_synced_at";
tableName: "domains";
@@ -2232,6 +2340,23 @@ declare const schema: {
}, {}, {
length: number | undefined;
}>;
enabled: drizzle_orm_sqlite_core.SQLiteColumn<{
name: "enabled";
tableName: "subdomains";
dataType: "boolean";
columnType: "SQLiteBoolean";
data: boolean;
driverParam: number;
notNull: true;
hasDefault: true;
isPrimaryKey: false;
isAutoincrement: false;
hasRuntimeDefault: false;
enumValues: undefined;
baseColumn: never;
identity: undefined;
generated: undefined;
}, {}, {}>;
created_at: drizzle_orm_sqlite_core.SQLiteColumn<{
name: "created_at";
tableName: "subdomains";
@@ -2610,6 +2735,25 @@ declare const schema: {
}, {}, {
length: number | undefined;
}>;
cname_target: drizzle_orm_sqlite_core.SQLiteColumn<{
name: "cname_target";
tableName: "service_bindings";
dataType: "string";
columnType: "SQLiteText";
data: string;
driverParam: string;
notNull: false;
hasDefault: false;
isPrimaryKey: false;
isAutoincrement: false;
hasRuntimeDefault: false;
enumValues: [string, ...string[]];
baseColumn: never;
identity: undefined;
generated: undefined;
}, {}, {
length: number | undefined;
}>;
dns_record_id: drizzle_orm_sqlite_core.SQLiteColumn<{
name: "dns_record_id";
tableName: "service_bindings";
@@ -3227,15 +3371,22 @@ declare function listDomainsEnriched(db: Db, groupId?: number): DomainListItem[]
declare function findDomainByZoneName(db: Db, zoneName: string): Domain | null;
declare function getDomain(db: Db, id: number): Domain;
declare function createDomain(db: Db, groupId: number | null, zoneName: string, cfZoneId: string): Domain;
declare function updateDomain(db: Db, id: number, groupId: number | null, status: string): Domain;
declare function updateDomain(db: Db, id: number, groupId: number | null, status: string, certMonitoring?: string): Domain;
declare function deleteDomain(db: Db, id: number): void;
declare function setDomainLastSynced(db: Db, id: number): void;
declare function listAllDomains(db: Db): Domain[];
declare function listSubdomainsByDomain(db: Db, domainId: number): Subdomain[];
declare function getSubdomain(db: Db, id: number): Subdomain;
declare function findSubdomainByDomainAndName(db: Db, domainId: number, name: string): Subdomain | null;
declare function upsertSubdomain(db: Db, domainId: number, name: string, fqdn: string): void;
declare function createSubdomain(db: Db, domainId: number, name: string, fqdn: string): Subdomain;
declare function updateSubdomain(db: Db, id: number, name: string, fqdn: string): Subdomain;
interface UpdateSubdomainPatch {
name?: string;
fqdn?: string;
enabled?: boolean;
cert_monitoring?: string;
}
declare function updateSubdomain(db: Db, id: number, patch: UpdateSubdomainPatch): Subdomain;
declare function deleteSubdomain(db: Db, id: number): void;
declare function listAllSubdomains(db: Db): Subdomain[];
declare function listDnsRecords(db: Db, domainId: number, filter?: DnsListFilter): DnsRecord[];
@@ -3255,6 +3406,7 @@ declare function createService(db: Db, name: string, slug: string): Service;
declare function updateService(db: Db, id: number, name: string, slug: string): Service;
declare function setServiceEnabled(db: Db, id: number, enabled: boolean): Service;
declare function setServiceGroup(db: Db, id: number, groupId: number | null): void;
declare function reorderServices(db: Db, groupId: number | null, orderedIds: number[]): void;
declare function deleteService(db: Db, id: number): void;
declare function listServiceGroups(db: Db): ServiceGroup[];
declare function getServiceGroup(db: Db, id: number): ServiceGroup;
@@ -3266,6 +3418,7 @@ declare function listServiceIps(db: Db, serviceId: number): string[];
declare function replaceServiceIps(db: Db, serviceId: number, ips: string[]): void;
declare function listBindingIps(db: Db, bindingId: number): string[];
declare function replaceBindingIps(db: Db, bindingId: number, ips: string[]): void;
declare function setBindingCnameTarget(db: Db, bindingId: number, target: string | null): void;
declare function listRecordsForBinding(db: Db, bindingId: number): DnsRecord[];
declare function linkBindingRecord(db: Db, bindingId: number, dnsRecordId: number): void;
declare function unlinkBindingRecord(db: Db, bindingId: number, dnsRecordId: number): void;
@@ -3290,11 +3443,13 @@ declare function listCertificates(db: Db, status?: string): Certificate[];
declare function getCertificate(db: Db, id: number): Certificate;
declare function upsertCertificateCheck(db: Db, domainId: number, subdomainId: number | null, hostname: string, expiresAt: string | null, status: string, lastError: string | null): Certificate;
declare function countCertificatesByStatus(db: Db): Array<[string, number]>;
declare function deleteCertificatesNotIn(db: Db, hostnames: string[]): number;
declare function createSyncJob(db: Db, id: string, domainId: number | null): void;
declare function getSyncJob(db: Db, id: string): SyncJob;
declare function finishSyncJob(db: Db, id: string, status: string, message: string | null): void;
type repos_DnsListFilter = DnsListFilter;
type repos_UpdateSubdomainPatch = UpdateSubdomainPatch;
declare const repos_bindingsToRemove: typeof bindingsToRemove;
declare const repos_countCertificatesByStatus: typeof countCertificatesByStatus;
declare const repos_createDomain: typeof createDomain;
@@ -3305,6 +3460,7 @@ declare const repos_createSubdomain: typeof createSubdomain;
declare const repos_createSyncJob: typeof createSyncJob;
declare const repos_deleteBinding: typeof deleteBinding;
declare const repos_deleteBindingsExcept: typeof deleteBindingsExcept;
declare const repos_deleteCertificatesNotIn: typeof deleteCertificatesNotIn;
declare const repos_deleteDnsRecord: typeof deleteDnsRecord;
declare const repos_deleteDomain: typeof deleteDomain;
declare const repos_deleteGroup: typeof deleteGroup;
@@ -3314,6 +3470,7 @@ declare const repos_deleteSubdomain: typeof deleteSubdomain;
declare const repos_findBinding: typeof findBinding;
declare const repos_findDnsByCfId: typeof findDnsByCfId;
declare const repos_findDomainByZoneName: typeof findDomainByZoneName;
declare const repos_findSubdomainByDomainAndName: typeof findSubdomainByDomainAndName;
declare const repos_finishSyncJob: typeof finishSyncJob;
declare const repos_getBinding: typeof getBinding;
declare const repos_getBindingView: typeof getBindingView;
@@ -3351,8 +3508,10 @@ declare const repos_listServicesByGroup: typeof listServicesByGroup;
declare const repos_listSubdomainsByDomain: typeof listSubdomainsByDomain;
declare const repos_listUngroupedServices: typeof listUngroupedServices;
declare const repos_markDnsPendingDelete: typeof markDnsPendingDelete;
declare const repos_reorderServices: typeof reorderServices;
declare const repos_replaceBindingIps: typeof replaceBindingIps;
declare const repos_replaceServiceIps: typeof replaceServiceIps;
declare const repos_setBindingCnameTarget: typeof setBindingCnameTarget;
declare const repos_setBindingDnsRecordId: typeof setBindingDnsRecordId;
declare const repos_setDnsSyncStatus: typeof setDnsSyncStatus;
declare const repos_setDomainLastSynced: typeof setDomainLastSynced;
@@ -3371,7 +3530,7 @@ declare const repos_updateSubdomain: typeof updateSubdomain;
declare const repos_upsertCertificateCheck: typeof upsertCertificateCheck;
declare const repos_upsertSubdomain: typeof upsertSubdomain;
declare namespace repos {
export { type repos_DnsListFilter as DnsListFilter, repos_bindingsToRemove as bindingsToRemove, repos_countCertificatesByStatus as countCertificatesByStatus, repos_createDomain as createDomain, repos_createGroup as createGroup, repos_createService as createService, repos_createServiceGroup as createServiceGroup, repos_createSubdomain as createSubdomain, repos_createSyncJob as createSyncJob, repos_deleteBinding as deleteBinding, repos_deleteBindingsExcept as deleteBindingsExcept, repos_deleteDnsRecord as deleteDnsRecord, repos_deleteDomain as deleteDomain, repos_deleteGroup as deleteGroup, repos_deleteService as deleteService, repos_deleteServiceGroup as deleteServiceGroup, repos_deleteSubdomain as deleteSubdomain, repos_findBinding as findBinding, repos_findDnsByCfId as findDnsByCfId, repos_findDomainByZoneName as findDomainByZoneName, repos_finishSyncJob as finishSyncJob, repos_getBinding as getBinding, repos_getBindingView as getBindingView, repos_getCertificate as getCertificate, repos_getDnsRecord as getDnsRecord, repos_getDomain as getDomain, repos_getGroup as getGroup, repos_getGroupWithStats as getGroupWithStats, repos_getService as getService, repos_getServiceGroup as getServiceGroup, repos_getSubdomain as getSubdomain, repos_getSyncJob as getSyncJob, repos_insertBinding as insertBinding, repos_insertDnsRecord as insertDnsRecord, repos_linkBindingRecord as linkBindingRecord, repos_linkGroupDnsRecord as linkGroupDnsRecord, repos_listAllBindings as listAllBindings, repos_listAllDomains as listAllDomains, repos_listAllSubdomains as listAllSubdomains, repos_listBindingIps as listBindingIps, repos_listBindingsByDomain as listBindingsByDomain, repos_listBindingsByService as listBindingsByService, repos_listCertificates as listCertificates, repos_listDnsByDomain as listDnsByDomain, repos_listDnsRecords as listDnsRecords, repos_listDomains as listDomains, repos_listDomainsEnriched as listDomainsEnriched, repos_listGroupDnsRecords as listGroupDnsRecords, repos_listGroups as listGroups, repos_listRecordsForBinding as listRecordsForBinding, repos_listServiceGroups as listServiceGroups, repos_listServiceIps as listServiceIps, repos_listServices as listServices, repos_listServicesByGroup as listServicesByGroup, repos_listSubdomainsByDomain as listSubdomainsByDomain, repos_listUngroupedServices as listUngroupedServices, repos_markDnsPendingDelete as markDnsPendingDelete, repos_replaceBindingIps as replaceBindingIps, repos_replaceServiceIps as replaceServiceIps, repos_setBindingDnsRecordId as setBindingDnsRecordId, repos_setDnsSyncStatus as setDnsSyncStatus, repos_setDomainLastSynced as setDomainLastSynced, repos_setServiceEnabled as setServiceEnabled, repos_setServiceGroup as setServiceGroup, repos_setServiceGroupEnabled as setServiceGroupEnabled, repos_unlinkBindingRecord as unlinkBindingRecord, repos_unlinkGroupDnsRecord as unlinkGroupDnsRecord, repos_updateBindingFields as updateBindingFields, repos_updateDnsFields as updateDnsFields, repos_updateDomain as updateDomain, repos_updateGroup as updateGroup, repos_updateService as updateService, repos_updateServiceGroup as updateServiceGroup, repos_updateSubdomain as updateSubdomain, repos_upsertCertificateCheck as upsertCertificateCheck, repos_upsertSubdomain as upsertSubdomain };
export { type repos_DnsListFilter as DnsListFilter, type repos_UpdateSubdomainPatch as UpdateSubdomainPatch, repos_bindingsToRemove as bindingsToRemove, repos_countCertificatesByStatus as countCertificatesByStatus, repos_createDomain as createDomain, repos_createGroup as createGroup, repos_createService as createService, repos_createServiceGroup as createServiceGroup, repos_createSubdomain as createSubdomain, repos_createSyncJob as createSyncJob, repos_deleteBinding as deleteBinding, repos_deleteBindingsExcept as deleteBindingsExcept, repos_deleteCertificatesNotIn as deleteCertificatesNotIn, repos_deleteDnsRecord as deleteDnsRecord, repos_deleteDomain as deleteDomain, repos_deleteGroup as deleteGroup, repos_deleteService as deleteService, repos_deleteServiceGroup as deleteServiceGroup, repos_deleteSubdomain as deleteSubdomain, repos_findBinding as findBinding, repos_findDnsByCfId as findDnsByCfId, repos_findDomainByZoneName as findDomainByZoneName, repos_findSubdomainByDomainAndName as findSubdomainByDomainAndName, repos_finishSyncJob as finishSyncJob, repos_getBinding as getBinding, repos_getBindingView as getBindingView, repos_getCertificate as getCertificate, repos_getDnsRecord as getDnsRecord, repos_getDomain as getDomain, repos_getGroup as getGroup, repos_getGroupWithStats as getGroupWithStats, repos_getService as getService, repos_getServiceGroup as getServiceGroup, repos_getSubdomain as getSubdomain, repos_getSyncJob as getSyncJob, repos_insertBinding as insertBinding, repos_insertDnsRecord as insertDnsRecord, repos_linkBindingRecord as linkBindingRecord, repos_linkGroupDnsRecord as linkGroupDnsRecord, repos_listAllBindings as listAllBindings, repos_listAllDomains as listAllDomains, repos_listAllSubdomains as listAllSubdomains, repos_listBindingIps as listBindingIps, repos_listBindingsByDomain as listBindingsByDomain, repos_listBindingsByService as listBindingsByService, repos_listCertificates as listCertificates, repos_listDnsByDomain as listDnsByDomain, repos_listDnsRecords as listDnsRecords, repos_listDomains as listDomains, repos_listDomainsEnriched as listDomainsEnriched, repos_listGroupDnsRecords as listGroupDnsRecords, repos_listGroups as listGroups, repos_listRecordsForBinding as listRecordsForBinding, repos_listServiceGroups as listServiceGroups, repos_listServiceIps as listServiceIps, repos_listServices as listServices, repos_listServicesByGroup as listServicesByGroup, repos_listSubdomainsByDomain as listSubdomainsByDomain, repos_listUngroupedServices as listUngroupedServices, repos_markDnsPendingDelete as markDnsPendingDelete, repos_reorderServices as reorderServices, repos_replaceBindingIps as replaceBindingIps, repos_replaceServiceIps as replaceServiceIps, repos_setBindingCnameTarget as setBindingCnameTarget, repos_setBindingDnsRecordId as setBindingDnsRecordId, repos_setDnsSyncStatus as setDnsSyncStatus, repos_setDomainLastSynced as setDomainLastSynced, repos_setServiceEnabled as setServiceEnabled, repos_setServiceGroup as setServiceGroup, repos_setServiceGroupEnabled as setServiceGroupEnabled, repos_unlinkBindingRecord as unlinkBindingRecord, repos_unlinkGroupDnsRecord as unlinkGroupDnsRecord, repos_updateBindingFields as updateBindingFields, repos_updateDnsFields as updateDnsFields, repos_updateDomain as updateDomain, repos_updateGroup as updateGroup, repos_updateService as updateService, repos_updateServiceGroup as updateServiceGroup, repos_updateSubdomain as updateSubdomain, repos_upsertCertificateCheck as upsertCertificateCheck, repos_upsertSubdomain as upsertSubdomain };
}
export { ConflictError, type Db, type DnsListFilter, NotFoundError, type Sqlite, certificates, createDb, createMemoryDb, dnsRecords, domains, groups, healthCheck, repos, resolveDatabasePath, runMigrations, schema, serviceBindingIps, serviceBindingRecords, serviceBindings, serviceGroupDnsRecords, serviceGroups, serviceIps, services, subdomains, syncJobs };
export { ConflictError, type Db, type DnsListFilter, NotFoundError, type Sqlite, type UpdateSubdomainPatch, certificates, createDb, createMemoryDb, dnsRecords, domains, groups, healthCheck, repos, resolveDatabasePath, runMigrations, schema, serviceBindingIps, serviceBindingRecords, serviceBindings, serviceGroupDnsRecords, serviceGroups, serviceIps, services, subdomains, syncJobs };
+117 -14
View File
@@ -29,6 +29,7 @@ var 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')`),
updated_at: text("updated_at").notNull().default(sql`datetime('now')`)
});
@@ -50,6 +51,7 @@ var 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().default(sql`datetime('now')`),
updated_at: text("updated_at").notNull().default(sql`datetime('now')`)
@@ -59,6 +61,8 @@ var subdomains = sqliteTable("subdomains", {
domain_id: integer("domain_id").notNull().references(() => domains.id, { onDelete: "cascade" }),
name: text("name").notNull(),
fqdn: text("fqdn").notNull(),
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
cert_monitoring: text("cert_monitoring").notNull().default("auto"),
created_at: text("created_at").notNull().default(sql`datetime('now')`),
updated_at: text("updated_at").notNull().default(sql`datetime('now')`)
});
@@ -83,6 +87,7 @@ var serviceBindings = sqliteTable("service_bindings", {
domain_id: integer("domain_id").notNull().references(() => domains.id, { onDelete: "cascade" }),
service_id: integer("service_id").notNull().references(() => services.id, { onDelete: "cascade" }),
hostname: text("hostname").notNull().default("@"),
cname_target: text("cname_target"),
dns_record_id: integer("dns_record_id").references(() => dnsRecords.id, {
onDelete: "set null"
}),
@@ -234,6 +239,7 @@ __export(repos_exports, {
createSyncJob: () => createSyncJob,
deleteBinding: () => deleteBinding,
deleteBindingsExcept: () => deleteBindingsExcept,
deleteCertificatesNotIn: () => deleteCertificatesNotIn,
deleteDnsRecord: () => deleteDnsRecord,
deleteDomain: () => deleteDomain,
deleteGroup: () => deleteGroup,
@@ -243,6 +249,7 @@ __export(repos_exports, {
findBinding: () => findBinding,
findDnsByCfId: () => findDnsByCfId,
findDomainByZoneName: () => findDomainByZoneName,
findSubdomainByDomainAndName: () => findSubdomainByDomainAndName,
finishSyncJob: () => finishSyncJob,
getBinding: () => getBinding,
getBindingView: () => getBindingView,
@@ -280,8 +287,10 @@ __export(repos_exports, {
listSubdomainsByDomain: () => listSubdomainsByDomain,
listUngroupedServices: () => listUngroupedServices,
markDnsPendingDelete: () => markDnsPendingDelete,
reorderServices: () => reorderServices,
replaceBindingIps: () => replaceBindingIps,
replaceServiceIps: () => replaceServiceIps,
setBindingCnameTarget: () => setBindingCnameTarget,
setBindingDnsRecordId: () => setBindingDnsRecordId,
setDnsSyncStatus: () => setDnsSyncStatus,
setDomainLastSynced: () => setDomainLastSynced,
@@ -300,7 +309,8 @@ __export(repos_exports, {
upsertCertificateCheck: () => upsertCertificateCheck,
upsertSubdomain: () => upsertSubdomain
});
import { and, asc, count, eq, isNull, like, or, sql as sql2 } from "drizzle-orm";
import { dnsRecordNamesMatch } from "@cfdm/shared";
import { and, asc, count, eq, isNull, like, notInArray, or, sql as sql2 } from "drizzle-orm";
function listGroups(db) {
return db.select().from(groups).orderBy(asc(groups.name)).all();
}
@@ -367,12 +377,16 @@ function createDomain(db, groupId, zoneName, cfZoneId) {
}).returning({ id: domains.id }).get().id;
return getDomain(db, id);
}
function updateDomain(db, id, groupId, status) {
const result = db.update(domains).set({
function updateDomain(db, id, groupId, status, certMonitoring) {
const updates = {
group_id: groupId,
status,
updated_at: sql2`datetime('now')`
}).where(eq(domains.id, id)).run();
};
if (certMonitoring !== void 0) {
updates.cert_monitoring = certMonitoring;
}
const result = db.update(domains).set(updates).where(eq(domains.id, id)).run();
if (result.changes === 0) throw new NotFoundError(`domain ${id}`);
return getDomain(db, id);
}
@@ -397,6 +411,10 @@ function getSubdomain(db, id) {
if (!row) throw new NotFoundError(`subdomain ${id}`);
return row;
}
function findSubdomainByDomainAndName(db, domainId, name) {
const row = db.select().from(subdomains).where(and(eq(subdomains.domain_id, domainId), eq(subdomains.name, name))).get();
return row ?? null;
}
function upsertSubdomain(db, domainId, name, fqdn) {
db.run(sql2`
INSERT INTO subdomains (domain_id, name, fqdn)
@@ -410,8 +428,15 @@ function createSubdomain(db, domainId, name, fqdn) {
const id = db.insert(subdomains).values({ domain_id: domainId, name, fqdn }).returning({ id: subdomains.id }).get().id;
return getSubdomain(db, id);
}
function updateSubdomain(db, id, name, fqdn) {
const result = db.update(subdomains).set({ name, fqdn, updated_at: sql2`datetime('now')` }).where(eq(subdomains.id, id)).run();
function updateSubdomain(db, id, patch) {
const updates = { updated_at: sql2`datetime('now')` };
if (patch.name !== void 0) updates.name = patch.name;
if (patch.fqdn !== void 0) updates.fqdn = patch.fqdn;
if (patch.enabled !== void 0) updates.enabled = patch.enabled;
if (patch.cert_monitoring !== void 0) {
updates.cert_monitoring = patch.cert_monitoring;
}
const result = db.update(subdomains).set(updates).where(eq(subdomains.id, id)).run();
if (result.changes === 0) throw new NotFoundError(`subdomain ${id}`);
return getSubdomain(db, id);
}
@@ -518,14 +543,19 @@ function findDnsByCfId(db, domainId, cfRecordId) {
function markDnsPendingDelete(db, id) {
setDnsSyncStatus(db, id, "pending_delete", null, null);
}
function maxSortOrderInGroup(db, groupId) {
const condition = groupId === null ? isNull(services.service_group_id) : eq(services.service_group_id, groupId);
const row = db.select({ maxOrder: sql2`coalesce(max(${services.sort_order}), -1)` }).from(services).where(condition).get();
return row?.maxOrder ?? -1;
}
function listServices(db) {
return db.select().from(services).orderBy(asc(services.name)).all();
return db.select().from(services).orderBy(asc(services.sort_order), asc(services.name)).all();
}
function listServicesByGroup(db, groupId) {
return db.select().from(services).where(eq(services.service_group_id, groupId)).orderBy(asc(services.name)).all();
return db.select().from(services).where(eq(services.service_group_id, groupId)).orderBy(asc(services.sort_order), asc(services.name)).all();
}
function listUngroupedServices(db) {
return db.select().from(services).where(isNull(services.service_group_id)).orderBy(asc(services.name)).all();
return db.select().from(services).where(isNull(services.service_group_id)).orderBy(asc(services.sort_order), asc(services.name)).all();
}
function getService(db, id) {
const row = db.select().from(services).where(eq(services.id, id)).get();
@@ -533,7 +563,8 @@ function getService(db, id) {
return row;
}
function createService(db, name, slug) {
const id = db.insert(services).values({ name, slug, subdomain: slug }).returning({ id: services.id }).get().id;
const sortOrder = maxSortOrderInGroup(db, null) + 1;
const id = db.insert(services).values({ name, slug, subdomain: slug, sort_order: sortOrder }).returning({ id: services.id }).get().id;
return getService(db, id);
}
function updateService(db, id, name, slug) {
@@ -550,7 +581,31 @@ function setServiceEnabled(db, id, enabled) {
return getService(db, id);
}
function setServiceGroup(db, id, groupId) {
db.update(services).set({ service_group_id: groupId, updated_at: sql2`datetime('now')` }).where(eq(services.id, id)).run();
const sortOrder = maxSortOrderInGroup(db, groupId) + 1;
db.update(services).set({
service_group_id: groupId,
sort_order: sortOrder,
updated_at: sql2`datetime('now')`
}).where(eq(services.id, id)).run();
}
function reorderServices(db, groupId, orderedIds) {
const uniqueIds = new Set(orderedIds);
if (uniqueIds.size !== orderedIds.length) {
throw new Error("duplicate service ids in reorder request");
}
const condition = groupId === null ? isNull(services.service_group_id) : 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: sql2`datetime('now')` }).where(eq(services.id, orderedIds[index])).run();
}
});
}
function deleteService(db, id) {
const result = db.delete(services).where(eq(services.id, id)).run();
@@ -618,6 +673,12 @@ function replaceBindingIps(db, bindingId, ips) {
db.insert(serviceBindingIps).values({ binding_id: bindingId, ip }).run();
}
}
function setBindingCnameTarget(db, bindingId, target) {
db.update(serviceBindings).set({
cname_target: target,
updated_at: sql2`datetime('now')`
}).where(eq(serviceBindings.id, bindingId)).run();
}
function listRecordsForBinding(db, bindingId) {
return db.all(sql2`
SELECT dr.* FROM dns_records dr
@@ -662,6 +723,40 @@ function unlinkGroupDnsRecord(db, groupId, dnsRecordId) {
)
).run();
}
function dnsRecordMatchesHostname(recordName, hostname, zoneName) {
return dnsRecordNamesMatch(recordName, hostname, zoneName);
}
function enrichServiceBindingView(db, row) {
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 = [
.../* @__PURE__ */ 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
};
}
function listAllBindings(db) {
return db.all(sql2`
SELECT sb.id, sb.domain_id, sb.service_id, sb.hostname, sb.dns_record_id,
@@ -675,7 +770,7 @@ function listAllBindings(db) {
JOIN services s ON s.id = sb.service_id
LEFT JOIN dns_records dr ON dr.id = sb.dns_record_id
ORDER BY d.zone_name, s.name
`);
`).map((row) => enrichServiceBindingView(db, row));
}
function listBindingsByDomain(db, domainId) {
return db.all(sql2`
@@ -691,7 +786,7 @@ function listBindingsByDomain(db, domainId) {
LEFT JOIN dns_records dr ON dr.id = sb.dns_record_id
WHERE sb.domain_id = ${domainId}
ORDER BY s.name
`);
`).map((row) => enrichServiceBindingView(db, row));
}
function listBindingsByService(db, serviceId) {
return db.all(sql2`
@@ -720,7 +815,7 @@ function getBindingView(db, id) {
WHERE sb.id = ${id}
`);
if (!rows[0]) throw new NotFoundError(`service binding ${id}`);
return rows[0];
return enrichServiceBindingView(db, rows[0]);
}
function findBinding(db, serviceId, domainId, hostname) {
const row = db.select().from(serviceBindings).where(
@@ -814,6 +909,14 @@ function countCertificatesByStatus(db) {
}).from(certificates).groupBy(certificates.status).all();
return rows.map((r) => [r.status, r.cnt]);
}
function deleteCertificatesNotIn(db, hostnames) {
if (hostnames.length === 0) {
const result2 = db.delete(certificates).run();
return result2.changes;
}
const result = db.delete(certificates).where(notInArray(certificates.hostname, hostnames)).run();
return result.changes;
}
function createSyncJob(db, id, domainId) {
db.insert(syncJobs).values({ id, domain_id: domainId, status: "pending" }).run();
}
@@ -0,0 +1 @@
ALTER TABLE subdomains ADD COLUMN enabled INTEGER NOT NULL DEFAULT 1;
@@ -0,0 +1 @@
ALTER TABLE service_bindings ADD COLUMN cname_target TEXT;
@@ -0,0 +1,2 @@
ALTER TABLE services ADD COLUMN sort_order INTEGER NOT NULL DEFAULT 0;
UPDATE services SET sort_order = id;
@@ -0,0 +1,5 @@
ALTER TABLE domains ADD COLUMN cert_monitoring TEXT NOT NULL DEFAULT 'auto'
CHECK (cert_monitoring IN ('auto', 'required', 'skipped'));
ALTER TABLE subdomains ADD COLUMN cert_monitoring TEXT NOT NULL DEFAULT 'auto'
CHECK (cert_monitoring IN ('auto', 'required', 'skipped'));
+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",
}),