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 }