Documentation
¶
Overview ¶
Package shared — общие helpers для всех api-слайсов (parity с services/vpc/internal/apps/kacho/shared).
Содержит:
- errors.go — sentinel → gRPC status mapping (MapRepoErr, MapValidationErr); заменяет per-resource `mapRepoErr`.
- errdetails.go — InvalidArgument-builder с BadRequest FieldViolations.
- ids.go — `ValidateResourceID(id, prefix, name)` (parity с YC-style ошибкой `"invalid <resource> id '<id>'"`).
- proto.go — `TimestampProto` (truncate-to-seconds), `OperationToProto` (corelib.Operation → proto.Operation).
Package shared — errdetails.go: builder для InvalidArgument с BadRequest FieldViolations details (Kachō error-format convention).
Заменяет 6+ копий per-resource `invalidArg(field, desc)` (account/helpers.go, project/helpers.go, …) — все идентичны bit-for-bit.
Package shared — errors.go: единый sentinel → gRPC status mapping для всех api-слайсов (account / project / user / service_account / group / role / access_binding).
Заменяет 7+ копий per-resource `mapRepoErr` (account/helpers.go, project/helpers.go, …). Все вызывающие должны маппить sentinel-ошибки именно через эти функции — единственный authoritative point of translation между internal-sentinels и gRPC-кодами, чтобы (а) не дрейфил mapping per-package, (б) добавление нового sentinel'а требовало правки одного места.
Package shared — ids.go: resource-id format validator.
Заменяет 11+ копий per-resource `validate<Resource>ID(id)` и `validateAccountIDFor<Resource>(id)` функций (account/get.go, project/get.go, role/helpers.go, group/helpers.go, ServiceAccount, User, AccessBinding — все одинаковые: prefix + length check).
Package shared — list_operations.go: ListOperationsUseCase backs the per-resource ListOperations RPC of RoleService / GroupService / ProjectService / ServiceAccountService, plus MapOperationsListErr — the single classifier every IAM operations feed uses to decide whether a listing failure belongs to the caller or to the store.
All four resources list operations identically — filter the common `operations` table by the denormalized `resource_id` column with (created_at, id) cursor pagination (corelib operations.Repo). The query lives in the repo layer (corelib); this use-case is the thin reuse point so the four handlers stay transport-only (architecture.md clean-arch) and the no-op placeholders are replaced by one shared implementation.
Package shared — proto.go: общие helper'ы для конверсии domain ↔ proto.
Заменяет:
- 7 копий `tsProto(t)` truncate-to-seconds timestamp helper'ов (account/handler.go, project/handler.go, user/handler.go, role/helpers.go, group/helpers.go, service_account/helpers.go, access_binding/helpers.go);
- копии `operationToProto(op)` corelib.Operation → proto.Operation mapping функций (account, access_binding, group, sa_keys, internal_authorize, project, user, conditions, role, service_account).
Все копии — bit-for-bit идентичны; разные имена per-package — единственное отличие.
Package shared — txworker.go: generic Write-TX scaffolding для worker'ов async-операций (doCreate / doUpdate / doDelete / etc).
Заменяет ~12 LOC boilerplate'а begin→commit→rollback→committed flag, повторяющегося в 15+ worker'ах через все 7 ресурсных пакетов (account / project / user / sa / group / role / access_binding).
Package shared — updatemask.go: UpdateMask validation + predicate.
Заменяет повторяющиеся ~16 LOC (validation loop + apply closure) в каждом из 5 Update use-case'ов: account/project/service_account/group/role.
Per-resource maps `<resource>MutableFields` + `<resource>ImmutableFields` остаются package-level в каждом ресурсе (контекстные error-messages типа `"ownerUserId is immutable after Account.Create"` — имя поля в тексте всегда в wire-форме camelCase, см. immutable_message_camelcase_test.go) — они конфигурация, не дубликат.
Index ¶
- Constants
- func DoWithWriteTx[T any](ctx context.Context, repo kanamerepo.Repository, ...) (T, error)
- func DoWithWriteTxVoid(ctx context.Context, repo kanamerepo.Repository, ...) error
- func EffectiveListPageSize(pageSize int32) int
- func EncodeVisiblePageToken(c VisibleCursor) string
- func GoPostCommit(callerCtx context.Context, logger *slog.Logger, what string, ...)
- func InvalidArg(field, desc string) error
- func IsMembershipCarriesRights(err error) bool
- func LabelsEqual(a, b domain.Labels) bool
- func LogRepoErr(ctx context.Context, logger *slog.Logger, op string, err error) error
- func MapOperationsListErr(err error) error
- func MapRepoErr(err error) error
- func MapValidationErr(err error) error
- func MaskAllows(mask []string, field string) bool
- func NameBlockingGrants(err error, named []string, total int) error
- func OperationToProto(op *operations.Operation) *operationpb.Operation
- func ResolveLabelsUpdate(mask []string, body domain.Labels) (newLabels domain.Labels, apply bool)
- func RevokeBindingsInScope(ctx context.Context, w kanamerepo.Writer, resourceType domain.ResourceType, ...) ([]outboxtypes.RelationTuple, int, error)
- func TimestampProto(t time.Time) *timestamppb.Timestamp
- func ValidatePageToken(field, token string) error
- func ValidatePagination(pageToken string, pageSize int32) error
- func ValidateRawPagination(pageToken string, pageSize int64) error
- func ValidateRawVisiblePagination(pageToken string, pageSize int64) error
- func ValidateResourceID(id, prefix, resourceName string) error
- func ValidateUpdateMask(mask []string, mutable map[string]struct{}, immutable map[string]string) error
- func ValidateVisiblePagination(pageToken string, pageSize int32) error
- type CreatedByLane
- type ListOperationsUseCase
- type ListScan
- type ListScanRecorder
- type NoopListScanRecorder
- type VisibleCursor
Constants ¶
const ( // ReconcileEventUpsert — an object appeared or changed (mirror upsert / iam-native // Create / label update). Drives a forward re-evaluation of matching bindings. ReconcileEventUpsert = "mirror.upsert" // ReconcileEventDelete — an object was removed (mirror delete). Drives eager-revoke // of any materialized member referencing it. ReconcileEventDelete = "mirror.delete" )
reconcile_event.go — app-layer reconcile-event type literals.
The γ reconciler-worker drains kaname.resource_reconcile_outbox and re-evaluates the bindings that reference the changed object (ReconcileObject keys on object_type/object_id — it does NOT branch on the event type). So an IAM-OWN resource CHANGE (label-update OR a brand-new resource that must forward-materialize under an owner `*.*` binding, rbac-contract-a-fix) re-uses the SAME "mirror.upsert" literal the β mirror-change path uses — there is no separate "created" type.
These literals are kept in lockstep with repo/kaname/pg/reconcile_outbox.Event* (the drainer reads the same strings) but declared HERE so the use-case layer does not import the pg adapter package (Clean Architecture dependency rule — the same reason internal_iam.register_resource.go inlines them).
const ( ScopeBindingRevokePageSize = 1000 ScopeBindingRevokeMaxPasses = 50 )
ScopeBindingRevokePageSize / ScopeBindingRevokeMaxPasses — границы дренажа. Размер страницы — платформенный максимум списка; потолок проходов обращает патологическую область в отказ, а не в тихую частичную работу.
const DefaultListPageSize = 50
DefaultListPageSize — the page a caller gets when he names no size. Same value the repository applies (pg.defaultListPageSize); stated here because the use-case now decides the length of the answer.
const MaxListPageSize int32 = 1000
MaxListPageSize — верхняя граница page_size, общая с репозиторием (pg.effectivePageSize) и с платформенной validate.PageSize. Значение вне диапазона ОТВЕРГАЕТСЯ, а не зажимается: молчаливое зажатие отдаёт не ту страницу, о которой просили, и вызывающий об этом не узнаёт.
const MaxNamedBlockingGrants = 5
MaxNamedBlockingGrants — сколько выдач называется поимённо.
Предел есть, и он не вкусовщина: число мешающих выдач НИЧЕМ не ограничено сверху, а сообщение об отказе читает человек. Отказ, вываливающий двести идентификаторов, перестают читать целиком — вместе с той частью, ради которой он написан. Полная величина при этом не теряется: она уезжает и прозой, и машинно.
const PostCommitTimeout = 2 * time.Minute
PostCommitTimeout bounds a detached post-commit materialization pass. Generous relative to a healthy pass (milliseconds to low seconds) but finite, so a pass wedged on a lock or an unresponsive peer cannot pin a goroutine for the process lifetime. Exceeding it is not data loss — the durable outbox/event backstop re-converges the object.
UnavailableMessage — текст, который получает вызывающий на признаке недоступности. ЕДИНСТВЕННЫЙ производитель этого текста в службе.
Почему константа, а не литерал у каждого переводчика ¶
Переводчик отказа в этой службе не один, и предмет у них общий. Пока текст стоял литералом, копии разошлись с каноном молча: канон был переведён на фиксированный текст, копии остались на тексте цепочки, и решения об этом расхождении никто не принимал (задача #2464). Второе место об одном предмете расходится на первом же уточнении — здесь оно расходилось полтора месяца.
Почему текст НЕ называет подсистему ¶
Признак недоступности ставит и база, и сосед, и гейт прав, поэтому «database unavailable» на проводе был бы собственной маленькой ложью в двух случаях из трёх. Вызывающему довольно кода и того, что повтор осмыслен.
Variables ¶
This section is empty.
Functions ¶
func DoWithWriteTx ¶
func DoWithWriteTx[T any]( ctx context.Context, repo kanamerepo.Repository, action func(ctx context.Context, w kanamerepo.Writer) (T, error), ) (T, error)
DoWithWriteTx оборачивает стандартный writer-tx паттерн вокруг одной мутации: Begin → action → Commit (если action вернул nil) или Rollback (если action вернул err / panic — defer-Rollback срабатывает на любом not-committed возврате).
Использование (account/create.go::doCreate):
created, err := shared.DoWithWriteTx(ctx, u.repo,
func(ctx context.Context, w kanamerepo.Writer) (domain.Account, error) {
return w.AccountsW().Insert(ctx, a)
})
if err != nil { return nil, err }
// post-commit hooks here
return marshalAccount(created)
Generic T — domain-тип, возвращаемый action'ом. Go infer'ит его из closure.
Все ошибки из репо (action error + Writer/Commit) маппятся через MapRepoErr — caller получает уже gRPC-status.
func DoWithWriteTxVoid ¶
func DoWithWriteTxVoid( ctx context.Context, repo kanamerepo.Repository, action func(ctx context.Context, w kanamerepo.Writer) error, ) error
DoWithWriteTxVoid — вариант для action'ов без возвращаемого domain-объекта (например, doDelete). Возвращает только error.
Использование (account/delete.go::doDelete):
if err := shared.DoWithWriteTxVoid(ctx, u.repo,
func(ctx context.Context, w kanamerepo.Writer) error {
return w.AccountsW().Delete(ctx, id)
}); err != nil {
return nil, err
}
return anypb.New(&emptypb.Empty{})
func EffectiveListPageSize ¶
EffectiveListPageSize resolves a requested page size to the number of visible rows a page holds. It mirrors the repository's own resolution (0 means the default) and is needed in the use-case because THAT is where the page is now filled — the repository no longer knows how large the answer is.
The value is assumed already validated by ValidateVisiblePagination; out of range it clamps to the default rather than inventing a limit, and the guard above is what makes that branch unreachable on the served path.
func EncodeVisiblePageToken ¶
func EncodeVisiblePageToken(c VisibleCursor) string
EncodeVisiblePageToken renders the cursor. Nanosecond precision is kept because the keyset compares the stored timestamp, not the truncated one the resource message carries.
func GoPostCommit ¶
func GoPostCommit(callerCtx context.Context, logger *slog.Logger, what string, fn func(context.Context))
GoPostCommit runs fn asynchronously, detached from callerCtx's cancellation and deadline but carrying its values, under PostCommitTimeout. `what` names the pass in the panic log. A nil fn is a no-op.
The caller MUST NOT rely on fn having completed when GoPostCommit returns — that is the entire point. Use it only for work whose durable record is already committed.
func InvalidArg ¶
InvalidArg — InvalidArgument-error с одним BadRequest_FieldViolation. Если WithDetails не смог приклеить детали (proto-marshal-fail), возвращает голый status без них (best-effort — клиент получит code+message, отсутствие details не критично).
func IsMembershipCarriesRights ¶
IsMembershipCarriesRights — отказ ли это полосы «членство несёт права».
Спрашивается ПОСЛЕ `MapRepoErr`, поэтому смотрит на машинный признак, а не на цепочку sentinel'ов: `MapRepoErr` пересобирает статус и цепочку не сохраняет. Это же делает распознавание НЕЗАВИСИМЫМ ОТ ЯЗЫКА прозы — ровно то свойство, ради которого признак и заводится.
func LabelsEqual ¶
LabelsEqual reports whether two label maps are equal (same keys + values). Used by Update use-cases to decide whether `labels` is a real change for the audit `changed_fields` set (no-op label re-set must not count as a change).
func LogRepoErr ¶
LogRepoErr переводит ошибку хранилища в gRPC-статус и, если перевод СТИРАЕТ причину, называет её журналу сервера.
Какие исходы пишутся и почему не все ¶
Пишутся ровно те, чьё сообщение на проводе причины НЕ несёт:
- `INTERNAL` — текст фиксирован («internal error») by construction, иначе наружу уехал бы текст драйвера;
- `UNAVAILABLE` — текст тоже фиксирован («service unavailable»), по той же причине: цепочка ведёт к драйверу. До этой правки он собирался из цепочки, и обёртка вызывающего уезжала на провод дословно. Подсистему текст не называет: признак ставит и база, и сосед, и гейт прав.
Остальные отказы называют свою причину САМИ и адресованы вызывающему: «Project %s not found», «Illegal argument …». Дублировать их в журнале значило бы писать строку на каждое обычное обращение арендатора — и та единственная, ради которой журнал и читают, утонула бы среди них. Заметность, введённая таким способом, уничтожается тем же средством, которым вводится.
Что пишется ¶
Полоса (`op`), полная цепочка ошибки (там же код состояния, который мост SQLSTATE оставляет намеренно) и КОНКРЕТНЫЙ ТИП ошибки. Тип назван отдельно, потому что отказ, не несущий строки состояния вовсе (не дозвонились до базы), цепочкой не отличим от прочих, а типом — отличим сразу.
Журнал — серверный, на провод из него не идёт ничего: возвращаемый статус собирает `MapRepoErr`, и он остаётся единственным местом перевода.
func MapOperationsListErr ¶
MapOperationsListErr classifies a failure of operations.Repo.List by WHAT THE STORE ANSWERED, never by what the caller happened to send.
The operations repo validates the caller's page format itself and reports it as a gRPC InvalidArgument naming the field — a page_token it could not decode, a page_size outside [0..1000] (corevalidate.PageSize). That classification is authoritative and passes through unchanged, field violations included. Everything else the repo returns is a store failure and gets the fixed INTERNAL text (never the pgx/SQL text).
The shape this replaces keyed on "was a page_token supplied", which is not a fact about the failure at all. It mislabelled in both directions: a database that was down was reported as a malformed cursor — sending the operator to fix a client that was fine — and a page_size out of range on the FIRST page (no token yet) was reported as an internal fault, filing a caller's mistake as an outage. Both are now decided by the store's own answer.
func MapRepoErr ¶
MapRepoErr — sentinel → gRPC status. Возвращает nil на nil-input.
Полное покрытие 8 sentinel'ов (включая ErrPermissionDenied / ErrUnauthenticated, которых не было в per-resource копиях — leak'ало `codes.Internal` клиенту до этой консолидации).
Fallback'и:
- если err уже несет gRPC status (не codes.Unknown) — пропускаем через;
- если err-текст начинается с "Illegal argument" — YC-style InvalidArgument (parity с verbatim-формой error-сообщений);
- иначе — Internal с переданным err-текстом (StripSentinel снимает sentinel-prefix чтобы клиент не увидел "not found: ...").
Порядок веток — сначала pass-through, потом sentinel-switch (форма kacho-nlb). Он несущий, а не косметический: pkg/validate кладёт имя поля ТОЛЬКО в google.rpc.BadRequest-details, сообщение остаётся общим «invalid argument». Пересборка статуса в sentinel-ветке (`status.Error(code, StripSentinel(err))`) детали теряет, поэтому ошибка, обёрнутая через `%w` на iamerr.Err*, обязана пройти pass-through ПЕРВОЙ. status с codes.Unknown под pass-through НЕ попадает (guard `!= Unknown`) — он падает в sentinel-switch и дальше в фиксированный INTERNAL, без leak'а.
func MapValidationErr ¶
MapValidationErr — обертка для результатов `domain.<Type>.Validate()` (cumulative multierr). Все sync-handler'ы вызывают ее на validation-stage перед эмитом Operation, чтобы InvalidArgument имел единую форму.
func MaskAllows ¶
MaskAllows возвращает true, если поле `field` присутствует в mask, ИЛИ если mask пустой (full-PATCH semantics — все поля применяются).
Заменяет inlined-closure pattern:
apply := func(field string) bool {
if len(mask) == 0 { return true }
for _, m := range mask { if m == field { return true } }
return false
}
func NameBlockingGrants ¶
NameBlockingGrants дополняет отказ полосы перечнем выдач, которые ДЕРЖАТ членство: тем же перечнем прозой и машинно.
`total` — сколько их всего, `named` — сколько из них названо. Пустой перечень возвращает отказ КАК ЕСТЬ: дочитать не удалось (выдачи успели отозвать между отказом и чтением, либо база не ответила), и выдумывать перечень не из чего. Отказ при этом не портится — он просто остаётся прежним, а не превращается в утверждение «мешающих выдач ноль», которого база не делала.
func OperationToProto ¶
func OperationToProto(op *operations.Operation) *operationpb.Operation
OperationToProto — прослойка к общему слою: перевод строки операции в контракт объявлен в дереве ОДИН раз (`pkg/operations/operationspb`, задача #1369).
До сведения объявлений было двенадцать, а смысловых версий — пять; расходились они именем помощника усечения времени и охраной пустого значения, то есть там, где расхождение не ломает сборку и видно только тому, кто сравнит копии.
func ResolveLabelsUpdate ¶
ResolveLabelsUpdate решает, нужно ли применить labels в Update, и возвращает целевое значение метки.
proto3-map не несет presence: пустой `labels:{}` и отсутствующий labels в теле приходят в use-case как nil domain.Labels — неотличимо. Поэтому единственный надежный сигнал «очистить labels» — присутствие "labels" в update_mask:
- mask содержит "labels" → apply, значение = тело (nil/пусто → очистка);
- mask пустой (full-PATCH) и labels переданы в теле → apply, значение = тело;
- иначе → labels не трогаем.
При apply=true newLabels всегда non-nil (пустой набор = очистка), чтобы writer записал labels='{}' и LabelsEqual корректно сравнил с текущим значением.
func RevokeBindingsInScope ¶
func RevokeBindingsInScope( ctx context.Context, w kanamerepo.Writer, resourceType domain.ResourceType, resourceID string, displayNoun string, ) ([]outboxtypes.RelationTuple, int, error)
RevokeBindingsInScope сносит КАЖДУЮ выдачу, сделанную на область (resourceType, resourceID), внутри транзакции вызывающего: читает её персистентную ведомость выпущенных кортежей, удаляет строку выдачи (её ведомость уходит каскадом) и ВОЗВРАЩАЕТ собранный набор кортежей на снятие.
Эмитит собранное САМ ВЫЗЫВАЮЩИЙ — вместе со своими собственными кортежами жизненного цикла (указатель аккаунта на кластер, структурные указатели проекта), которые в ведомости выдач по построению не значатся.
displayNoun — существительное ресурса в тоне контракта («Account», «Project»); попадает в текст отказа, который читает оператор.
func TimestampProto ¶
func TimestampProto(t time.Time) *timestamppb.Timestamp
TimestampProto конвертирует time.Time в *timestamppb.Timestamp с truncate'ом до секунд (конвенция Kachō: timestamp precision = seconds, не nanoseconds). Zero-time → nil (parity с handler-стиль омитом null timestamp полей).
func ValidatePageToken ¶
ValidatePageToken returns InvalidArgument when a non-empty token is not a well-formed keyset cursor (base64 of `<RFC3339Nano>|<id>`). Empty → OK (first page). The message is the stable contract form "Illegal argument <field>".
func ValidatePagination ¶
ValidatePagination — sync-проверка формата пагинации целиком (page_size + page_token) для списочного use-case'а.
Зачем в use-case'е, а не только в хендлере. Списочный use-case решает, кто спрашивает, раньше, чем читает страницу: анонимный или неуправомоченный вызывающий получает пустую страницу и до репозитория не доходит. Пока формат проверял только репозиторий, один и тот же мусорный курсор получал разный ответ в зависимости от того, что вызывающему выдано, — то есть проверка ввода зависела от прав. Здесь она от них не зависит.
Репозиторий остаётся авторитетным: он повторяет обе проверки на служимом пути.
ВАЖНО про место вызова: сюда приходит уже суженное до int32 значение. Если вызывающий сузил его насыщающим преобразованием, отрицательный ввод превратился в 0 ещё ДО этой строки, а 0 в контракте значит «применить умолчание» — проверка стоит, верна и не может сработать. Формат СЫРОГО запроса судит ValidateRawPagination; эта функция остаётся проверкой уже разобранного фильтра.
func ValidateRawPagination ¶
ValidateRawPagination — формат пагинации по СЫРЫМ значениям запроса, до любого преобразования типа и до любого решения о доступе.
Зачем отдельная функция и почему она принимает int64. `page_size` приезжает по проводу как int64, а внутренние фильтры iam — int32, поэтому транспорт сужал значение насыщающим преобразованием (safeconv.ClampNonNegInt32). Насыщение — не проверка: −1 становится нулём, ноль означает «умолчание», и вызывающий получает страницу, о которой не просил, ничего об этом не узнав. Судить надо то, что прислали, а сужать — уже проверенное (после этой строки значение лежит в [0..1000] и в int32 помещается by construction).
Порядок тоже её предмет. Вопрос «правильно ли составлен запрос» имеет ОДИН ответ для всех вызывающих, поэтому отвечать на него надо раньше, чем на вопрос «что этому вызывающему видно» (api-conventions.md: формат → authz → repo). Иначе один и тот же мусорный курсор даёт InvalidArgument тому, у кого грант есть, и отказ либо пустую страницу тому, у кого его нет.
Текст отказа — платформенный (`validate.PageSize`), тот же, что у vpc/geo/storage и что записан в требованиях к продукту: "page_size must be in [0..1000]".
func ValidateRawVisiblePagination ¶
ValidateRawVisiblePagination is ValidateVisiblePagination over the RAW request values — the transport's int64 page_size before it is narrowed to int32. Narrowing saturates, and saturation is not validation: −1 becomes 0, and 0 means "apply the default", so a caller would receive a page it did not ask for and never learn of it.
func ValidateResourceID ¶
ValidateResourceID проверяет соответствие id формату `<prefix><17-char-tail>` (общая для всех IAM-ресурсов длина — `domain.ShortIDLen`).
СТРОГОСТЬ ЗДЕСЬ — РЕШЕНИЕ, А НЕ УПУЩЕНИЕ, и она отличает эту проверку от платформенного маршрутизатора `corevalidate.ResourceID` по ТРЁМ осям, а не по одной: пустая строка (здесь отвергается, там проходит), чужой тип с известным префиксом (здесь отвергается, там проходит в полосу отсутствия) и длина тела (здесь фиксирована, там не проверяется).
Поэтому «привести к конвенции» заменой вызова — ослабление по трём осям сразу; ось пустой строки при этом завела бы отказ с вырезанным id (`"Account not found"`) во все места разом. Расхождение с `api-conventions.md` §«By-lane code-split», довод, три границы и ВНЕШНИЙ предикат пересмотра записаны решением: `docs/engineering/architecture/known-divergences.md`, §19.
ЗВАТЬ ТОЛЬКО ДЛЯ СВОЕГО ИДЕНТИФИКАТОРА. Тип чужого решает его владелец; строгая сверка префикса на чужой ссылке — нарушение конвенции, и она стережётся гейтом `TestStrictIDFormatCheckStaysOwnerScoped` (ids_owner_scope_test.go): префикс обязан быть константой собственного пакета `domain`.
На несоответствии возвращает InvalidArgument с сообщением в каноническом Kachō-формате: `"invalid <resource-name> id '<id>'"`. resourceName — для error-сообщения (например "account", "service account", "access binding"; **именно** в той форме, в какой Kachō показывает ошибку — с пробелами, не camelCase).
func ValidateUpdateMask ¶
func ValidateUpdateMask(mask []string, mutable map[string]struct{}, immutable map[string]string) error
ValidateUpdateMask проверяет, что каждое поле в mask:
- НЕ присутствует в `immutable` map (если присутствует → InvalidArg с per-field error message — по контракту error-format Kachō), И
- присутствует в `mutable` set (если нет — Illegal-argument update_mask field — по контракту error-format Kachō).
Пустая mask проходит без ошибок (full-PATCH semantics).
`immutable` value-string — сообщение для конкретного field (`"<field> is immutable after <Resource>.Create"` — см. per-resource maps).
func ValidateVisiblePagination ¶
ValidateVisiblePagination — the whole page-format question for a visible-page list, answered before anything about the caller is decided.
Types ¶
type CreatedByLane ¶
type CreatedByLane struct {
// Principal — идентификатор аутентифицированного вызывающего.
Principal string
// CallerIsServiceAccount — вызывающий есть машина (служебная учётка). Её
// идентификатор строкой users(id) не является, поэтому ответственным она
// быть не может ни на одной полосе.
CallerIsServiceAccount bool
// MachineLaneKnowsRecord — знает ли КРАЙ, какого ответственного полоса
// запишет вызывающему-машине. false → присланное сверить нечем.
MachineLaneKnowsRecord bool
// MachineLaneRecords — то самое значение. Читается только при
// MachineLaneKnowsRecord.
MachineLaneRecords string
// MachineLaneRecordSource — как ответственный называется в отказе, который
// читает вызывающий. Часть контракта сообщений.
MachineLaneRecordSource string
}
CreatedByLane описывает, что полоса выдачи запишет в `created_by_user_id`.
Знание значения и само значение — РАЗНЫЕ поля намеренно: пустая строка в MachineLaneRecords означала бы одновременно «записывается пустое» и «край не знает», а это два разных состояния, и правило на них отвечает по-разному.
func CreatedByLaneForSAKey ¶
func CreatedByLaneForSAKey(principal string, callerIsServiceAccount bool) CreatedByLane
CreatedByLaneForSAKey — полоса ключа служебной учётки: вызывающей машине ответственным записывается владелец аккаунта ЦЕЛЕВОЙ учётки, и край его не знает — резолв идёт в use-case, из репозитория.
func CreatedByLaneForUserToken ¶
func CreatedByLaneForUserToken(principal string, callerIsServiceAccount bool, targetUserID string) CreatedByLane
CreatedByLaneForUserToken — полоса персонального токена: вызывающей машине ответственным записывается ЦЕЛЕВОЙ пользователь, и он же назван запросом.
func (CreatedByLane) ValidateRequested ¶
func (l CreatedByLane) ValidateRequested(requested string) error
ValidateRequested судит присланного ответственного. nil — вход законен; InvalidArgument с именем поля — сервис его не запишет.
Тексты отказов — часть контракта и здесь дословно те, что полосы произносили до сведения: сведение правила не есть повод переписать сообщения.
type ListOperationsUseCase ¶
type ListOperationsUseCase struct {
// contains filtered or unexported fields
}
ListOperationsUseCase lists the operations recorded for a single resource id.
func NewListOperationsUseCase ¶
func NewListOperationsUseCase(opsRepo operations.Repo) *ListOperationsUseCase
NewListOperationsUseCase wires the use-case to the operations repo.
func (*ListOperationsUseCase) Execute ¶
func (u *ListOperationsUseCase) Execute(ctx context.Context, resourceID string, pageSize int64, pageToken string) ([]operations.Operation, string, error)
Execute returns the resource's operations (cursor-paginated) and the next_page_token. Failures are classified by MapOperationsListErr.
type ListScan ¶
ListScan — накопитель стоимости одной страницы. Считает по мере догрузок и отдаётся регистратору один раз, на выходе.
type ListScanRecorder ¶
type ListScanRecorder interface {
ObserveListScan(ctx context.Context, resource string, rows, checks int)
}
ListScanRecorder принимает стоимость одной отданной страницы.
resource — вид ресурса («account», «project», …), чтобы дорогая поверхность была отличима от дешёвой; rows — сколько строк рассмотрено всеми догрузками вместе; checks — сколько раз спрошена модель прав.
type NoopListScanRecorder ¶
type NoopListScanRecorder struct{}
NoopListScanRecorder — умолчание для мест, где наблюдение не провязано (пробы, инструменты). Именованный тип, а не nil-проверка у каждого вызывающего: «не провязано» обязано быть решением, а не забытой ветвью.
func (NoopListScanRecorder) ObserveListScan ¶
ObserveListScan ничего не делает.
type VisibleCursor ¶
VisibleCursor — the boundary of a visible page: the last row RETURNED, in keyset order.
func DecodeVisiblePageToken ¶
func DecodeVisiblePageToken(field, token string) (*VisibleCursor, error)
DecodeVisiblePageToken parses a token of this form. An empty token is the first page and yields (nil, nil) — absence is representable apart from any value. Anything else that is not this form is INVALID_ARGUMENT with the contract tone, including a token of the previous form.