Archives openspec change slice3-walk-fixes and syncs its five delta specs (fedwiki-sites, entitlements, operator-panel-navigation, operator-list-scale, ui-quality-gate). - FedWiki site usage is read from active site rows in both quota readers; the reservation counter converges on the rows: raise-only after farm sync and inside the create quota check, exact at boot. The understated production counters repair on the first boot. - The People tile caption excludes the reserved system person through the same query parameter the directory uses. - The operator Domains live-claims list is a governed list: pages of 50, true total, search over root name and organization, a pending/active facet. - New lint rule table-without-list-controls refuses an unpaged page-body table unless it carries a list-scale exempt marker with a reason; six curated or detail tables carry one. Its first run caught the operator FedWiki sites list, which is now governed the same way. - Entitlement-set rule copy: "Per unit", "Multiplied by the quantity purchased or granted."
827 lines
25 KiB
Go
827 lines
25 KiB
Go
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial
|
|
// SPDX-FileCopyrightText: 2025-2026 Christian Galo
|
|
|
|
// 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 listAllSitesForOperatorPage = `-- name: ListAllSitesForOperatorPage :many
|
|
SELECT s.site_id, s.workspace_id, s.domain, s.status, s.created_at,
|
|
w.name AS workspace_name,
|
|
o.org_id, o.name AS org_name,
|
|
(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,
|
|
count(*) OVER() AS total_count
|
|
FROM fedwiki.sites AS s
|
|
JOIN core.workspaces w ON w.workspace_id = s.workspace_id
|
|
JOIN core.organizations o ON o.org_id = w.org_id
|
|
WHERE ($1::text IS NULL OR s.status = $1::text)
|
|
AND ($2::text IS NULL
|
|
OR s.domain ILIKE '%' || $2::text || '%'
|
|
OR w.name ILIKE '%' || $2::text || '%'
|
|
OR o.name ILIKE '%' || $2::text || '%')
|
|
ORDER BY s.domain ASC
|
|
LIMIT $4::int OFFSET $3::int
|
|
`
|
|
|
|
type ListAllSitesForOperatorPageParams struct {
|
|
Status sql.NullString `json:"status"`
|
|
Q sql.NullString `json:"q"`
|
|
PageOffset int32 `json:"page_offset"`
|
|
PageLimit int32 `json:"page_limit"`
|
|
}
|
|
|
|
type ListAllSitesForOperatorPageRow struct {
|
|
SiteID string `json:"site_id"`
|
|
WorkspaceID string `json:"workspace_id"`
|
|
Domain string `json:"domain"`
|
|
Status string `json:"status"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
WorkspaceName string `json:"workspace_name"`
|
|
OrgID string `json:"org_id"`
|
|
OrgName string `json:"org_name"`
|
|
IsCustomDomain bool `json:"is_custom_domain"`
|
|
TotalCount int64 `json:"total_count"`
|
|
}
|
|
|
|
// The operator sites surface, governed by operator-list-scale: one page of
|
|
// every FedWiki site in the deployment with its holder attached, ordered by
|
|
// domain over the whole filtered set before LIMIT/OFFSET so paging is
|
|
// stable.
|
|
//
|
|
// The owning organization is joined here rather than looked up per row: the
|
|
// handler used to resolve workspace -> organization with a cache, which
|
|
// cannot answer a search over the holder's name at all once only fifty rows
|
|
// are in hand. This is the one query in this package that reads core
|
|
// (00001_init grants core_reader to fedwiki_writer); it projects columns
|
|
// rather than whole rows so no core model enters this package's API.
|
|
//
|
|
// sqlc.narg(q): NULL matches every row; set, a case-insensitive substring
|
|
// match against the site's domain, its workspace's name, or the owning
|
|
// organization's name.
|
|
// sqlc.narg(status): the status facet over fedwiki.sites' own vocabulary
|
|
// (active, readonly, archived); NULL means every status.
|
|
// count(*) OVER() carries the true total for the filtered set alongside the
|
|
// page's rows, so the section count never reflects only the page.
|
|
func (q *Queries) ListAllSitesForOperatorPage(ctx context.Context, arg ListAllSitesForOperatorPageParams) ([]ListAllSitesForOperatorPageRow, error) {
|
|
rows, err := q.db.QueryContext(ctx, listAllSitesForOperatorPage,
|
|
arg.Status,
|
|
arg.Q,
|
|
arg.PageOffset,
|
|
arg.PageLimit,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []ListAllSitesForOperatorPageRow{}
|
|
for rows.Next() {
|
|
var i ListAllSitesForOperatorPageRow
|
|
if err := rows.Scan(
|
|
&i.SiteID,
|
|
&i.WorkspaceID,
|
|
&i.Domain,
|
|
&i.Status,
|
|
&i.CreatedAt,
|
|
&i.WorkspaceName,
|
|
&i.OrgID,
|
|
&i.OrgName,
|
|
&i.IsCustomDomain,
|
|
&i.TotalCount,
|
|
); 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
|
|
}
|