package store import ( "context" "errors" "github.com/google/uuid" "github.com/jackc/pgx/v5" "github.com/vasyakrg/dns-autoresolver/internal/provider" "github.com/vasyakrg/dns-autoresolver/internal/store/db" "github.com/vasyakrg/dns-autoresolver/internal/store/dto" ) // Account/Template/Domain are provider-neutral domain structs returned by the // thin wrappers below, so callers (internal/api) never need to import // internal/store/db directly. type Account struct { ID uuid.UUID ProjectID uuid.UUID Provider string SecretEnc string Comment string } func accountFromDB(a db.ProviderAccount) Account { return Account{ID: a.ID, ProjectID: a.ProjectID, Provider: a.Provider, SecretEnc: a.SecretEnc, Comment: a.Comment} } func (s *Store) CreateAccount(ctx context.Context, projectID uuid.UUID, provider, secretEnc, comment string) (Account, error) { a, err := s.q.CreateAccount(ctx, db.CreateAccountParams{ ID: uuid.New(), ProjectID: projectID, Provider: provider, SecretEnc: secretEnc, Comment: comment, }) if err != nil { return Account{}, err } return accountFromDB(a), nil } func (s *Store) ListAccounts(ctx context.Context, projectID uuid.UUID) ([]Account, error) { rows, err := s.q.ListAccounts(ctx, projectID) if err != nil { return nil, err } out := make([]Account, 0, len(rows)) for _, r := range rows { out = append(out, accountFromDB(r)) } return out, nil } func (s *Store) GetAccount(ctx context.Context, id, projectID uuid.UUID) (Account, error) { a, err := s.q.GetAccount(ctx, db.GetAccountParams{ID: id, ProjectID: projectID}) if err != nil { return Account{}, err } return accountFromDB(a), nil } func (s *Store) DeleteAccount(ctx context.Context, id, projectID uuid.UUID) error { return s.q.DeleteAccount(ctx, db.DeleteAccountParams{ID: id, ProjectID: projectID}) } type Template struct { ID uuid.UUID ProjectID uuid.UUID Name string Doc dto.TemplateDoc Version int32 } func templateFromDB(t db.Template) Template { var doc dto.TemplateDoc if t.Doc != nil { doc = *t.Doc } return Template{ID: t.ID, ProjectID: t.ProjectID, Name: t.Name, Doc: doc, Version: t.Version} } func (s *Store) CreateTemplate(ctx context.Context, projectID uuid.UUID, name string, doc dto.TemplateDoc) (Template, error) { d := doc t, err := s.q.CreateTemplate(ctx, db.CreateTemplateParams{ID: uuid.New(), ProjectID: projectID, Name: name, Doc: &d}) if err != nil { return Template{}, err } return templateFromDB(t), nil } func (s *Store) ListTemplates(ctx context.Context, projectID uuid.UUID) ([]Template, error) { rows, err := s.q.ListTemplates(ctx, projectID) if err != nil { return nil, err } out := make([]Template, 0, len(rows)) for _, r := range rows { out = append(out, templateFromDB(r)) } return out, nil } func (s *Store) UpdateTemplate(ctx context.Context, id, projectID uuid.UUID, name string, doc dto.TemplateDoc) (Template, error) { d := doc t, err := s.q.UpdateTemplate(ctx, db.UpdateTemplateParams{ID: id, ProjectID: projectID, Name: name, Doc: &d}) if err != nil { return Template{}, err } return templateFromDB(t), nil } func (s *Store) DeleteTemplate(ctx context.Context, id, projectID uuid.UUID) error { return s.q.DeleteTemplate(ctx, db.DeleteTemplateParams{ID: id, ProjectID: projectID}) } // GetTemplate is a scoped lookup used to verify a template belongs to // projectID before it is referenced elsewhere (e.g. CreateDomain). func (s *Store) GetTemplate(ctx context.Context, id, projectID uuid.UUID) (Template, error) { t, err := s.q.GetTemplate(ctx, db.GetTemplateParams{ID: id, ProjectID: projectID}) if err != nil { return Template{}, err } return templateFromDB(t), nil } type Domain struct { ID uuid.UUID ProjectID uuid.UUID ProviderAccountID uuid.UUID ZoneName string ZoneID string TemplateID *uuid.UUID } func domainFromDB(d db.Domain) Domain { return Domain{ ID: d.ID, ProjectID: d.ProjectID, ProviderAccountID: d.ProviderAccountID, ZoneName: d.ZoneName, ZoneID: d.ZoneID, TemplateID: d.TemplateID, } } func (s *Store) CreateDomain(ctx context.Context, projectID, accountID uuid.UUID, zoneName, zoneID string, templateID *uuid.UUID) (Domain, error) { d, err := s.q.CreateDomain(ctx, db.CreateDomainParams{ ID: uuid.New(), ProjectID: projectID, ProviderAccountID: accountID, ZoneName: zoneName, ZoneID: zoneID, TemplateID: templateID, }) if err != nil { return Domain{}, err } return domainFromDB(d), nil } func (s *Store) ListDomains(ctx context.Context, projectID uuid.UUID) ([]Domain, error) { rows, err := s.q.ListDomains(ctx, projectID) if err != nil { return nil, err } out := make([]Domain, 0, len(rows)) for _, r := range rows { out = append(out, domainFromDB(r)) } return out, nil } func (s *Store) DeleteDomain(ctx context.Context, id, projectID uuid.UUID) error { return s.q.DeleteDomain(ctx, db.DeleteDomainParams{ID: id, ProjectID: projectID}) } // ImportDomains creates one domain per zone inside a single transaction: if // any zone fails to be created, the whole batch is rolled back so callers // never observe a partially-imported set of domains. // // Import is idempotent: zones that already have a domain for this project // (enforced by the domains_project_zone_uniq constraint) are silently // skipped via ON CONFLICT DO NOTHING rather than erroring or duplicating — // so a repeated POST .../import never creates duplicate domains. Only the // zones that were actually newly created are returned. func (s *Store) ImportDomains(ctx context.Context, projectID, accountID uuid.UUID, zones []provider.Zone) ([]Domain, error) { tx, err := s.pool.Begin(ctx) if err != nil { return nil, err } defer tx.Rollback(ctx) // no-op once Commit has succeeded q := s.q.WithTx(tx) out := make([]Domain, 0, len(zones)) for _, z := range zones { d, err := q.ImportDomain(ctx, db.ImportDomainParams{ ID: uuid.New(), ProjectID: projectID, ProviderAccountID: accountID, ZoneName: z.Name, ZoneID: z.ID, TemplateID: nil, }) if err != nil { if errors.Is(err, pgx.ErrNoRows) { // ON CONFLICT DO NOTHING: this zone was already imported // for this project — skip it rather than fail the batch. continue } return nil, err } out = append(out, domainFromDB(d)) } if err := tx.Commit(ctx); err != nil { return nil, err } return out, nil } // SetDomainTemplate attaches (or clears, when templateID is nil) the DNS // template used to check/apply a domain. When templateID is non-nil it must // belong to the same project — verified via scoped GetTemplate — otherwise // a caller could bind a domain to another tenant's template. func (s *Store) SetDomainTemplate(ctx context.Context, domainID, projectID uuid.UUID, templateID *uuid.UUID) (Domain, error) { if templateID != nil { if _, err := s.GetTemplate(ctx, *templateID, projectID); err != nil { return Domain{}, err } } d, err := s.q.UpdateDomainTemplate(ctx, db.UpdateDomainTemplateParams{ ID: domainID, ProjectID: projectID, TemplateID: templateID, }) if err != nil { return Domain{}, err } return domainFromDB(d), nil }