ZDDC/zddc/internal/zddc/virtualviews.go
ZDDC f94defc8c1 feat(browse,tables): flat-peer clients + dual-mode cross-party aggregate
browse: the party picker reads the ssr/ registry (the authoritative party
list) and creates at physical peer paths <project>/<peer>/<party>/…;
"register new party" writes ssr/<party>.yaml first (party_source: ssr).
stage.js + accept-transmittal.js repointed to the top-level workspace peers
(working/staging/incoming) — received/issued + plan-review stay under the
WORM archive.

tables: mdl/ and rsk/ render the cross-party aggregate by recursing ONE
level into the party subdirs CLIENT-side (works online AND offline), with
$party from the server-injected row content (or derived from the subdir
offline). Rows carry the <party>/ prefix so reads/edits hit the real
per-party path. The server just lists the peer root normally (party subdirs
+ synthetic table.yaml/form.yaml) — the fs/tree flattening + ListRollupRows
are dropped in favour of this dual-mode client recursion.

Full Go suite + all 256 Playwright tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 12:35:31 -05:00

124 lines
4.3 KiB
Go

package zddc
import (
"errors"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
)
// Project-level helpers for the physical top-level peer layout.
//
// There is no virtual URL space: every record row is addressed at its
// real path (mdl/<party>/<file>.yaml, ssr/<party>.yaml). mdl/ and rsk/
// AGGREGATE across their party subdirs — the peer root renders one
// table of every party's rows (a $party column derived from the real
// subdir name), while <peer>/<party>/ shows that party's rows flat.
// ssr/ aggregates naturally (one flat ssr/<party>.yaml per party) and
// is the authoritative party registry. These helpers back the
// aggregation listing and party-name validation.
// partyNameRE matches a valid party folder / registry-row token —
// starts with [A-Za-z0-9], then any of [A-Za-z0-9.-].
var partyNameRE = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9.-]*$`)
// ValidPartyName reports whether s is a valid party name. Used by URL
// resolution AND by the SSR create handler to validate user input.
func ValidPartyName(s string) bool {
return partyNameRE.MatchString(s)
}
// planReviewURLRE matches /<project>/archive/<party>/received/<tracking>/
// — the only URL shape Plan Review accepts. Trailing slash optional.
var planReviewURLRE = regexp.MustCompile(`^/[^/]+/archive/[^/]+/received/[^/]+/?$`)
// IsPlanReviewURL reports whether urlPath is a directory URL eligible
// for the Plan Review composite endpoint — i.e. it points at the
// canonical received/<tracking>/ folder under archive/<party>/.
// Eligibility is purely structural; the handler-side authorisation
// check still gates the actual operation.
func IsPlanReviewURL(urlPath string) bool {
return planReviewURLRE.MatchString(urlPath)
}
// StripYAMLHTML returns urlPath with a trailing ".html" stripped iff
// the URL has the form-edit shape ".../<name>.yaml.html". Otherwise
// returns urlPath unchanged + false. The form recognizer calls this
// to map a form-edit URL onto the underlying data file.
func StripYAMLHTML(urlPath string) (string, bool) {
if strings.HasSuffix(urlPath, ".yaml.html") {
return strings.TrimSuffix(urlPath, ".html"), true
}
return urlPath, false
}
// IsSSRCreateURL reports whether urlPath is /<project>/ssr/form.html —
// the SSR "+ Register party" target. Returns the project name when
// matched. The handler writes the new ssr/<party>.yaml registry row.
func IsSSRCreateURL(urlPath string) (string, bool) {
if urlPath == "" || urlPath[0] != '/' {
return "", false
}
parts := strings.Split(strings.TrimPrefix(urlPath, "/"), "/")
if len(parts) != 3 || parts[1] != "ssr" || parts[2] != "form.html" {
return "", false
}
project := parts[0]
if project == "" || strings.HasPrefix(project, ".") || strings.HasPrefix(project, "_") {
return "", false
}
return project, true
}
// IsRollupCreateURL reports whether urlPath is
// /<project>/(mdl|rsk)/form.html — the "+ Add row" target on a
// project-level MDL or RSK aggregate view. Returns the project name +
// peer ("mdl" or "rsk") when matched. The handler reads a `party` field
// from the body and routes the new row into <project>/<peer>/<party>/.
func IsRollupCreateURL(urlPath string) (project, peer string, ok bool) {
if urlPath == "" || urlPath[0] != '/' {
return "", "", false
}
parts := strings.Split(strings.TrimPrefix(urlPath, "/"), "/")
if len(parts) != 3 || parts[2] != "form.html" {
return "", "", false
}
if parts[1] != "mdl" && parts[1] != "rsk" {
return "", "", false
}
project = parts[0]
if project == "" || strings.HasPrefix(project, ".") || strings.HasPrefix(project, "_") {
return "", "", false
}
return project, parts[1], true
}
// ListParties returns the registered party names under <project>/ssr/
// — one per ssr/<party>.yaml file (the authoritative party registry).
// Names are filtered through ValidPartyName. Returns nil + nil when
// ssr/ doesn't exist on disk.
func ListParties(projectAbs string) ([]string, error) {
reg := filepath.Join(projectAbs, "ssr")
entries, err := os.ReadDir(reg)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil, nil
}
return nil, err
}
out := make([]string, 0, len(entries))
for _, e := range entries {
if e.IsDir() || !strings.HasSuffix(e.Name(), ".yaml") {
continue
}
name := strings.TrimSuffix(e.Name(), ".yaml")
if !ValidPartyName(name) {
continue
}
out = append(out, name)
}
sort.Strings(out)
return out, nil
}