Domain names become an allocatable resource with one authority. A new core module (schema `domains`, own migration stream between core and the integrations) owns claims — a DNS node plus its whole subtree, mutually disjoint: operator shared-domain roots, member claims carved from them, and bring-your-own names proven by TXT verification — and placements, which bind a name inside a claim to a provider slug and resource ref. Verification moves to the claim and decouples from creation. A member proves control of a domain once; afterwards every name inside it places instantly, wildcard-CNAME friendly, with no further DNS work. The claim workflow activates the claim and stops — it no longer creates a site — so the sites list offers a one-click create once a domain verifies. /domains/ask answers from placements and is registered by core rather than the FedWiki adapter; its HTTP contract is unchanged. A configured `domains-ask-fallback-url` forwards names the registry does not know to a legacy answerer, the strangler seam wiki.cafe's migration needs; a name the registry knows but has archived is refused locally. FedWiki's create saga reserves the name before the farm call, carrying a workflow-minted site id so retries are idempotent, and compensates on failure. Sync places only names it owns, never stealing a member's; lifecycle transitions and the retention purge maintain servability. An unconditional boot pass seeds operator roots, releases orphaned placements, and adopts pre-existing sites — grandfathering member-owned external domains shortest-name-first, and skipping name policy, so a live single-letter site cannot lose its certificate. Members manage domains at /domains: claims with verification status, DNS records including an optional wildcard row, check-now, cancel, release. Name policy (reserved, blocked, premium, plus a single-letter guard) is operator data; refusals collapse to a plain "unavailable" so the console never becomes an oracle for who holds what. BREAKING (pre-release): `fedwiki.custom_domain_verifications` and `sites.is_custom_domain` are dropped, the flag now derived from the placement's claim kind; resource key `fedwiki_custom_domains` migrates to the platform-owned `external_domain_claims`; running verify-custom-domain workflows must be terminated before deploy.
722 lines
22 KiB
Go
722 lines
22 KiB
Go
// Code generated by sqlc. DO NOT EDIT.
|
|
// versions:
|
|
// sqlc v1.29.0
|
|
// source: sites.sql
|
|
|
|
package fedwiki
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"time"
|
|
)
|
|
|
|
const clearForceReducedByDomain = `-- name: ClearForceReducedByDomain :exec
|
|
UPDATE fedwiki.sites
|
|
SET force_reduced_at = NULL, updated_at = NOW()
|
|
WHERE domain = $1
|
|
`
|
|
|
|
func (q *Queries) ClearForceReducedByDomain(ctx context.Context, domain string) error {
|
|
_, err := q.db.ExecContext(ctx, clearForceReducedByDomain, domain)
|
|
return err
|
|
}
|
|
|
|
const countActiveSitesByWorkspace = `-- name: CountActiveSitesByWorkspace :one
|
|
SELECT COUNT(*) AS count FROM fedwiki.sites
|
|
WHERE workspace_id = $1 AND status = 'active'
|
|
`
|
|
|
|
func (q *Queries) CountActiveSitesByWorkspace(ctx context.Context, workspaceID string) (int64, error) {
|
|
row := q.db.QueryRowContext(ctx, countActiveSitesByWorkspace, workspaceID)
|
|
var count int64
|
|
err := row.Scan(&count)
|
|
return count, err
|
|
}
|
|
|
|
const countSitesByWorkspace = `-- name: CountSitesByWorkspace :one
|
|
SELECT COUNT(*) AS count FROM fedwiki.sites
|
|
WHERE workspace_id = $1
|
|
`
|
|
|
|
func (q *Queries) CountSitesByWorkspace(ctx context.Context, workspaceID string) (int64, error) {
|
|
row := q.db.QueryRowContext(ctx, countSitesByWorkspace, workspaceID)
|
|
var count int64
|
|
err := row.Scan(&count)
|
|
return count, err
|
|
}
|
|
|
|
const createSite = `-- name: CreateSite :one
|
|
INSERT INTO fedwiki.sites (site_id, workspace_id, domain)
|
|
VALUES ($1, $2, $3)
|
|
RETURNING site_id, workspace_id, domain, created_at, updated_at, status, storage_bytes, last_modified_at, force_reduced_at, archived_at
|
|
`
|
|
|
|
type CreateSiteParams struct {
|
|
SiteID string `json:"site_id"`
|
|
WorkspaceID string `json:"workspace_id"`
|
|
Domain string `json:"domain"`
|
|
}
|
|
|
|
// site_id is supplied by the caller, not defaulted: the creation saga
|
|
// pre-generates it so the registry placement can reserve the name
|
|
// (resource_ref = site_id) BEFORE the farm call, and this insert then binds
|
|
// the same id (domains-registry D8).
|
|
func (q *Queries) CreateSite(ctx context.Context, arg CreateSiteParams) (Site, error) {
|
|
row := q.db.QueryRowContext(ctx, createSite, arg.SiteID, arg.WorkspaceID, arg.Domain)
|
|
var i Site
|
|
err := row.Scan(
|
|
&i.SiteID,
|
|
&i.WorkspaceID,
|
|
&i.Domain,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.Status,
|
|
&i.StorageBytes,
|
|
&i.LastModifiedAt,
|
|
&i.ForceReducedAt,
|
|
&i.ArchivedAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const deleteSite = `-- name: DeleteSite :exec
|
|
DELETE FROM fedwiki.sites
|
|
WHERE site_id = $1
|
|
`
|
|
|
|
func (q *Queries) DeleteSite(ctx context.Context, siteID string) error {
|
|
_, err := q.db.ExecContext(ctx, deleteSite, siteID)
|
|
return err
|
|
}
|
|
|
|
const deleteSiteByDomain = `-- name: DeleteSiteByDomain :exec
|
|
DELETE FROM fedwiki.sites
|
|
WHERE domain = $1
|
|
`
|
|
|
|
func (q *Queries) DeleteSiteByDomain(ctx context.Context, domain string) error {
|
|
_, err := q.db.ExecContext(ctx, deleteSiteByDomain, domain)
|
|
return err
|
|
}
|
|
|
|
const getSiteByDomain = `-- name: GetSiteByDomain :one
|
|
SELECT s.site_id, s.workspace_id, s.domain, s.created_at, s.updated_at, s.status, s.storage_bytes, s.last_modified_at, s.force_reduced_at, s.archived_at, (EXISTS (
|
|
SELECT 1 FROM domains.placements p
|
|
JOIN domains.claims c ON c.claim_id = p.claim_id
|
|
WHERE p.provider = 'fedwiki'
|
|
AND p.resource_ref = s.site_id::text
|
|
AND c.kind = 'external'
|
|
))::boolean AS is_custom_domain
|
|
FROM fedwiki.sites AS s
|
|
WHERE s.domain = $1
|
|
`
|
|
|
|
type GetSiteByDomainRow struct {
|
|
SiteID string `json:"site_id"`
|
|
WorkspaceID string `json:"workspace_id"`
|
|
Domain string `json:"domain"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
Status string `json:"status"`
|
|
StorageBytes sql.NullInt64 `json:"storage_bytes"`
|
|
LastModifiedAt sql.NullTime `json:"last_modified_at"`
|
|
ForceReducedAt sql.NullTime `json:"force_reduced_at"`
|
|
ArchivedAt sql.NullTime `json:"archived_at"`
|
|
IsCustomDomain bool `json:"is_custom_domain"`
|
|
}
|
|
|
|
func (q *Queries) GetSiteByDomain(ctx context.Context, domain string) (GetSiteByDomainRow, error) {
|
|
row := q.db.QueryRowContext(ctx, getSiteByDomain, domain)
|
|
var i GetSiteByDomainRow
|
|
err := row.Scan(
|
|
&i.SiteID,
|
|
&i.WorkspaceID,
|
|
&i.Domain,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.Status,
|
|
&i.StorageBytes,
|
|
&i.LastModifiedAt,
|
|
&i.ForceReducedAt,
|
|
&i.ArchivedAt,
|
|
&i.IsCustomDomain,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getSiteByID = `-- name: GetSiteByID :one
|
|
|
|
SELECT s.site_id, s.workspace_id, s.domain, s.created_at, s.updated_at, s.status, s.storage_bytes, s.last_modified_at, s.force_reduced_at, s.archived_at, (EXISTS (
|
|
SELECT 1 FROM domains.placements p
|
|
JOIN domains.claims c ON c.claim_id = p.claim_id
|
|
WHERE p.provider = 'fedwiki'
|
|
AND p.resource_ref = s.site_id::text
|
|
AND c.kind = 'external'
|
|
))::boolean AS is_custom_domain
|
|
FROM fedwiki.sites AS s
|
|
WHERE s.site_id = $1
|
|
`
|
|
|
|
type GetSiteByIDRow struct {
|
|
SiteID string `json:"site_id"`
|
|
WorkspaceID string `json:"workspace_id"`
|
|
Domain string `json:"domain"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
Status string `json:"status"`
|
|
StorageBytes sql.NullInt64 `json:"storage_bytes"`
|
|
LastModifiedAt sql.NullTime `json:"last_modified_at"`
|
|
ForceReducedAt sql.NullTime `json:"force_reduced_at"`
|
|
ArchivedAt sql.NullTime `json:"archived_at"`
|
|
IsCustomDomain bool `json:"is_custom_domain"`
|
|
}
|
|
|
|
// The display queries below derive `is_custom_domain` instead of reading a
|
|
// column: whether a site sits on a member-owned domain is a fact about its
|
|
// registry placement's claim, not about the site (domains-registry D8, the
|
|
// dropped fedwiki.sites.is_custom_domain). The EXISTS is cast to boolean so
|
|
// the generated field stays a plain `bool` named IsCustomDomain, keeping
|
|
// every render site unchanged. Only the queries that feed member, API, and
|
|
// operator display carry it; the reconcile/sweep queries
|
|
// (ListActiveSitesByWorkspace, ListForceReducedSitesByWorkspace,
|
|
// ListArchivedSitesArchivedBefore) do not render it and stay plain
|
|
// SELECT * so they keep returning the Site model.
|
|
func (q *Queries) GetSiteByID(ctx context.Context, siteID string) (GetSiteByIDRow, error) {
|
|
row := q.db.QueryRowContext(ctx, getSiteByID, siteID)
|
|
var i GetSiteByIDRow
|
|
err := row.Scan(
|
|
&i.SiteID,
|
|
&i.WorkspaceID,
|
|
&i.Domain,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.Status,
|
|
&i.StorageBytes,
|
|
&i.LastModifiedAt,
|
|
&i.ForceReducedAt,
|
|
&i.ArchivedAt,
|
|
&i.IsCustomDomain,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getSiteSwapPolicy = `-- name: GetSiteSwapPolicy :one
|
|
SELECT last_swap_at FROM fedwiki.site_swap_policy WHERE workspace_id = $1
|
|
`
|
|
|
|
func (q *Queries) GetSiteSwapPolicy(ctx context.Context, workspaceID string) (time.Time, error) {
|
|
row := q.db.QueryRowContext(ctx, getSiteSwapPolicy, workspaceID)
|
|
var last_swap_at time.Time
|
|
err := row.Scan(&last_swap_at)
|
|
return last_swap_at, err
|
|
}
|
|
|
|
const listActiveSitesByWorkspace = `-- name: ListActiveSitesByWorkspace :many
|
|
SELECT site_id, workspace_id, domain, created_at, updated_at, status, storage_bytes, last_modified_at, force_reduced_at, archived_at FROM fedwiki.sites
|
|
WHERE workspace_id = $1 AND status = 'active'
|
|
ORDER BY last_modified_at DESC NULLS LAST, created_at DESC
|
|
`
|
|
|
|
// Active sites, most-recently-modified first — the force_reduce keep/park order.
|
|
func (q *Queries) ListActiveSitesByWorkspace(ctx context.Context, workspaceID string) ([]Site, error) {
|
|
rows, err := q.db.QueryContext(ctx, listActiveSitesByWorkspace, workspaceID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []Site{}
|
|
for rows.Next() {
|
|
var i Site
|
|
if err := rows.Scan(
|
|
&i.SiteID,
|
|
&i.WorkspaceID,
|
|
&i.Domain,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.Status,
|
|
&i.StorageBytes,
|
|
&i.LastModifiedAt,
|
|
&i.ForceReducedAt,
|
|
&i.ArchivedAt,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listAllSites = `-- name: ListAllSites :many
|
|
SELECT s.site_id, s.workspace_id, s.domain, s.created_at, s.updated_at, s.status, s.storage_bytes, s.last_modified_at, s.force_reduced_at, s.archived_at, (EXISTS (
|
|
SELECT 1 FROM domains.placements p
|
|
JOIN domains.claims c ON c.claim_id = p.claim_id
|
|
WHERE p.provider = 'fedwiki'
|
|
AND p.resource_ref = s.site_id::text
|
|
AND c.kind = 'external'
|
|
))::boolean AS is_custom_domain
|
|
FROM fedwiki.sites AS s
|
|
ORDER BY s.domain ASC
|
|
`
|
|
|
|
type ListAllSitesRow struct {
|
|
SiteID string `json:"site_id"`
|
|
WorkspaceID string `json:"workspace_id"`
|
|
Domain string `json:"domain"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
Status string `json:"status"`
|
|
StorageBytes sql.NullInt64 `json:"storage_bytes"`
|
|
LastModifiedAt sql.NullTime `json:"last_modified_at"`
|
|
ForceReducedAt sql.NullTime `json:"force_reduced_at"`
|
|
ArchivedAt sql.NullTime `json:"archived_at"`
|
|
IsCustomDomain bool `json:"is_custom_domain"`
|
|
}
|
|
|
|
func (q *Queries) ListAllSites(ctx context.Context) ([]ListAllSitesRow, error) {
|
|
rows, err := q.db.QueryContext(ctx, listAllSites)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []ListAllSitesRow{}
|
|
for rows.Next() {
|
|
var i ListAllSitesRow
|
|
if err := rows.Scan(
|
|
&i.SiteID,
|
|
&i.WorkspaceID,
|
|
&i.Domain,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.Status,
|
|
&i.StorageBytes,
|
|
&i.LastModifiedAt,
|
|
&i.ForceReducedAt,
|
|
&i.ArchivedAt,
|
|
&i.IsCustomDomain,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listArchivedSitesArchivedBefore = `-- name: ListArchivedSitesArchivedBefore :many
|
|
SELECT site_id, workspace_id, domain, created_at, updated_at, status, storage_bytes, last_modified_at, force_reduced_at, archived_at FROM fedwiki.sites
|
|
WHERE status = 'archived' AND archived_at IS NOT NULL AND archived_at < $1
|
|
ORDER BY archived_at ASC
|
|
`
|
|
|
|
// Retention purge: archived sites whose archive predates the cutoff.
|
|
func (q *Queries) ListArchivedSitesArchivedBefore(ctx context.Context, archivedAt sql.NullTime) ([]Site, error) {
|
|
rows, err := q.db.QueryContext(ctx, listArchivedSitesArchivedBefore, archivedAt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []Site{}
|
|
for rows.Next() {
|
|
var i Site
|
|
if err := rows.Scan(
|
|
&i.SiteID,
|
|
&i.WorkspaceID,
|
|
&i.Domain,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.Status,
|
|
&i.StorageBytes,
|
|
&i.LastModifiedAt,
|
|
&i.ForceReducedAt,
|
|
&i.ArchivedAt,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listForceReducedSitesByWorkspace = `-- name: ListForceReducedSitesByWorkspace :many
|
|
SELECT site_id, workspace_id, domain, created_at, updated_at, status, storage_bytes, last_modified_at, force_reduced_at, archived_at FROM fedwiki.sites
|
|
WHERE workspace_id = $1 AND force_reduced_at IS NOT NULL
|
|
ORDER BY last_modified_at DESC NULLS LAST, created_at DESC
|
|
`
|
|
|
|
// Sites parked read-only by a downgrade, most-recently-modified first — the
|
|
// re-upgrade reactivation order.
|
|
func (q *Queries) ListForceReducedSitesByWorkspace(ctx context.Context, workspaceID string) ([]Site, error) {
|
|
rows, err := q.db.QueryContext(ctx, listForceReducedSitesByWorkspace, workspaceID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []Site{}
|
|
for rows.Next() {
|
|
var i Site
|
|
if err := rows.Scan(
|
|
&i.SiteID,
|
|
&i.WorkspaceID,
|
|
&i.Domain,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.Status,
|
|
&i.StorageBytes,
|
|
&i.LastModifiedAt,
|
|
&i.ForceReducedAt,
|
|
&i.ArchivedAt,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listOrphanedPlacements = `-- name: ListOrphanedPlacements :many
|
|
SELECT p.placement_id, p.fqdn, p.resource_ref
|
|
FROM domains.placements p
|
|
WHERE p.provider = 'fedwiki'
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM fedwiki.sites s WHERE s.site_id::text = p.resource_ref
|
|
)
|
|
ORDER BY p.fqdn ASC
|
|
`
|
|
|
|
type ListOrphanedPlacementsRow struct {
|
|
PlacementID string `json:"placement_id"`
|
|
Fqdn string `json:"fqdn"`
|
|
ResourceRef string `json:"resource_ref"`
|
|
}
|
|
|
|
// Boot reconciliation's orphan set (domains-registry D8), the mirror image of
|
|
// ListSitesWithoutPlacement: every `fedwiki` placement whose resource_ref
|
|
// names no `fedwiki.sites` row. A create saga whose compensation never ran and
|
|
// a delete interrupted between freeing the row and freeing the name both leave
|
|
// one, and until it is released `/domains/ask` keeps authorizing certificates
|
|
// for a name nothing serves. Ordered by name for a stable pass.
|
|
func (q *Queries) ListOrphanedPlacements(ctx context.Context) ([]ListOrphanedPlacementsRow, error) {
|
|
rows, err := q.db.QueryContext(ctx, listOrphanedPlacements)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []ListOrphanedPlacementsRow{}
|
|
for rows.Next() {
|
|
var i ListOrphanedPlacementsRow
|
|
if err := rows.Scan(&i.PlacementID, &i.Fqdn, &i.ResourceRef); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listSitesByWorkspace = `-- name: ListSitesByWorkspace :many
|
|
SELECT s.site_id, s.workspace_id, s.domain, s.created_at, s.updated_at, s.status, s.storage_bytes, s.last_modified_at, s.force_reduced_at, s.archived_at, (EXISTS (
|
|
SELECT 1 FROM domains.placements p
|
|
JOIN domains.claims c ON c.claim_id = p.claim_id
|
|
WHERE p.provider = 'fedwiki'
|
|
AND p.resource_ref = s.site_id::text
|
|
AND c.kind = 'external'
|
|
))::boolean AS is_custom_domain
|
|
FROM fedwiki.sites AS s
|
|
WHERE s.workspace_id = $1
|
|
ORDER BY s.domain ASC
|
|
`
|
|
|
|
type ListSitesByWorkspaceRow struct {
|
|
SiteID string `json:"site_id"`
|
|
WorkspaceID string `json:"workspace_id"`
|
|
Domain string `json:"domain"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
Status string `json:"status"`
|
|
StorageBytes sql.NullInt64 `json:"storage_bytes"`
|
|
LastModifiedAt sql.NullTime `json:"last_modified_at"`
|
|
ForceReducedAt sql.NullTime `json:"force_reduced_at"`
|
|
ArchivedAt sql.NullTime `json:"archived_at"`
|
|
IsCustomDomain bool `json:"is_custom_domain"`
|
|
}
|
|
|
|
func (q *Queries) ListSitesByWorkspace(ctx context.Context, workspaceID string) ([]ListSitesByWorkspaceRow, error) {
|
|
rows, err := q.db.QueryContext(ctx, listSitesByWorkspace, workspaceID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []ListSitesByWorkspaceRow{}
|
|
for rows.Next() {
|
|
var i ListSitesByWorkspaceRow
|
|
if err := rows.Scan(
|
|
&i.SiteID,
|
|
&i.WorkspaceID,
|
|
&i.Domain,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.Status,
|
|
&i.StorageBytes,
|
|
&i.LastModifiedAt,
|
|
&i.ForceReducedAt,
|
|
&i.ArchivedAt,
|
|
&i.IsCustomDomain,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listSitesWithoutPlacement = `-- name: ListSitesWithoutPlacement :many
|
|
SELECT site_id, workspace_id, domain, created_at, updated_at, status, storage_bytes, last_modified_at, force_reduced_at, archived_at FROM fedwiki.sites
|
|
WHERE NOT EXISTS (
|
|
SELECT 1 FROM domains.placements p
|
|
WHERE p.provider = 'fedwiki' AND p.resource_ref = fedwiki.sites.site_id::text
|
|
)
|
|
ORDER BY length(domain) ASC, domain ASC
|
|
`
|
|
|
|
// Boot reconciliation's backfill set (domains-registry D8): every site row the
|
|
// registry holds no placement for. Ordered shortest name first so a nested
|
|
// pre-registry site (`blog.example.org`) is classified AFTER the name that
|
|
// contains it (`example.org`) and lands as a placement inside the claim that
|
|
// one created, instead of minting an overlapping second claim.
|
|
func (q *Queries) ListSitesWithoutPlacement(ctx context.Context) ([]Site, error) {
|
|
rows, err := q.db.QueryContext(ctx, listSitesWithoutPlacement)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []Site{}
|
|
for rows.Next() {
|
|
var i Site
|
|
if err := rows.Scan(
|
|
&i.SiteID,
|
|
&i.WorkspaceID,
|
|
&i.Domain,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.Status,
|
|
&i.StorageBytes,
|
|
&i.LastModifiedAt,
|
|
&i.ForceReducedAt,
|
|
&i.ArchivedAt,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listWorkspaceIDsWithSites = `-- name: ListWorkspaceIDsWithSites :many
|
|
SELECT DISTINCT workspace_id FROM fedwiki.sites
|
|
`
|
|
|
|
// Workspaces that own at least one site — the units the quota reconcile sweeps.
|
|
func (q *Queries) ListWorkspaceIDsWithSites(ctx context.Context) ([]string, error) {
|
|
rows, err := q.db.QueryContext(ctx, listWorkspaceIDsWithSites)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []string{}
|
|
for rows.Next() {
|
|
var workspace_id string
|
|
if err := rows.Scan(&workspace_id); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, workspace_id)
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const setForceReducedByDomain = `-- name: SetForceReducedByDomain :exec
|
|
UPDATE fedwiki.sites
|
|
SET force_reduced_at = NOW(), updated_at = NOW()
|
|
WHERE domain = $1 AND force_reduced_at IS NULL
|
|
`
|
|
|
|
func (q *Queries) SetForceReducedByDomain(ctx context.Context, domain string) error {
|
|
_, err := q.db.ExecContext(ctx, setForceReducedByDomain, domain)
|
|
return err
|
|
}
|
|
|
|
const setSiteStatusByDomain = `-- name: SetSiteStatusByDomain :one
|
|
UPDATE fedwiki.sites
|
|
SET status = $2,
|
|
archived_at = CASE
|
|
WHEN $2 = 'archived' AND status <> 'archived' THEN NOW()
|
|
WHEN $2 <> 'archived' THEN NULL
|
|
ELSE archived_at
|
|
END,
|
|
updated_at = NOW()
|
|
WHERE domain = $1
|
|
RETURNING site_id, workspace_id, domain, created_at, updated_at, status, storage_bytes, last_modified_at, force_reduced_at, archived_at
|
|
`
|
|
|
|
type SetSiteStatusByDomainParams struct {
|
|
Domain string `json:"domain"`
|
|
Status string `json:"status"`
|
|
}
|
|
|
|
// Project a lifecycle status change locally. archived_at is set on entering
|
|
// `archived` (idempotent — preserves the original timestamp) and cleared on
|
|
// leaving it, driving the Trash retention purge.
|
|
func (q *Queries) SetSiteStatusByDomain(ctx context.Context, arg SetSiteStatusByDomainParams) (Site, error) {
|
|
row := q.db.QueryRowContext(ctx, setSiteStatusByDomain, arg.Domain, arg.Status)
|
|
var i Site
|
|
err := row.Scan(
|
|
&i.SiteID,
|
|
&i.WorkspaceID,
|
|
&i.Domain,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.Status,
|
|
&i.StorageBytes,
|
|
&i.LastModifiedAt,
|
|
&i.ForceReducedAt,
|
|
&i.ArchivedAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const upsertSiteByDomain = `-- name: UpsertSiteByDomain :one
|
|
INSERT INTO fedwiki.sites (workspace_id, domain)
|
|
VALUES ($1, $2)
|
|
ON CONFLICT(domain) DO UPDATE SET
|
|
workspace_id = excluded.workspace_id,
|
|
updated_at = NOW()
|
|
RETURNING site_id, workspace_id, domain, created_at, updated_at, status, storage_bytes, last_modified_at, force_reduced_at, archived_at
|
|
`
|
|
|
|
type UpsertSiteByDomainParams struct {
|
|
WorkspaceID string `json:"workspace_id"`
|
|
Domain string `json:"domain"`
|
|
}
|
|
|
|
func (q *Queries) UpsertSiteByDomain(ctx context.Context, arg UpsertSiteByDomainParams) (Site, error) {
|
|
row := q.db.QueryRowContext(ctx, upsertSiteByDomain, arg.WorkspaceID, arg.Domain)
|
|
var i Site
|
|
err := row.Scan(
|
|
&i.SiteID,
|
|
&i.WorkspaceID,
|
|
&i.Domain,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.Status,
|
|
&i.StorageBytes,
|
|
&i.LastModifiedAt,
|
|
&i.ForceReducedAt,
|
|
&i.ArchivedAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const upsertSiteFromFarm = `-- name: UpsertSiteFromFarm :one
|
|
INSERT INTO fedwiki.sites (workspace_id, domain, status, storage_bytes, last_modified_at)
|
|
VALUES ($1, $2, $3, $4, $5)
|
|
ON CONFLICT(domain) DO UPDATE SET
|
|
status = excluded.status,
|
|
storage_bytes = excluded.storage_bytes,
|
|
last_modified_at = excluded.last_modified_at,
|
|
updated_at = NOW()
|
|
RETURNING site_id, workspace_id, domain, created_at, updated_at, status, storage_bytes, last_modified_at, force_reduced_at, archived_at
|
|
`
|
|
|
|
type UpsertSiteFromFarmParams struct {
|
|
WorkspaceID string `json:"workspace_id"`
|
|
Domain string `json:"domain"`
|
|
Status string `json:"status"`
|
|
StorageBytes sql.NullInt64 `json:"storage_bytes"`
|
|
LastModifiedAt sql.NullTime `json:"last_modified_at"`
|
|
}
|
|
|
|
// Sync projection: insert a newly-seen farm site (default workspace), or project
|
|
// the observed status/storage/last-modified onto an existing row WITHOUT touching
|
|
// its workspace_id (user-created sites keep their owner).
|
|
func (q *Queries) UpsertSiteFromFarm(ctx context.Context, arg UpsertSiteFromFarmParams) (Site, error) {
|
|
row := q.db.QueryRowContext(ctx, upsertSiteFromFarm,
|
|
arg.WorkspaceID,
|
|
arg.Domain,
|
|
arg.Status,
|
|
arg.StorageBytes,
|
|
arg.LastModifiedAt,
|
|
)
|
|
var i Site
|
|
err := row.Scan(
|
|
&i.SiteID,
|
|
&i.WorkspaceID,
|
|
&i.Domain,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.Status,
|
|
&i.StorageBytes,
|
|
&i.LastModifiedAt,
|
|
&i.ForceReducedAt,
|
|
&i.ArchivedAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const upsertSiteSwapPolicy = `-- name: UpsertSiteSwapPolicy :exec
|
|
INSERT INTO fedwiki.site_swap_policy (workspace_id, last_swap_at)
|
|
VALUES ($1, NOW())
|
|
ON CONFLICT (workspace_id) DO UPDATE SET last_swap_at = NOW()
|
|
`
|
|
|
|
// Records that a swap just happened (resets the cooldown clock for the workspace).
|
|
func (q *Queries) UpsertSiteSwapPolicy(ctx context.Context, workspaceID string) error {
|
|
_, err := q.db.ExecContext(ctx, upsertSiteSwapPolicy, workspaceID)
|
|
return err
|
|
}
|