package graph import ( "context" "errors" "fmt" "log/slog" "go.bigb.es/auxilia/scribe" "sourcecraft.dev/bigbes/sr-ht-dolt/authn" "sourcecraft.dev/bigbes/sr-ht-dolt/core" "sourcecraft.dev/bigbes/sr-ht-dolt/db" ) // Resolver holds what every field of the schema resolves through. It is built // once by New and is safe for concurrent use: it owns no per-request state, and // the caller is read from the context the transport installed. type Resolver struct { repos Repos opener BrowseOpener } // defaultLogLimit is how many commits Database.log answers when the query names // no limit, and maxLogLimit is the most it will answer for any limit. The cap is // the surface's and not the client's: a log page is a walk of the on-disk store, // and an unbounded limit is a way to ask this service to walk all of history in // one request. const ( defaultLogLimit = 20 maxLogLimit = 200 ) // errUnavailable is what a resolver answers when it could not read, as opposed // to when there was nothing to read. The two are never merged: "I could not // check" reported as "there is none" is how a client learns a false fact about // the instance and acts on it. var errUnavailable = errors.New("the database could not be read, try again") // internalError logs the cause against the field that produced it and returns // the sentence a client sees. The cause never travels: the store layer's errors // carry on-disk paths and the metadata layer's carry connection strings. func internalError(field string, err error) error { slog.Error("a GraphQL field could not be resolved", "component", "graph", "field", field, scribe.Err(err)) return errUnavailable } // callerOf is the request's principal as the access matrix wants it: nil for an // anonymous caller, which is a normal caller on this schema. func callerOf(ctx context.Context) *core.Caller { return authn.AsCoreCaller(authn.CallerFromContext(ctx)) } // resolveDatabase turns an owner and a name into a row the caller may read, or // into (nil, nil) — "no such database" — which is the schema's single refusal. // // It is the browse handlers' dance call for call (web's loadRepoForBrowse, // mcpsrv's resolveDatabase): the row, the caller's ACL grant, core.Allowed for // OpBrowse. Like the MCP surface and unlike the web, there is no forbidden arm: // a caller who may not read a database is told it does not exist, so its // existence cannot be read out of the shape of the refusal. // // An ACL lookup that fails is an error rather than a fall-through to // visibility. On a page that degradation costs a signed-in user a rendering; // here it would tell a client that a database it holds a grant on is not there. func (r *Resolver) resolveDatabase(ctx context.Context, owner, name string) (*core.Repo, error) { caller := callerOf(ctx) repo, err := r.repos.GetRepoByOwnerAndName(ctx, owner, name) if err != nil { if errors.Is(err, db.ErrNotFound) { return nil, nil } return nil, internalError("database", err) } // An anonymous caller holds no ACL entry and there is no user id to look one // up by; visibility decides alone, which is what core.Allowed does with a // nil grant. var mode *core.AccessMode if caller != nil { if mode, err = r.repos.EffectiveAccess(ctx, caller.UserID, repo.ID); err != nil { return nil, internalError("database", err) } } if !core.Allowed(caller, repo, mode, core.OpBrowse) { return nil, nil } return repo, nil } // openStore opens the bare store of a database whose row the caller has already // been allowed to read. The caller closes the session. func (r *Resolver) openStore(ctx context.Context, field string, repo *core.Repo) (BrowseSession, error) { sess, err := r.opener.Open(ctx, repo.Path) if err != nil { return nil, internalError(fmt.Sprintf("Database.%s", field), err) } return sess, nil }