payload

package
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 17, 2026 License: MIT Imports: 29 Imported by: 0

Documentation

Overview

Package payload is PayCLI's Payload CMS REST/GraphQL client.

Everything that talks to a Payload server goes through this package, and every *http.Request is built in transport.go (§3.1) so that Accept-Language, the User-Agent, the request id and the "Authorization is never logged" rule cannot be bypassed by a call site. The package never imports internal/cli or cobra, and it never reads a clock or an environment variable of its own — Config.Now and Config.Sleep are injected by the caller so retries and date arithmetic are deterministic under test.

Index

Constants

View Source
const (
	AuthModeAPIKey    = apierr.AuthModeAPIKey
	AuthModeJWT       = apierr.AuthModeJWT
	AuthModeAnonymous = apierr.AuthModeAnonymous
)

Auth modes (§5.0). These are the same string values apierr uses.

View Source
const (
	SchemeJWT    = "JWT"
	SchemeBearer = "Bearer"
)

Authorization schemes accepted for jwt mode. PayCLI sends JWT by default because Bearer is what many reverse proxies consume and strip (§5.0).

View Source
const (
	DefaultTimeout       = 30 * time.Second
	DefaultUploadTimeout = 120 * time.Second
	DefaultConcurrency   = 8
	MaxConcurrency       = 32
	DefaultMaxRetries    = 3
	DefaultAcceptLang    = "en"
	// DefaultMaxBodyBytes caps a buffered JSON response. Downloads stream and
	// are not affected.
	DefaultMaxBodyBytes int64 = 64 << 20
)

Defaults from §6 and §9.1.

View Source
const (
	StaleQueryPath    = "query_path"
	StaleRouteMissing = "route_missing"
	StaleEndpoints    = "endpoints_disabled"
)

Staleness signal kinds.

View Source
const (
	RetryBase          = 250 * time.Millisecond
	RetryCap           = 8 * time.Second
	RetryAfterCap      = 120 * time.Second
	DefaultMaxAttempts = 4
)

Retry constants from §6.1.

View Source
const (
	HeaderRequestID      = "X-Request-Id"
	HeaderAcceptLanguage = "Accept-Language"
	HeaderAuthorization  = "Authorization"
	HeaderPoweredBy      = "X-Powered-By"
	HeaderRetryAfter     = "Retry-After"
)

Header names PayCLI sets or reads.

View Source
const (
	UploadPartFile   = "file"
	UploadPartFields = "_payload"
)

Multipart part names (§13). Both are verified: `-F upload=@…` answers 400 "No files were uploaded.", and a JSON body with a `url` key answers the same — the URL form is an admin-client feature, not a REST one.

View Source
const AuthCollectionAuto = "auto"

AuthCollectionAuto is the *unresolved* auth-collection placeholder §4.2 gives the config key its default value. It is never a real slug: the header `Authorization: auto API-Key <key>` returns HTTP 200 with the anonymous view (§7.0), so the client refuses to send it and callers must replace it with the slug §7.0's Stage -1 resolved (see WithAuthCollection).

View Source
const BulkChunk = 100

BulkChunk is the id-set size of one bulk request (§12.3 step 3). Chunking keeps each URL well inside the 8 KB budget, which matters because the method-override fallback is forbidden for PATCH and DELETE.

View Source
const DefaultMaxBulk = 100

DefaultMaxBulk is defaults.max_bulk (§12.3 step 2).

View Source
const DefaultMaxUploadSize int64 = 100 << 20

DefaultMaxUploadSize is --max-size's default (§13).

View Source
const FormURLEncoded = "application/x-www-form-urlencoded"

FormURLEncoded is the Content-Type the override path requires. Payload's check is `=== 'application/x-www-form-urlencoded'`, so a `; charset=utf-8` suffix makes it silently discard the entire body — verified live: the same request with the suffix returned the default 10 documents instead of the 2 the body asked for. This constant is compared byte-for-byte before any override request is allowed to leave.

View Source
const GlobalsPrefix = "/globals/"

GlobalsPrefix is Payload's globals route prefix.

View Source
const HeaderMethodOverride = "X-Payload-HTTP-Method-Override"

HeaderMethodOverride is Payload's read-method override header (§6.2).

View Source
const URLBudget = 8000

URLBudget is §6.2's promotion threshold. Node's default --max-http-header-size is 16,384 bytes for the whole header block, and the request line is only part of it.

Variables

This section is empty.

Functions

func AssertIdentity

func AssertIdentity(id *Identity) error

AssertIdentity turns an unverified identity into §7.2's auth_invalid.

func CheckUploadCollection

func CheckUploadCollection(collection string, isUpload *bool) error

CheckUploadCollection is §13's preflight. It follows the tri-state rule: a nil isUpload means the manifest never learned the fact, so the request is sent rather than rejected on a guess.

func FilenameChanged

func FilenameChanged(sent string, doc Doc) (string, bool)

FilenameChanged reports whether the server renamed the file, which it does silently on a collision.

func GlobalPath

func GlobalPath(slug string) string

GlobalPath renders the REST path of one global.

func GraphQLErrorMessages

func GraphQLErrorMessages(errs []GraphQLError) []string

GraphQLErrorMessages flattens the error list for an error message.

func IntrospectionBlocked

func IntrospectionBlocked(in GraphQLRequest, res *GraphQLResult) bool

IntrospectionBlocked reports Payload's NoProductionIntrospection guard.

The guard fires only on a field literally named __schema or __type, and only under NODE_ENV=production, so a {__typename} probe can never detect it. The signal used here is structural — the request asked for introspection and the response carries errors with no data — with the English message kept only as a confirming hint.

func IsReadOnlyGraphQL

func IsReadOnlyGraphQL(doc string) bool

IsReadOnlyGraphQL reports whether a document contains only queries, which is §6.1's condition for retrying a GraphQL POST. It is deliberately conservative: anything it cannot prove is read-only is treated as a write.

func LimitReader

func LimitReader(r io.Reader, max int64) io.Reader

LimitReader wraps an upload source with --max-size (default 100 MB), failing with request_too_large rather than streaming gigabytes at a dev server. A max of 0 or less means unlimited.

func NormalizePath

func NormalizePath(p string) string

NormalizePath gives a path a leading slash and no trailing slash. A trailing slash is not cosmetic: `GET /api/pages/` answers 308 to `/api/pages` (verified), costing an extra round trip on every request.

func ParseRetryAfter

func ParseRetryAfter(v string, now time.Time) (time.Duration, bool)

ParseRetryAfter reads both Retry-After forms, capped at 120 s. `now` is passed in for the HTTP-date form; a zero `now` makes the date form unusable and falls back to the backoff schedule.

func Permission

func Permission(entry any, permission string) (permitted bool, known bool)

Permission reads one permission out of an /api/access entry.

Payload emits TWO shapes for the same fact: a collection with field-level access control answers {"read": {"permission": true, "fields": {…}}}, and one without answers a bare {"read": true} (verified live). Accepting only the nested form made PayCLI report "unknown" for a question the server had already answered, so both are decoded here, at the source.

func ReactiveRequested

func ReactiveRequested(ctx context.Context) bool

ReactiveRequested reports whether the context opted in.

func SentPaths

func SentPaths(data map[string]any) map[string]bool

SentPaths returns the leaf-path set of a document body, which the error classifier uses to mark which invalid fields the caller actually sent.

func WithReactiveInvalidation

func WithReactiveInvalidation(ctx context.Context) context.Context

WithReactiveInvalidation marks a context as eligible for Level-3 invalidation. It is called ONLY by user-facing command handlers in internal/cli; arch-lint fails the build if it appears anywhere under internal/discovery.

Types

type AccessResult

type AccessResult struct {
	CanAccessAdmin bool           `json:"canAccessAdmin"`
	Collections    map[string]any `json:"collections"`
	Globals        map[string]any `json:"globals"`

	Raw  []byte    `json:"-"`
	HTTP *Response `json:"-"`
}

AccessResult is GET /{api}/access.

It is NOT a complete inventory: a collection on which the identity has zero permissions is absent entirely (payload-kv is missing live even though GET /api/payload-kv answers 403). collections.{slug}.fields is the boolean `true` when every field permission is granted, and an object otherwise.

func (*AccessResult) Can

func (a *AccessResult) Can(collection, permission string) (bool, bool)

Can reports whether a permission is granted in an access result. permission is one of create, read, update, delete. The second return is §7.6's tri-state: false means the server reported nothing about this permission, which is never the same as a denial.

func (*AccessResult) CanGlobal

func (a *AccessResult) CanGlobal(slug, permission string) (bool, bool)

CanGlobal is Can for a global.

func (*AccessResult) GlobalSlugs

func (a *AccessResult) GlobalSlugs() []string

GlobalSlugs returns the global slugs present, sorted.

func (*AccessResult) Slugs

func (a *AccessResult) Slugs() []string

Slugs returns the collection slugs present, sorted.

type Budget

type Budget struct {
	// contains filtered or unexported fields
}

Budget is the shared retry budget of §6.1: parallel operations draw from one pool of 3 × concurrency so a dead server cannot turn a 500-item batch into 1,500 doomed requests.

func NewBudget

func NewBudget(n int) *Budget

NewBudget creates a budget with n retries in it.

func (*Budget) Remaining

func (b *Budget) Remaining() int64

Remaining reports the budget left, for diagnostics.

func (*Budget) Take

func (b *Budget) Take() bool

Take consumes one retry, reporting whether any was left.

type BulkFailure

type BulkFailure struct {
	ID      any    `json:"id"`
	Message string `json:"message"`
}

BulkFailure is one per-id failure from a bulk verb.

type BulkPlan

type BulkPlan struct {
	// Count is the server's count for the where clause.
	Count int
	// IDs are the exact documents the write will touch.
	IDs []any
}

BulkPlan is the outcome of §12.3's phases 1 and 2.

type BulkResult

type BulkResult struct {
	Docs    []Doc         `json:"docs"`
	Errors  []BulkFailure `json:"errors"`
	Message string        `json:"message"`

	// Raw and HTTP are the response of ONE request. A bulk write is chunked
	// (BulkChunk ids per request), so when Chunks > 1 they belong to the chunk
	// named by FailedChunk — the FIRST chunk that failed, because that is the
	// request whose diagnostics the caller has to read. With no failure they
	// are the last chunk's.
	Raw  []byte    `json:"-"`
	HTTP *Response `json:"-"`

	// Chunks is how many bulk requests were issued for this result.
	Chunks int `json:"-"`
	// FailedChunk is the 1-based index of the first chunk that failed (a
	// transport/HTTP error, or a non-empty errors[] under any status). It is 0
	// when every chunk succeeded.
	FailedChunk int `json:"-"`
}

BulkResult is a bulk write response. Payload answers HTTP 400 whenever errors[] is non-empty even though the documents in docs[] were committed, so a caller must read Docs even when the error is non-nil.

func (*BulkResult) IDs

func (b *BulkResult) IDs() []any

IDs returns the ids of the committed documents.

type ClassifyContext

type ClassifyContext struct {
	// Collection is the target slug, used in messages.
	Collection string
	// UploadCollection is the manifest's flags.upload for that slug. A 400 on
	// an upload collection with no errors[0].name is file_missing — the
	// structural signal, not the English "No files were uploaded.".
	UploadCollection bool
	// SentPaths is the leaf-path set PayCLI actually sent, which decides
	// error.fields[].sent and therefore which validation hint is shown
	// (Payload re-validates the whole document on PATCH).
	SentPaths map[string]bool
	// IncludeRaw is false for --no-raw; NoRedact is true for --no-redact.
	IncludeRaw bool
	NoRedact   bool
}

ClassifyContext is the per-request context §11.2 and §11.5 need to fold a Payload error body into exactly one code. Every field is a fact PayCLI already knows; none of them is a translated string.

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client is a Payload API client. It is safe for concurrent use.

func New

func New(cfg Config) (*Client, error)

New validates the configuration and builds the client.

func (*Client) APIPath

func (c *Client) APIPath() string

APIPath is the resolved REST prefix — the one §7.2's ladder settled on when it ran, and the configured one otherwise.

func (*Client) Access

func (c *Client) Access(ctx context.Context, opts ...Option) (*AccessResult, error)

Access issues GET {api_path}/access and enforces H1: 200 + JSON + a decoded object with a `collections` key. Status alone is useless — a wrong base URL answers 200 text/html (verified) — so the Content-Type check is load-bearing.

func (*Client) AdoptAPIPath

func (c *Client) AdoptAPIPath(apiPath, graphQLPath string)

AdoptAPIPath installs the api_path §7.2's autodiscovery probed, on this client and on every clone of it, so a handle a command took before discovery ran still sends its requests to the prefix the project actually serves.

It is deliberately not copy-on-write, unlike WithAuthCollection: the whole point is to reach handles that were already handed out. It is idempotent and is called from internal/cli's adoption site once a manifest — fresh or cached — has settled the prefix, before any data request is issued.

graphQLPath moves with it (§2.8/§4.2): swapping the REST prefix alone would leave GraphQL on the old one. An empty graphQLPath re-derives apiPath + "/graphql".

func (*Client) AnonymousAccess

func (c *Client) AnonymousAccess(ctx context.Context, opts ...Option) (*AccessResult, error)

AnonymousAccess fetches /access with no credential, which is Stage -1 step 1's candidate source. The result is truncated by construction and must never be cached or used as an inventory.

func (*Client) AuthCollection

func (c *Client) AuthCollection() string

AuthCollection is the resolved auth-collection slug ("" in anonymous mode, and "" while the §7.0 placeholder has not been resolved yet).

func (*Client) AuthMode

func (c *Client) AuthMode() string

AuthMode is the resolved auth mode.

func (*Client) BaseURL

func (c *Client) BaseURL() string

BaseURL is the normalised scheme://host[:port].

func (*Client) Budget

func (c *Client) Budget() *Budget

Budget exposes the shared retry budget so a fan-out can hand the same one to every worker (§6.1).

func (*Client) Concurrency

func (c *Client) Concurrency() int

Concurrency is the resolved worker-pool size.

func (*Client) Config

func (c *Client) Config() Config

Config returns a copy of the client's configuration. The credential is included because callers such as the cache scope need its fingerprint; it is never printed.

func (*Client) Count

func (c *Client) Count(ctx context.Context, collection string, p query.Params, opts ...Option) (int, *Response, error)

Count issues GET /{collection}/count. Payload's count accepts only `where` and `trash`; it ignores `draft` entirely (verified), so the caller is expected to have stripped the rest.

func (*Client) Create

func (c *Client) Create(ctx context.Context, collection string, data map[string]any, p query.Params, opts ...Option) (*WriteResult, error)

Create issues POST /{collection}.

func (*Client) Delete

func (c *Client) Delete(ctx context.Context, collection, id string, p query.Params, opts ...Option) (*WriteResult, error)

Delete issues DELETE /{collection}/{id}. On a trash-enabled collection this is a HARD delete (verified); §12.4's soft delete is Trash.

Hard-deleting an already-trashed document requires trash=true, otherwise Payload answers 404.

func (*Client) DeleteByIDs

func (c *Client) DeleteByIDs(ctx context.Context, collection string, ids []any, p query.Params, opts ...Option) (*BulkResult, error)

DeleteByIDs issues chunked DELETE ?where={"id":{"in":[…]}} requests.

func (*Client) DeleteWhereUnsafe

func (c *Client) DeleteWhereUnsafe(ctx context.Context, collection string, where query.Where, p query.Params, opts ...Option) (*BulkResult, error)

DeleteWhereUnsafe is --unsafe-passthrough-where: the raw server semantics, in which DELETE ignores `limit` and removes every match. It exists so the escape hatch is explicit and greppable, never the default path.

func (*Client) Do

func (c *Client) Do(ctx context.Context, req *Request) (*Response, error)

Do sends a request, applying the §6.1 retry policy and the §6.2 URL-length fallback. It returns a non-nil *Response whenever the server answered, and a non-nil error whenever the request failed or the status was not 2xx — so a discovery probe can inspect resp.Status while a command can just check err.

func (*Client) DocAccess

func (c *Client) DocAccess(ctx context.Context, collection, id string, opts ...Option) (Doc, error)

DocAccess issues POST /{collection}/access/{id} for per-document permissions.

func (*Client) Download

func (c *Client) Download(ctx context.Context, collection, filename string, w io.Writer, opts ...Option) (*Response, error)

Download streams GET /{collection}/file/{filename} into w.

This route is served unauthenticated in a stock project and answers 500 — not 404 — for a missing file, so the caller resolves sizes.<name>.filename from the document first rather than guessing a name.

func (*Client) Duplicate

func (c *Client) Duplicate(ctx context.Context, collection, id string, p query.Params, opts ...Option) (*WriteResult, error)

Duplicate issues POST /{collection}/{id}/duplicate.

This is only safe for an id that casts to the collection's id_type: a non-castable id CREATES a document (verified), so callers must pre-validate the id with apierr.CheckID.

func (*Client) ErrorFor

func (c *Client) ErrorFor(req *Request, resp *Response) *apierr.Error

ErrorFor exposes the classifier for a response a caller obtained itself (a discovery probe that wants the code for a status it already has).

func (*Client) Find

func (c *Client) Find(ctx context.Context, collection string, p query.Params, opts ...Option) (*ListResult, error)

Find issues GET /{collection} with the encoded parameters.

func (*Client) FindPages

func (c *Client) FindPages(ctx context.Context, collection string, p query.Params, max int, fn func(*ListResult) error, opts ...Option) error

FindPages walks every page of a query, calling fn once per page. It stops after max documents (max <= 0 means no client-side cap) or when fn returns an error. Paging is explicit rather than limit=0 because limit=0 means *unlimited* to Payload, which is exactly the footgun this avoids.

func (*Client) Get

func (c *Client) Get(ctx context.Context, collection, id string, p query.Params, opts ...Option) (Doc, *Response, error)

Get issues GET /{collection}/{id}. Payload returns the document itself, not a wrapper.

func (*Client) GlobalGet

func (c *Client) GlobalGet(ctx context.Context, slug string, p query.Params, opts ...Option) (Doc, *Response, error)

GlobalGet issues GET /globals/{slug}. A global has no id argument — its GraphQL Query field takes only draft and select (verified) — so `--id` is meaningless here and the caller must not pass one.

func (*Client) GlobalUpdate

func (c *Client) GlobalUpdate(ctx context.Context, slug string, data map[string]any, p query.Params, opts ...Option) (*WriteResult, error)

GlobalUpdate issues POST /globals/{slug} — Payload's update operation for a global is a POST, not a PATCH. The whole-document revalidation rule of §9.10.3 applies here too.

func (*Client) GraphQL

func (c *Client) GraphQL(ctx context.Context, in GraphQLRequest, opts ...Option) (*GraphQLResult, error)

GraphQL posts an operation to the GraphQL endpoint.

func (*Client) GraphQLPath

func (c *Client) GraphQLPath() string

GraphQLPath is the resolved endpoint path, derived from api_path rather than configured independently: Payload computes it as routes.api + routes.graphQL.

func (*Client) Init

func (c *Client) Init(ctx context.Context, slug string, opts ...Option) (*InitResult, error)

Init issues GET /{slug}/init with no Authorization header — Stage -1 step 3.

func (*Client) LastRequestID

func (c *Client) LastRequestID() string

LastRequestID is the X-Request-Id PayCLI sent on its most recent attempt, or "" when no request has been made yet (§6, meta.request_id).

func (*Client) Login

func (c *Client) Login(ctx context.Context, in LoginInput, opts ...Option) (*LoginResult, error)

Login POSTs {api_path}/{collection}/login.

func (*Client) Logout

func (c *Client) Logout(ctx context.Context, collection string, opts ...Option) error

Logout POSTs {api_path}/{collection}/logout. Failing to log out server-side is not fatal — the local credential is what PayCLI controls — so callers treat the error as a warning.

func (*Client) Me

func (c *Client) Me(ctx context.Context, collection string, opts ...Option) (*Identity, error)

Me issues GET /{collection}/me. Pass the empty string to use the client's configured auth collection.

func (*Client) PlanBulk

func (c *Client) PlanBulk(ctx context.Context, collection string, where query.Where, maxDocs int, all bool, opts ...Option) (*BulkPlan, error)

PlanBulk implements §12.3 phases 1-3: count, enforce the blast-radius cap, then resolve the exact ids client-side. It is used for bulk PATCH exactly as for bulk DELETE, so both verbs touch precisely what --dry-run printed — bulk DELETE ignores `limit` entirely (verified: limit=1 deleted all 4).

func (*Client) ProbeIdentity

func (c *Client) ProbeIdentity(ctx context.Context, candidate string, opts ...Option) (*Identity, error)

ProbeIdentity is Stage -1 step 4: try the credential against one candidate auth collection without mutating the client's configuration.

func (*Client) RefreshToken

func (c *Client) RefreshToken(ctx context.Context, collection string, opts ...Option) (*LoginResult, error)

RefreshToken POSTs {api_path}/{collection}/refresh-token using the client's current JWT, which is how §5.0's single re-login is performed without a password when the token is merely stale.

func (*Client) ResolveIDs

func (c *Client) ResolveIDs(ctx context.Context, collection string, where query.Where, count int, opts ...Option) ([]any, error)

ResolveIDs is the where-only form of §12.3 phase 3, kept for callers that genuinely have no other scope to apply.

PREFER ResolveIDsScoped. Taking a bare Where made it easy to resolve on a different population than the one that was counted and than the one the write then touches — the trash/draft/locale scope simply had nowhere to go in this signature, so callers dropped it. Delegating rather than duplicating the paging logic means the two can no longer drift.

func (*Client) ResolveIDsScoped

func (c *Client) ResolveIDsScoped(ctx context.Context, collection string, p query.Params, count int, opts ...Option) ([]any, error)

ResolveIDsScoped is §12.3 phase 3 with the write's OWN read scope.

ResolveIDs takes a bare where clause, which silently resolves a DIFFERENT population than the one phase 1 counted whenever the write is scoped by anything else: `delete --permanent` sends trash=true (so its count covers live + trashed while a where-only resolution can never see a trashed document), and `update --trash` addresses soft-deleted documents that a where-only find excludes entirely. All three phases must address the same documents, so the scope is passed through rather than re-derived.

p is the write's params. Only the paging/projection fields this function owns are overwritten (select, depth, limit, sort, page); where, trash, draft and locale are preserved. Extra is dropped: it carries write-only switches (autosave, overrideLock) that have no meaning on a GET.

func (*Client) Restore

func (c *Client) Restore(ctx context.Context, collection, id string, p query.Params, opts ...Option) (*WriteResult, error)

Restore un-trashes a document: PATCH ?trash=true {"deletedAt": null}. The null is exactly why `data` is sent as JSON and not bracket notation.

func (*Client) Stats

func (c *Client) Stats() Stats

Stats snapshots the counters.

func (*Client) Trash

func (c *Client) Trash(ctx context.Context, collection, id, deletedAt string, p query.Params, opts ...Option) (*WriteResult, error)

Trash implements §12.4's soft delete: PATCH {"deletedAt": now}. The instant is supplied by the caller so this package never reads a clock.

func (*Client) URLFor

func (c *Client) URLFor(req *Request) string

URLFor renders the absolute URL a request would be sent to. It is used for the §6.2 length check and for error messages, where it is redacted first.

func (*Client) Update

func (c *Client) Update(ctx context.Context, collection, id string, data map[string]any, p query.Params, opts ...Option) (*WriteResult, error)

Update issues PATCH /{collection}/{id}.

Payload re-validates the WHOLE document on a PATCH: a one-field update can return 400 naming fields the caller never sent (verified). That is why the classification context carries SentPaths.

func (*Client) UpdateByIDs

func (c *Client) UpdateByIDs(ctx context.Context, collection string, ids []any, data map[string]any, p query.Params, opts ...Option) (*BulkResult, error)

UpdateByIDs issues chunked PATCH ?where={"id":{"in":[…]}} requests.

No server-side `limit` is ever sent on a bulk write, even though bulk PATCH honours it: relying on that asymmetry would make PATCH and DELETE behave differently for the same flags.

func (*Client) Upload

func (c *Client) Upload(ctx context.Context, in UploadInput, opts ...Option) (*WriteResult, error)

Upload builds the multipart body of §13 and sends it.

func (*Client) VersionGet

func (c *Client) VersionGet(ctx context.Context, target VersionTarget, versionID string, p query.Params, opts ...Option) (Doc, *Response, error)

VersionGet issues GET /{target}/versions/{id}.

func (*Client) VersionRestore

func (c *Client) VersionRestore(ctx context.Context, target VersionTarget, versionID string, p query.Params, opts ...Option) (*WriteResult, error)

VersionRestore issues POST /{target}/versions/{id}, which promotes that version back to the live document.

func (*Client) VersionsList

func (c *Client) VersionsList(ctx context.Context, target VersionTarget, p query.Params, opts ...Option) (*ListResult, bool, error)

VersionsList issues GET /{target}/versions.

A 200 is not proof the collection has versions: payload-preferences answers 200 with {"message":"Not Found","value":null} because a custom GET /:key route shadows the versions route (verified). The `docs` array is the only trustworthy signal, so this returns HasDocs alongside the result.

func (*Client) WithAuthCollection

func (c *Client) WithAuthCollection(collection string) *Client

WithAuthCollection returns a shallow copy of the client that sends a different auth-collection slug, sharing the same connection pool, retry budget and counters. It is how the CLI installs the slug §7.0's Stage -1 resolved into a client that was built with the "auto" placeholder.

func (*Client) WithCredential

func (c *Client) WithCredential(mode, collection, credential string) *Client

WithCredential returns a shallow copy of the client that authenticates with a different credential, sharing the same HTTP connection pool and retry budget. Used after a jwt re-login (§5.0).

type Config

type Config struct {
	// BaseURL is scheme://host[:port]; any path component is ignored in
	// favour of APIPath.
	BaseURL string
	// APIPath is Payload's routes.api, e.g. "/api". It may be "" for a
	// project that serves the REST API at the root.
	APIPath string
	// GraphQLPath is the full base-relative GraphQL path, which Payload
	// derives as routes.api + routes.graphQL (§2.8). Empty means APIPath +
	// "/graphql".
	GraphQLPath string

	AuthMode       string
	AuthCollection string
	// Credential is the API key in api-key mode and the JWT in jwt mode. It is
	// never logged, never written to an error and never cached.
	Credential string
	// AuthScheme overrides "JWT" for a project whose auth.jwtOrder excludes it.
	AuthScheme string
	// IdentityVerified reports whether Stage 0 confirmed `user != null`. It
	// drives the §11.5 403 split and nothing else.
	IdentityVerified bool

	// Headers are the profile's extra headers, already interpolated.
	Headers map[string]string
	// AcceptLanguage pins the server's translated strings. "en" is
	// load-bearing (§6) and is the default.
	AcceptLanguage string

	Timeout       time.Duration
	UploadTimeout time.Duration
	MaxRetries    int
	Concurrency   int

	InsecureSkipVerify bool
	UserAgent          string
	MaxBodyBytes       int64

	Logger *slog.Logger

	// Now, Sleep, Rand and RequestID are injected so that §3.1's "only app.go
	// reads the wall clock" rule holds and every retry test is deterministic.
	Now       func() time.Time
	Sleep     func(ctx context.Context, d time.Duration) error
	Rand      func() float64
	RequestID func() string

	// RoundTripper replaces the §6 transport wholesale. Tests use it; nothing
	// else should.
	RoundTripper http.RoundTripper

	// Budget is the shared retry budget for a fan-out (§6.1). Nil means one
	// is created per client.
	Budget *Budget

	// Invalidator receives Level-3 reactive-invalidation signals (§8.4) for
	// requests that opted in with WithReactiveInvalidation. Nil disables the
	// mechanism entirely.
	Invalidator Invalidator
}

Config describes one client. All durations and counts fall back to the §6 defaults when zero.

type Doc

type Doc map[string]any

Doc is one decoded Payload document. Numbers are json.Number, so a large integer id round-trips verbatim instead of through float64.

func (Doc) ID

func (d Doc) ID() any

ID returns the document id, or nil when the document has none.

func (Doc) IDString

func (d Doc) IDString() string

IDString renders the id for a URL path.

type GraphQLError

type GraphQLError struct {
	Message    string         `json:"message"`
	Path       []any          `json:"path,omitempty"`
	Locations  []any          `json:"locations,omitempty"`
	Extensions map[string]any `json:"extensions,omitempty"`
}

GraphQLError is one entry of the GraphQL errors array.

type GraphQLRequest

type GraphQLRequest struct {
	Query         string         `json:"query"`
	Variables     map[string]any `json:"variables,omitempty"`
	OperationName string         `json:"operationName,omitempty"`
}

GraphQLRequest is one GraphQL operation.

In development Payload rebuilds its entire schema on every /api/graphql request, so every call costs ~0.39 s regardless of query size (a 30-alias batch cost the same as a single type lookup). Batch aggressively; never loop.

type GraphQLResult

type GraphQLResult struct {
	Data   json.RawMessage `json:"data"`
	Errors []GraphQLError  `json:"errors,omitempty"`

	Raw  []byte    `json:"-"`
	HTTP *Response `json:"-"`
}

GraphQLResult is a GraphQL response. GraphQL answers HTTP 200 for a partially failed query, so Errors must be inspected even when err is nil.

func (*GraphQLResult) AsError

func (g *GraphQLResult) AsError() error

AsError folds GraphQL errors into one *apierr.Error. A result with both data and errors is a partial success and yields nil, so the caller can use what resolved (§7's degradation ladder).

func (*GraphQLResult) Partial

func (g *GraphQLResult) Partial() bool

Partial reports data alongside errors — the shape §7 tolerates by using whatever resolved and degrading the rest.

type Identity

type Identity struct {
	// User is nil when the credential did not authenticate. Note that a WRONG
	// API key returns HTTP 200 with {"user":null} — status codes are useless
	// for auth here, which is why this field, not the status, is the signal.
	User Doc
	// Collection is the auth collection the identity was resolved against.
	Collection string
	// UserID is user.id.
	UserID any
	// Strategy is Payload's auth strategy name, when it reports one.
	Strategy string
	// CanAccessAdmin is true only when the key is present and true: Payload
	// omits the key entirely when the answer is false.
	CanAccessAdmin bool
	// Verified reports user != null.
	Verified bool
	// Token and Exp are set when the endpoint refreshed a JWT.
	Token string
	Exp   int64

	Raw  []byte
	HTTP *Response
}

Identity is the result of GET /{authCollection}/me (§7.2 Stage 0).

The user object contains the API key IN PLAINTEXT on a project with useAPIKey (verified), so it is never logged and every consumer must render it through internal/redact.

type InitResult

type InitResult struct {
	// IsAuthCollection reports that the body carried an `initialized`
	// boolean, which is the only trustworthy signal that the slug is an auth
	// collection. The route is itself access-gated (crm-contacts answers 403
	// live), so this filter can narrow a candidate set but never widen it.
	IsAuthCollection bool
	Initialized      bool
	HTTP             *Response
}

InitResult is GET /{slug}/init.

type Invalidator

type Invalidator interface {
	Invalidate(ctx context.Context, sig StaleSignal) (Outcome, error)
}

Invalidator is implemented by the cache/discovery layer. The client never imports it concretely, which keeps internal/payload free of a dependency on internal/discovery.

type ListResult

type ListResult struct {
	Docs []Doc `json:"docs"`
	Page
	// Raw is the untouched response body, for --output raw.
	Raw []byte `json:"-"`
	// HTTP is the response that produced it.
	HTTP *Response `json:"-"`
}

ListResult is a find response.

func (*ListResult) IDs

func (l *ListResult) IDs() []any

IDs returns the ids of the returned documents in order.

type LoginInput

type LoginInput struct {
	Collection string
	Email      string
	Username   string
	// Password is sent once and never stored, never logged and never written
	// to an error: only {token, token_exp} is persisted (§5.0).
	Password string
}

LoginInput is `pay auth login --jwt`'s credential (§5.0).

Exactly one identifier is sent. Which one the collection wants is discoverable from mutation{Singular}Input, and when a collection accepts both (loginWithUsername) PayCLI refuses to guess.

type LoginResult

type LoginResult struct {
	Token string `json:"token"`
	// Exp is the JWT expiry in Unix seconds, as Payload reports it.
	Exp     int64  `json:"exp"`
	User    Doc    `json:"user"`
	Message string `json:"message"`

	HTTP *Response `json:"-"`
}

LoginResult is the 200 body of POST /{collection}/login.

func (*LoginResult) Expired

func (l *LoginResult) Expired(now time.Time, skew time.Duration) bool

Expired reports whether the token is expired, or expires within the given skew. `now` is passed in so this package never reads a clock; §5.0 uses a 60 s skew and re-logs-in BEFORE sending, which is what makes a refresh safe even for a write.

func (*LoginResult) ExpiresAt

func (l *LoginResult) ExpiresAt() time.Time

ExpiresAt converts Exp to a time. A zero Exp yields the zero time.

type Multipart

type Multipart struct {
	// Boundary is generated when empty.
	Boundary string
	// Write streams the parts. It runs on its own goroutine and its error is
	// propagated to the caller of Do.
	Write func(w io.Writer, boundary string) error
}

Multipart is a streaming multipart/form-data body (§13).

type Option

type Option func(*Request)

Option customises one request without widening every operation signature.

func WithAuthCollection

func WithAuthCollection(slug string) Option

WithAuthCollection sends the Authorization header for a specific candidate slug without mutating the client (Stage -1 step 4).

func WithClassify

func WithClassify(ctx ClassifyContext) Option

WithClassify attaches the §11.2 classification context.

func WithHeader

func WithHeader(name, value string) Option

WithHeader adds a request-specific header. Authorization is owned by the transport and cannot be set here.

func WithKnownRoute

func WithKnownRoute() Option

WithKnownRoute marks the route as one the manifest claims exists, which is what lets a 404 become a Level-3 staleness signal (§8.4).

func WithTimeout

func WithTimeout(d time.Duration) Option

WithTimeout overrides the per-request timeout (uploads and bulk use 120 s).

func WithoutAuth

func WithoutAuth() Option

WithoutAuth suppresses the Authorization header (Stage -1's candidate probes).

type Outcome

type Outcome struct {
	// Revalidated reports that discovery re-ran and the manifest is fresh.
	Revalidated bool
	// Fields is the refreshed field list, carried into the schema_stale error
	// when the request cannot be re-sent.
	Fields []string
}

Outcome is what an Invalidator did.

type Page

type Page struct {
	TotalDocs     int  `json:"totalDocs"`
	Limit         int  `json:"limit"`
	Page          int  `json:"page"`
	TotalPages    int  `json:"totalPages"`
	PagingCounter int  `json:"pagingCounter"`
	HasNextPage   bool `json:"hasNextPage"`
	HasPrevPage   bool `json:"hasPrevPage"`
	NextPage      *int `json:"nextPage"`
	PrevPage      *int `json:"prevPage"`
}

Page is Payload's pagination envelope.

type Request

type Request struct {
	Method string
	// Path is relative to APIPath unless Absolute is set.
	Path     string
	Absolute bool
	// Query is an already-encoded query string (see internal/payload/query).
	Query string

	Body        []byte
	ContentType string
	Accept      string
	// Header carries request-specific headers. Authorization is never set
	// here; the transport owns it.
	Header http.Header

	// Multipart streams an upload body. Mutually exclusive with Body.
	Multipart *Multipart
	// Sink receives a 2xx body instead of buffering it (pay download).
	Sink io.Writer

	Timeout time.Duration

	// NoAuth suppresses the Authorization header (Stage -1 steps 1 and 3,
	// and the unauthenticated Level-2 schema probe).
	NoAuth bool
	// AuthCollectionOverride sends `Authorization: {slug} API-Key …` for a
	// candidate slug during Stage -1 step 4 without mutating the client.
	AuthCollectionOverride string

	// ReadOnly marks a POST that is semantically a read (a GraphQL query, a
	// method-override GET). GET and HEAD are read-only implicitly.
	ReadOnly bool
	// NoOverride forbids the §6.2 method-override promotion. It is implied
	// for PATCH and DELETE, where a wrong Content-Type makes Payload discard
	// the body and act on every document.
	NoOverride bool

	// KnownRoute says the manifest claims this route exists, which is what
	// turns a 404 `Route not found` into a Level-3 staleness signal (§8.4).
	KnownRoute bool

	// Classify supplies the context §11.2/§11.5 need to fold an error body
	// into one code.
	Classify ClassifyContext
}

Request is a protocol-level request description. transport.go turns it into the single *http.Request this package is allowed to construct.

type Response

type Response struct {
	Status      int
	Header      http.Header
	Body        []byte
	ContentType string
	// Method and URL describe the request that produced it; URL is already
	// redacted (§5.3) and is safe to print.
	Method    string
	URL       string
	RequestID string
	Attempts  int
	Retries   int
	Duration  time.Duration
	Bytes     int64
	// Promoted reports that §6.2's method-override fallback was used.
	Promoted bool
}

Response is what came back. Body is buffered unless the request had a Sink.

func (*Response) OK

func (r *Response) OK() bool

OK reports a 2xx status.

type RetryPolicy

type RetryPolicy struct {
	MaxAttempts   int
	Base          time.Duration
	Cap           time.Duration
	RetryAfterCap time.Duration
	Rand          func() float64
}

RetryPolicy is §6.1's decorrelated-jitter backoff.

type StaleSignal

type StaleSignal struct {
	// Kind is one of the Stale* constants.
	Kind string
	// Paths are the field paths a QueryError rejected.
	Paths []string
	// Method and Route describe the request that produced the signal.
	Method string
	Route  string
	// Idempotent reports whether the original request may be re-sent after a
	// successful re-discovery (§8.4c).
	Idempotent bool
}

StaleSignal describes the proof that the cached schema is out of date.

func DetectStale

func DetectStale(resp *Response, knownRoute bool) *StaleSignal

DetectStale classifies a response as a staleness proof. knownRoute says the manifest claims the route exists, which is what makes a 404 meaningful — a 404 for a slug PayCLI never heard of is just a typo.

type Stats

type Stats struct {
	Requests int64
	Retries  int64
	Bytes    int64
}

Stats are the per-process counters meta.http_requests, meta.retries and meta.bytes are built from.

type UploadInput

type UploadInput struct {
	Collection string
	// Filename is the name sent in the Content-Disposition. Payload
	// auto-renames on a collision (paycli-probe.png -> paycli-probe-1.png), so
	// the caller must read the returned filename rather than assume this one.
	Filename string
	// ContentType is the part's own Content-Type, sniffed or --content-type.
	ContentType string
	// Body streams the bytes. It is never buffered whole.
	Body io.Reader
	// Fields are the sibling document fields, already merged per §9.10.2.
	// They travel as a JSON string in the _payload part.
	Fields map[string]any
	// ReplaceID turns the upload into PATCH /{coll}/{id}. Note that Payload
	// mints a NEW filename rather than overwriting in place, so any hardcoded
	// URL to the old file breaks.
	ReplaceID string

	Params query.Params
}

UploadInput describes one multipart upload.

type VersionTarget

type VersionTarget struct {
	Collection string
	Global     string
}

VersionTarget names either a collection or a global. Exactly one of the two fields is set.

func CollectionTarget

func CollectionTarget(slug string) VersionTarget

CollectionTarget builds a target for a collection.

func GlobalTarget

func GlobalTarget(slug string) VersionTarget

GlobalTarget builds a target for a global.

func (VersionTarget) IsGlobal

func (t VersionTarget) IsGlobal() bool

IsGlobal reports whether the target is a global.

func (VersionTarget) Slug

func (t VersionTarget) Slug() string

Slug is the entity slug, whichever kind it is.

type WriteResult

type WriteResult struct {
	Doc     Doc    `json:"doc"`
	Message string `json:"message"`

	Raw  []byte    `json:"-"`
	HTTP *Response `json:"-"`
}

WriteResult is a single-document write response.

Directories

Path Synopsis
Package query builds Payload REST query strings.
Package query builds Payload REST query strings.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL