package graph
// This file will be automatically regenerated based on the schema, any resolver implementations
// will be copied through when generating and any unknown code will be moved to the end.
// Code generated by github.com/99designs/gqlgen version v0.17.36
import (
"context"
"errors"
"fmt"
"strings"
"sourcecraft.dev/bigbes/sr-ht-spec/core"
"sourcecraft.dev/bigbes/sr-ht-spec/doc"
"sourcecraft.dev/bigbes/sr-ht-spec/graph/api"
"sourcecraft.dev/bigbes/sr-ht-spec/graph/model"
"sourcecraft.dev/bigbes/sr-ht-spec/search"
"sourcecraft.dev/bigbes/sr-ht-spec/service"
)
// Spaces is the resolver for the spaces field.
func (r *projectResolver) Spaces(ctx context.Context, obj *model.Project) ([]*model.Space, error) {
ref := core.ProjectRef{Owner: obj.Owner, Name: obj.Name}
spaces, err := r.reader.ProjectSpaces(ctx, ref)
if err != nil {
return nil, err
}
return spaceModels(spaces), nil
}
// Spaces is the resolver for the spaces field.
func (r *queryResolver) Spaces(ctx context.Context) ([]*model.Space, error) {
spaces, err := r.reader.ListSpaces(ctx)
if err != nil {
return nil, err
}
return spaceModels(spaces), nil
}
// Space is the resolver for the space field.
func (r *queryResolver) Space(ctx context.Context, owner string, name string) (*model.Space, error) {
ref, err := spaceRef(owner, name)
if err != nil {
return nil, err
}
sp, err := r.reader.OpenSpace(ctx, ref)
if err != nil {
return nil, orNull(err)
}
return spaceModel(sp), nil
}
// Document is the resolver for the document field.
func (r *queryResolver) Document(ctx context.Context, space string, id *string, path *string, rev *string) (*model.Document, error) {
arc, bodies, resolved, pinned, err := r.archive(ctx, space, rev)
if err != nil {
return nil, orNull(err)
}
page, found, err := address(arc, deref(id), deref(path))
if err != nil || !found {
return nil, err
}
body, ok := bodies[page.Path]
if !ok {
// The archive is built from these very bodies, so a page without one is
// a broken invariant rather than a missing document. Reporting an empty
// markdown field would be indistinguishable from an empty document.
return nil, fmt.Errorf("document %s of %s at %s has no body", page.Path, arc.Space, resolved)
}
return documentModel(arc.Space, resolved, pinned, page, body), nil
}
// Documents is the resolver for the documents field.
func (r *queryResolver) Documents(ctx context.Context, space string, rev *string) ([]*model.Document, error) {
arc, bodies, resolved, pinned, err := r.archive(ctx, space, rev)
if err != nil {
return nil, err
}
pages := arc.All()
out := make([]*model.Document, 0, len(pages))
for _, page := range pages {
body, ok := bodies[page.Path]
if !ok {
return nil, fmt.Errorf("document %s of %s at %s has no body", page.Path, arc.Space, resolved)
}
out = append(out, documentModel(arc.Space, resolved, pinned, page, body))
}
return out, nil
}
// Search is the resolver for the search field.
func (r *queryResolver) Search(ctx context.Context, query string, spaces []string, limit *int, offset *int) (*model.SearchResults, error) {
q := search.Query{Text: strings.TrimSpace(query)}
// Filter polarity, which is the one thing here that is dangerous to get
// wrong — and the reason core.SpaceFilter exists rather than a slice.
// Absent means every space, which is the meta-project. A list means exactly
// those spaces, and a list that is present but empty therefore selects
// nothing at all: that is what an empty project's membership is, and a
// client passing one through must get no hits rather than the whole corpus.
// GraphQL distinguishes the two cases for free — null is not [] — so the
// argument's own shape carries the polarity that used to be a convention.
if spaces == nil {
q.Spaces = core.EverythingFilter()
} else {
refs := make([]core.SpaceRef, 0, len(spaces))
for _, s := range spaces {
ref, err := core.ParseSpaceRef(strings.TrimSpace(s))
if err != nil {
return nil, fmt.Errorf("spaces: %w", err)
}
refs = append(refs, ref)
}
q.Spaces = core.SpacesFilter(refs, nil)
}
if limit != nil {
if *limit < 0 {
return nil, fmt.Errorf("limit: %d is negative", *limit)
}
q.Limit = *limit
}
if offset != nil {
if *offset < 0 {
return nil, fmt.Errorf("offset: %d is negative", *offset)
}
q.Offset = *offset
}
res, err := r.searcher.Search(ctx, q)
if err != nil {
return nil, err
}
out := &model.SearchResults{
Total: int(res.Total),
Took: res.Took.String(),
Hits: make([]*model.SearchHit, 0, len(res.Hits)),
}
for _, h := range res.Hits {
out.Hits = append(out.Hits, &model.SearchHit{
Space: h.Space.String(),
ID: h.ID,
Rev: h.Rev,
Path: h.Path,
Anchor: h.Anchor,
Title: h.Title,
Section: h.Section,
Lang: string(h.Lang),
Score: h.Score,
Snippet: h.Snippet,
})
}
return out, nil
}
// Projects is the resolver for the projects field.
func (r *queryResolver) Projects(ctx context.Context) ([]*model.Project, error) {
projects, err := r.reader.ListProjects(ctx)
if err != nil {
return nil, err
}
out := make([]*model.Project, 0, len(projects))
for _, p := range projects {
out = append(out, projectModel(p.Ref))
}
return out, nil
}
// Project is the resolver for the project field.
func (r *queryResolver) Project(ctx context.Context, owner string, name string) (*model.Project, error) {
ref, err := projectRef(owner, name)
if err != nil {
return nil, err
}
// The meta-project is an address that resolves to a filter, not a row: it
// needs no storage, cannot be renamed or deleted, and adding a space to the
// service adds it to the meta-project by construction. So it is answered
// here rather than looked up — a lookup would report the one project that
// can never be missing as missing.
if ref.IsMeta() {
return projectModel(ref), nil
}
if _, err := r.reader.GetProject(ctx, ref); err != nil {
return nil, orNull(err)
}
return projectModel(ref), nil
}
// Proposals is the resolver for the proposals field.
func (r *queryResolver) Proposals(ctx context.Context, space string, state model.ProposalState) ([]*model.Proposal, error) {
ref, err := core.ParseSpaceRef(strings.TrimSpace(space))
if err != nil {
return nil, fmt.Errorf("space: %w", err)
}
// GraphQL has already validated the enum, so this cannot fail over a
// caller's spelling; it fails only if this schema and core disagree about
// the state machine, which is worth hearing about rather than defaulting
// past.
st, err := core.ParseProposalState(strings.ToLower(string(state)))
if err != nil {
return nil, err
}
if r.proposals == nil {
return nil, errors.New("proposals cannot be read: service/ exposes no proposal listing yet, " +
"and nothing above it may query the database directly (the write plane is Phase 3)")
}
proposals, err := r.proposals.ListProposals(ctx, ref, st)
if err != nil {
return nil, err
}
out := make([]*model.Proposal, 0, len(proposals))
for _, p := range proposals {
m, err := proposalModel(p)
if err != nil {
return nil, err
}
out = append(out, m)
}
return out, nil
}
// ApprovedRev is the resolver for the approvedRev field.
func (r *spaceResolver) ApprovedRev(ctx context.Context, obj *model.Space) (string, error) {
// A field resolver rather than a struct field: ListSpaces deliberately does
// not open repositories, so resolving the approved head of every space in a
// listing would make the cheapest view in the service the most expensive
// one. Here it is paid for only by a query that asks for it.
sp, err := r.reader.OpenSpace(ctx, core.SpaceRef{Owner: obj.Owner, Name: obj.Name})
if err != nil {
return "", err
}
return r.reader.ResolveRev(ctx, sp, service.ApprovedRev)
}
// Project returns api.ProjectResolver implementation.
func (r *Resolver) Project() api.ProjectResolver { return &projectResolver{r} }
// Query returns api.QueryResolver implementation.
func (r *Resolver) Query() api.QueryResolver { return &queryResolver{r} }
// Space returns api.SpaceResolver implementation.
func (r *Resolver) Space() api.SpaceResolver { return &spaceResolver{r} }
type projectResolver struct{ *Resolver }
type queryResolver struct{ *Resolver }
type spaceResolver struct{ *Resolver }
// !!! WARNING !!!
// The code below was going to be deleted when updating resolvers. It has been copied here so you have
// one last chance to move it out of harms way if you want. There are two reasons this happens:
// - When renaming or deleting a resolver the old code will be put in here. You can safely delete
// it when you're done.
// - You have helper methods in this file. Move them out to keep these resolver files clean.
func (r *queryResolver) archive(ctx context.Context, space string, rev *string) (*doc.Archive, map[string][]byte, string, bool, error) {
ref, err := core.ParseSpaceRef(strings.TrimSpace(space))
if err != nil {
return nil, nil, "", false, fmt.Errorf("space: %w", err)
}
want := deref(rev)
if err := service.ValidateReadRev(want); err != nil {
return nil, nil, "", false, fmt.Errorf("rev: %w", err)
}
sp, err := r.reader.OpenSpace(ctx, ref)
if err != nil {
return nil, nil, "", false, err
}
arc, bodies, err := r.reader.Archive(ctx, sp, want)
if err != nil {
return nil, nil, "", false, err
}
return arc, bodies, arc.Rev, want != service.ApprovedRev, nil
}
func address(arc *doc.Archive, id, path string) (*doc.Page, bool, error) {
id = strings.TrimSpace(id)
path = strings.TrimSpace(path)
switch {
case id == "" && path == "":
return nil, false, errors.New("give either id or path: a document has to be addressed")
case id != "" && path != "":
return nil, false, fmt.Errorf("give either id or path, not both: %q and %q cannot both name the document wanted", id, path)
}
if path != "" {
if err := core.ValidateDocPath(path); err != nil {
return nil, false, fmt.Errorf("path: %w", err)
}
page, ok := arc.ByPath(path)
return page, ok, nil
}
if page, ok := arc.Page(id); ok {
return page, true, nil
}
// Nothing is addressed by that id. Saying only "no such document" would
// hide the one case a caller cannot otherwise diagnose: an id two documents
// claim resolves to neither of them, and the collision is invisible from
// outside the space.
var claimed []string
for _, p := range arc.All() {
if p.DocID == id {
claimed = append(claimed, p.Path)
}
}
if len(claimed) > 1 {
return nil, false, fmt.Errorf("id: %q is claimed by %d documents (%s) and resolves to none of them; address one of them by path instead",
id, len(claimed), strings.Join(claimed, ", "))
}
return nil, false, nil
}
func spaceModel(sp *service.Space) *model.Space {
return &model.Space{
Owner: sp.Ref.Owner,
Name: sp.Ref.Name,
Ref: sp.Ref.String(),
Created: sp.Created,
}
}
func spaceModels(spaces []*service.Space) []*model.Space {
out := make([]*model.Space, 0, len(spaces))
for _, sp := range spaces {
out = append(out, spaceModel(sp))
}
return out
}
func projectModel(ref core.ProjectRef) *model.Project {
return &model.Project{
Owner: ref.Owner,
Name: ref.Name,
Ref: ref.String(),
Meta: ref.IsMeta(),
}
}
func documentModel(space core.SpaceRef, rev string, pinned bool, page *doc.Page, body []byte) *model.Document {
front, _ := doc.ParseFront(body)
return &model.Document{
Space: space.String(),
ID: page.ID,
DocID: nilIfEmpty(page.DocID),
Path: page.Path,
Rev: rev,
Blob: page.Blob,
Pinned: pinned,
Title: page.Title,
Status: string(page.Status),
Section: page.Section,
Summary: page.Summary,
Type: front.Type,
Supersedes: front.Supersedes,
Tags: nonNil(page.Tags),
Owners: nonNil(front.Owners),
Markdown: string(body),
}
}
func proposalModel(p Proposal) (*model.Proposal, error) {
state, err := proposalState(p.State)
if err != nil {
return nil, err
}
m := &model.Proposal{
ID: p.ID,
Space: p.Space.String(),
Title: p.Title,
Rationale: p.Rationale,
BaseRev: p.BaseRev,
Branch: p.Branch,
State: state,
MergedRev: nilIfEmpty(p.MergedRev),
Agent: p.Agent,
AgentSession: p.AgentSession,
Created: p.Created,
Resolved: p.Resolved,
}
// Approval is empty until a proposal merges, and the difference between
// human and policy approval is one a reader must be able to see: reporting
// either value for an unmerged proposal would launder unreviewed agent
// output as blessed, or claim a review that did not happen.
if p.Approval != "" {
approval, err := proposalApproval(p.Approval)
if err != nil {
return nil, err
}
m.Approval = &approval
}
return m, nil
}
func proposalState(s core.ProposalState) (model.ProposalState, error) {
out := model.ProposalState(strings.ToUpper(string(s)))
if !out.IsValid() {
return "", fmt.Errorf("proposal state %q is not one of %v", s, model.AllProposalState)
}
return out, nil
}
func proposalApproval(a core.Approval) (model.Approval, error) {
out := model.Approval(strings.ToUpper(string(a)))
if !out.IsValid() {
return "", fmt.Errorf("approval %q is not one of %v", a, model.AllApproval)
}
return out, nil
}
func spaceRef(owner, name string) (core.SpaceRef, error) {
ref := core.SpaceRef{Owner: strings.TrimSpace(owner), Name: strings.TrimSpace(name)}
if err := core.ValidateOwner(ref.Owner); err != nil {
return core.SpaceRef{}, fmt.Errorf("owner: %w", err)
}
if err := core.ValidateSpaceName(ref.Name); err != nil {
return core.SpaceRef{}, fmt.Errorf("name: %w", err)
}
return ref, nil
}
func projectRef(owner, name string) (core.ProjectRef, error) {
ref := core.ProjectRef{Owner: strings.TrimSpace(owner), Name: strings.TrimSpace(name)}
if err := core.ValidateOwner(ref.Owner); err != nil {
return core.ProjectRef{}, fmt.Errorf("owner: %w", err)
}
if ref.IsMeta() {
return ref, nil
}
if err := core.ValidateProjectName(ref.Name); err != nil {
return core.ProjectRef{}, fmt.Errorf("name: %w", err)
}
return ref, nil
}
func orNull(err error) error {
if errors.Is(err, service.ErrNotFound) {
return nil
}
return err
}
func deref(s *string) string {
if s == nil {
return ""
}
return *s
}
func nilIfEmpty(s string) *string {
if s == "" {
return nil
}
return &s
}
func nonNil(s []string) []string {
if s == nil {
return []string{}
}
return s
}