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
- func AssertIdentity(id *Identity) error
- func CheckUploadCollection(collection string, isUpload *bool) error
- func FilenameChanged(sent string, doc Doc) (string, bool)
- func GlobalPath(slug string) string
- func GraphQLErrorMessages(errs []GraphQLError) []string
- func IntrospectionBlocked(in GraphQLRequest, res *GraphQLResult) bool
- func IsReadOnlyGraphQL(doc string) bool
- func LimitReader(r io.Reader, max int64) io.Reader
- func NormalizePath(p string) string
- func ParseRetryAfter(v string, now time.Time) (time.Duration, bool)
- func Permission(entry any, permission string) (permitted bool, known bool)
- func ReactiveRequested(ctx context.Context) bool
- func SentPaths(data map[string]any) map[string]bool
- func WithReactiveInvalidation(ctx context.Context) context.Context
- type AccessResult
- type Budget
- type BulkFailure
- type BulkPlan
- type BulkResult
- type ClassifyContext
- type Client
- func (c *Client) APIPath() string
- func (c *Client) Access(ctx context.Context, opts ...Option) (*AccessResult, error)
- func (c *Client) AdoptAPIPath(apiPath, graphQLPath string)
- func (c *Client) AnonymousAccess(ctx context.Context, opts ...Option) (*AccessResult, error)
- func (c *Client) AuthCollection() string
- func (c *Client) AuthMode() string
- func (c *Client) BaseURL() string
- func (c *Client) Budget() *Budget
- func (c *Client) Concurrency() int
- func (c *Client) Config() Config
- func (c *Client) Count(ctx context.Context, collection string, p query.Params, opts ...Option) (int, *Response, error)
- func (c *Client) Create(ctx context.Context, collection string, data map[string]any, p query.Params, ...) (*WriteResult, error)
- func (c *Client) Delete(ctx context.Context, collection, id string, p query.Params, opts ...Option) (*WriteResult, error)
- func (c *Client) DeleteByIDs(ctx context.Context, collection string, ids []any, p query.Params, ...) (*BulkResult, error)
- func (c *Client) DeleteWhereUnsafe(ctx context.Context, collection string, where query.Where, p query.Params, ...) (*BulkResult, error)
- func (c *Client) Do(ctx context.Context, req *Request) (*Response, error)
- func (c *Client) DocAccess(ctx context.Context, collection, id string, opts ...Option) (Doc, error)
- func (c *Client) Download(ctx context.Context, collection, filename string, w io.Writer, opts ...Option) (*Response, error)
- func (c *Client) Duplicate(ctx context.Context, collection, id string, p query.Params, opts ...Option) (*WriteResult, error)
- func (c *Client) ErrorFor(req *Request, resp *Response) *apierr.Error
- func (c *Client) Find(ctx context.Context, collection string, p query.Params, opts ...Option) (*ListResult, error)
- func (c *Client) FindPages(ctx context.Context, collection string, p query.Params, max int, ...) error
- func (c *Client) Get(ctx context.Context, collection, id string, p query.Params, opts ...Option) (Doc, *Response, error)
- func (c *Client) GlobalGet(ctx context.Context, slug string, p query.Params, opts ...Option) (Doc, *Response, error)
- func (c *Client) GlobalUpdate(ctx context.Context, slug string, data map[string]any, p query.Params, ...) (*WriteResult, error)
- func (c *Client) GraphQL(ctx context.Context, in GraphQLRequest, opts ...Option) (*GraphQLResult, error)
- func (c *Client) GraphQLPath() string
- func (c *Client) Init(ctx context.Context, slug string, opts ...Option) (*InitResult, error)
- func (c *Client) LastRequestID() string
- func (c *Client) Login(ctx context.Context, in LoginInput, opts ...Option) (*LoginResult, error)
- func (c *Client) Logout(ctx context.Context, collection string, opts ...Option) error
- func (c *Client) Me(ctx context.Context, collection string, opts ...Option) (*Identity, error)
- func (c *Client) PlanBulk(ctx context.Context, collection string, where query.Where, maxDocs int, ...) (*BulkPlan, error)
- func (c *Client) ProbeIdentity(ctx context.Context, candidate string, opts ...Option) (*Identity, error)
- func (c *Client) RefreshToken(ctx context.Context, collection string, opts ...Option) (*LoginResult, error)
- func (c *Client) ResolveIDs(ctx context.Context, collection string, where query.Where, count int, ...) ([]any, error)
- func (c *Client) ResolveIDsScoped(ctx context.Context, collection string, p query.Params, count int, ...) ([]any, error)
- func (c *Client) Restore(ctx context.Context, collection, id string, p query.Params, opts ...Option) (*WriteResult, error)
- func (c *Client) Stats() Stats
- func (c *Client) Trash(ctx context.Context, collection, id, deletedAt string, p query.Params, ...) (*WriteResult, error)
- func (c *Client) URLFor(req *Request) string
- func (c *Client) Update(ctx context.Context, collection, id string, data map[string]any, ...) (*WriteResult, error)
- func (c *Client) UpdateByIDs(ctx context.Context, collection string, ids []any, data map[string]any, ...) (*BulkResult, error)
- func (c *Client) Upload(ctx context.Context, in UploadInput, opts ...Option) (*WriteResult, error)
- func (c *Client) VersionGet(ctx context.Context, target VersionTarget, versionID string, p query.Params, ...) (Doc, *Response, error)
- func (c *Client) VersionRestore(ctx context.Context, target VersionTarget, versionID string, p query.Params, ...) (*WriteResult, error)
- func (c *Client) VersionsList(ctx context.Context, target VersionTarget, p query.Params, opts ...Option) (*ListResult, bool, error)
- func (c *Client) WithAuthCollection(collection string) *Client
- func (c *Client) WithCredential(mode, collection, credential string) *Client
- type Config
- type Doc
- type GraphQLError
- type GraphQLRequest
- type GraphQLResult
- type Identity
- type InitResult
- type Invalidator
- type ListResult
- type LoginInput
- type LoginResult
- type Multipart
- type Option
- type Outcome
- type Page
- type Request
- type Response
- type RetryPolicy
- type StaleSignal
- type Stats
- type UploadInput
- type VersionTarget
- type WriteResult
Constants ¶
const ( AuthModeAPIKey = apierr.AuthModeAPIKey AuthModeJWT = apierr.AuthModeJWT AuthModeAnonymous = apierr.AuthModeAnonymous )
Auth modes (§5.0). These are the same string values apierr uses.
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).
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.
const ( StaleQueryPath = "query_path" StaleRouteMissing = "route_missing" StaleEndpoints = "endpoints_disabled" )
Staleness signal kinds.
const ( RetryBase = 250 * time.Millisecond RetryCap = 8 * time.Second RetryAfterCap = 120 * time.Second DefaultMaxAttempts = 4 )
Retry constants from §6.1.
const ( HeaderRequestID = "X-Request-Id" HeaderAcceptLanguage = "Accept-Language" HeaderAuthorization = "Authorization" HeaderPoweredBy = "X-Powered-By" HeaderRetryAfter = "Retry-After" )
Header names PayCLI sets or reads.
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.
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).
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.
const DefaultMaxBulk = 100
DefaultMaxBulk is defaults.max_bulk (§12.3 step 2).
const DefaultMaxUploadSize int64 = 100 << 20
DefaultMaxUploadSize is --max-size's default (§13).
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.
const GlobalsPrefix = "/globals/"
GlobalsPrefix is Payload's globals route prefix.
const HeaderMethodOverride = "X-Payload-HTTP-Method-Override"
HeaderMethodOverride is Payload's read-method override header (§6.2).
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 ¶
AssertIdentity turns an unverified identity into §7.2's auth_invalid.
func CheckUploadCollection ¶
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 ¶
FilenameChanged reports whether the server renamed the file, which it does silently on a collision.
func GlobalPath ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
ReactiveRequested reports whether the context opted in.
func SentPaths ¶
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 ¶
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.
type BulkFailure ¶
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 (*Client) APIPath ¶
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 ¶
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 ¶
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 ¶
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 ¶
AuthCollection is the resolved auth-collection slug ("" in anonymous mode, and "" while the §7.0 placeholder has not been resolved yet).
func (*Client) Budget ¶
Budget exposes the shared retry budget so a fan-out can hand the same one to every worker (§6.1).
func (*Client) Concurrency ¶
Concurrency is the resolved worker-pool size.
func (*Client) 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 ¶
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 ¶
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 ¶
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 ¶
GraphQLPath is the resolved endpoint path, derived from api_path rather than configured independently: Payload computes it as routes.api + routes.graphQL.
func (*Client) LastRequestID ¶
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 ¶
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 ¶
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) 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 ¶
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 ¶
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 ¶
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 ¶
Doc is one decoded Payload document. Numbers are json.Number, so a large integer id round-trips verbatim instead of through float64.
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 ¶
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 ¶
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 ¶
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 ¶
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.
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 ¶
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 ¶
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.