You've already forked coredns-powerdns-api-wrapper
124 lines
3.4 KiB
Go
124 lines
3.4 KiB
Go
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
// Copyright (C) 2026 Bryan Joshua Pedini
|
|
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
)
|
|
|
|
// Store wraps DB access against the {TablePrefix}records table (schema per
|
|
// docs/SEMANTICS.md §1).
|
|
type Store struct {
|
|
db *sql.DB
|
|
tableName string
|
|
}
|
|
|
|
// NewStore builds a Store bound to {prefix}records.
|
|
func NewStore(db *sql.DB, tablePrefix string) *Store {
|
|
return &Store{
|
|
db: db,
|
|
tableName: tablePrefix + "records",
|
|
}
|
|
}
|
|
|
|
// Record is one row of {prefix}records.
|
|
type Record struct {
|
|
Name string
|
|
TTL sql.NullInt64
|
|
Content string
|
|
RecordType string
|
|
}
|
|
|
|
// ListZones returns distinct zones present in the table, ordered by name.
|
|
func (st *Store) ListZones(ctx context.Context) ([]string, error) {
|
|
query := fmt.Sprintf("SELECT DISTINCT zone FROM %s ORDER BY zone", st.tableName)
|
|
rows, err := st.db.QueryContext(ctx, query)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var zones []string
|
|
for rows.Next() {
|
|
var zone string
|
|
if err := rows.Scan(&zone); err != nil {
|
|
return nil, err
|
|
}
|
|
zones = append(zones, zone)
|
|
}
|
|
return zones, rows.Err()
|
|
}
|
|
|
|
// GetZoneRecords returns every row for the given zone.
|
|
func (st *Store) GetZoneRecords(ctx context.Context, zone string) ([]Record, error) {
|
|
query := fmt.Sprintf("SELECT name, ttl, content, record_type FROM %s WHERE zone = ?", st.tableName)
|
|
rows, err := st.db.QueryContext(ctx, query, zone)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var records []Record
|
|
for rows.Next() {
|
|
var rec Record
|
|
if err := rows.Scan(&rec.Name, &rec.TTL, &rec.Content, &rec.RecordType); err != nil {
|
|
return nil, err
|
|
}
|
|
records = append(records, rec)
|
|
}
|
|
return records, rows.Err()
|
|
}
|
|
|
|
// ZoneExists reports whether at least one row exists for the zone.
|
|
func (st *Store) ZoneExists(ctx context.Context, zone string) (bool, error) {
|
|
query := fmt.Sprintf("SELECT EXISTS(SELECT 1 FROM %s WHERE zone = ?)", st.tableName)
|
|
var exists bool
|
|
if err := st.db.QueryRowContext(ctx, query, zone).Scan(&exists); err != nil {
|
|
return false, err
|
|
}
|
|
return exists, nil
|
|
}
|
|
|
|
// RRsetChange is one decomposed rrset change ready to apply to the DB.
|
|
type RRsetChange struct {
|
|
Zone string
|
|
Name string // relative name, per SEMANTICS §1 (empty string at apex)
|
|
RecordType string
|
|
ChangeType string // REPLACE or DELETE
|
|
TTL int
|
|
Contents []string // marshalled JSON content, one per record; unused for DELETE
|
|
}
|
|
|
|
// ApplyRRsets runs every rrset change of one PATCH inside a single
|
|
// transaction. REPLACE deletes existing rows for (zone, name, record_type)
|
|
// then inserts one row per record. DELETE just deletes. Any error rolls
|
|
// back the entire transaction.
|
|
func (st *Store) ApplyRRsets(ctx context.Context, changes []RRsetChange) error {
|
|
tx, err := st.db.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer tx.Rollback()
|
|
|
|
deleteQuery := fmt.Sprintf("DELETE FROM %s WHERE zone = ? AND name = ? AND record_type = ?", st.tableName)
|
|
insertQuery := fmt.Sprintf("INSERT INTO %s (zone, name, ttl, content, record_type) VALUES (?, ?, ?, ?, ?)", st.tableName)
|
|
|
|
for _, ch := range changes {
|
|
if _, err := tx.ExecContext(ctx, deleteQuery, ch.Zone, ch.Name, ch.RecordType); err != nil {
|
|
return err
|
|
}
|
|
if ch.ChangeType == "REPLACE" {
|
|
for _, content := range ch.Contents {
|
|
if _, err := tx.ExecContext(ctx, insertQuery, ch.Zone, ch.Name, ch.TTL, content, ch.RecordType); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return tx.Commit()
|
|
}
|