initial commit
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
/**
|
||||
* Simple JSON file-based cache for DNS records.
|
||||
* No native dependencies — works on any Node version.
|
||||
*
|
||||
* Structure of dns-cache.json:
|
||||
* {
|
||||
* "cloudflare": {
|
||||
* "zone123": {
|
||||
* "synced_at": "2024-01-01T00:00:00.000Z",
|
||||
* "records": [ { id, type, name, content, ttl, priority }, ... ]
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const CACHE_PATH = process.env.DB_PATH || path.join(__dirname, '..', 'dns-cache.json');
|
||||
|
||||
// ─── Load / save ─────────────────────────────────────────────────────────────
|
||||
|
||||
function load() {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(CACHE_PATH, 'utf8'));
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function save(data) {
|
||||
fs.writeFileSync(CACHE_PATH, JSON.stringify(data, null, 2), 'utf8');
|
||||
}
|
||||
|
||||
// ─── Public API ──────────────────────────────────────────────────────────────
|
||||
|
||||
function getRecords(provider, zoneId) {
|
||||
const data = load();
|
||||
return data[provider]?.[zoneId]?.records ?? [];
|
||||
}
|
||||
|
||||
function getSyncedAt(provider, zoneId) {
|
||||
const data = load();
|
||||
return data[provider]?.[zoneId]?.synced_at ?? null;
|
||||
}
|
||||
|
||||
// Returns { [zoneId]: { synced_at, record_count } } for all cached zones of a provider
|
||||
function getProviderSyncStatus(provider) {
|
||||
const data = load();
|
||||
const zones = data[provider] ?? {};
|
||||
const result = {};
|
||||
for (const [zoneId, zone] of Object.entries(zones)) {
|
||||
result[zoneId] = {
|
||||
synced_at: zone.synced_at ?? null,
|
||||
record_count: (zone.records ?? []).length,
|
||||
};
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function getZoneName(provider, zoneId) {
|
||||
const data = load();
|
||||
return data[provider]?.[zoneId]?.zone_name ?? zoneId;
|
||||
}
|
||||
|
||||
function replaceZoneRecords(provider, zoneId, records, zoneName) {
|
||||
const data = load();
|
||||
if (!data[provider]) data[provider] = {};
|
||||
data[provider][zoneId] = {
|
||||
zone_name: zoneName ?? data[provider]?.[zoneId]?.zone_name ?? zoneId,
|
||||
synced_at: new Date().toISOString(),
|
||||
records: records.map(r => ({
|
||||
id: r.id,
|
||||
type: r.type,
|
||||
name: r.name,
|
||||
content: r.content,
|
||||
ttl: r.ttl ?? null,
|
||||
priority: r.priority ?? null,
|
||||
})),
|
||||
};
|
||||
save(data);
|
||||
}
|
||||
|
||||
function upsertRecord(provider, zoneId, record) {
|
||||
const data = load();
|
||||
if (!data[provider]) data[provider] = {};
|
||||
if (!data[provider][zoneId]) data[provider][zoneId] = { synced_at: null, records: [] };
|
||||
|
||||
const records = data[provider][zoneId].records;
|
||||
const idx = records.findIndex(r => r.id === record.id);
|
||||
const entry = {
|
||||
id: record.id,
|
||||
type: record.type,
|
||||
name: record.name,
|
||||
content: record.content,
|
||||
ttl: record.ttl ?? null,
|
||||
priority: record.priority ?? null,
|
||||
};
|
||||
|
||||
if (idx >= 0) records[idx] = entry;
|
||||
else records.push(entry);
|
||||
|
||||
save(data);
|
||||
}
|
||||
|
||||
function deleteRecord(provider, zoneId, recordId) {
|
||||
const data = load();
|
||||
const zone = data[provider]?.[zoneId];
|
||||
if (!zone) return;
|
||||
zone.records = zone.records.filter(r => r.id !== recordId);
|
||||
save(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all cached zones across all providers as a flat list.
|
||||
* Groups duplicate zone names so you can spot the same domain on multiple providers.
|
||||
*/
|
||||
function getAllZones() {
|
||||
const data = load();
|
||||
const byName = {};
|
||||
|
||||
for (const [provider, zones] of Object.entries(data)) {
|
||||
for (const [zoneId, zone] of Object.entries(zones)) {
|
||||
const name = zone.zone_name ?? zoneId;
|
||||
if (!byName[name]) byName[name] = [];
|
||||
byName[name].push({
|
||||
provider,
|
||||
zone_id: zoneId,
|
||||
record_count: (zone.records ?? []).length,
|
||||
synced_at: zone.synced_at ?? null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return Object.entries(byName)
|
||||
.map(([name, entries]) => ({ name, entries, duplicate: entries.length > 1 }))
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
|
||||
function getStats() {
|
||||
const data = load();
|
||||
const typeCounts = {};
|
||||
let totalZones = 0;
|
||||
let zonesWithMx = 0;
|
||||
const perProvider = {};
|
||||
|
||||
for (const [provider, zones] of Object.entries(data)) {
|
||||
const zoneCount = Object.keys(zones).length;
|
||||
totalZones += zoneCount;
|
||||
perProvider[provider] = { zones: zoneCount };
|
||||
|
||||
for (const zone of Object.values(zones)) {
|
||||
const records = zone.records ?? [];
|
||||
const hasMx = records.some(r => r.type === 'MX');
|
||||
if (hasMx) zonesWithMx++;
|
||||
|
||||
for (const r of records) {
|
||||
typeCounts[r.type] = (typeCounts[r.type] || 0) + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const recordTypes = Object.entries(typeCounts)
|
||||
.map(([type, count]) => ({ type, count }))
|
||||
.sort((a, b) => b.count - a.count);
|
||||
|
||||
return { totalZones, zonesWithMx, recordTypes, perProvider };
|
||||
}
|
||||
|
||||
module.exports = { getRecords, getSyncedAt, getZoneName, getProviderSyncStatus, getAllZones, replaceZoneRecords, upsertRecord, deleteRecord, getStats };
|
||||
Reference in New Issue
Block a user