Refactor project to transition from Rust backend to Node.js with Fastify; update Dockerfile and Docker configurations for new build process; enhance local development instructions in CONTRIBUTING.md; implement health checks in Docker Compose; update pnpm-lock.yaml with new dependencies for API and shared packages; revise README.md to reflect new stack and development setup.
Build, Test, and Push CFDM Docker Image / test (push) Has been cancelled
Build, Test, and Push CFDM Docker Image / build-and-push (push) Has been cancelled
Build, Test, and Push CFDM Docker Image / create-release (push) Has been cancelled
Build, Test, and Push CFDM Docker Image / update-wiki (push) Has been cancelled
Build, Test, and Push CFDM Docker Image / test (push) Has been cancelled
Build, Test, and Push CFDM Docker Image / build-and-push (push) Has been cancelled
Build, Test, and Push CFDM Docker Image / create-release (push) Has been cancelled
Build, Test, and Push CFDM Docker Image / update-wiki (push) Has been cancelled
This commit is contained in:
1 parent
0a0a1a92f1
commit
d11414666f
2065 files changed
+15782
-16699
No files matched your search
Vendored
+3377
File diff suppressed because it is too large.
Load diff
Vendored
+855
@@ -0,0 +1,855 @@
|
||||
var __defProp = Object.defineProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
|
||||
// src/schema.ts
|
||||
import { sql } from "drizzle-orm";
|
||||
import {
|
||||
integer,
|
||||
primaryKey,
|
||||
sqliteTable,
|
||||
text
|
||||
} from "drizzle-orm/sqlite-core";
|
||||
var groups = sqliteTable("groups", {
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
name: text("name").notNull(),
|
||||
slug: text("slug").notNull().unique(),
|
||||
created_at: text("created_at").notNull().default(sql`datetime('now')`),
|
||||
updated_at: text("updated_at").notNull().default(sql`datetime('now')`)
|
||||
});
|
||||
var services = sqliteTable("services", {
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
name: text("name").notNull(),
|
||||
slug: text("slug").notNull().unique(),
|
||||
service_group_id: integer("service_group_id").references(
|
||||
() => serviceGroups.id,
|
||||
{ onDelete: "set null" }
|
||||
),
|
||||
subdomain: text("subdomain"),
|
||||
enabled: integer("enabled", { mode: "boolean" }).notNull().default(false),
|
||||
created_at: text("created_at").notNull().default(sql`datetime('now')`),
|
||||
updated_at: text("updated_at").notNull().default(sql`datetime('now')`)
|
||||
});
|
||||
var serviceGroups = sqliteTable("service_groups", {
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
name: text("name").notNull(),
|
||||
type: text("type").notNull().default("custom"),
|
||||
icon: text("icon"),
|
||||
domain: text("domain"),
|
||||
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
|
||||
created_at: text("created_at").notNull().default(sql`datetime('now')`),
|
||||
updated_at: text("updated_at").notNull().default(sql`datetime('now')`)
|
||||
});
|
||||
var domains = sqliteTable("domains", {
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
group_id: integer("group_id").references(() => groups.id, {
|
||||
onDelete: "set null"
|
||||
}),
|
||||
zone_name: text("zone_name").notNull().unique(),
|
||||
cf_zone_id: text("cf_zone_id").notNull(),
|
||||
status: text("status").notNull().default("active"),
|
||||
last_synced_at: text("last_synced_at"),
|
||||
created_at: text("created_at").notNull().default(sql`datetime('now')`),
|
||||
updated_at: text("updated_at").notNull().default(sql`datetime('now')`)
|
||||
});
|
||||
var subdomains = sqliteTable("subdomains", {
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
domain_id: integer("domain_id").notNull().references(() => domains.id, { onDelete: "cascade" }),
|
||||
name: text("name").notNull(),
|
||||
fqdn: text("fqdn").notNull(),
|
||||
created_at: text("created_at").notNull().default(sql`datetime('now')`),
|
||||
updated_at: text("updated_at").notNull().default(sql`datetime('now')`)
|
||||
});
|
||||
var dnsRecords = sqliteTable("dns_records", {
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
domain_id: integer("domain_id").notNull().references(() => domains.id, { onDelete: "cascade" }),
|
||||
cf_record_id: text("cf_record_id"),
|
||||
record_type: text("record_type").notNull(),
|
||||
name: text("name").notNull(),
|
||||
content: text("content").notNull(),
|
||||
ttl: integer("ttl").notNull().default(1),
|
||||
proxied: integer("proxied", { mode: "boolean" }).notNull().default(false),
|
||||
priority: integer("priority"),
|
||||
sync_status: text("sync_status").notNull().default("pending_push"),
|
||||
origin: text("origin").notNull().default("local"),
|
||||
last_error: text("last_error"),
|
||||
created_at: text("created_at").notNull().default(sql`datetime('now')`),
|
||||
updated_at: text("updated_at").notNull().default(sql`datetime('now')`)
|
||||
});
|
||||
var serviceBindings = sqliteTable("service_bindings", {
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
domain_id: integer("domain_id").notNull().references(() => domains.id, { onDelete: "cascade" }),
|
||||
service_id: integer("service_id").notNull().references(() => services.id, { onDelete: "cascade" }),
|
||||
hostname: text("hostname").notNull().default("@"),
|
||||
dns_record_id: integer("dns_record_id").references(() => dnsRecords.id, {
|
||||
onDelete: "set null"
|
||||
}),
|
||||
created_at: text("created_at").notNull().default(sql`datetime('now')`),
|
||||
updated_at: text("updated_at").notNull().default(sql`datetime('now')`)
|
||||
});
|
||||
var serviceIps = sqliteTable("service_ips", {
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
service_id: integer("service_id").notNull().references(() => services.id, { onDelete: "cascade" }),
|
||||
ip: text("ip").notNull(),
|
||||
created_at: text("created_at").notNull().default(sql`datetime('now')`)
|
||||
});
|
||||
var serviceBindingRecords = sqliteTable(
|
||||
"service_binding_records",
|
||||
{
|
||||
binding_id: integer("binding_id").notNull().references(() => serviceBindings.id, { onDelete: "cascade" }),
|
||||
dns_record_id: integer("dns_record_id").notNull().references(() => dnsRecords.id, { onDelete: "cascade" })
|
||||
},
|
||||
(t) => [primaryKey({ columns: [t.binding_id, t.dns_record_id] })]
|
||||
);
|
||||
var serviceBindingIps = sqliteTable(
|
||||
"service_binding_ips",
|
||||
{
|
||||
binding_id: integer("binding_id").notNull().references(() => serviceBindings.id, { onDelete: "cascade" }),
|
||||
ip: text("ip").notNull()
|
||||
},
|
||||
(t) => [primaryKey({ columns: [t.binding_id, t.ip] })]
|
||||
);
|
||||
var serviceGroupDnsRecords = sqliteTable(
|
||||
"service_group_dns_records",
|
||||
{
|
||||
group_id: integer("group_id").notNull().references(() => serviceGroups.id, { onDelete: "cascade" }),
|
||||
dns_record_id: integer("dns_record_id").notNull().references(() => dnsRecords.id, { onDelete: "cascade" })
|
||||
},
|
||||
(t) => [primaryKey({ columns: [t.group_id, t.dns_record_id] })]
|
||||
);
|
||||
var certificates = sqliteTable("certificates", {
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
domain_id: integer("domain_id").notNull().references(() => domains.id, { onDelete: "cascade" }),
|
||||
subdomain_id: integer("subdomain_id").references(() => subdomains.id, {
|
||||
onDelete: "set null"
|
||||
}),
|
||||
hostname: text("hostname").notNull().unique(),
|
||||
expires_at: text("expires_at"),
|
||||
last_checked_at: text("last_checked_at"),
|
||||
last_error: text("last_error"),
|
||||
status: text("status").notNull().default("unknown"),
|
||||
created_at: text("created_at").notNull().default(sql`datetime('now')`),
|
||||
updated_at: text("updated_at").notNull().default(sql`datetime('now')`)
|
||||
});
|
||||
var syncJobs = sqliteTable("sync_jobs", {
|
||||
id: text("id").primaryKey(),
|
||||
status: text("status").notNull().default("pending"),
|
||||
domain_id: integer("domain_id").references(() => domains.id, {
|
||||
onDelete: "set null"
|
||||
}),
|
||||
message: text("message"),
|
||||
created_at: text("created_at").notNull().default(sql`datetime('now')`),
|
||||
finished_at: text("finished_at")
|
||||
});
|
||||
var schema = {
|
||||
groups,
|
||||
services,
|
||||
serviceGroups,
|
||||
domains,
|
||||
subdomains,
|
||||
dnsRecords,
|
||||
serviceBindings,
|
||||
serviceIps,
|
||||
serviceBindingRecords,
|
||||
serviceBindingIps,
|
||||
serviceGroupDnsRecords,
|
||||
certificates,
|
||||
syncJobs
|
||||
};
|
||||
|
||||
// src/client.ts
|
||||
import { dirname, join } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { readFileSync, readdirSync } from "fs";
|
||||
import Database from "better-sqlite3";
|
||||
import { drizzle } from "drizzle-orm/better-sqlite3";
|
||||
var __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
function resolveDatabasePath(databaseUrl) {
|
||||
const url = databaseUrl.startsWith("sqlite:") ? databaseUrl.slice("sqlite:".length) : databaseUrl;
|
||||
return url;
|
||||
}
|
||||
function createDb(databaseUrl) {
|
||||
const path = resolveDatabasePath(databaseUrl);
|
||||
const sqlite = new Database(path);
|
||||
sqlite.pragma("journal_mode = WAL");
|
||||
sqlite.pragma("synchronous = NORMAL");
|
||||
sqlite.pragma("foreign_keys = ON");
|
||||
const db = drizzle(sqlite, { schema });
|
||||
return { db, sqlite };
|
||||
}
|
||||
function createMemoryDb() {
|
||||
const sqlite = new Database(":memory:");
|
||||
sqlite.pragma("foreign_keys = ON");
|
||||
const db = drizzle(sqlite, { schema });
|
||||
return { db, sqlite };
|
||||
}
|
||||
function runMigrations(sqlite) {
|
||||
const migrationsDir = join(__dirname, "..", "migrations");
|
||||
const files = readdirSync(migrationsDir).filter((f) => f.endsWith(".sql")).sort();
|
||||
sqlite.exec(
|
||||
`CREATE TABLE IF NOT EXISTS _migrations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
)`
|
||||
);
|
||||
for (const file of files) {
|
||||
const applied = sqlite.prepare("SELECT 1 FROM _migrations WHERE name = ?").get(file);
|
||||
if (applied) continue;
|
||||
const sql3 = readFileSync(join(migrationsDir, file), "utf-8");
|
||||
sqlite.exec(sql3);
|
||||
sqlite.prepare("INSERT INTO _migrations (name) VALUES (?)").run(file);
|
||||
}
|
||||
}
|
||||
function healthCheck(sqlite) {
|
||||
sqlite.prepare("SELECT 1").get();
|
||||
}
|
||||
|
||||
// src/errors.ts
|
||||
var NotFoundError = class extends Error {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.name = "NotFoundError";
|
||||
}
|
||||
};
|
||||
var ConflictError = class extends Error {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.name = "ConflictError";
|
||||
}
|
||||
};
|
||||
|
||||
// src/repos.ts
|
||||
var repos_exports = {};
|
||||
__export(repos_exports, {
|
||||
bindingsToRemove: () => bindingsToRemove,
|
||||
countCertificatesByStatus: () => countCertificatesByStatus,
|
||||
createDomain: () => createDomain,
|
||||
createGroup: () => createGroup,
|
||||
createService: () => createService,
|
||||
createServiceGroup: () => createServiceGroup,
|
||||
createSubdomain: () => createSubdomain,
|
||||
createSyncJob: () => createSyncJob,
|
||||
deleteBinding: () => deleteBinding,
|
||||
deleteBindingsExcept: () => deleteBindingsExcept,
|
||||
deleteDnsRecord: () => deleteDnsRecord,
|
||||
deleteDomain: () => deleteDomain,
|
||||
deleteGroup: () => deleteGroup,
|
||||
deleteService: () => deleteService,
|
||||
deleteServiceGroup: () => deleteServiceGroup,
|
||||
deleteSubdomain: () => deleteSubdomain,
|
||||
findBinding: () => findBinding,
|
||||
findDnsByCfId: () => findDnsByCfId,
|
||||
findDomainByZoneName: () => findDomainByZoneName,
|
||||
finishSyncJob: () => finishSyncJob,
|
||||
getBinding: () => getBinding,
|
||||
getBindingView: () => getBindingView,
|
||||
getCertificate: () => getCertificate,
|
||||
getDnsRecord: () => getDnsRecord,
|
||||
getDomain: () => getDomain,
|
||||
getGroup: () => getGroup,
|
||||
getGroupWithStats: () => getGroupWithStats,
|
||||
getService: () => getService,
|
||||
getServiceGroup: () => getServiceGroup,
|
||||
getSubdomain: () => getSubdomain,
|
||||
getSyncJob: () => getSyncJob,
|
||||
insertBinding: () => insertBinding,
|
||||
insertDnsRecord: () => insertDnsRecord,
|
||||
linkBindingRecord: () => linkBindingRecord,
|
||||
linkGroupDnsRecord: () => linkGroupDnsRecord,
|
||||
listAllBindings: () => listAllBindings,
|
||||
listAllDomains: () => listAllDomains,
|
||||
listAllSubdomains: () => listAllSubdomains,
|
||||
listBindingIps: () => listBindingIps,
|
||||
listBindingsByDomain: () => listBindingsByDomain,
|
||||
listBindingsByService: () => listBindingsByService,
|
||||
listCertificates: () => listCertificates,
|
||||
listDnsByDomain: () => listDnsByDomain,
|
||||
listDnsRecords: () => listDnsRecords,
|
||||
listDomains: () => listDomains,
|
||||
listDomainsEnriched: () => listDomainsEnriched,
|
||||
listGroupDnsRecords: () => listGroupDnsRecords,
|
||||
listGroups: () => listGroups,
|
||||
listRecordsForBinding: () => listRecordsForBinding,
|
||||
listServiceGroups: () => listServiceGroups,
|
||||
listServiceIps: () => listServiceIps,
|
||||
listServices: () => listServices,
|
||||
listServicesByGroup: () => listServicesByGroup,
|
||||
listSubdomainsByDomain: () => listSubdomainsByDomain,
|
||||
listUngroupedServices: () => listUngroupedServices,
|
||||
markDnsPendingDelete: () => markDnsPendingDelete,
|
||||
replaceBindingIps: () => replaceBindingIps,
|
||||
replaceServiceIps: () => replaceServiceIps,
|
||||
setBindingDnsRecordId: () => setBindingDnsRecordId,
|
||||
setDnsSyncStatus: () => setDnsSyncStatus,
|
||||
setDomainLastSynced: () => setDomainLastSynced,
|
||||
setServiceEnabled: () => setServiceEnabled,
|
||||
setServiceGroup: () => setServiceGroup,
|
||||
setServiceGroupEnabled: () => setServiceGroupEnabled,
|
||||
unlinkBindingRecord: () => unlinkBindingRecord,
|
||||
unlinkGroupDnsRecord: () => unlinkGroupDnsRecord,
|
||||
updateBindingFields: () => updateBindingFields,
|
||||
updateDnsFields: () => updateDnsFields,
|
||||
updateDomain: () => updateDomain,
|
||||
updateGroup: () => updateGroup,
|
||||
updateService: () => updateService,
|
||||
updateServiceGroup: () => updateServiceGroup,
|
||||
updateSubdomain: () => updateSubdomain,
|
||||
upsertCertificateCheck: () => upsertCertificateCheck,
|
||||
upsertSubdomain: () => upsertSubdomain
|
||||
});
|
||||
import { and, asc, count, eq, isNull, like, or, sql as sql2 } from "drizzle-orm";
|
||||
function listGroups(db) {
|
||||
return db.select().from(groups).orderBy(asc(groups.name)).all();
|
||||
}
|
||||
function getGroup(db, id) {
|
||||
const row = db.select().from(groups).where(eq(groups.id, id)).get();
|
||||
if (!row) throw new NotFoundError(`group ${id}`);
|
||||
return row;
|
||||
}
|
||||
function getGroupWithStats(db, id) {
|
||||
const result = db.all(sql2`
|
||||
SELECT g.id, g.name, g.slug, g.created_at, g.updated_at,
|
||||
(SELECT COUNT(*) FROM domains d WHERE d.group_id = g.id) AS domain_count
|
||||
FROM groups g WHERE g.id = ${id}
|
||||
`);
|
||||
if (!result[0]) throw new NotFoundError(`group ${id}`);
|
||||
return result[0];
|
||||
}
|
||||
function createGroup(db, name, slug) {
|
||||
const id = db.insert(groups).values({ name, slug }).returning({ id: groups.id }).get().id;
|
||||
return getGroup(db, id);
|
||||
}
|
||||
function updateGroup(db, id, name, slug) {
|
||||
const result = db.update(groups).set({ name, slug, updated_at: sql2`datetime('now')` }).where(eq(groups.id, id)).run();
|
||||
if (result.changes === 0) throw new NotFoundError(`group ${id}`);
|
||||
return getGroup(db, id);
|
||||
}
|
||||
function deleteGroup(db, id) {
|
||||
const result = db.delete(groups).where(eq(groups.id, id)).run();
|
||||
if (result.changes === 0) throw new NotFoundError(`group ${id}`);
|
||||
}
|
||||
function listDomains(db, groupId) {
|
||||
if (groupId != null) {
|
||||
return db.select().from(domains).where(eq(domains.group_id, groupId)).orderBy(asc(domains.zone_name)).all();
|
||||
}
|
||||
return db.select().from(domains).orderBy(asc(domains.zone_name)).all();
|
||||
}
|
||||
function listDomainsEnriched(db, groupId) {
|
||||
const base = groupId != null ? sql2`WHERE d.group_id = ${groupId}` : sql2``;
|
||||
return db.all(sql2`
|
||||
SELECT d.*, g.name AS group_name,
|
||||
(SELECT COUNT(*) FROM service_bindings sb WHERE sb.domain_id = d.id) AS service_count
|
||||
FROM domains d
|
||||
LEFT JOIN groups g ON g.id = d.group_id
|
||||
${base}
|
||||
ORDER BY d.zone_name ASC
|
||||
`);
|
||||
}
|
||||
function findDomainByZoneName(db, zoneName) {
|
||||
const rows = db.all(sql2`
|
||||
SELECT * FROM domains WHERE LOWER(zone_name) = LOWER(${zoneName}) LIMIT 1
|
||||
`);
|
||||
return rows[0] ?? null;
|
||||
}
|
||||
function getDomain(db, id) {
|
||||
const row = db.select().from(domains).where(eq(domains.id, id)).get();
|
||||
if (!row) throw new NotFoundError(`domain ${id}`);
|
||||
return row;
|
||||
}
|
||||
function createDomain(db, groupId, zoneName, cfZoneId) {
|
||||
const id = db.insert(domains).values({
|
||||
group_id: groupId,
|
||||
zone_name: zoneName,
|
||||
cf_zone_id: cfZoneId
|
||||
}).returning({ id: domains.id }).get().id;
|
||||
return getDomain(db, id);
|
||||
}
|
||||
function updateDomain(db, id, groupId, status) {
|
||||
const result = db.update(domains).set({
|
||||
group_id: groupId,
|
||||
status,
|
||||
updated_at: sql2`datetime('now')`
|
||||
}).where(eq(domains.id, id)).run();
|
||||
if (result.changes === 0) throw new NotFoundError(`domain ${id}`);
|
||||
return getDomain(db, id);
|
||||
}
|
||||
function deleteDomain(db, id) {
|
||||
const result = db.delete(domains).where(eq(domains.id, id)).run();
|
||||
if (result.changes === 0) throw new NotFoundError(`domain ${id}`);
|
||||
}
|
||||
function setDomainLastSynced(db, id) {
|
||||
db.update(domains).set({
|
||||
last_synced_at: sql2`datetime('now')`,
|
||||
updated_at: sql2`datetime('now')`
|
||||
}).where(eq(domains.id, id)).run();
|
||||
}
|
||||
function listAllDomains(db) {
|
||||
return listDomains(db);
|
||||
}
|
||||
function listSubdomainsByDomain(db, domainId) {
|
||||
return db.select().from(subdomains).where(eq(subdomains.domain_id, domainId)).orderBy(asc(subdomains.name)).all();
|
||||
}
|
||||
function getSubdomain(db, id) {
|
||||
const row = db.select().from(subdomains).where(eq(subdomains.id, id)).get();
|
||||
if (!row) throw new NotFoundError(`subdomain ${id}`);
|
||||
return row;
|
||||
}
|
||||
function upsertSubdomain(db, domainId, name, fqdn) {
|
||||
db.run(sql2`
|
||||
INSERT INTO subdomains (domain_id, name, fqdn)
|
||||
VALUES (${domainId}, ${name}, ${fqdn})
|
||||
ON CONFLICT(domain_id, name) DO UPDATE SET
|
||||
fqdn = excluded.fqdn,
|
||||
updated_at = datetime('now')
|
||||
`);
|
||||
}
|
||||
function createSubdomain(db, domainId, name, fqdn) {
|
||||
const id = db.insert(subdomains).values({ domain_id: domainId, name, fqdn }).returning({ id: subdomains.id }).get().id;
|
||||
return getSubdomain(db, id);
|
||||
}
|
||||
function updateSubdomain(db, id, name, fqdn) {
|
||||
const result = db.update(subdomains).set({ name, fqdn, updated_at: sql2`datetime('now')` }).where(eq(subdomains.id, id)).run();
|
||||
if (result.changes === 0) throw new NotFoundError(`subdomain ${id}`);
|
||||
return getSubdomain(db, id);
|
||||
}
|
||||
function deleteSubdomain(db, id) {
|
||||
const result = db.delete(subdomains).where(eq(subdomains.id, id)).run();
|
||||
if (result.changes === 0) throw new NotFoundError(`subdomain ${id}`);
|
||||
}
|
||||
function listAllSubdomains(db) {
|
||||
return db.select().from(subdomains).orderBy(asc(subdomains.fqdn)).all();
|
||||
}
|
||||
function mapDnsRecord(row) {
|
||||
return row;
|
||||
}
|
||||
function listDnsRecords(db, domainId, filter = {}) {
|
||||
const conditions = [eq(dnsRecords.domain_id, domainId)];
|
||||
if (filter.record_type) {
|
||||
conditions.push(eq(dnsRecords.record_type, filter.record_type.toUpperCase()));
|
||||
}
|
||||
if (filter.name) {
|
||||
conditions.push(like(dnsRecords.name, `%${filter.name}%`));
|
||||
}
|
||||
if (filter.content) {
|
||||
conditions.push(like(dnsRecords.content, `%${filter.content}%`));
|
||||
}
|
||||
if (filter.proxied != null) {
|
||||
conditions.push(eq(dnsRecords.proxied, filter.proxied));
|
||||
}
|
||||
if (filter.sync_status) {
|
||||
conditions.push(eq(dnsRecords.sync_status, filter.sync_status));
|
||||
}
|
||||
if (filter.q) {
|
||||
const pat = `%${filter.q}%`;
|
||||
conditions.push(
|
||||
or(
|
||||
like(dnsRecords.name, pat),
|
||||
like(dnsRecords.content, pat),
|
||||
like(dnsRecords.record_type, pat)
|
||||
)
|
||||
);
|
||||
}
|
||||
const sortCol = filter.sort === "type" ? dnsRecords.record_type : filter.sort === "updated_at" ? dnsRecords.updated_at : dnsRecords.name;
|
||||
const page = Math.max(1, filter.page ?? 1);
|
||||
const limit = Math.min(200, Math.max(1, filter.limit ?? 50));
|
||||
const offset = (page - 1) * limit;
|
||||
return db.select().from(dnsRecords).where(and(...conditions)).orderBy(asc(sortCol)).limit(limit).offset(offset).all().map(mapDnsRecord);
|
||||
}
|
||||
function getDnsRecord(db, domainId, id) {
|
||||
const row = db.select().from(dnsRecords).where(and(eq(dnsRecords.id, id), eq(dnsRecords.domain_id, domainId))).get();
|
||||
if (!row) throw new NotFoundError(`dns record ${id}`);
|
||||
return mapDnsRecord(row);
|
||||
}
|
||||
function insertDnsRecord(db, domainId, recordType, name, content, ttl, proxied, priority, syncStatus, origin, cfRecordId) {
|
||||
const id = db.insert(dnsRecords).values({
|
||||
domain_id: domainId,
|
||||
cf_record_id: cfRecordId,
|
||||
record_type: recordType.toUpperCase(),
|
||||
name,
|
||||
content,
|
||||
ttl,
|
||||
proxied,
|
||||
priority,
|
||||
sync_status: syncStatus,
|
||||
origin
|
||||
}).returning({ id: dnsRecords.id }).get().id;
|
||||
return getDnsRecord(db, domainId, id);
|
||||
}
|
||||
function updateDnsFields(db, id, recordType, name, content, ttl, proxied, priority, syncStatus, cfRecordId, lastError) {
|
||||
db.update(dnsRecords).set({
|
||||
cf_record_id: cfRecordId,
|
||||
record_type: recordType.toUpperCase(),
|
||||
name,
|
||||
content,
|
||||
ttl,
|
||||
proxied,
|
||||
priority,
|
||||
sync_status: syncStatus,
|
||||
last_error: lastError,
|
||||
updated_at: sql2`datetime('now')`
|
||||
}).where(eq(dnsRecords.id, id)).run();
|
||||
}
|
||||
function setDnsSyncStatus(db, id, syncStatus, cfRecordId, lastError) {
|
||||
db.update(dnsRecords).set({
|
||||
sync_status: syncStatus,
|
||||
cf_record_id: cfRecordId,
|
||||
last_error: lastError,
|
||||
updated_at: sql2`datetime('now')`
|
||||
}).where(eq(dnsRecords.id, id)).run();
|
||||
}
|
||||
function deleteDnsRecord(db, id) {
|
||||
db.delete(dnsRecords).where(eq(dnsRecords.id, id)).run();
|
||||
}
|
||||
function listDnsByDomain(db, domainId) {
|
||||
return db.select().from(dnsRecords).where(eq(dnsRecords.domain_id, domainId)).all().map(mapDnsRecord);
|
||||
}
|
||||
function findDnsByCfId(db, domainId, cfRecordId) {
|
||||
const row = db.select().from(dnsRecords).where(
|
||||
and(
|
||||
eq(dnsRecords.domain_id, domainId),
|
||||
eq(dnsRecords.cf_record_id, cfRecordId)
|
||||
)
|
||||
).get();
|
||||
return row ? mapDnsRecord(row) : null;
|
||||
}
|
||||
function markDnsPendingDelete(db, id) {
|
||||
setDnsSyncStatus(db, id, "pending_delete", null, null);
|
||||
}
|
||||
function listServices(db) {
|
||||
return db.select().from(services).orderBy(asc(services.name)).all();
|
||||
}
|
||||
function listServicesByGroup(db, groupId) {
|
||||
return db.select().from(services).where(eq(services.service_group_id, groupId)).orderBy(asc(services.name)).all();
|
||||
}
|
||||
function listUngroupedServices(db) {
|
||||
return db.select().from(services).where(isNull(services.service_group_id)).orderBy(asc(services.name)).all();
|
||||
}
|
||||
function getService(db, id) {
|
||||
const row = db.select().from(services).where(eq(services.id, id)).get();
|
||||
if (!row) throw new NotFoundError(`service ${id}`);
|
||||
return row;
|
||||
}
|
||||
function createService(db, name, slug) {
|
||||
const id = db.insert(services).values({ name, slug, subdomain: slug }).returning({ id: services.id }).get().id;
|
||||
return getService(db, id);
|
||||
}
|
||||
function updateService(db, id, name, slug) {
|
||||
db.update(services).set({
|
||||
name,
|
||||
slug,
|
||||
subdomain: slug,
|
||||
updated_at: sql2`datetime('now')`
|
||||
}).where(eq(services.id, id)).run();
|
||||
return getService(db, id);
|
||||
}
|
||||
function setServiceEnabled(db, id, enabled) {
|
||||
db.update(services).set({ enabled, updated_at: sql2`datetime('now')` }).where(eq(services.id, id)).run();
|
||||
return getService(db, id);
|
||||
}
|
||||
function setServiceGroup(db, id, groupId) {
|
||||
db.update(services).set({ service_group_id: groupId, updated_at: sql2`datetime('now')` }).where(eq(services.id, id)).run();
|
||||
}
|
||||
function deleteService(db, id) {
|
||||
const result = db.delete(services).where(eq(services.id, id)).run();
|
||||
if (result.changes === 0) throw new NotFoundError(`service ${id}`);
|
||||
}
|
||||
function mapServiceGroup(row) {
|
||||
return {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
type: row.type,
|
||||
icon: row.icon,
|
||||
domain: row.domain,
|
||||
enabled: row.enabled,
|
||||
created_at: row.created_at,
|
||||
updated_at: row.updated_at
|
||||
};
|
||||
}
|
||||
function listServiceGroups(db) {
|
||||
return db.select().from(serviceGroups).orderBy(asc(serviceGroups.name)).all().map(mapServiceGroup);
|
||||
}
|
||||
function getServiceGroup(db, id) {
|
||||
const row = db.select().from(serviceGroups).where(eq(serviceGroups.id, id)).get();
|
||||
if (!row) throw new NotFoundError(`service group ${id}`);
|
||||
return mapServiceGroup(row);
|
||||
}
|
||||
function createServiceGroup(db, name, groupType, icon, domain) {
|
||||
const id = db.insert(serviceGroups).values({ name, type: groupType, icon, domain }).returning({ id: serviceGroups.id }).get().id;
|
||||
return getServiceGroup(db, id);
|
||||
}
|
||||
function updateServiceGroup(db, id, name, groupType, icon, domain) {
|
||||
const result = db.update(serviceGroups).set({
|
||||
name,
|
||||
type: groupType,
|
||||
icon,
|
||||
domain,
|
||||
updated_at: sql2`datetime('now')`
|
||||
}).where(eq(serviceGroups.id, id)).run();
|
||||
if (result.changes === 0) throw new NotFoundError(`service group ${id}`);
|
||||
return getServiceGroup(db, id);
|
||||
}
|
||||
function setServiceGroupEnabled(db, id, enabled) {
|
||||
const result = db.update(serviceGroups).set({ enabled, updated_at: sql2`datetime('now')` }).where(eq(serviceGroups.id, id)).run();
|
||||
if (result.changes === 0) throw new NotFoundError(`service group ${id}`);
|
||||
return getServiceGroup(db, id);
|
||||
}
|
||||
function deleteServiceGroup(db, id) {
|
||||
const result = db.delete(serviceGroups).where(eq(serviceGroups.id, id)).run();
|
||||
if (result.changes === 0) throw new NotFoundError(`service group ${id}`);
|
||||
}
|
||||
function listServiceIps(db, serviceId) {
|
||||
return db.select({ ip: serviceIps.ip }).from(serviceIps).where(eq(serviceIps.service_id, serviceId)).all().map((r) => r.ip);
|
||||
}
|
||||
function replaceServiceIps(db, serviceId, ips) {
|
||||
db.delete(serviceIps).where(eq(serviceIps.service_id, serviceId)).run();
|
||||
for (const ip of ips) {
|
||||
db.insert(serviceIps).values({ service_id: serviceId, ip }).run();
|
||||
}
|
||||
}
|
||||
function listBindingIps(db, bindingId) {
|
||||
return db.select({ ip: serviceBindingIps.ip }).from(serviceBindingIps).where(eq(serviceBindingIps.binding_id, bindingId)).all().map((r) => r.ip);
|
||||
}
|
||||
function replaceBindingIps(db, bindingId, ips) {
|
||||
db.delete(serviceBindingIps).where(eq(serviceBindingIps.binding_id, bindingId)).run();
|
||||
for (const ip of ips) {
|
||||
db.insert(serviceBindingIps).values({ binding_id: bindingId, ip }).run();
|
||||
}
|
||||
}
|
||||
function listRecordsForBinding(db, bindingId) {
|
||||
return db.all(sql2`
|
||||
SELECT dr.* FROM dns_records dr
|
||||
INNER JOIN service_binding_records sbr ON sbr.dns_record_id = dr.id
|
||||
WHERE sbr.binding_id = ${bindingId}
|
||||
`);
|
||||
}
|
||||
function linkBindingRecord(db, bindingId, dnsRecordId) {
|
||||
db.run(sql2`
|
||||
INSERT INTO service_binding_records (binding_id, dns_record_id)
|
||||
VALUES (${bindingId}, ${dnsRecordId})
|
||||
ON CONFLICT(binding_id, dns_record_id) DO NOTHING
|
||||
`);
|
||||
}
|
||||
function unlinkBindingRecord(db, bindingId, dnsRecordId) {
|
||||
db.delete(serviceBindingRecords).where(
|
||||
and(
|
||||
eq(serviceBindingRecords.binding_id, bindingId),
|
||||
eq(serviceBindingRecords.dns_record_id, dnsRecordId)
|
||||
)
|
||||
).run();
|
||||
}
|
||||
function listGroupDnsRecords(db, groupId) {
|
||||
return db.all(sql2`
|
||||
SELECT dr.* FROM dns_records dr
|
||||
INNER JOIN service_group_dns_records sgdr ON sgdr.dns_record_id = dr.id
|
||||
WHERE sgdr.group_id = ${groupId}
|
||||
`);
|
||||
}
|
||||
function linkGroupDnsRecord(db, groupId, dnsRecordId) {
|
||||
db.run(sql2`
|
||||
INSERT INTO service_group_dns_records (group_id, dns_record_id)
|
||||
VALUES (${groupId}, ${dnsRecordId})
|
||||
ON CONFLICT(group_id, dns_record_id) DO NOTHING
|
||||
`);
|
||||
}
|
||||
function unlinkGroupDnsRecord(db, groupId, dnsRecordId) {
|
||||
db.delete(serviceGroupDnsRecords).where(
|
||||
and(
|
||||
eq(serviceGroupDnsRecords.group_id, groupId),
|
||||
eq(serviceGroupDnsRecords.dns_record_id, dnsRecordId)
|
||||
)
|
||||
).run();
|
||||
}
|
||||
function listAllBindings(db) {
|
||||
return db.all(sql2`
|
||||
SELECT sb.id, sb.domain_id, sb.service_id, sb.hostname, sb.dns_record_id,
|
||||
d.zone_name, d.group_id, g.name AS group_name,
|
||||
s.name AS service_name, s.slug AS service_slug,
|
||||
dr.content AS target_ip, dr.sync_status,
|
||||
sb.created_at, sb.updated_at
|
||||
FROM service_bindings sb
|
||||
JOIN domains d ON d.id = sb.domain_id
|
||||
LEFT JOIN groups g ON g.id = d.group_id
|
||||
JOIN services s ON s.id = sb.service_id
|
||||
LEFT JOIN dns_records dr ON dr.id = sb.dns_record_id
|
||||
ORDER BY d.zone_name, s.name
|
||||
`);
|
||||
}
|
||||
function listBindingsByDomain(db, domainId) {
|
||||
return db.all(sql2`
|
||||
SELECT sb.id, sb.domain_id, sb.service_id, sb.hostname, sb.dns_record_id,
|
||||
d.zone_name, d.group_id, g.name AS group_name,
|
||||
s.name AS service_name, s.slug AS service_slug,
|
||||
dr.content AS target_ip, dr.sync_status,
|
||||
sb.created_at, sb.updated_at
|
||||
FROM service_bindings sb
|
||||
JOIN domains d ON d.id = sb.domain_id
|
||||
LEFT JOIN groups g ON g.id = d.group_id
|
||||
JOIN services s ON s.id = sb.service_id
|
||||
LEFT JOIN dns_records dr ON dr.id = sb.dns_record_id
|
||||
WHERE sb.domain_id = ${domainId}
|
||||
ORDER BY s.name
|
||||
`);
|
||||
}
|
||||
function listBindingsByService(db, serviceId) {
|
||||
return db.all(sql2`
|
||||
SELECT sb.*, d.zone_name FROM service_bindings sb
|
||||
JOIN domains d ON d.id = sb.domain_id
|
||||
WHERE sb.service_id = ${serviceId}
|
||||
`);
|
||||
}
|
||||
function getBinding(db, id) {
|
||||
const row = db.select().from(serviceBindings).where(eq(serviceBindings.id, id)).get();
|
||||
if (!row) throw new NotFoundError(`service binding ${id}`);
|
||||
return row;
|
||||
}
|
||||
function getBindingView(db, id) {
|
||||
const rows = db.all(sql2`
|
||||
SELECT sb.id, sb.domain_id, sb.service_id, sb.hostname, sb.dns_record_id,
|
||||
d.zone_name, d.group_id, g.name AS group_name,
|
||||
s.name AS service_name, s.slug AS service_slug,
|
||||
dr.content AS target_ip, dr.sync_status,
|
||||
sb.created_at, sb.updated_at
|
||||
FROM service_bindings sb
|
||||
JOIN domains d ON d.id = sb.domain_id
|
||||
LEFT JOIN groups g ON g.id = d.group_id
|
||||
JOIN services s ON s.id = sb.service_id
|
||||
LEFT JOIN dns_records dr ON dr.id = sb.dns_record_id
|
||||
WHERE sb.id = ${id}
|
||||
`);
|
||||
if (!rows[0]) throw new NotFoundError(`service binding ${id}`);
|
||||
return rows[0];
|
||||
}
|
||||
function findBinding(db, serviceId, domainId, hostname) {
|
||||
const row = db.select().from(serviceBindings).where(
|
||||
and(
|
||||
eq(serviceBindings.service_id, serviceId),
|
||||
eq(serviceBindings.domain_id, domainId),
|
||||
eq(serviceBindings.hostname, hostname)
|
||||
)
|
||||
).get();
|
||||
return row ?? null;
|
||||
}
|
||||
function insertBinding(db, domainId, serviceId, hostname, dnsRecordId) {
|
||||
const id = db.insert(serviceBindings).values({
|
||||
domain_id: domainId,
|
||||
service_id: serviceId,
|
||||
hostname,
|
||||
dns_record_id: dnsRecordId
|
||||
}).returning({ id: serviceBindings.id }).get().id;
|
||||
return getBinding(db, id);
|
||||
}
|
||||
function updateBindingFields(db, id, serviceId, hostname, dnsRecordId) {
|
||||
db.update(serviceBindings).set({
|
||||
service_id: serviceId,
|
||||
hostname,
|
||||
dns_record_id: dnsRecordId,
|
||||
updated_at: sql2`datetime('now')`
|
||||
}).where(eq(serviceBindings.id, id)).run();
|
||||
}
|
||||
function setBindingDnsRecordId(db, bindingId, dnsRecordId) {
|
||||
db.update(serviceBindings).set({
|
||||
dns_record_id: dnsRecordId,
|
||||
updated_at: sql2`datetime('now')`
|
||||
}).where(eq(serviceBindings.id, bindingId)).run();
|
||||
}
|
||||
function bindingsToRemove(db, serviceId, keepIds) {
|
||||
const all = db.select().from(serviceBindings).where(eq(serviceBindings.service_id, serviceId)).all();
|
||||
return all.filter((b) => !keepIds.includes(b.id));
|
||||
}
|
||||
function deleteBindingsExcept(db, serviceId, keepIds) {
|
||||
const all = db.select().from(serviceBindings).where(eq(serviceBindings.service_id, serviceId)).all();
|
||||
for (const binding of all) {
|
||||
if (!keepIds.includes(binding.id)) {
|
||||
db.delete(serviceBindings).where(eq(serviceBindings.id, binding.id)).run();
|
||||
}
|
||||
}
|
||||
}
|
||||
function deleteBinding(db, id) {
|
||||
const result = db.delete(serviceBindings).where(eq(serviceBindings.id, id)).run();
|
||||
if (result.changes === 0) throw new NotFoundError(`service binding ${id}`);
|
||||
}
|
||||
function listCertificates(db, status) {
|
||||
if (status) {
|
||||
return db.select().from(certificates).where(eq(certificates.status, status)).orderBy(asc(certificates.expires_at)).all();
|
||||
}
|
||||
return db.select().from(certificates).orderBy(asc(certificates.expires_at)).all();
|
||||
}
|
||||
function getCertificate(db, id) {
|
||||
const row = db.select().from(certificates).where(eq(certificates.id, id)).get();
|
||||
if (!row) throw new NotFoundError(`certificate ${id}`);
|
||||
return row;
|
||||
}
|
||||
function upsertCertificateCheck(db, domainId, subdomainId, hostname, expiresAt, status, lastError) {
|
||||
const existing = db.select().from(certificates).where(eq(certificates.hostname, hostname)).get();
|
||||
if (existing) {
|
||||
db.update(certificates).set({
|
||||
domain_id: domainId,
|
||||
subdomain_id: subdomainId,
|
||||
expires_at: expiresAt,
|
||||
last_checked_at: sql2`datetime('now')`,
|
||||
last_error: lastError,
|
||||
status,
|
||||
updated_at: sql2`datetime('now')`
|
||||
}).where(eq(certificates.id, existing.id)).run();
|
||||
return getCertificate(db, existing.id);
|
||||
}
|
||||
const id = db.insert(certificates).values({
|
||||
domain_id: domainId,
|
||||
subdomain_id: subdomainId,
|
||||
hostname,
|
||||
expires_at: expiresAt,
|
||||
last_checked_at: sql2`datetime('now')`,
|
||||
last_error: lastError,
|
||||
status
|
||||
}).returning({ id: certificates.id }).get().id;
|
||||
return getCertificate(db, id);
|
||||
}
|
||||
function countCertificatesByStatus(db) {
|
||||
const rows = db.select({
|
||||
status: certificates.status,
|
||||
cnt: count()
|
||||
}).from(certificates).groupBy(certificates.status).all();
|
||||
return rows.map((r) => [r.status, r.cnt]);
|
||||
}
|
||||
function createSyncJob(db, id, domainId) {
|
||||
db.insert(syncJobs).values({ id, domain_id: domainId, status: "pending" }).run();
|
||||
}
|
||||
function getSyncJob(db, id) {
|
||||
const row = db.select().from(syncJobs).where(eq(syncJobs.id, id)).get();
|
||||
if (!row) throw new NotFoundError(`sync job ${id}`);
|
||||
return row;
|
||||
}
|
||||
function finishSyncJob(db, id, status, message) {
|
||||
db.update(syncJobs).set({
|
||||
status,
|
||||
message,
|
||||
finished_at: sql2`datetime('now')`
|
||||
}).where(eq(syncJobs.id, id)).run();
|
||||
}
|
||||
export {
|
||||
ConflictError,
|
||||
NotFoundError,
|
||||
certificates,
|
||||
createDb,
|
||||
createMemoryDb,
|
||||
dnsRecords,
|
||||
domains,
|
||||
groups,
|
||||
healthCheck,
|
||||
repos_exports as repos,
|
||||
resolveDatabasePath,
|
||||
runMigrations,
|
||||
schema,
|
||||
serviceBindingIps,
|
||||
serviceBindingRecords,
|
||||
serviceBindings,
|
||||
serviceGroupDnsRecords,
|
||||
serviceGroups,
|
||||
serviceIps,
|
||||
services,
|
||||
subdomains,
|
||||
syncJobs
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
import { defineConfig } from "drizzle-kit";
|
||||
|
||||
export default defineConfig({
|
||||
schema: "./src/schema.ts",
|
||||
out: "./drizzle",
|
||||
dialect: "sqlite",
|
||||
dbCredentials: {
|
||||
url: "data/app.db",
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,110 @@
|
||||
PRAGMA foreign_keys = ON;
|
||||
|
||||
CREATE TABLE groups (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
slug TEXT NOT NULL UNIQUE,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE TABLE services (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
slug TEXT NOT NULL UNIQUE,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE TABLE domains (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
group_id INTEGER REFERENCES groups(id) ON DELETE SET NULL,
|
||||
zone_name TEXT NOT NULL UNIQUE,
|
||||
cf_zone_id TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'active',
|
||||
last_synced_at TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE INDEX idx_domains_cf_zone_id ON domains(cf_zone_id);
|
||||
CREATE INDEX idx_domains_group_id ON domains(group_id);
|
||||
|
||||
CREATE TABLE subdomains (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
domain_id INTEGER NOT NULL REFERENCES domains(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
fqdn TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
UNIQUE(domain_id, name)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_subdomains_domain_id ON subdomains(domain_id);
|
||||
|
||||
CREATE TABLE dns_records (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
domain_id INTEGER NOT NULL REFERENCES domains(id) ON DELETE CASCADE,
|
||||
cf_record_id TEXT,
|
||||
record_type TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
ttl INTEGER NOT NULL DEFAULT 1,
|
||||
proxied INTEGER NOT NULL DEFAULT 0,
|
||||
priority INTEGER,
|
||||
sync_status TEXT NOT NULL DEFAULT 'pending_push',
|
||||
origin TEXT NOT NULL DEFAULT 'local',
|
||||
last_error TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE INDEX idx_dns_records_domain_type_name ON dns_records(domain_id, record_type, name);
|
||||
CREATE INDEX idx_dns_records_sync_status ON dns_records(sync_status);
|
||||
|
||||
CREATE TABLE domain_services (
|
||||
domain_id INTEGER NOT NULL REFERENCES domains(id) ON DELETE CASCADE,
|
||||
service_id INTEGER NOT NULL REFERENCES services(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (domain_id, service_id)
|
||||
);
|
||||
|
||||
CREATE TABLE subdomain_services (
|
||||
subdomain_id INTEGER NOT NULL REFERENCES subdomains(id) ON DELETE CASCADE,
|
||||
service_id INTEGER NOT NULL REFERENCES services(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (subdomain_id, service_id)
|
||||
);
|
||||
|
||||
CREATE TABLE certificates (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
domain_id INTEGER NOT NULL REFERENCES domains(id) ON DELETE CASCADE,
|
||||
subdomain_id INTEGER REFERENCES subdomains(id) ON DELETE SET NULL,
|
||||
hostname TEXT NOT NULL UNIQUE,
|
||||
expires_at TEXT,
|
||||
last_checked_at TEXT,
|
||||
last_error TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'unknown',
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE INDEX idx_certificates_status_expires ON certificates(status, expires_at);
|
||||
|
||||
CREATE TABLE sync_jobs (
|
||||
id TEXT PRIMARY KEY,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
domain_id INTEGER REFERENCES domains(id) ON DELETE SET NULL,
|
||||
message TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
finished_at TEXT
|
||||
);
|
||||
|
||||
INSERT INTO groups (name, slug) VALUES
|
||||
('Local', 'local'),
|
||||
('VPN', 'vpn'),
|
||||
('External', 'external');
|
||||
|
||||
INSERT INTO services (name, slug) VALUES
|
||||
('DNS', 'dns'),
|
||||
('BGP', 'bgp'),
|
||||
('CDN', 'cdn'),
|
||||
('Mail', 'mail');
|
||||
@@ -0,0 +1,23 @@
|
||||
PRAGMA foreign_keys = OFF;
|
||||
|
||||
CREATE TABLE service_bindings (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
domain_id INTEGER NOT NULL REFERENCES domains(id) ON DELETE CASCADE,
|
||||
service_id INTEGER NOT NULL REFERENCES services(id) ON DELETE CASCADE,
|
||||
hostname TEXT NOT NULL DEFAULT '@',
|
||||
dns_record_id INTEGER REFERENCES dns_records(id) ON DELETE SET NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
UNIQUE(domain_id, service_id, hostname)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_service_bindings_domain_id ON service_bindings(domain_id);
|
||||
CREATE INDEX idx_service_bindings_service_id ON service_bindings(service_id);
|
||||
|
||||
INSERT INTO service_bindings (domain_id, service_id, hostname, created_at, updated_at)
|
||||
SELECT domain_id, service_id, '@', datetime('now'), datetime('now')
|
||||
FROM domain_services;
|
||||
|
||||
DROP TABLE domain_services;
|
||||
|
||||
PRAGMA foreign_keys = ON;
|
||||
@@ -0,0 +1,40 @@
|
||||
PRAGMA foreign_keys = OFF;
|
||||
|
||||
CREATE TABLE service_ips (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
service_id INTEGER NOT NULL REFERENCES services(id) ON DELETE CASCADE,
|
||||
ip TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
UNIQUE(service_id, ip)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_service_ips_service_id ON service_ips(service_id);
|
||||
|
||||
CREATE TABLE service_binding_records (
|
||||
binding_id INTEGER NOT NULL REFERENCES service_bindings(id) ON DELETE CASCADE,
|
||||
dns_record_id INTEGER NOT NULL REFERENCES dns_records(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (binding_id, dns_record_id)
|
||||
);
|
||||
|
||||
INSERT INTO service_ips (service_id, ip, created_at)
|
||||
SELECT DISTINCT sb.service_id, dr.content, datetime('now')
|
||||
FROM service_bindings sb
|
||||
JOIN dns_records dr ON dr.id = sb.dns_record_id
|
||||
WHERE dr.record_type = 'A'
|
||||
AND dr.content IS NOT NULL
|
||||
AND TRIM(dr.content) != ''
|
||||
ON CONFLICT(service_id, ip) DO NOTHING;
|
||||
|
||||
INSERT INTO service_binding_records (binding_id, dns_record_id)
|
||||
SELECT sb.id, sb.dns_record_id
|
||||
FROM service_bindings sb
|
||||
WHERE sb.dns_record_id IS NOT NULL
|
||||
ON CONFLICT(binding_id, dns_record_id) DO NOTHING;
|
||||
|
||||
INSERT INTO services (name, slug) VALUES
|
||||
('VPN Panel', 'vpn-panel'),
|
||||
('VPN Node', 'vpn-node'),
|
||||
('Home Assistant', 'home-assistant')
|
||||
ON CONFLICT(slug) DO NOTHING;
|
||||
|
||||
PRAGMA foreign_keys = ON;
|
||||
@@ -0,0 +1,29 @@
|
||||
PRAGMA foreign_keys = OFF;
|
||||
|
||||
CREATE TABLE service_groups (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
type TEXT NOT NULL DEFAULT 'custom',
|
||||
icon TEXT,
|
||||
domain TEXT,
|
||||
enabled INTEGER NOT NULL DEFAULT 1,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
ALTER TABLE services ADD COLUMN service_group_id INTEGER REFERENCES service_groups(id) ON DELETE SET NULL;
|
||||
ALTER TABLE services ADD COLUMN subdomain TEXT;
|
||||
ALTER TABLE services ADD COLUMN enabled INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
UPDATE services SET subdomain = slug WHERE subdomain IS NULL;
|
||||
|
||||
INSERT INTO service_groups (name, type, enabled) VALUES
|
||||
('VPN', 'vpn', 1),
|
||||
('Сеть', 'network', 1),
|
||||
('Интернет', 'internet', 1);
|
||||
|
||||
UPDATE services
|
||||
SET service_group_id = (SELECT id FROM service_groups WHERE type = 'vpn' LIMIT 1)
|
||||
WHERE slug IN ('vpn-panel', 'vpn-node');
|
||||
|
||||
PRAGMA foreign_keys = ON;
|
||||
@@ -0,0 +1,25 @@
|
||||
CREATE TABLE service_binding_ips (
|
||||
binding_id INTEGER NOT NULL REFERENCES service_bindings(id) ON DELETE CASCADE,
|
||||
ip TEXT NOT NULL,
|
||||
PRIMARY KEY (binding_id, ip)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_service_binding_ips_binding_id ON service_binding_ips(binding_id);
|
||||
|
||||
INSERT INTO service_binding_ips (binding_id, ip)
|
||||
SELECT sbr.binding_id, dr.content
|
||||
FROM service_binding_records sbr
|
||||
JOIN dns_records dr ON dr.id = sbr.dns_record_id
|
||||
WHERE dr.record_type = 'A'
|
||||
AND dr.content IS NOT NULL
|
||||
AND TRIM(dr.content) != ''
|
||||
ON CONFLICT(binding_id, ip) DO NOTHING;
|
||||
|
||||
INSERT INTO service_binding_ips (binding_id, ip)
|
||||
SELECT sb.id, dr.content
|
||||
FROM service_bindings sb
|
||||
JOIN dns_records dr ON dr.id = sb.dns_record_id
|
||||
WHERE dr.record_type = 'A'
|
||||
AND dr.content IS NOT NULL
|
||||
AND TRIM(dr.content) != ''
|
||||
ON CONFLICT(binding_id, ip) DO NOTHING;
|
||||
@@ -0,0 +1,7 @@
|
||||
CREATE TABLE service_group_dns_records (
|
||||
group_id INTEGER NOT NULL REFERENCES service_groups(id) ON DELETE CASCADE,
|
||||
dns_record_id INTEGER NOT NULL REFERENCES dns_records(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (group_id, dns_record_id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_service_group_dns_records_group_id ON service_group_dns_records(group_id);
|
||||
@@ -0,0 +1 @@
|
||||
CREATE INDEX IF NOT EXISTS idx_dns_records_domain_cf_id ON dns_records(domain_id, cf_record_id);
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "@cfdm/db",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsup src/index.ts --format esm --dts",
|
||||
"dev": "tsup src/index.ts --format esm --dts --watch",
|
||||
"db:generate": "drizzle-kit generate",
|
||||
"db:push": "drizzle-kit push"
|
||||
},
|
||||
"dependencies": {
|
||||
"@cfdm/shared": "workspace:*",
|
||||
"better-sqlite3": "^11.10.0",
|
||||
"drizzle-orm": "^0.44.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/better-sqlite3": "^7.6.13",
|
||||
"drizzle-kit": "^0.31.1",
|
||||
"tsup": "^8.5.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import Database from "better-sqlite3";
|
||||
import { existsSync } from "node:fs";
|
||||
|
||||
const path = process.argv[2] ?? "../../data/app.db";
|
||||
if (!existsSync(path)) {
|
||||
console.log("missing:", path);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const db = new Database(path, { readonly: true });
|
||||
const tables = db
|
||||
.prepare("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")
|
||||
.all()
|
||||
.map((t) => t.name);
|
||||
console.log("path:", path);
|
||||
console.log("tables:", tables.join(", "));
|
||||
for (const t of [
|
||||
"domains",
|
||||
"groups",
|
||||
"dns_records",
|
||||
"services",
|
||||
"_migrations",
|
||||
]) {
|
||||
if (!tables.includes(t)) continue;
|
||||
console.log(t, db.prepare(`SELECT COUNT(*) as c FROM ${t}`).get().c);
|
||||
}
|
||||
if (tables.includes("domains")) {
|
||||
console.log(
|
||||
"sample:",
|
||||
db.prepare("SELECT id, zone_name FROM domains LIMIT 5").all(),
|
||||
);
|
||||
}
|
||||
if (tables.includes("_migrations")) {
|
||||
console.log(
|
||||
"migrations:",
|
||||
db.prepare("SELECT name FROM _migrations ORDER BY name").all(),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { readFileSync, readdirSync } from "node:fs";
|
||||
import Database from "better-sqlite3";
|
||||
import { drizzle } from "drizzle-orm/better-sqlite3";
|
||||
import { schema } from "./schema.js";
|
||||
|
||||
export type Sqlite = Database.Database;
|
||||
export type Db = ReturnType<typeof drizzle<typeof schema>>;
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
export function resolveDatabasePath(databaseUrl: string): string {
|
||||
const url = databaseUrl.startsWith("sqlite:")
|
||||
? databaseUrl.slice("sqlite:".length)
|
||||
: databaseUrl;
|
||||
return url;
|
||||
}
|
||||
|
||||
export function createDb(databaseUrl: string): { db: Db; sqlite: Sqlite } {
|
||||
const path = resolveDatabasePath(databaseUrl);
|
||||
const sqlite = new Database(path);
|
||||
sqlite.pragma("journal_mode = WAL");
|
||||
sqlite.pragma("synchronous = NORMAL");
|
||||
sqlite.pragma("foreign_keys = ON");
|
||||
const db = drizzle(sqlite, { schema });
|
||||
return { db, sqlite };
|
||||
}
|
||||
|
||||
export function createMemoryDb(): { db: Db; sqlite: Sqlite } {
|
||||
const sqlite = new Database(":memory:");
|
||||
sqlite.pragma("foreign_keys = ON");
|
||||
const db = drizzle(sqlite, { schema });
|
||||
return { db, sqlite };
|
||||
}
|
||||
|
||||
export function runMigrations(sqlite: Sqlite): void {
|
||||
const migrationsDir = join(__dirname, "..", "migrations");
|
||||
const files = readdirSync(migrationsDir)
|
||||
.filter((f) => f.endsWith(".sql"))
|
||||
.sort();
|
||||
|
||||
sqlite.exec(
|
||||
`CREATE TABLE IF NOT EXISTS _migrations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
)`,
|
||||
);
|
||||
|
||||
for (const file of files) {
|
||||
const applied = sqlite
|
||||
.prepare("SELECT 1 FROM _migrations WHERE name = ?")
|
||||
.get(file);
|
||||
if (applied) continue;
|
||||
|
||||
const sql = readFileSync(join(migrationsDir, file), "utf-8");
|
||||
sqlite.exec(sql);
|
||||
sqlite.prepare("INSERT INTO _migrations (name) VALUES (?)").run(file);
|
||||
}
|
||||
}
|
||||
|
||||
export function healthCheck(sqlite: Sqlite): void {
|
||||
sqlite.prepare("SELECT 1").get();
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
export class NotFoundError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = "NotFoundError";
|
||||
}
|
||||
}
|
||||
|
||||
export class ConflictError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = "ConflictError";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export * from "./schema.js";
|
||||
export * from "./client.js";
|
||||
export * from "./errors.js";
|
||||
export * as repos from "./repos.js";
|
||||
export type { DnsListFilter } from "./repos.js";
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,225 @@
|
||||
import { sql } from "drizzle-orm";
|
||||
import {
|
||||
integer,
|
||||
primaryKey,
|
||||
sqliteTable,
|
||||
text,
|
||||
} from "drizzle-orm/sqlite-core";
|
||||
|
||||
export const groups = sqliteTable("groups", {
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
name: text("name").notNull(),
|
||||
slug: text("slug").notNull().unique(),
|
||||
created_at: text("created_at")
|
||||
.notNull()
|
||||
.default(sql`datetime('now')`),
|
||||
updated_at: text("updated_at")
|
||||
.notNull()
|
||||
.default(sql`datetime('now')`),
|
||||
});
|
||||
|
||||
export const services = sqliteTable("services", {
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
name: text("name").notNull(),
|
||||
slug: text("slug").notNull().unique(),
|
||||
service_group_id: integer("service_group_id").references(
|
||||
() => serviceGroups.id,
|
||||
{ onDelete: "set null" },
|
||||
),
|
||||
subdomain: text("subdomain"),
|
||||
enabled: integer("enabled", { mode: "boolean" }).notNull().default(false),
|
||||
created_at: text("created_at")
|
||||
.notNull()
|
||||
.default(sql`datetime('now')`),
|
||||
updated_at: text("updated_at")
|
||||
.notNull()
|
||||
.default(sql`datetime('now')`),
|
||||
});
|
||||
|
||||
export const serviceGroups = sqliteTable("service_groups", {
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
name: text("name").notNull(),
|
||||
type: text("type").notNull().default("custom"),
|
||||
icon: text("icon"),
|
||||
domain: text("domain"),
|
||||
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
|
||||
created_at: text("created_at")
|
||||
.notNull()
|
||||
.default(sql`datetime('now')`),
|
||||
updated_at: text("updated_at")
|
||||
.notNull()
|
||||
.default(sql`datetime('now')`),
|
||||
});
|
||||
|
||||
export const domains = sqliteTable("domains", {
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
group_id: integer("group_id").references(() => groups.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
zone_name: text("zone_name").notNull().unique(),
|
||||
cf_zone_id: text("cf_zone_id").notNull(),
|
||||
status: text("status").notNull().default("active"),
|
||||
last_synced_at: text("last_synced_at"),
|
||||
created_at: text("created_at")
|
||||
.notNull()
|
||||
.default(sql`datetime('now')`),
|
||||
updated_at: text("updated_at")
|
||||
.notNull()
|
||||
.default(sql`datetime('now')`),
|
||||
});
|
||||
|
||||
export const subdomains = sqliteTable("subdomains", {
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
domain_id: integer("domain_id")
|
||||
.notNull()
|
||||
.references(() => domains.id, { onDelete: "cascade" }),
|
||||
name: text("name").notNull(),
|
||||
fqdn: text("fqdn").notNull(),
|
||||
created_at: text("created_at")
|
||||
.notNull()
|
||||
.default(sql`datetime('now')`),
|
||||
updated_at: text("updated_at")
|
||||
.notNull()
|
||||
.default(sql`datetime('now')`),
|
||||
});
|
||||
|
||||
export const dnsRecords = sqliteTable("dns_records", {
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
domain_id: integer("domain_id")
|
||||
.notNull()
|
||||
.references(() => domains.id, { onDelete: "cascade" }),
|
||||
cf_record_id: text("cf_record_id"),
|
||||
record_type: text("record_type").notNull(),
|
||||
name: text("name").notNull(),
|
||||
content: text("content").notNull(),
|
||||
ttl: integer("ttl").notNull().default(1),
|
||||
proxied: integer("proxied", { mode: "boolean" }).notNull().default(false),
|
||||
priority: integer("priority"),
|
||||
sync_status: text("sync_status").notNull().default("pending_push"),
|
||||
origin: text("origin").notNull().default("local"),
|
||||
last_error: text("last_error"),
|
||||
created_at: text("created_at")
|
||||
.notNull()
|
||||
.default(sql`datetime('now')`),
|
||||
updated_at: text("updated_at")
|
||||
.notNull()
|
||||
.default(sql`datetime('now')`),
|
||||
});
|
||||
|
||||
export const serviceBindings = sqliteTable("service_bindings", {
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
domain_id: integer("domain_id")
|
||||
.notNull()
|
||||
.references(() => domains.id, { onDelete: "cascade" }),
|
||||
service_id: integer("service_id")
|
||||
.notNull()
|
||||
.references(() => services.id, { onDelete: "cascade" }),
|
||||
hostname: text("hostname").notNull().default("@"),
|
||||
dns_record_id: integer("dns_record_id").references(() => dnsRecords.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
created_at: text("created_at")
|
||||
.notNull()
|
||||
.default(sql`datetime('now')`),
|
||||
updated_at: text("updated_at")
|
||||
.notNull()
|
||||
.default(sql`datetime('now')`),
|
||||
});
|
||||
|
||||
export const serviceIps = sqliteTable("service_ips", {
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
service_id: integer("service_id")
|
||||
.notNull()
|
||||
.references(() => services.id, { onDelete: "cascade" }),
|
||||
ip: text("ip").notNull(),
|
||||
created_at: text("created_at")
|
||||
.notNull()
|
||||
.default(sql`datetime('now')`),
|
||||
});
|
||||
|
||||
export const serviceBindingRecords = sqliteTable(
|
||||
"service_binding_records",
|
||||
{
|
||||
binding_id: integer("binding_id")
|
||||
.notNull()
|
||||
.references(() => serviceBindings.id, { onDelete: "cascade" }),
|
||||
dns_record_id: integer("dns_record_id")
|
||||
.notNull()
|
||||
.references(() => dnsRecords.id, { onDelete: "cascade" }),
|
||||
},
|
||||
(t) => [primaryKey({ columns: [t.binding_id, t.dns_record_id] })],
|
||||
);
|
||||
|
||||
export const serviceBindingIps = sqliteTable(
|
||||
"service_binding_ips",
|
||||
{
|
||||
binding_id: integer("binding_id")
|
||||
.notNull()
|
||||
.references(() => serviceBindings.id, { onDelete: "cascade" }),
|
||||
ip: text("ip").notNull(),
|
||||
},
|
||||
(t) => [primaryKey({ columns: [t.binding_id, t.ip] })],
|
||||
);
|
||||
|
||||
export const serviceGroupDnsRecords = sqliteTable(
|
||||
"service_group_dns_records",
|
||||
{
|
||||
group_id: integer("group_id")
|
||||
.notNull()
|
||||
.references(() => serviceGroups.id, { onDelete: "cascade" }),
|
||||
dns_record_id: integer("dns_record_id")
|
||||
.notNull()
|
||||
.references(() => dnsRecords.id, { onDelete: "cascade" }),
|
||||
},
|
||||
(t) => [primaryKey({ columns: [t.group_id, t.dns_record_id] })],
|
||||
);
|
||||
|
||||
export const certificates = sqliteTable("certificates", {
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
domain_id: integer("domain_id")
|
||||
.notNull()
|
||||
.references(() => domains.id, { onDelete: "cascade" }),
|
||||
subdomain_id: integer("subdomain_id").references(() => subdomains.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
hostname: text("hostname").notNull().unique(),
|
||||
expires_at: text("expires_at"),
|
||||
last_checked_at: text("last_checked_at"),
|
||||
last_error: text("last_error"),
|
||||
status: text("status").notNull().default("unknown"),
|
||||
created_at: text("created_at")
|
||||
.notNull()
|
||||
.default(sql`datetime('now')`),
|
||||
updated_at: text("updated_at")
|
||||
.notNull()
|
||||
.default(sql`datetime('now')`),
|
||||
});
|
||||
|
||||
export const syncJobs = sqliteTable("sync_jobs", {
|
||||
id: text("id").primaryKey(),
|
||||
status: text("status").notNull().default("pending"),
|
||||
domain_id: integer("domain_id").references(() => domains.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
message: text("message"),
|
||||
created_at: text("created_at")
|
||||
.notNull()
|
||||
.default(sql`datetime('now')`),
|
||||
finished_at: text("finished_at"),
|
||||
});
|
||||
|
||||
export const schema = {
|
||||
groups,
|
||||
services,
|
||||
serviceGroups,
|
||||
domains,
|
||||
subdomains,
|
||||
dnsRecords,
|
||||
serviceBindings,
|
||||
serviceIps,
|
||||
serviceBindingRecords,
|
||||
serviceBindingIps,
|
||||
serviceGroupDnsRecords,
|
||||
certificates,
|
||||
syncJobs,
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"declaration": true,
|
||||
"outDir": "dist",
|
||||
"rootDir": "src"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
Reference in new issue
Block a user