Documentation
¶
Overview ¶
Package notifymodel is the Go over rig's own notification tables: the rows, the write inputs, and the enums their columns are.
It is here rather than generated into each application because the schema is rig's. The five tables come from notify/foundation, a project cannot change them, and the Go over them was the same five thousand lines copied into every repository that turned notifications on. What a project still generates for them is what its own schema reaches into: the filter grammar, which carries a member per table of its own that points at rig_notification, and the repository that renders those subqueries.
An application does not usually import this package. `model.Notification` in a generated project is an alias for Notification, so a service signature, a hook and a repository are all talking about this struct without naming it.
**The JSON keys are camelCase and do not follow `naming.json_case`.** Struct tags are fixed at compile time and this package is compiled once, so a project that asked for snake_case gets it on its own tables and camelCase on rig's. That is the trade rig already made for its hand-written routes — the authentication endpoints have answered camelCase in a snake_case project since they existed — and `rig check` says so rather than leaving it to be discovered.
Index ¶
- Constants
- Variables
- type Notification
- type NotificationChannel
- type NotificationCreateInput
- type NotificationCreateInputError
- type NotificationCreateValidator
- type NotificationDeleteInput
- type NotificationDelivery
- type NotificationDeliveryCreateInput
- type NotificationDeliveryCreateInputError
- type NotificationDeliveryCreateValidator
- type NotificationDeliveryDeleteInput
- type NotificationDeliveryState
- type NotificationDeliveryUpdateInput
- type NotificationDeliveryUpdateInputError
- type NotificationDeliveryUpdateValidator
- type NotificationDeliveryValidatorContext
- func (c *NotificationDeliveryValidatorContext) AccountIDChanged() bool
- func (c *NotificationDeliveryValidatorContext) AttemptsChanged() bool
- func (c *NotificationDeliveryValidatorContext) Changed(column string) bool
- func (c *NotificationDeliveryValidatorContext) ChannelChanged() bool
- func (c *NotificationDeliveryValidatorContext) ClaimedAtChanged() bool
- func (c *NotificationDeliveryValidatorContext) ClaimedByChanged() bool
- func (c *NotificationDeliveryValidatorContext) DeliverAtChanged() bool
- func (c *NotificationDeliveryValidatorContext) DigestChanged() bool
- func (c *NotificationDeliveryValidatorContext) FailedReasonChanged() bool
- func (c *NotificationDeliveryValidatorContext) IsUpdate() bool
- func (c *NotificationDeliveryValidatorContext) KindChanged() bool
- func (c *NotificationDeliveryValidatorContext) Previous() NotificationDelivery
- func (c *NotificationDeliveryValidatorContext) RecipientIDChanged() bool
- func (c *NotificationDeliveryValidatorContext) SentAtChanged() bool
- func (c *NotificationDeliveryValidatorContext) StateChanged() bool
- type NotificationDevice
- type NotificationDeviceCreateInput
- type NotificationDeviceCreateInputError
- type NotificationDeviceCreateValidator
- type NotificationDeviceDeleteInput
- type NotificationDeviceUpdateInput
- type NotificationDeviceUpdateInputError
- type NotificationDeviceUpdateValidator
- type NotificationDeviceValidatorContext
- func (c *NotificationDeviceValidatorContext) AccountIDChanged() bool
- func (c *NotificationDeviceValidatorContext) Changed(column string) bool
- func (c *NotificationDeviceValidatorContext) ChannelChanged() bool
- func (c *NotificationDeviceValidatorContext) IsUpdate() bool
- func (c *NotificationDeviceValidatorContext) LabelChanged() bool
- func (c *NotificationDeviceValidatorContext) LastSeenAtChanged() bool
- func (c *NotificationDeviceValidatorContext) Previous() NotificationDevice
- func (c *NotificationDeviceValidatorContext) RevokedAtChanged() bool
- func (c *NotificationDeviceValidatorContext) TokenChanged() bool
- type NotificationDigest
- type NotificationRecipient
- type NotificationRecipientCreateInput
- type NotificationRecipientCreateInputError
- type NotificationRecipientCreateValidator
- type NotificationRecipientDeleteInput
- type NotificationRecipientUpdateInput
- type NotificationRecipientUpdateInputError
- type NotificationRecipientUpdateValidator
- type NotificationRecipientValidatorContext
- func (c *NotificationRecipientValidatorContext) AccountIDChanged() bool
- func (c *NotificationRecipientValidatorContext) Changed(column string) bool
- func (c *NotificationRecipientValidatorContext) EventCountChanged() bool
- func (c *NotificationRecipientValidatorContext) GroupKeyChanged() bool
- func (c *NotificationRecipientValidatorContext) IsUpdate() bool
- func (c *NotificationRecipientValidatorContext) KindChanged() bool
- func (c *NotificationRecipientValidatorContext) NotificationIDChanged() bool
- func (c *NotificationRecipientValidatorContext) Previous() NotificationRecipient
- func (c *NotificationRecipientValidatorContext) ReadAtChanged() bool
- type NotificationSetting
- type NotificationSettingCreateInput
- type NotificationSettingCreateInputError
- type NotificationSettingCreateValidator
- type NotificationSettingDeleteInput
- type NotificationSettingUpdateInput
- type NotificationSettingUpdateInputError
- type NotificationSettingUpdateValidator
- type NotificationSettingValidatorContext
- func (c *NotificationSettingValidatorContext) AccountIDChanged() bool
- func (c *NotificationSettingValidatorContext) ActiveDaysChanged() bool
- func (c *NotificationSettingValidatorContext) ActiveFromChanged() bool
- func (c *NotificationSettingValidatorContext) ActiveUntilChanged() bool
- func (c *NotificationSettingValidatorContext) Changed(column string) bool
- func (c *NotificationSettingValidatorContext) ChannelChanged() bool
- func (c *NotificationSettingValidatorContext) DigestChanged() bool
- func (c *NotificationSettingValidatorContext) IsEnabledChanged() bool
- func (c *NotificationSettingValidatorContext) IsUpdate() bool
- func (c *NotificationSettingValidatorContext) KindChanged() bool
- func (c *NotificationSettingValidatorContext) Previous() NotificationSetting
- type NotificationState
- type NotificationUpdateInput
- type NotificationUpdateInputError
- type NotificationUpdateValidator
- type NotificationValidatorContext
- func (c *NotificationValidatorContext) AccountIdsChanged() bool
- func (c *NotificationValidatorContext) AttemptsChanged() bool
- func (c *NotificationValidatorContext) Changed(column string) bool
- func (c *NotificationValidatorContext) ClaimedAtChanged() bool
- func (c *NotificationValidatorContext) ClaimedByChanged() bool
- func (c *NotificationValidatorContext) DeliverAtChanged() bool
- func (c *NotificationValidatorContext) GroupKeyChanged() bool
- func (c *NotificationValidatorContext) IsUpdate() bool
- func (c *NotificationValidatorContext) KindChanged() bool
- func (c *NotificationValidatorContext) PayloadChanged() bool
- func (c *NotificationValidatorContext) Previous() Notification
- func (c *NotificationValidatorContext) ResolvedAtChanged() bool
- func (c *NotificationValidatorContext) StateChanged() bool
Constants ¶
const ( ColumnNotificationDeliveryID = "id" ColumnNotificationDeliveryTenantID = "tenant_id" ColumnNotificationDeliveryRecipientID = "recipient_id" ColumnNotificationDeliveryAccountID = "account_id" ColumnNotificationDeliveryCreatedAt = "created_at" ColumnNotificationDeliveryUpdatedAt = "updated_at" ColumnNotificationDeliveryChannel = "channel" ColumnNotificationDeliveryKind = "kind" ColumnNotificationDeliveryDigest = "digest" ColumnNotificationDeliveryState = "state" ColumnNotificationDeliveryDeliverAt = "deliver_at" ColumnNotificationDeliverySentAt = "sent_at" ColumnNotificationDeliveryFailedReason = "failed_reason" ColumnNotificationDeliveryAttempts = "attempts" ColumnNotificationDeliveryClaimedAt = "claimed_at" ColumnNotificationDeliveryClaimedBy = "claimed_by" )
Column names for rig_notification_delivery, so nothing has to spell one out.
const ( ColumnNotificationDeviceID = "id" ColumnNotificationDeviceTenantID = "tenant_id" ColumnNotificationDeviceAccountID = "account_id" ColumnNotificationDeviceCreatedAt = "created_at" ColumnNotificationDeviceUpdatedAt = "updated_at" ColumnNotificationDeviceChannel = "channel" ColumnNotificationDeviceToken = "token" ColumnNotificationDeviceLabel = "label" ColumnNotificationDeviceLastSeenAt = "last_seen_at" ColumnNotificationDeviceRevokedAt = "revoked_at" )
Column names for rig_notification_device, so nothing has to spell one out.
const ( ColumnNotificationID = "id" ColumnNotificationTenantID = "tenant_id" ColumnNotificationCreatedAt = "created_at" ColumnNotificationCreatedByAccountID = "created_by_account_id" ColumnNotificationCreatedByAPIKeyID = "created_by_api_key_id" ColumnNotificationUpdatedAt = "updated_at" ColumnNotificationKind = "kind" ColumnNotificationState = "state" ColumnNotificationDeliverAt = "deliver_at" ColumnNotificationResolvedAt = "resolved_at" ColumnNotificationPayload = "payload" ColumnNotificationGroupKey = "group_key" ColumnNotificationAccountIds = "account_ids" ColumnNotificationClaimedAt = "claimed_at" ColumnNotificationClaimedBy = "claimed_by" ColumnNotificationAttempts = "attempts" )
Column names for rig_notification, so nothing has to spell one out.
const ( ColumnNotificationRecipientID = "id" ColumnNotificationRecipientTenantID = "tenant_id" ColumnNotificationRecipientNotificationID = "notification_id" ColumnNotificationRecipientAccountID = "account_id" ColumnNotificationRecipientCreatedAt = "created_at" ColumnNotificationRecipientUpdatedAt = "updated_at" ColumnNotificationRecipientDeletedAt = "deleted_at" ColumnNotificationRecipientDeletedByAccountID = "deleted_by_account_id" ColumnNotificationRecipientKind = "kind" ColumnNotificationRecipientGroupKey = "group_key" ColumnNotificationRecipientEventCount = "event_count" ColumnNotificationRecipientReadAt = "read_at" )
Column names for rig_notification_recipient, so nothing has to spell one out.
const ( ColumnNotificationSettingID = "id" ColumnNotificationSettingTenantID = "tenant_id" ColumnNotificationSettingAccountID = "account_id" ColumnNotificationSettingCreatedAt = "created_at" ColumnNotificationSettingUpdatedAt = "updated_at" ColumnNotificationSettingKind = "kind" ColumnNotificationSettingChannel = "channel" ColumnNotificationSettingIsEnabled = "is_enabled" ColumnNotificationSettingDigest = "digest" ColumnNotificationSettingActiveFrom = "active_from" ColumnNotificationSettingActiveUntil = "active_until" ColumnNotificationSettingActiveDays = "active_days" )
Column names for rig_notification_setting, so nothing has to spell one out.
const TableNotification = "rig_notification"
TableNotification is the table this entity is stored in.
const TableNotificationDelivery = "rig_notification_delivery"
TableNotificationDelivery is the table this entity is stored in.
const TableNotificationDevice = "rig_notification_device"
TableNotificationDevice is the table this entity is stored in.
const TableNotificationRecipient = "rig_notification_recipient"
TableNotificationRecipient is the table this entity is stored in.
const TableNotificationSetting = "rig_notification_setting"
TableNotificationSetting is the table this entity is stored in.
Variables ¶
var AllNotificationChannel = []NotificationChannel{NotificationChannelDesktop, NotificationChannelMobile, NotificationChannelEmail}
AllNotificationChannel is every value, in declaration order.
var AllNotificationDeliveryState = []NotificationDeliveryState{NotificationDeliveryStatePending, NotificationDeliveryStateSent, NotificationDeliveryStateFailed, NotificationDeliveryStateSkipped}
AllNotificationDeliveryState is every value, in declaration order.
var AllNotificationDigest = []NotificationDigest{NotificationDigestImmediate, NotificationDigestHourly, NotificationDigestDaily, NotificationDigestWeekly, NotificationDigestOff}
AllNotificationDigest is every value, in declaration order.
var AllNotificationState = []NotificationState{NotificationStatePending, NotificationStateResolved, NotificationStateCancelled}
AllNotificationState is every value, in declaration order.
var NotificationColumns = []string{"id", "tenant_id", "created_at", "created_by_account_id", "created_by_api_key_id", "updated_at", "kind", "state", "deliver_at", "resolved_at", "payload", "group_key", "account_ids", "claimed_at", "claimed_by", "attempts"}
NotificationColumns is every column, in the order the row is scanned.
var NotificationDeliveryColumns = []string{"id", "tenant_id", "recipient_id", "account_id", "created_at", "updated_at", "channel", "kind", "digest", "state", "deliver_at", "sent_at", "failed_reason", "attempts", "claimed_at", "claimed_by"}
NotificationDeliveryColumns is every column, in the order the row is scanned.
var NotificationDeviceColumns = []string{"id", "tenant_id", "account_id", "created_at", "updated_at", "channel", "token", "label", "last_seen_at", "revoked_at"}
NotificationDeviceColumns is every column, in the order the row is scanned.
var NotificationRecipientColumns = []string{"id", "tenant_id", "notification_id", "account_id", "created_at", "updated_at", "deleted_at", "deleted_by_account_id", "kind", "group_key", "event_count", "read_at"}
NotificationRecipientColumns is every column, in the order the row is scanned.
var NotificationSettingColumns = []string{"id", "tenant_id", "account_id", "created_at", "updated_at", "kind", "channel", "is_enabled", "digest", "active_from", "active_until", "active_days"}
NotificationSettingColumns is every column, in the order the row is scanned.
Functions ¶
This section is empty.
Types ¶
type Notification ¶
type Notification struct {
// Unique identifier for this row.
ID uuid.UUID `json:"id"`
// Tenant this row belongs to. Every query is scoped by it.
TenantID uuid.UUID `json:"tenantId"`
// When this row was created. Set automatically and never changes.
CreatedAt time.Time `json:"createdAt"`
// Account that created this row, taken from the request's claims.
CreatedByAccountID *uuid.UUID `json:"createdByAccountId,omitempty"`
// API key this row was created through, when it was an integration rather than
// a person.
CreatedByAPIKeyID *uuid.UUID `json:"createdByApiKeyId,omitempty"`
// When this row was last updated. Set automatically on every update.
UpdatedAt *time.Time `json:"updatedAt,omitempty"`
// What happened, as the application names it. Narrow it to an enum of your own
// to get a switch the compiler can see.
Kind string `json:"kind"`
// Resolved means the audience was determined and the inbox lines exist. It
// does not mean anything was sent.
State NotificationState `json:"state"`
// When this is due. now() is the ordinary case; a scheduled notification is
// the same row with a later time, which is the only difference between the
// two.
DeliverAt time.Time `json:"deliverAt"`
// When the audience was computed. Null while the notification is still pending
// or was cancelled.
ResolvedAt *time.Time `json:"resolvedAt,omitempty"`
// What a template needs beyond the linked row. Give it a Go type with the
// go_type key if it has a shape.
Payload json.RawMessage `json:"payload"`
// What collapses several of these into one inbox line, decided when the
// announcement was written and copied onto the line when the audience is
// resolved.
GroupKey *string `json:"groupKey,omitempty"`
// A recipient list captured at write time, and the one exception to computing
// the audience late. Null is the ordinary case; a list here skips the question
// entirely, for an audience that genuinely cannot be re-derived.
AccountIds []uuid.UUID `json:"accountIds,omitempty"`
// When a dispatcher took this to resolve. Past notifications.claim_ttl another
// may, which is what makes a crashed process recoverable.
ClaimedAt *time.Time `json:"claimedAt,omitempty"`
// Which process holds the lease, so a stuck one traces to a pod rather than to
// a mystery.
ClaimedBy *uuid.UUID `json:"claimedBy,omitempty"`
// How many times resolving this has been attempted.
Attempts int `json:"attempts"`
}
Something worth telling somebody about, and when it is due. Carries no recipients: the audience is computed when it is sent.
It is the one definition of a Notification: the repository scans into it and the API returns it, so there is no conversion between two shapes of the same thing and no field that can go missing from one of them.
type NotificationChannel ¶
type NotificationChannel string
Where a copy of an inbox line can be sent. The inbox itself is not one of them.
const ( // A browser or a desktop application, addressed by a device row. NotificationChannelDesktop NotificationChannel = "Desktop" // A phone, addressed by a device row. NotificationChannelMobile NotificationChannel = "Mobile" // The address on the account. No device row: there is nothing to register, // because the address is on the account already. NotificationChannelEmail NotificationChannel = "Email" )
The values of NotificationChannel.
func ParseNotificationChannel ¶
func ParseNotificationChannel(s string) (NotificationChannel, bool)
ParseNotificationChannel reads a value, accepting any casing and surrounding space.
Normalization uses it, so "IN_PROGRESS" from one client and "in_progress" from another mean the same thing rather than one of them being a validation failure nobody can explain. The spelling still has to be the label’s: a value is a name the database knows, not a phrase.
func (NotificationChannel) String ¶
func (v NotificationChannel) String() string
String implements fmt.Stringer.
func (NotificationChannel) Valid ¶
func (v NotificationChannel) Valid() bool
Valid reports whether the value is one the database will accept.
type NotificationCreateInput ¶
type NotificationCreateInput struct {
// What happened, as the application names it. Narrow it to an enum of your own
// to get a switch the compiler can see.
Kind string `json:"kind"`
// Resolved means the audience was determined and the inbox lines exist. It
// does not mean anything was sent.
State NotificationState `json:"state"`
// When this is due. now() is the ordinary case; a scheduled notification is
// the same row with a later time, which is the only difference between the
// two.
DeliverAt time.Time `json:"deliverAt"`
// When the audience was computed. Null while the notification is still pending
// or was cancelled.
ResolvedAt *time.Time `json:"resolvedAt"`
// What a template needs beyond the linked row. Give it a Go type with the
// go_type key if it has a shape.
Payload json.RawMessage `json:"payload"`
// What collapses several of these into one inbox line, decided when the
// announcement was written and copied onto the line when the audience is
// resolved.
GroupKey *string `json:"groupKey"`
// A recipient list captured at write time, and the one exception to computing
// the audience late. Null is the ordinary case; a list here skips the question
// entirely, for an audience that genuinely cannot be re-derived.
AccountIds []uuid.UUID `json:"accountIds"`
// When a dispatcher took this to resolve. Past notifications.claim_ttl another
// may, which is what makes a crashed process recoverable.
ClaimedAt *time.Time `json:"claimedAt"`
// Which process holds the lease, so a stuck one traces to a pod rather than to
// a mystery.
ClaimedBy *uuid.UUID `json:"claimedBy"`
// How many times resolving this has been attempted.
Attempts int `json:"attempts"`
}
NotificationCreateInput is what creating a Notification takes.
The identifier, the tenant, and the audit columns are absent: those are stamped by the repository from the request's claims.
func (*NotificationCreateInput) Normalize ¶
func (i *NotificationCreateInput) Normalize()
Normalize tidies what was given before anything checks it.
It runs first so that validation sees the value that will actually be stored: a title with a trailing space and one without are the same title, and rejecting the second for a length rule the first passes would be indefensible.
func (*NotificationCreateInput) Validate ¶
func (i *NotificationCreateInput) Validate() error
Validate checks what the schema can decide on its own.
Everything a column declares — NOT NULL, a length, an enumeration's values — is checked here, so a service only writes the rules that are actually about the business. Every field is checked before returning, because a form that reports one problem per round trip is a form people give up on.
What comes back is a *NotificationCreateInputError, shaped like the input itself.
type NotificationCreateInputError ¶
type NotificationCreateInputError struct {
// What happened, as the application names it. Narrow it to an enum of your own
// to get a switch the compiler can see.
Kind *rigerr.FieldError `json:"kind,omitempty"`
// Resolved means the audience was determined and the inbox lines exist. It
// does not mean anything was sent.
State *rigerr.FieldError `json:"state,omitempty"`
// When this is due. now() is the ordinary case; a scheduled notification is
// the same row with a later time, which is the only difference between the
// two.
DeliverAt *rigerr.FieldError `json:"deliverAt,omitempty"`
// When the audience was computed. Null while the notification is still pending
// or was cancelled.
ResolvedAt *rigerr.FieldError `json:"resolvedAt,omitempty"`
// What a template needs beyond the linked row. Give it a Go type with the
// go_type key if it has a shape.
Payload *rigerr.FieldError `json:"payload,omitempty"`
// What collapses several of these into one inbox line, decided when the
// announcement was written and copied onto the line when the audience is
// resolved.
GroupKey *rigerr.FieldError `json:"groupKey,omitempty"`
// A recipient list captured at write time, and the one exception to computing
// the audience late. Null is the ordinary case; a list here skips the question
// entirely, for an audience that genuinely cannot be re-derived.
AccountIds *rigerr.FieldError `json:"accountIds,omitempty"`
// When a dispatcher took this to resolve. Past notifications.claim_ttl another
// may, which is what makes a crashed process recoverable.
ClaimedAt *rigerr.FieldError `json:"claimedAt,omitempty"`
// Which process holds the lease, so a stuck one traces to a pod rather than to
// a mystery.
ClaimedBy *rigerr.FieldError `json:"claimedBy,omitempty"`
// How many times resolving this has been attempted.
Attempts *rigerr.FieldError `json:"attempts,omitempty"`
// Entity is a problem with the row as a whole rather than with one field: what
// the Entity rule said.
Entity *rigerr.FieldError `json:"entity,omitempty"`
}
NotificationCreateInputError says what was wrong with each field of a NotificationCreateInput.
Its shape is the input's shape, so a client can attach every message to the field it is about without matching on strings. A member is nil when that field was fine, and the whole value is nil when the input was. It is what the 422 carries.
func (*NotificationCreateInputError) Empty ¶
func (e *NotificationCreateInputError) Empty() bool
Empty reports whether anything went wrong. A validator that found nothing returns nil rather than one of these.
func (*NotificationCreateInputError) Error ¶
func (e *NotificationCreateInputError) Error() string
Error implements error. The sentence is for logs and for a person; the structure above is what a client acts on.
func (*NotificationCreateInputError) ErrorCode ¶
func (e *NotificationCreateInputError) ErrorCode() rigerr.Code
ErrorCode implements rigerr.Coder: the request was understood and its content is what is wrong, which is 422 and not 400.
func (*NotificationCreateInputError) ErrorFields ¶
func (e *NotificationCreateInputError) ErrorFields() any
ErrorFields implements rigerr.FieldReporter, which is how the HTTP layer finds this and answers with it rather than with prose.
type NotificationCreateValidator ¶
type NotificationCreateValidator struct {
// What happened, as the application names it. Narrow it to an enum of your own
// to get a switch the compiler can see.
Kind func(ctx context.Context, c *NotificationValidatorContext, value string) error
// Resolved means the audience was determined and the inbox lines exist. It
// does not mean anything was sent.
State func(ctx context.Context, c *NotificationValidatorContext, value NotificationState) error
// When this is due. now() is the ordinary case; a scheduled notification is
// the same row with a later time, which is the only difference between the
// two.
DeliverAt func(ctx context.Context, c *NotificationValidatorContext, value time.Time) error
// When the audience was computed. Null while the notification is still pending
// or was cancelled.
ResolvedAt func(ctx context.Context, c *NotificationValidatorContext, value *time.Time) error
// What a template needs beyond the linked row. Give it a Go type with the
// go_type key if it has a shape.
Payload func(ctx context.Context, c *NotificationValidatorContext, value json.RawMessage) error
// What collapses several of these into one inbox line, decided when the
// announcement was written and copied onto the line when the audience is
// resolved.
GroupKey func(ctx context.Context, c *NotificationValidatorContext, value *string) error
// A recipient list captured at write time, and the one exception to computing
// the audience late. Null is the ordinary case; a list here skips the question
// entirely, for an audience that genuinely cannot be re-derived.
AccountIds func(ctx context.Context, c *NotificationValidatorContext, value []uuid.UUID) error
// When a dispatcher took this to resolve. Past notifications.claim_ttl another
// may, which is what makes a crashed process recoverable.
ClaimedAt func(ctx context.Context, c *NotificationValidatorContext, value *time.Time) error
// Which process holds the lease, so a stuck one traces to a pod rather than to
// a mystery.
ClaimedBy func(ctx context.Context, c *NotificationValidatorContext, value *uuid.UUID) error
// How many times resolving this has been attempted.
Attempts func(ctx context.Context, c *NotificationValidatorContext, value int) error
// Entity runs after the per-field hooks, for a rule that is about the row
// rather than about one column.
Entity func(ctx context.Context, c *NotificationValidatorContext) error
}
NotificationCreateValidator is the rules for bringing a Notification into existence: what the schema cannot express.
One optional function per field this operation can set, so the set of fields is the set of rules that could apply — a column an update cannot touch has no hook here to write by mistake. A nil one is skipped. Every configured hook runs, because a rule that fails should not hide the next one, so a request reports everything wrong with it.
A hook returns a FieldError to attach the message to a specific field, or any other error to fail the request outright.
func (NotificationCreateValidator) RunCreate ¶
func (v NotificationCreateValidator) RunCreate(ctx context.Context, claims tenancy.Claims, i *NotificationCreateInput) error
RunCreate implements [dbhook.CreateValidator]: it runs the service's rules against the row this input would produce.
The generated checks are not repeated here. The repository runs Normalize and Validate first, so by the time a hook sees the input it is tidy and the schema is satisfied — which is what lets a hook be about the business rather than about NOT NULL.
type NotificationDeleteInput ¶
NotificationDeleteInput is what deleting a Notification takes.
type NotificationDelivery ¶
type NotificationDelivery struct {
// Unique identifier for this row.
ID uuid.UUID `json:"id"`
// Tenant this row belongs to. Every query is scoped by it.
TenantID uuid.UUID `json:"tenantId"`
// The inbox line this is a copy of. The line is the truth and this is one way
// it was repeated.
RecipientID uuid.UUID `json:"recipientId"`
// Who it is for, denormalized off the line so a claim can group by it without
// a join.
AccountID uuid.UUID `json:"accountId"`
// When this row was created. Set automatically and never changes.
CreatedAt time.Time `json:"createdAt"`
// When this row was last updated. Set automatically on every update.
UpdatedAt *time.Time `json:"updatedAt,omitempty"`
// Where it is going. One row per line per channel, which is what the unique
// index below enforces.
Channel NotificationChannel `json:"channel"`
// Copied from the notification, so the settings resolution needs no join
// either.
Kind string `json:"kind"`
// What the setting said when this row was written. Immediate rows are sent on
// their own; the rest are batched per account and channel.
Digest NotificationDigest `json:"digest"`
// Skipped means a setting refused it, which is different from Failed and worth
// telling apart in a report.
State NotificationDeliveryState `json:"state"`
// When this is due. A delivery held outside somebody hours moves to the next
// opening; a digested one moves to its window close.
DeliverAt time.Time `json:"deliverAt"`
// When a channel accepted it, which is not the same as it arriving.
SentAt *time.Time `json:"sentAt,omitempty"`
// What the channel said last time. Kept on a retry as well as on a failure, so
// a pattern is visible before the cap is reached.
FailedReason *string `json:"failedReason,omitempty"`
// How many times this has been claimed. Past notifications.max_attempts it is
// Failed and stops being claimed.
Attempts int `json:"attempts"`
// When a dispatcher took it. Past notifications.claim_ttl another one may,
// which is what makes a crashed process recoverable.
ClaimedAt *time.Time `json:"claimedAt,omitempty"`
// Which process holds the lease. A uuid generated once per process, with the
// hostname beside it in the log line, so a stuck lease traces to a pod rather
// than to a mystery.
ClaimedBy *uuid.UUID `json:"claimedBy,omitempty"`
}
One copy of an inbox line on its way to a channel. Claimed by lease, sent outside any transaction, and marked afterwards.
It is the one definition of a NotificationDelivery: the repository scans into it and the API returns it, so there is no conversion between two shapes of the same thing and no field that can go missing from one of them.
type NotificationDeliveryCreateInput ¶
type NotificationDeliveryCreateInput struct {
// The inbox line this is a copy of. The line is the truth and this is one way
// it was repeated.
RecipientID uuid.UUID `json:"recipientId"`
// Who it is for, denormalized off the line so a claim can group by it without
// a join.
AccountID uuid.UUID `json:"accountId"`
// Where it is going. One row per line per channel, which is what the unique
// index below enforces.
Channel NotificationChannel `json:"channel"`
// Copied from the notification, so the settings resolution needs no join
// either.
Kind string `json:"kind"`
// What the setting said when this row was written. Immediate rows are sent on
// their own; the rest are batched per account and channel.
Digest NotificationDigest `json:"digest"`
// Skipped means a setting refused it, which is different from Failed and worth
// telling apart in a report.
State NotificationDeliveryState `json:"state"`
// When this is due. A delivery held outside somebody hours moves to the next
// opening; a digested one moves to its window close.
DeliverAt time.Time `json:"deliverAt"`
// When a channel accepted it, which is not the same as it arriving.
SentAt *time.Time `json:"sentAt"`
// What the channel said last time. Kept on a retry as well as on a failure, so
// a pattern is visible before the cap is reached.
FailedReason *string `json:"failedReason"`
// How many times this has been claimed. Past notifications.max_attempts it is
// Failed and stops being claimed.
Attempts int `json:"attempts"`
// When a dispatcher took it. Past notifications.claim_ttl another one may,
// which is what makes a crashed process recoverable.
ClaimedAt *time.Time `json:"claimedAt"`
// Which process holds the lease. A uuid generated once per process, with the
// hostname beside it in the log line, so a stuck lease traces to a pod rather
// than to a mystery.
ClaimedBy *uuid.UUID `json:"claimedBy"`
}
NotificationDeliveryCreateInput is what creating a NotificationDelivery takes.
The identifier, the tenant, and the audit columns are absent: those are stamped by the repository from the request's claims.
func (*NotificationDeliveryCreateInput) Normalize ¶
func (i *NotificationDeliveryCreateInput) Normalize()
Normalize tidies what was given before anything checks it.
It runs first so that validation sees the value that will actually be stored: a title with a trailing space and one without are the same title, and rejecting the second for a length rule the first passes would be indefensible.
func (*NotificationDeliveryCreateInput) Validate ¶
func (i *NotificationDeliveryCreateInput) Validate() error
Validate checks what the schema can decide on its own.
Everything a column declares — NOT NULL, a length, an enumeration's values — is checked here, so a service only writes the rules that are actually about the business. Every field is checked before returning, because a form that reports one problem per round trip is a form people give up on.
What comes back is a *NotificationDeliveryCreateInputError, shaped like the input itself.
type NotificationDeliveryCreateInputError ¶
type NotificationDeliveryCreateInputError struct {
// The inbox line this is a copy of. The line is the truth and this is one way
// it was repeated.
RecipientID *rigerr.FieldError `json:"recipientId,omitempty"`
// Who it is for, denormalized off the line so a claim can group by it without
// a join.
AccountID *rigerr.FieldError `json:"accountId,omitempty"`
// Where it is going. One row per line per channel, which is what the unique
// index below enforces.
Channel *rigerr.FieldError `json:"channel,omitempty"`
// Copied from the notification, so the settings resolution needs no join
// either.
Kind *rigerr.FieldError `json:"kind,omitempty"`
// What the setting said when this row was written. Immediate rows are sent on
// their own; the rest are batched per account and channel.
Digest *rigerr.FieldError `json:"digest,omitempty"`
// Skipped means a setting refused it, which is different from Failed and worth
// telling apart in a report.
State *rigerr.FieldError `json:"state,omitempty"`
// When this is due. A delivery held outside somebody hours moves to the next
// opening; a digested one moves to its window close.
DeliverAt *rigerr.FieldError `json:"deliverAt,omitempty"`
// When a channel accepted it, which is not the same as it arriving.
SentAt *rigerr.FieldError `json:"sentAt,omitempty"`
// What the channel said last time. Kept on a retry as well as on a failure, so
// a pattern is visible before the cap is reached.
FailedReason *rigerr.FieldError `json:"failedReason,omitempty"`
// How many times this has been claimed. Past notifications.max_attempts it is
// Failed and stops being claimed.
Attempts *rigerr.FieldError `json:"attempts,omitempty"`
// When a dispatcher took it. Past notifications.claim_ttl another one may,
// which is what makes a crashed process recoverable.
ClaimedAt *rigerr.FieldError `json:"claimedAt,omitempty"`
// Which process holds the lease. A uuid generated once per process, with the
// hostname beside it in the log line, so a stuck lease traces to a pod rather
// than to a mystery.
ClaimedBy *rigerr.FieldError `json:"claimedBy,omitempty"`
// Entity is a problem with the row as a whole rather than with one field: what
// the Entity rule said.
Entity *rigerr.FieldError `json:"entity,omitempty"`
}
NotificationDeliveryCreateInputError says what was wrong with each field of a NotificationDeliveryCreateInput.
Its shape is the input's shape, so a client can attach every message to the field it is about without matching on strings. A member is nil when that field was fine, and the whole value is nil when the input was. It is what the 422 carries.
func (*NotificationDeliveryCreateInputError) Empty ¶
func (e *NotificationDeliveryCreateInputError) Empty() bool
Empty reports whether anything went wrong. A validator that found nothing returns nil rather than one of these.
func (*NotificationDeliveryCreateInputError) Error ¶
func (e *NotificationDeliveryCreateInputError) Error() string
Error implements error. The sentence is for logs and for a person; the structure above is what a client acts on.
func (*NotificationDeliveryCreateInputError) ErrorCode ¶
func (e *NotificationDeliveryCreateInputError) ErrorCode() rigerr.Code
ErrorCode implements rigerr.Coder: the request was understood and its content is what is wrong, which is 422 and not 400.
func (*NotificationDeliveryCreateInputError) ErrorFields ¶
func (e *NotificationDeliveryCreateInputError) ErrorFields() any
ErrorFields implements rigerr.FieldReporter, which is how the HTTP layer finds this and answers with it rather than with prose.
type NotificationDeliveryCreateValidator ¶
type NotificationDeliveryCreateValidator struct {
// The inbox line this is a copy of. The line is the truth and this is one way
// it was repeated.
RecipientID func(ctx context.Context, c *NotificationDeliveryValidatorContext, value uuid.UUID) error
// Who it is for, denormalized off the line so a claim can group by it without
// a join.
AccountID func(ctx context.Context, c *NotificationDeliveryValidatorContext, value uuid.UUID) error
// Where it is going. One row per line per channel, which is what the unique
// index below enforces.
Channel func(ctx context.Context, c *NotificationDeliveryValidatorContext, value NotificationChannel) error
// Copied from the notification, so the settings resolution needs no join
// either.
Kind func(ctx context.Context, c *NotificationDeliveryValidatorContext, value string) error
// What the setting said when this row was written. Immediate rows are sent on
// their own; the rest are batched per account and channel.
Digest func(ctx context.Context, c *NotificationDeliveryValidatorContext, value NotificationDigest) error
// Skipped means a setting refused it, which is different from Failed and worth
// telling apart in a report.
State func(ctx context.Context, c *NotificationDeliveryValidatorContext, value NotificationDeliveryState) error
// When this is due. A delivery held outside somebody hours moves to the next
// opening; a digested one moves to its window close.
DeliverAt func(ctx context.Context, c *NotificationDeliveryValidatorContext, value time.Time) error
// When a channel accepted it, which is not the same as it arriving.
SentAt func(ctx context.Context, c *NotificationDeliveryValidatorContext, value *time.Time) error
// What the channel said last time. Kept on a retry as well as on a failure, so
// a pattern is visible before the cap is reached.
FailedReason func(ctx context.Context, c *NotificationDeliveryValidatorContext, value *string) error
// How many times this has been claimed. Past notifications.max_attempts it is
// Failed and stops being claimed.
Attempts func(ctx context.Context, c *NotificationDeliveryValidatorContext, value int) error
// When a dispatcher took it. Past notifications.claim_ttl another one may,
// which is what makes a crashed process recoverable.
ClaimedAt func(ctx context.Context, c *NotificationDeliveryValidatorContext, value *time.Time) error
// Which process holds the lease. A uuid generated once per process, with the
// hostname beside it in the log line, so a stuck lease traces to a pod rather
// than to a mystery.
ClaimedBy func(ctx context.Context, c *NotificationDeliveryValidatorContext, value *uuid.UUID) error
// Entity runs after the per-field hooks, for a rule that is about the row
// rather than about one column.
Entity func(ctx context.Context, c *NotificationDeliveryValidatorContext) error
}
NotificationDeliveryCreateValidator is the rules for bringing a NotificationDelivery into existence: what the schema cannot express.
One optional function per field this operation can set, so the set of fields is the set of rules that could apply — a column an update cannot touch has no hook here to write by mistake. A nil one is skipped. Every configured hook runs, because a rule that fails should not hide the next one, so a request reports everything wrong with it.
A hook returns a FieldError to attach the message to a specific field, or any other error to fail the request outright.
func (NotificationDeliveryCreateValidator) RunCreate ¶
func (v NotificationDeliveryCreateValidator) RunCreate(ctx context.Context, claims tenancy.Claims, i *NotificationDeliveryCreateInput) error
RunCreate implements [dbhook.CreateValidator]: it runs the service's rules against the row this input would produce.
The generated checks are not repeated here. The repository runs Normalize and Validate first, so by the time a hook sees the input it is tidy and the schema is satisfied — which is what lets a hook be about the business rather than about NOT NULL.
type NotificationDeliveryDeleteInput ¶
type NotificationDeliveryDeleteInput struct {
// ID is the row to delete.
ID uuid.UUID `json:"id"`
}
NotificationDeliveryDeleteInput is what deleting a NotificationDelivery takes.
type NotificationDeliveryState ¶
type NotificationDeliveryState string
What happened to one copy of an inbox line on its way to a channel.
const ( // Owed and not yet claimed, or claimed by a dispatcher that has not marked it. NotificationDeliveryStatePending NotificationDeliveryState = "Pending" // A channel accepted it, which is not the same as it arriving — rig does not // pretend to know the difference. NotificationDeliveryStateSent NotificationDeliveryState = "Sent" // Past notifications.max_attempts, and no longer claimed. Without the cap a // permanently broken address would consume a lease forever. NotificationDeliveryStateFailed NotificationDeliveryState = "Failed" // A setting refused it, or the row it was a copy of was retired before it // went. Worth telling apart from Failed in a report: it is 'we decided against // this' rather than 'this did not work'. NotificationDeliveryStateSkipped NotificationDeliveryState = "Skipped" )
The values of NotificationDeliveryState.
func ParseNotificationDeliveryState ¶
func ParseNotificationDeliveryState(s string) (NotificationDeliveryState, bool)
ParseNotificationDeliveryState reads a value, accepting any casing and surrounding space.
Normalization uses it, so "IN_PROGRESS" from one client and "in_progress" from another mean the same thing rather than one of them being a validation failure nobody can explain. The spelling still has to be the label’s: a value is a name the database knows, not a phrase.
func (NotificationDeliveryState) String ¶
func (v NotificationDeliveryState) String() string
String implements fmt.Stringer.
func (NotificationDeliveryState) Valid ¶
func (v NotificationDeliveryState) Valid() bool
Valid reports whether the value is one the database will accept.
type NotificationDeliveryUpdateInput ¶
type NotificationDeliveryUpdateInput struct {
// The inbox line this is a copy of. The line is the truth and this is one way
// it was repeated.
RecipientID patch.Optional[uuid.UUID] `json:"recipientId"`
// Who it is for, denormalized off the line so a claim can group by it without
// a join.
AccountID patch.Optional[uuid.UUID] `json:"accountId"`
// Where it is going. One row per line per channel, which is what the unique
// index below enforces.
Channel patch.Optional[NotificationChannel] `json:"channel"`
// Copied from the notification, so the settings resolution needs no join
// either.
Kind patch.Optional[string] `json:"kind"`
// What the setting said when this row was written. Immediate rows are sent on
// their own; the rest are batched per account and channel.
Digest patch.Optional[NotificationDigest] `json:"digest"`
// Skipped means a setting refused it, which is different from Failed and worth
// telling apart in a report.
State patch.Optional[NotificationDeliveryState] `json:"state"`
// When this is due. A delivery held outside somebody hours moves to the next
// opening; a digested one moves to its window close.
DeliverAt patch.Optional[time.Time] `json:"deliverAt"`
// When a channel accepted it, which is not the same as it arriving.
SentAt patch.Nullable[time.Time] `json:"sentAt"`
// What the channel said last time. Kept on a retry as well as on a failure, so
// a pattern is visible before the cap is reached.
FailedReason patch.Nullable[string] `json:"failedReason"`
// How many times this has been claimed. Past notifications.max_attempts it is
// Failed and stops being claimed.
Attempts patch.Optional[int] `json:"attempts"`
// When a dispatcher took it. Past notifications.claim_ttl another one may,
// which is what makes a crashed process recoverable.
ClaimedAt patch.Nullable[time.Time] `json:"claimedAt"`
// Which process holds the lease. A uuid generated once per process, with the
// hostname beside it in the log line, so a stuck lease traces to a pod rather
// than to a mystery.
ClaimedBy patch.Nullable[uuid.UUID] `json:"claimedBy"`
}
NotificationDeliveryUpdateInput is what changing a NotificationDelivery takes.
A field left out is untouched. A nullable field set to null is cleared — which is why the two wrappers differ: a column that cannot hold null has no way to be given one, so clearing it is a compile error rather than a rejection at runtime. Immutable fields are not here at all.
func (NotificationDeliveryUpdateInput) Merged ¶
func (i NotificationDeliveryUpdateInput) Merged(prev *NotificationDelivery) NotificationDelivery
Merged is the row as it will be once this update is applied.
It is what validation runs against, and the reason it exists: a rule spanning two fields cannot be checked from a partial request. "Ends after starts" is unanswerable when only one of them was sent.
It returns a copy. The input keeps its patches, so the repository still writes only the columns that were actually given.
func (*NotificationDeliveryUpdateInput) Normalize ¶
func (i *NotificationDeliveryUpdateInput) Normalize()
Normalize tidies the fields this request actually carries.
It does not fill in the ones it does not: the repository writes exactly the columns that were sent, and filling them here would turn every update into a write of every column — so two requests changing different fields of one row would start overwriting each other instead of composing.
func (*NotificationDeliveryUpdateInput) Validate ¶
func (i *NotificationDeliveryUpdateInput) Validate(prev *NotificationDelivery) error
Validate checks the row this update would produce.
Against the merged state, not the request: a length rule on a field nobody sent still has to hold, and a rule about two fields needs both.
What comes back is a *NotificationDeliveryUpdateInputError, shaped like the input itself.
type NotificationDeliveryUpdateInputError ¶
type NotificationDeliveryUpdateInputError struct {
// The inbox line this is a copy of. The line is the truth and this is one way
// it was repeated.
RecipientID *rigerr.FieldError `json:"recipientId,omitempty"`
// Who it is for, denormalized off the line so a claim can group by it without
// a join.
AccountID *rigerr.FieldError `json:"accountId,omitempty"`
// Where it is going. One row per line per channel, which is what the unique
// index below enforces.
Channel *rigerr.FieldError `json:"channel,omitempty"`
// Copied from the notification, so the settings resolution needs no join
// either.
Kind *rigerr.FieldError `json:"kind,omitempty"`
// What the setting said when this row was written. Immediate rows are sent on
// their own; the rest are batched per account and channel.
Digest *rigerr.FieldError `json:"digest,omitempty"`
// Skipped means a setting refused it, which is different from Failed and worth
// telling apart in a report.
State *rigerr.FieldError `json:"state,omitempty"`
// When this is due. A delivery held outside somebody hours moves to the next
// opening; a digested one moves to its window close.
DeliverAt *rigerr.FieldError `json:"deliverAt,omitempty"`
// When a channel accepted it, which is not the same as it arriving.
SentAt *rigerr.FieldError `json:"sentAt,omitempty"`
// What the channel said last time. Kept on a retry as well as on a failure, so
// a pattern is visible before the cap is reached.
FailedReason *rigerr.FieldError `json:"failedReason,omitempty"`
// How many times this has been claimed. Past notifications.max_attempts it is
// Failed and stops being claimed.
Attempts *rigerr.FieldError `json:"attempts,omitempty"`
// When a dispatcher took it. Past notifications.claim_ttl another one may,
// which is what makes a crashed process recoverable.
ClaimedAt *rigerr.FieldError `json:"claimedAt,omitempty"`
// Which process holds the lease. A uuid generated once per process, with the
// hostname beside it in the log line, so a stuck lease traces to a pod rather
// than to a mystery.
ClaimedBy *rigerr.FieldError `json:"claimedBy,omitempty"`
// Entity is a problem with the row as a whole rather than with one field: what
// the Entity rule said.
Entity *rigerr.FieldError `json:"entity,omitempty"`
}
NotificationDeliveryUpdateInputError says what was wrong with each field of a NotificationDeliveryUpdateInput.
Its shape is the input's shape, so a client can attach every message to the field it is about without matching on strings. A member is nil when that field was fine, and the whole value is nil when the input was. It is what the 422 carries.
func (*NotificationDeliveryUpdateInputError) Empty ¶
func (e *NotificationDeliveryUpdateInputError) Empty() bool
Empty reports whether anything went wrong. A validator that found nothing returns nil rather than one of these.
func (*NotificationDeliveryUpdateInputError) Error ¶
func (e *NotificationDeliveryUpdateInputError) Error() string
Error implements error. The sentence is for logs and for a person; the structure above is what a client acts on.
func (*NotificationDeliveryUpdateInputError) ErrorCode ¶
func (e *NotificationDeliveryUpdateInputError) ErrorCode() rigerr.Code
ErrorCode implements rigerr.Coder: the request was understood and its content is what is wrong, which is 422 and not 400.
func (*NotificationDeliveryUpdateInputError) ErrorFields ¶
func (e *NotificationDeliveryUpdateInputError) ErrorFields() any
ErrorFields implements rigerr.FieldReporter, which is how the HTTP layer finds this and answers with it rather than with prose.
type NotificationDeliveryUpdateValidator ¶
type NotificationDeliveryUpdateValidator struct {
// The inbox line this is a copy of. The line is the truth and this is one way
// it was repeated.
RecipientID func(ctx context.Context, c *NotificationDeliveryValidatorContext, value uuid.UUID) error
// Who it is for, denormalized off the line so a claim can group by it without
// a join.
AccountID func(ctx context.Context, c *NotificationDeliveryValidatorContext, value uuid.UUID) error
// Where it is going. One row per line per channel, which is what the unique
// index below enforces.
Channel func(ctx context.Context, c *NotificationDeliveryValidatorContext, value NotificationChannel) error
// Copied from the notification, so the settings resolution needs no join
// either.
Kind func(ctx context.Context, c *NotificationDeliveryValidatorContext, value string) error
// What the setting said when this row was written. Immediate rows are sent on
// their own; the rest are batched per account and channel.
Digest func(ctx context.Context, c *NotificationDeliveryValidatorContext, value NotificationDigest) error
// Skipped means a setting refused it, which is different from Failed and worth
// telling apart in a report.
State func(ctx context.Context, c *NotificationDeliveryValidatorContext, value NotificationDeliveryState) error
// When this is due. A delivery held outside somebody hours moves to the next
// opening; a digested one moves to its window close.
DeliverAt func(ctx context.Context, c *NotificationDeliveryValidatorContext, value time.Time) error
// When a channel accepted it, which is not the same as it arriving.
SentAt func(ctx context.Context, c *NotificationDeliveryValidatorContext, value *time.Time) error
// What the channel said last time. Kept on a retry as well as on a failure, so
// a pattern is visible before the cap is reached.
FailedReason func(ctx context.Context, c *NotificationDeliveryValidatorContext, value *string) error
// How many times this has been claimed. Past notifications.max_attempts it is
// Failed and stops being claimed.
Attempts func(ctx context.Context, c *NotificationDeliveryValidatorContext, value int) error
// When a dispatcher took it. Past notifications.claim_ttl another one may,
// which is what makes a crashed process recoverable.
ClaimedAt func(ctx context.Context, c *NotificationDeliveryValidatorContext, value *time.Time) error
// Which process holds the lease. A uuid generated once per process, with the
// hostname beside it in the log line, so a stuck lease traces to a pod rather
// than to a mystery.
ClaimedBy func(ctx context.Context, c *NotificationDeliveryValidatorContext, value *uuid.UUID) error
// Entity runs after the per-field hooks, for a rule that is about the row
// rather than about one column.
Entity func(ctx context.Context, c *NotificationDeliveryValidatorContext) error
}
NotificationDeliveryUpdateValidator is the rules for changing one that already exists: what the schema cannot express.
One optional function per field this operation can set, so the set of fields is the set of rules that could apply — a column an update cannot touch has no hook here to write by mistake. A nil one is skipped. Every configured hook runs, because a rule that fails should not hide the next one, so a request reports everything wrong with it.
A hook returns a FieldError to attach the message to a specific field, or any other error to fail the request outright.
func (NotificationDeliveryUpdateValidator) RunUpdate ¶
func (v NotificationDeliveryUpdateValidator) RunUpdate(ctx context.Context, claims tenancy.Claims, i *NotificationDeliveryUpdateInput, prev *NotificationDelivery) error
RunUpdate implements [dbhook.UpdateValidator]: it runs the service's rules against the row this update would produce, with the row as it was available for a rule about the change itself.
type NotificationDeliveryValidatorContext ¶
type NotificationDeliveryValidatorContext struct {
// Values is the intended end state.
Values NotificationDelivery
// Claims are who is asking. They are a value rather than something to fetch
// from the context because a rule that has to look them up is a rule that can
// forget to, and because there is no case where they are absent: a write
// without a caller is refused by the repository before any rule runs.
Claims tenancy.Claims
// contains filtered or unexported fields
}
NotificationDeliveryValidatorContext is what a rule sees.
Values is the row as it will be if this goes through — merged from the previous state on an update, so every field is set whether or not the request mentioned it. That is the point: "ends after starts" cannot be answered from a request that only carried one of them.
func (*NotificationDeliveryValidatorContext) AccountIDChanged ¶
func (c *NotificationDeliveryValidatorContext) AccountIDChanged() bool
AccountIDChanged reports whether this request set account_id.
func (*NotificationDeliveryValidatorContext) AttemptsChanged ¶
func (c *NotificationDeliveryValidatorContext) AttemptsChanged() bool
AttemptsChanged reports whether this request set attempts.
func (*NotificationDeliveryValidatorContext) Changed ¶
func (c *NotificationDeliveryValidatorContext) Changed(column string) bool
Changed reports whether this request carried a new value for a column.
It is what keeps an expensive rule from running on every update: a check that reaches another service to confirm a reference only needs to run when the reference actually moved. On a create everything is changed, because everything is new.
func (*NotificationDeliveryValidatorContext) ChannelChanged ¶
func (c *NotificationDeliveryValidatorContext) ChannelChanged() bool
ChannelChanged reports whether this request set channel.
func (*NotificationDeliveryValidatorContext) ClaimedAtChanged ¶
func (c *NotificationDeliveryValidatorContext) ClaimedAtChanged() bool
ClaimedAtChanged reports whether this request set claimed_at.
func (*NotificationDeliveryValidatorContext) ClaimedByChanged ¶
func (c *NotificationDeliveryValidatorContext) ClaimedByChanged() bool
ClaimedByChanged reports whether this request set claimed_by.
func (*NotificationDeliveryValidatorContext) DeliverAtChanged ¶
func (c *NotificationDeliveryValidatorContext) DeliverAtChanged() bool
DeliverAtChanged reports whether this request set deliver_at.
func (*NotificationDeliveryValidatorContext) DigestChanged ¶
func (c *NotificationDeliveryValidatorContext) DigestChanged() bool
DigestChanged reports whether this request set digest.
func (*NotificationDeliveryValidatorContext) FailedReasonChanged ¶
func (c *NotificationDeliveryValidatorContext) FailedReasonChanged() bool
FailedReasonChanged reports whether this request set failed_reason.
func (*NotificationDeliveryValidatorContext) IsUpdate ¶
func (c *NotificationDeliveryValidatorContext) IsUpdate() bool
IsUpdate reports whether there was a row before this.
func (*NotificationDeliveryValidatorContext) KindChanged ¶
func (c *NotificationDeliveryValidatorContext) KindChanged() bool
KindChanged reports whether this request set kind.
func (*NotificationDeliveryValidatorContext) Previous ¶
func (c *NotificationDeliveryValidatorContext) Previous() NotificationDelivery
Previous is the row as it was, and the zero value on a create. Check IsUpdate before reading it.
func (*NotificationDeliveryValidatorContext) RecipientIDChanged ¶
func (c *NotificationDeliveryValidatorContext) RecipientIDChanged() bool
RecipientIDChanged reports whether this request set recipient_id.
func (*NotificationDeliveryValidatorContext) SentAtChanged ¶
func (c *NotificationDeliveryValidatorContext) SentAtChanged() bool
SentAtChanged reports whether this request set sent_at.
func (*NotificationDeliveryValidatorContext) StateChanged ¶
func (c *NotificationDeliveryValidatorContext) StateChanged() bool
StateChanged reports whether this request set state.
type NotificationDevice ¶
type NotificationDevice struct {
// Unique identifier for this row.
ID uuid.UUID `json:"id"`
// Tenant this row belongs to. Every query is scoped by it.
TenantID uuid.UUID `json:"tenantId"`
// Whose device it is.
AccountID uuid.UUID `json:"accountId"`
// When this row was created. Set automatically and never changes.
CreatedAt time.Time `json:"createdAt"`
// When this row was last updated. Set automatically on every update.
UpdatedAt *time.Time `json:"updatedAt,omitempty"`
// Desktop or Mobile. Email is refused by the CHECK above.
Channel NotificationChannel `json:"channel"`
// What the provider was given to address this device. Opaque to rig.
Token string `json:"token"`
// What a person sees in a list of their devices, and the column that makes
// revoking one possible for somebody who has four.
Label *string `json:"label,omitempty"`
// When this device last checked in, so a stale one can be told apart from a
// revoked one.
LastSeenAt *time.Time `json:"lastSeenAt,omitempty"`
// When it stopped being a place to send to. Kept rather than deleted, so a
// token that comes back can be recognised.
RevokedAt *time.Time `json:"revokedAt,omitempty"`
}
Where a push can reach somebody. Email is refused: the address is on the account already.
It is the one definition of a NotificationDevice: the repository scans into it and the API returns it, so there is no conversion between two shapes of the same thing and no field that can go missing from one of them.
type NotificationDeviceCreateInput ¶
type NotificationDeviceCreateInput struct {
// Whose device it is.
AccountID uuid.UUID `json:"accountId"`
// Desktop or Mobile. Email is refused by the CHECK above.
Channel NotificationChannel `json:"channel"`
// What the provider was given to address this device. Opaque to rig.
Token string `json:"token"`
// What a person sees in a list of their devices, and the column that makes
// revoking one possible for somebody who has four.
Label *string `json:"label"`
// When this device last checked in, so a stale one can be told apart from a
// revoked one.
LastSeenAt *time.Time `json:"lastSeenAt"`
// When it stopped being a place to send to. Kept rather than deleted, so a
// token that comes back can be recognised.
RevokedAt *time.Time `json:"revokedAt"`
}
NotificationDeviceCreateInput is what creating a NotificationDevice takes.
The identifier, the tenant, and the audit columns are absent: those are stamped by the repository from the request's claims.
func (*NotificationDeviceCreateInput) Normalize ¶
func (i *NotificationDeviceCreateInput) Normalize()
Normalize tidies what was given before anything checks it.
It runs first so that validation sees the value that will actually be stored: a title with a trailing space and one without are the same title, and rejecting the second for a length rule the first passes would be indefensible.
func (*NotificationDeviceCreateInput) Validate ¶
func (i *NotificationDeviceCreateInput) Validate() error
Validate checks what the schema can decide on its own.
Everything a column declares — NOT NULL, a length, an enumeration's values — is checked here, so a service only writes the rules that are actually about the business. Every field is checked before returning, because a form that reports one problem per round trip is a form people give up on.
What comes back is a *NotificationDeviceCreateInputError, shaped like the input itself.
type NotificationDeviceCreateInputError ¶
type NotificationDeviceCreateInputError struct {
// Whose device it is.
AccountID *rigerr.FieldError `json:"accountId,omitempty"`
// Desktop or Mobile. Email is refused by the CHECK above.
Channel *rigerr.FieldError `json:"channel,omitempty"`
// What the provider was given to address this device. Opaque to rig.
Token *rigerr.FieldError `json:"token,omitempty"`
// What a person sees in a list of their devices, and the column that makes
// revoking one possible for somebody who has four.
Label *rigerr.FieldError `json:"label,omitempty"`
// When this device last checked in, so a stale one can be told apart from a
// revoked one.
LastSeenAt *rigerr.FieldError `json:"lastSeenAt,omitempty"`
// When it stopped being a place to send to. Kept rather than deleted, so a
// token that comes back can be recognised.
RevokedAt *rigerr.FieldError `json:"revokedAt,omitempty"`
// Entity is a problem with the row as a whole rather than with one field: what
// the Entity rule said.
Entity *rigerr.FieldError `json:"entity,omitempty"`
}
NotificationDeviceCreateInputError says what was wrong with each field of a NotificationDeviceCreateInput.
Its shape is the input's shape, so a client can attach every message to the field it is about without matching on strings. A member is nil when that field was fine, and the whole value is nil when the input was. It is what the 422 carries.
func (*NotificationDeviceCreateInputError) Empty ¶
func (e *NotificationDeviceCreateInputError) Empty() bool
Empty reports whether anything went wrong. A validator that found nothing returns nil rather than one of these.
func (*NotificationDeviceCreateInputError) Error ¶
func (e *NotificationDeviceCreateInputError) Error() string
Error implements error. The sentence is for logs and for a person; the structure above is what a client acts on.
func (*NotificationDeviceCreateInputError) ErrorCode ¶
func (e *NotificationDeviceCreateInputError) ErrorCode() rigerr.Code
ErrorCode implements rigerr.Coder: the request was understood and its content is what is wrong, which is 422 and not 400.
func (*NotificationDeviceCreateInputError) ErrorFields ¶
func (e *NotificationDeviceCreateInputError) ErrorFields() any
ErrorFields implements rigerr.FieldReporter, which is how the HTTP layer finds this and answers with it rather than with prose.
type NotificationDeviceCreateValidator ¶
type NotificationDeviceCreateValidator struct {
// Whose device it is.
AccountID func(ctx context.Context, c *NotificationDeviceValidatorContext, value uuid.UUID) error
// Desktop or Mobile. Email is refused by the CHECK above.
Channel func(ctx context.Context, c *NotificationDeviceValidatorContext, value NotificationChannel) error
// What the provider was given to address this device. Opaque to rig.
Token func(ctx context.Context, c *NotificationDeviceValidatorContext, value string) error
// What a person sees in a list of their devices, and the column that makes
// revoking one possible for somebody who has four.
Label func(ctx context.Context, c *NotificationDeviceValidatorContext, value *string) error
// When this device last checked in, so a stale one can be told apart from a
// revoked one.
LastSeenAt func(ctx context.Context, c *NotificationDeviceValidatorContext, value *time.Time) error
// When it stopped being a place to send to. Kept rather than deleted, so a
// token that comes back can be recognised.
RevokedAt func(ctx context.Context, c *NotificationDeviceValidatorContext, value *time.Time) error
// Entity runs after the per-field hooks, for a rule that is about the row
// rather than about one column.
Entity func(ctx context.Context, c *NotificationDeviceValidatorContext) error
}
NotificationDeviceCreateValidator is the rules for bringing a NotificationDevice into existence: what the schema cannot express.
One optional function per field this operation can set, so the set of fields is the set of rules that could apply — a column an update cannot touch has no hook here to write by mistake. A nil one is skipped. Every configured hook runs, because a rule that fails should not hide the next one, so a request reports everything wrong with it.
A hook returns a FieldError to attach the message to a specific field, or any other error to fail the request outright.
func (NotificationDeviceCreateValidator) RunCreate ¶
func (v NotificationDeviceCreateValidator) RunCreate(ctx context.Context, claims tenancy.Claims, i *NotificationDeviceCreateInput) error
RunCreate implements [dbhook.CreateValidator]: it runs the service's rules against the row this input would produce.
The generated checks are not repeated here. The repository runs Normalize and Validate first, so by the time a hook sees the input it is tidy and the schema is satisfied — which is what lets a hook be about the business rather than about NOT NULL.
type NotificationDeviceDeleteInput ¶
NotificationDeviceDeleteInput is what deleting a NotificationDevice takes.
type NotificationDeviceUpdateInput ¶
type NotificationDeviceUpdateInput struct {
// Whose device it is.
AccountID patch.Optional[uuid.UUID] `json:"accountId"`
// Desktop or Mobile. Email is refused by the CHECK above.
Channel patch.Optional[NotificationChannel] `json:"channel"`
// What the provider was given to address this device. Opaque to rig.
Token patch.Optional[string] `json:"token"`
// What a person sees in a list of their devices, and the column that makes
// revoking one possible for somebody who has four.
Label patch.Nullable[string] `json:"label"`
// When this device last checked in, so a stale one can be told apart from a
// revoked one.
LastSeenAt patch.Nullable[time.Time] `json:"lastSeenAt"`
// When it stopped being a place to send to. Kept rather than deleted, so a
// token that comes back can be recognised.
RevokedAt patch.Nullable[time.Time] `json:"revokedAt"`
}
NotificationDeviceUpdateInput is what changing a NotificationDevice takes.
A field left out is untouched. A nullable field set to null is cleared — which is why the two wrappers differ: a column that cannot hold null has no way to be given one, so clearing it is a compile error rather than a rejection at runtime. Immutable fields are not here at all.
func (NotificationDeviceUpdateInput) Merged ¶
func (i NotificationDeviceUpdateInput) Merged(prev *NotificationDevice) NotificationDevice
Merged is the row as it will be once this update is applied.
It is what validation runs against, and the reason it exists: a rule spanning two fields cannot be checked from a partial request. "Ends after starts" is unanswerable when only one of them was sent.
It returns a copy. The input keeps its patches, so the repository still writes only the columns that were actually given.
func (*NotificationDeviceUpdateInput) Normalize ¶
func (i *NotificationDeviceUpdateInput) Normalize()
Normalize tidies the fields this request actually carries.
It does not fill in the ones it does not: the repository writes exactly the columns that were sent, and filling them here would turn every update into a write of every column — so two requests changing different fields of one row would start overwriting each other instead of composing.
func (*NotificationDeviceUpdateInput) Validate ¶
func (i *NotificationDeviceUpdateInput) Validate(prev *NotificationDevice) error
Validate checks the row this update would produce.
Against the merged state, not the request: a length rule on a field nobody sent still has to hold, and a rule about two fields needs both.
What comes back is a *NotificationDeviceUpdateInputError, shaped like the input itself.
type NotificationDeviceUpdateInputError ¶
type NotificationDeviceUpdateInputError struct {
// Whose device it is.
AccountID *rigerr.FieldError `json:"accountId,omitempty"`
// Desktop or Mobile. Email is refused by the CHECK above.
Channel *rigerr.FieldError `json:"channel,omitempty"`
// What the provider was given to address this device. Opaque to rig.
Token *rigerr.FieldError `json:"token,omitempty"`
// What a person sees in a list of their devices, and the column that makes
// revoking one possible for somebody who has four.
Label *rigerr.FieldError `json:"label,omitempty"`
// When this device last checked in, so a stale one can be told apart from a
// revoked one.
LastSeenAt *rigerr.FieldError `json:"lastSeenAt,omitempty"`
// When it stopped being a place to send to. Kept rather than deleted, so a
// token that comes back can be recognised.
RevokedAt *rigerr.FieldError `json:"revokedAt,omitempty"`
// Entity is a problem with the row as a whole rather than with one field: what
// the Entity rule said.
Entity *rigerr.FieldError `json:"entity,omitempty"`
}
NotificationDeviceUpdateInputError says what was wrong with each field of a NotificationDeviceUpdateInput.
Its shape is the input's shape, so a client can attach every message to the field it is about without matching on strings. A member is nil when that field was fine, and the whole value is nil when the input was. It is what the 422 carries.
func (*NotificationDeviceUpdateInputError) Empty ¶
func (e *NotificationDeviceUpdateInputError) Empty() bool
Empty reports whether anything went wrong. A validator that found nothing returns nil rather than one of these.
func (*NotificationDeviceUpdateInputError) Error ¶
func (e *NotificationDeviceUpdateInputError) Error() string
Error implements error. The sentence is for logs and for a person; the structure above is what a client acts on.
func (*NotificationDeviceUpdateInputError) ErrorCode ¶
func (e *NotificationDeviceUpdateInputError) ErrorCode() rigerr.Code
ErrorCode implements rigerr.Coder: the request was understood and its content is what is wrong, which is 422 and not 400.
func (*NotificationDeviceUpdateInputError) ErrorFields ¶
func (e *NotificationDeviceUpdateInputError) ErrorFields() any
ErrorFields implements rigerr.FieldReporter, which is how the HTTP layer finds this and answers with it rather than with prose.
type NotificationDeviceUpdateValidator ¶
type NotificationDeviceUpdateValidator struct {
// Whose device it is.
AccountID func(ctx context.Context, c *NotificationDeviceValidatorContext, value uuid.UUID) error
// Desktop or Mobile. Email is refused by the CHECK above.
Channel func(ctx context.Context, c *NotificationDeviceValidatorContext, value NotificationChannel) error
// What the provider was given to address this device. Opaque to rig.
Token func(ctx context.Context, c *NotificationDeviceValidatorContext, value string) error
// What a person sees in a list of their devices, and the column that makes
// revoking one possible for somebody who has four.
Label func(ctx context.Context, c *NotificationDeviceValidatorContext, value *string) error
// When this device last checked in, so a stale one can be told apart from a
// revoked one.
LastSeenAt func(ctx context.Context, c *NotificationDeviceValidatorContext, value *time.Time) error
// When it stopped being a place to send to. Kept rather than deleted, so a
// token that comes back can be recognised.
RevokedAt func(ctx context.Context, c *NotificationDeviceValidatorContext, value *time.Time) error
// Entity runs after the per-field hooks, for a rule that is about the row
// rather than about one column.
Entity func(ctx context.Context, c *NotificationDeviceValidatorContext) error
}
NotificationDeviceUpdateValidator is the rules for changing one that already exists: what the schema cannot express.
One optional function per field this operation can set, so the set of fields is the set of rules that could apply — a column an update cannot touch has no hook here to write by mistake. A nil one is skipped. Every configured hook runs, because a rule that fails should not hide the next one, so a request reports everything wrong with it.
A hook returns a FieldError to attach the message to a specific field, or any other error to fail the request outright.
func (NotificationDeviceUpdateValidator) RunUpdate ¶
func (v NotificationDeviceUpdateValidator) RunUpdate(ctx context.Context, claims tenancy.Claims, i *NotificationDeviceUpdateInput, prev *NotificationDevice) error
RunUpdate implements [dbhook.UpdateValidator]: it runs the service's rules against the row this update would produce, with the row as it was available for a rule about the change itself.
type NotificationDeviceValidatorContext ¶
type NotificationDeviceValidatorContext struct {
// Values is the intended end state.
Values NotificationDevice
// Claims are who is asking. They are a value rather than something to fetch
// from the context because a rule that has to look them up is a rule that can
// forget to, and because there is no case where they are absent: a write
// without a caller is refused by the repository before any rule runs.
Claims tenancy.Claims
// contains filtered or unexported fields
}
NotificationDeviceValidatorContext is what a rule sees.
Values is the row as it will be if this goes through — merged from the previous state on an update, so every field is set whether or not the request mentioned it. That is the point: "ends after starts" cannot be answered from a request that only carried one of them.
func (*NotificationDeviceValidatorContext) AccountIDChanged ¶
func (c *NotificationDeviceValidatorContext) AccountIDChanged() bool
AccountIDChanged reports whether this request set account_id.
func (*NotificationDeviceValidatorContext) Changed ¶
func (c *NotificationDeviceValidatorContext) Changed(column string) bool
Changed reports whether this request carried a new value for a column.
It is what keeps an expensive rule from running on every update: a check that reaches another service to confirm a reference only needs to run when the reference actually moved. On a create everything is changed, because everything is new.
func (*NotificationDeviceValidatorContext) ChannelChanged ¶
func (c *NotificationDeviceValidatorContext) ChannelChanged() bool
ChannelChanged reports whether this request set channel.
func (*NotificationDeviceValidatorContext) IsUpdate ¶
func (c *NotificationDeviceValidatorContext) IsUpdate() bool
IsUpdate reports whether there was a row before this.
func (*NotificationDeviceValidatorContext) LabelChanged ¶
func (c *NotificationDeviceValidatorContext) LabelChanged() bool
LabelChanged reports whether this request set label.
func (*NotificationDeviceValidatorContext) LastSeenAtChanged ¶
func (c *NotificationDeviceValidatorContext) LastSeenAtChanged() bool
LastSeenAtChanged reports whether this request set last_seen_at.
func (*NotificationDeviceValidatorContext) Previous ¶
func (c *NotificationDeviceValidatorContext) Previous() NotificationDevice
Previous is the row as it was, and the zero value on a create. Check IsUpdate before reading it.
func (*NotificationDeviceValidatorContext) RevokedAtChanged ¶
func (c *NotificationDeviceValidatorContext) RevokedAtChanged() bool
RevokedAtChanged reports whether this request set revoked_at.
func (*NotificationDeviceValidatorContext) TokenChanged ¶
func (c *NotificationDeviceValidatorContext) TokenChanged() bool
TokenChanged reports whether this request set token.
type NotificationDigest ¶
type NotificationDigest string
How often somebody wants to be told, on one channel.
const ( // Each notification on its own, as soon as it is due. NotificationDigestImmediate NotificationDigest = "Immediate" // Whatever accumulated in an hour, as one message. NotificationDigestHourly NotificationDigest = "Hourly" // The same, a day at a time. NotificationDigestDaily NotificationDigest = "Daily" // The widest window, and the one notifications.retention has to outlive — a // weekly digest is assembled from rows a shorter retention would have pruned. NotificationDigestWeekly NotificationDigest = "Weekly" // Nothing on this channel, and the inbox line is written anyway: this is // somebody preferring to look rather than be told. is_enabled false is the // other thing, and says the channel is not available to them at all. NotificationDigestOff NotificationDigest = "Off" )
The values of NotificationDigest.
func ParseNotificationDigest ¶
func ParseNotificationDigest(s string) (NotificationDigest, bool)
ParseNotificationDigest reads a value, accepting any casing and surrounding space.
Normalization uses it, so "IN_PROGRESS" from one client and "in_progress" from another mean the same thing rather than one of them being a validation failure nobody can explain. The spelling still has to be the label’s: a value is a name the database knows, not a phrase.
func (NotificationDigest) String ¶
func (v NotificationDigest) String() string
String implements fmt.Stringer.
func (NotificationDigest) Valid ¶
func (v NotificationDigest) Valid() bool
Valid reports whether the value is one the database will accept.
type NotificationRecipient ¶
type NotificationRecipient struct {
// Unique identifier for this row.
ID uuid.UUID `json:"id"`
// Tenant this row belongs to. Every query is scoped by it.
TenantID uuid.UUID `json:"tenantId"`
// What happened. The line is separate from it so that one person clearing
// their inbox changes nothing anybody else sees.
NotificationID uuid.UUID `json:"notificationId"`
// Who this line is for. An account rather than an identity: an identity has no
// tenant, so a row addressed to one would fall outside every generated query.
AccountID uuid.UUID `json:"accountId"`
// When this row was created. Set automatically and never changes.
CreatedAt time.Time `json:"createdAt"`
// When this row was last updated. Set automatically on every update.
UpdatedAt *time.Time `json:"updatedAt,omitempty"`
// When this row was soft-deleted. Null while the row is live.
DeletedAt *time.Time `json:"deletedAt,omitempty"`
// Account that soft-deleted this row, taken from the request's claims.
DeletedByAccountID *uuid.UUID `json:"deletedByAccountId,omitempty"`
// Copied from the notification, so the inbox and its live-sync shape never
// touch a table holding rows for people who are not recipients.
Kind string `json:"kind"`
// What collapses several events into one line. Null opts out and every event
// is its own row.
GroupKey *string `json:"groupKey,omitempty"`
// How many events this line stands for. One unless a group key collapsed them.
EventCount int `json:"eventCount"`
// When the person read it. Null is unread, which is what the badge counts.
ReadAt *time.Time `json:"readAt,omitempty"`
}
One inbox line: a notification, an account, and whether it has been read.
It is the one definition of a NotificationRecipient: the repository scans into it and the API returns it, so there is no conversion between two shapes of the same thing and no field that can go missing from one of them.
type NotificationRecipientCreateInput ¶
type NotificationRecipientCreateInput struct {
// What happened. The line is separate from it so that one person clearing
// their inbox changes nothing anybody else sees.
NotificationID uuid.UUID `json:"notificationId"`
// Who this line is for. An account rather than an identity: an identity has no
// tenant, so a row addressed to one would fall outside every generated query.
AccountID uuid.UUID `json:"accountId"`
// Copied from the notification, so the inbox and its live-sync shape never
// touch a table holding rows for people who are not recipients.
Kind string `json:"kind"`
// What collapses several events into one line. Null opts out and every event
// is its own row.
GroupKey *string `json:"groupKey"`
// How many events this line stands for. One unless a group key collapsed them.
EventCount int `json:"eventCount"`
// When the person read it. Null is unread, which is what the badge counts.
ReadAt *time.Time `json:"readAt"`
}
NotificationRecipientCreateInput is what creating a NotificationRecipient takes.
The identifier, the tenant, and the audit columns are absent: those are stamped by the repository from the request's claims.
func (*NotificationRecipientCreateInput) Normalize ¶
func (i *NotificationRecipientCreateInput) Normalize()
Normalize tidies what was given before anything checks it.
It runs first so that validation sees the value that will actually be stored: a title with a trailing space and one without are the same title, and rejecting the second for a length rule the first passes would be indefensible.
func (*NotificationRecipientCreateInput) Validate ¶
func (i *NotificationRecipientCreateInput) Validate() error
Validate checks what the schema can decide on its own.
Everything a column declares — NOT NULL, a length, an enumeration's values — is checked here, so a service only writes the rules that are actually about the business. Every field is checked before returning, because a form that reports one problem per round trip is a form people give up on.
What comes back is a *NotificationRecipientCreateInputError, shaped like the input itself.
type NotificationRecipientCreateInputError ¶
type NotificationRecipientCreateInputError struct {
// What happened. The line is separate from it so that one person clearing
// their inbox changes nothing anybody else sees.
NotificationID *rigerr.FieldError `json:"notificationId,omitempty"`
// Who this line is for. An account rather than an identity: an identity has no
// tenant, so a row addressed to one would fall outside every generated query.
AccountID *rigerr.FieldError `json:"accountId,omitempty"`
// Copied from the notification, so the inbox and its live-sync shape never
// touch a table holding rows for people who are not recipients.
Kind *rigerr.FieldError `json:"kind,omitempty"`
// What collapses several events into one line. Null opts out and every event
// is its own row.
GroupKey *rigerr.FieldError `json:"groupKey,omitempty"`
// How many events this line stands for. One unless a group key collapsed them.
EventCount *rigerr.FieldError `json:"eventCount,omitempty"`
// When the person read it. Null is unread, which is what the badge counts.
ReadAt *rigerr.FieldError `json:"readAt,omitempty"`
// Entity is a problem with the row as a whole rather than with one field: what
// the Entity rule said.
Entity *rigerr.FieldError `json:"entity,omitempty"`
}
NotificationRecipientCreateInputError says what was wrong with each field of a NotificationRecipientCreateInput.
Its shape is the input's shape, so a client can attach every message to the field it is about without matching on strings. A member is nil when that field was fine, and the whole value is nil when the input was. It is what the 422 carries.
func (*NotificationRecipientCreateInputError) Empty ¶
func (e *NotificationRecipientCreateInputError) Empty() bool
Empty reports whether anything went wrong. A validator that found nothing returns nil rather than one of these.
func (*NotificationRecipientCreateInputError) Error ¶
func (e *NotificationRecipientCreateInputError) Error() string
Error implements error. The sentence is for logs and for a person; the structure above is what a client acts on.
func (*NotificationRecipientCreateInputError) ErrorCode ¶
func (e *NotificationRecipientCreateInputError) ErrorCode() rigerr.Code
ErrorCode implements rigerr.Coder: the request was understood and its content is what is wrong, which is 422 and not 400.
func (*NotificationRecipientCreateInputError) ErrorFields ¶
func (e *NotificationRecipientCreateInputError) ErrorFields() any
ErrorFields implements rigerr.FieldReporter, which is how the HTTP layer finds this and answers with it rather than with prose.
type NotificationRecipientCreateValidator ¶
type NotificationRecipientCreateValidator struct {
// What happened. The line is separate from it so that one person clearing
// their inbox changes nothing anybody else sees.
NotificationID func(ctx context.Context, c *NotificationRecipientValidatorContext, value uuid.UUID) error
// Who this line is for. An account rather than an identity: an identity has no
// tenant, so a row addressed to one would fall outside every generated query.
AccountID func(ctx context.Context, c *NotificationRecipientValidatorContext, value uuid.UUID) error
// Copied from the notification, so the inbox and its live-sync shape never
// touch a table holding rows for people who are not recipients.
Kind func(ctx context.Context, c *NotificationRecipientValidatorContext, value string) error
// What collapses several events into one line. Null opts out and every event
// is its own row.
GroupKey func(ctx context.Context, c *NotificationRecipientValidatorContext, value *string) error
// How many events this line stands for. One unless a group key collapsed them.
EventCount func(ctx context.Context, c *NotificationRecipientValidatorContext, value int) error
// When the person read it. Null is unread, which is what the badge counts.
ReadAt func(ctx context.Context, c *NotificationRecipientValidatorContext, value *time.Time) error
// Entity runs after the per-field hooks, for a rule that is about the row
// rather than about one column.
Entity func(ctx context.Context, c *NotificationRecipientValidatorContext) error
}
NotificationRecipientCreateValidator is the rules for bringing a NotificationRecipient into existence: what the schema cannot express.
One optional function per field this operation can set, so the set of fields is the set of rules that could apply — a column an update cannot touch has no hook here to write by mistake. A nil one is skipped. Every configured hook runs, because a rule that fails should not hide the next one, so a request reports everything wrong with it.
A hook returns a FieldError to attach the message to a specific field, or any other error to fail the request outright.
func (NotificationRecipientCreateValidator) RunCreate ¶
func (v NotificationRecipientCreateValidator) RunCreate(ctx context.Context, claims tenancy.Claims, i *NotificationRecipientCreateInput) error
RunCreate implements [dbhook.CreateValidator]: it runs the service's rules against the row this input would produce.
The generated checks are not repeated here. The repository runs Normalize and Validate first, so by the time a hook sees the input it is tidy and the schema is satisfied — which is what lets a hook be about the business rather than about NOT NULL.
type NotificationRecipientDeleteInput ¶
type NotificationRecipientDeleteInput struct {
// ID is the row to delete.
ID uuid.UUID `json:"id"`
// Hard removes the row outright instead of retiring it. A hard delete cannot
// be undone and takes the row's snapshots with it.
Hard bool `json:"hard,omitempty"`
}
NotificationRecipientDeleteInput is what deleting a NotificationRecipient takes.
type NotificationRecipientUpdateInput ¶
type NotificationRecipientUpdateInput struct {
// What happened. The line is separate from it so that one person clearing
// their inbox changes nothing anybody else sees.
NotificationID patch.Optional[uuid.UUID] `json:"notificationId"`
// Who this line is for. An account rather than an identity: an identity has no
// tenant, so a row addressed to one would fall outside every generated query.
AccountID patch.Optional[uuid.UUID] `json:"accountId"`
// Copied from the notification, so the inbox and its live-sync shape never
// touch a table holding rows for people who are not recipients.
Kind patch.Optional[string] `json:"kind"`
// What collapses several events into one line. Null opts out and every event
// is its own row.
GroupKey patch.Nullable[string] `json:"groupKey"`
// How many events this line stands for. One unless a group key collapsed them.
EventCount patch.Optional[int] `json:"eventCount"`
// When the person read it. Null is unread, which is what the badge counts.
ReadAt patch.Nullable[time.Time] `json:"readAt"`
}
NotificationRecipientUpdateInput is what changing a NotificationRecipient takes.
A field left out is untouched. A nullable field set to null is cleared — which is why the two wrappers differ: a column that cannot hold null has no way to be given one, so clearing it is a compile error rather than a rejection at runtime. Immutable fields are not here at all.
func (NotificationRecipientUpdateInput) Merged ¶
func (i NotificationRecipientUpdateInput) Merged(prev *NotificationRecipient) NotificationRecipient
Merged is the row as it will be once this update is applied.
It is what validation runs against, and the reason it exists: a rule spanning two fields cannot be checked from a partial request. "Ends after starts" is unanswerable when only one of them was sent.
It returns a copy. The input keeps its patches, so the repository still writes only the columns that were actually given.
func (*NotificationRecipientUpdateInput) Normalize ¶
func (i *NotificationRecipientUpdateInput) Normalize()
Normalize tidies the fields this request actually carries.
It does not fill in the ones it does not: the repository writes exactly the columns that were sent, and filling them here would turn every update into a write of every column — so two requests changing different fields of one row would start overwriting each other instead of composing.
func (*NotificationRecipientUpdateInput) Validate ¶
func (i *NotificationRecipientUpdateInput) Validate(prev *NotificationRecipient) error
Validate checks the row this update would produce.
Against the merged state, not the request: a length rule on a field nobody sent still has to hold, and a rule about two fields needs both.
What comes back is a *NotificationRecipientUpdateInputError, shaped like the input itself.
type NotificationRecipientUpdateInputError ¶
type NotificationRecipientUpdateInputError struct {
// What happened. The line is separate from it so that one person clearing
// their inbox changes nothing anybody else sees.
NotificationID *rigerr.FieldError `json:"notificationId,omitempty"`
// Who this line is for. An account rather than an identity: an identity has no
// tenant, so a row addressed to one would fall outside every generated query.
AccountID *rigerr.FieldError `json:"accountId,omitempty"`
// Copied from the notification, so the inbox and its live-sync shape never
// touch a table holding rows for people who are not recipients.
Kind *rigerr.FieldError `json:"kind,omitempty"`
// What collapses several events into one line. Null opts out and every event
// is its own row.
GroupKey *rigerr.FieldError `json:"groupKey,omitempty"`
// How many events this line stands for. One unless a group key collapsed them.
EventCount *rigerr.FieldError `json:"eventCount,omitempty"`
// When the person read it. Null is unread, which is what the badge counts.
ReadAt *rigerr.FieldError `json:"readAt,omitempty"`
// Entity is a problem with the row as a whole rather than with one field: what
// the Entity rule said.
Entity *rigerr.FieldError `json:"entity,omitempty"`
}
NotificationRecipientUpdateInputError says what was wrong with each field of a NotificationRecipientUpdateInput.
Its shape is the input's shape, so a client can attach every message to the field it is about without matching on strings. A member is nil when that field was fine, and the whole value is nil when the input was. It is what the 422 carries.
func (*NotificationRecipientUpdateInputError) Empty ¶
func (e *NotificationRecipientUpdateInputError) Empty() bool
Empty reports whether anything went wrong. A validator that found nothing returns nil rather than one of these.
func (*NotificationRecipientUpdateInputError) Error ¶
func (e *NotificationRecipientUpdateInputError) Error() string
Error implements error. The sentence is for logs and for a person; the structure above is what a client acts on.
func (*NotificationRecipientUpdateInputError) ErrorCode ¶
func (e *NotificationRecipientUpdateInputError) ErrorCode() rigerr.Code
ErrorCode implements rigerr.Coder: the request was understood and its content is what is wrong, which is 422 and not 400.
func (*NotificationRecipientUpdateInputError) ErrorFields ¶
func (e *NotificationRecipientUpdateInputError) ErrorFields() any
ErrorFields implements rigerr.FieldReporter, which is how the HTTP layer finds this and answers with it rather than with prose.
type NotificationRecipientUpdateValidator ¶
type NotificationRecipientUpdateValidator struct {
// What happened. The line is separate from it so that one person clearing
// their inbox changes nothing anybody else sees.
NotificationID func(ctx context.Context, c *NotificationRecipientValidatorContext, value uuid.UUID) error
// Who this line is for. An account rather than an identity: an identity has no
// tenant, so a row addressed to one would fall outside every generated query.
AccountID func(ctx context.Context, c *NotificationRecipientValidatorContext, value uuid.UUID) error
// Copied from the notification, so the inbox and its live-sync shape never
// touch a table holding rows for people who are not recipients.
Kind func(ctx context.Context, c *NotificationRecipientValidatorContext, value string) error
// What collapses several events into one line. Null opts out and every event
// is its own row.
GroupKey func(ctx context.Context, c *NotificationRecipientValidatorContext, value *string) error
// How many events this line stands for. One unless a group key collapsed them.
EventCount func(ctx context.Context, c *NotificationRecipientValidatorContext, value int) error
// When the person read it. Null is unread, which is what the badge counts.
ReadAt func(ctx context.Context, c *NotificationRecipientValidatorContext, value *time.Time) error
// Entity runs after the per-field hooks, for a rule that is about the row
// rather than about one column.
Entity func(ctx context.Context, c *NotificationRecipientValidatorContext) error
}
NotificationRecipientUpdateValidator is the rules for changing one that already exists: what the schema cannot express.
One optional function per field this operation can set, so the set of fields is the set of rules that could apply — a column an update cannot touch has no hook here to write by mistake. A nil one is skipped. Every configured hook runs, because a rule that fails should not hide the next one, so a request reports everything wrong with it.
A hook returns a FieldError to attach the message to a specific field, or any other error to fail the request outright.
func (NotificationRecipientUpdateValidator) RunRestore ¶
func (v NotificationRecipientUpdateValidator) RunRestore(ctx context.Context, claims tenancy.Claims, i *NotificationRecipientUpdateInput, prev *NotificationRecipient) error
RunRestore implements [dbhook.RestoreValidator]: it runs the service's rules against the row this restore would bring back.
Every rule runs, not only the ones whose field the request mentioned. The row was not live, so nothing about it has been checked against the world it is returning to.
func (NotificationRecipientUpdateValidator) RunUpdate ¶
func (v NotificationRecipientUpdateValidator) RunUpdate(ctx context.Context, claims tenancy.Claims, i *NotificationRecipientUpdateInput, prev *NotificationRecipient) error
RunUpdate implements [dbhook.UpdateValidator]: it runs the service's rules against the row this update would produce, with the row as it was available for a rule about the change itself.
type NotificationRecipientValidatorContext ¶
type NotificationRecipientValidatorContext struct {
// Values is the intended end state.
Values NotificationRecipient
// Claims are who is asking. They are a value rather than something to fetch
// from the context because a rule that has to look them up is a rule that can
// forget to, and because there is no case where they are absent: a write
// without a caller is refused by the repository before any rule runs.
Claims tenancy.Claims
// contains filtered or unexported fields
}
NotificationRecipientValidatorContext is what a rule sees.
Values is the row as it will be if this goes through — merged from the previous state on an update, so every field is set whether or not the request mentioned it. That is the point: "ends after starts" cannot be answered from a request that only carried one of them.
func (*NotificationRecipientValidatorContext) AccountIDChanged ¶
func (c *NotificationRecipientValidatorContext) AccountIDChanged() bool
AccountIDChanged reports whether this request set account_id.
func (*NotificationRecipientValidatorContext) Changed ¶
func (c *NotificationRecipientValidatorContext) Changed(column string) bool
Changed reports whether this request carried a new value for a column.
It is what keeps an expensive rule from running on every update: a check that reaches another service to confirm a reference only needs to run when the reference actually moved. On a create everything is changed, because everything is new.
func (*NotificationRecipientValidatorContext) EventCountChanged ¶
func (c *NotificationRecipientValidatorContext) EventCountChanged() bool
EventCountChanged reports whether this request set event_count.
func (*NotificationRecipientValidatorContext) GroupKeyChanged ¶
func (c *NotificationRecipientValidatorContext) GroupKeyChanged() bool
GroupKeyChanged reports whether this request set group_key.
func (*NotificationRecipientValidatorContext) IsUpdate ¶
func (c *NotificationRecipientValidatorContext) IsUpdate() bool
IsUpdate reports whether there was a row before this.
func (*NotificationRecipientValidatorContext) KindChanged ¶
func (c *NotificationRecipientValidatorContext) KindChanged() bool
KindChanged reports whether this request set kind.
func (*NotificationRecipientValidatorContext) NotificationIDChanged ¶
func (c *NotificationRecipientValidatorContext) NotificationIDChanged() bool
NotificationIDChanged reports whether this request set notification_id.
func (*NotificationRecipientValidatorContext) Previous ¶
func (c *NotificationRecipientValidatorContext) Previous() NotificationRecipient
Previous is the row as it was, and the zero value on a create. Check IsUpdate before reading it.
func (*NotificationRecipientValidatorContext) ReadAtChanged ¶
func (c *NotificationRecipientValidatorContext) ReadAtChanged() bool
ReadAtChanged reports whether this request set read_at.
type NotificationSetting ¶
type NotificationSetting struct {
// Unique identifier for this row.
ID uuid.UUID `json:"id"`
// Tenant this row belongs to. Every query is scoped by it.
TenantID uuid.UUID `json:"tenantId"`
// Whose preference it is.
AccountID uuid.UUID `json:"accountId"`
// When this row was created. Set automatically and never changes.
CreatedAt time.Time `json:"createdAt"`
// When this row was last updated. Set automatically on every update.
UpdatedAt *time.Time `json:"updatedAt,omitempty"`
// Null is the default for the channel, which is the second step of the
// resolution.
Kind *string `json:"kind,omitempty"`
// Which channel it answers for.
Channel NotificationChannel `json:"channel"`
// False writes no delivery row at all. Not the same as digest Off, which is
// about preferring to look rather than be told.
IsEnabled bool `json:"isEnabled"`
// Immediate sends each on its own; anything else waits for the window and goes
// out as one message.
Digest NotificationDigest `json:"digest"`
// When to start delivering, in the account time zone. Null means all day. A
// window that wraps midnight is ordinary: 22:00 to 06:00 is how somebody says
// "not overnight".
ActiveFrom *time.Time `json:"activeFrom,omitempty"`
// When to stop. Outside the window a delivery is held, not dropped: the inbox
// line exists either way, and a discarded copy makes the badge and the mailbox
// disagree.
ActiveUntil *time.Time `json:"activeUntil,omitempty"`
// ISO weekdays, 1 being Monday. Empty means every day. An array rather than a
// bitmask because [1,2,3,4,5] is legible in a settings payload and 62 is not.
ActiveDays []int16 `json:"activeDays,omitempty"`
}
What somebody wants on a channel, and when. Resolved in three steps: this kind, then this channel, then the project default.
It is the one definition of a NotificationSetting: the repository scans into it and the API returns it, so there is no conversion between two shapes of the same thing and no field that can go missing from one of them.
type NotificationSettingCreateInput ¶
type NotificationSettingCreateInput struct {
// Whose preference it is.
AccountID uuid.UUID `json:"accountId"`
// Null is the default for the channel, which is the second step of the
// resolution.
Kind *string `json:"kind"`
// Which channel it answers for.
Channel NotificationChannel `json:"channel"`
// False writes no delivery row at all. Not the same as digest Off, which is
// about preferring to look rather than be told.
IsEnabled bool `json:"isEnabled"`
// Immediate sends each on its own; anything else waits for the window and goes
// out as one message.
Digest NotificationDigest `json:"digest"`
// When to start delivering, in the account time zone. Null means all day. A
// window that wraps midnight is ordinary: 22:00 to 06:00 is how somebody says
// "not overnight".
ActiveFrom *time.Time `json:"activeFrom"`
// When to stop. Outside the window a delivery is held, not dropped: the inbox
// line exists either way, and a discarded copy makes the badge and the mailbox
// disagree.
ActiveUntil *time.Time `json:"activeUntil"`
// ISO weekdays, 1 being Monday. Empty means every day. An array rather than a
// bitmask because [1,2,3,4,5] is legible in a settings payload and 62 is not.
ActiveDays []int16 `json:"activeDays"`
}
NotificationSettingCreateInput is what creating a NotificationSetting takes.
The identifier, the tenant, and the audit columns are absent: those are stamped by the repository from the request's claims.
func (*NotificationSettingCreateInput) Normalize ¶
func (i *NotificationSettingCreateInput) Normalize()
Normalize tidies what was given before anything checks it.
It runs first so that validation sees the value that will actually be stored: a title with a trailing space and one without are the same title, and rejecting the second for a length rule the first passes would be indefensible.
func (*NotificationSettingCreateInput) Validate ¶
func (i *NotificationSettingCreateInput) Validate() error
Validate checks what the schema can decide on its own.
Everything a column declares — NOT NULL, a length, an enumeration's values — is checked here, so a service only writes the rules that are actually about the business. Every field is checked before returning, because a form that reports one problem per round trip is a form people give up on.
What comes back is a *NotificationSettingCreateInputError, shaped like the input itself.
type NotificationSettingCreateInputError ¶
type NotificationSettingCreateInputError struct {
// Whose preference it is.
AccountID *rigerr.FieldError `json:"accountId,omitempty"`
// Null is the default for the channel, which is the second step of the
// resolution.
Kind *rigerr.FieldError `json:"kind,omitempty"`
// Which channel it answers for.
Channel *rigerr.FieldError `json:"channel,omitempty"`
// False writes no delivery row at all. Not the same as digest Off, which is
// about preferring to look rather than be told.
IsEnabled *rigerr.FieldError `json:"isEnabled,omitempty"`
// Immediate sends each on its own; anything else waits for the window and goes
// out as one message.
Digest *rigerr.FieldError `json:"digest,omitempty"`
// When to start delivering, in the account time zone. Null means all day. A
// window that wraps midnight is ordinary: 22:00 to 06:00 is how somebody says
// "not overnight".
ActiveFrom *rigerr.FieldError `json:"activeFrom,omitempty"`
// When to stop. Outside the window a delivery is held, not dropped: the inbox
// line exists either way, and a discarded copy makes the badge and the mailbox
// disagree.
ActiveUntil *rigerr.FieldError `json:"activeUntil,omitempty"`
// ISO weekdays, 1 being Monday. Empty means every day. An array rather than a
// bitmask because [1,2,3,4,5] is legible in a settings payload and 62 is not.
ActiveDays *rigerr.FieldError `json:"activeDays,omitempty"`
// Entity is a problem with the row as a whole rather than with one field: what
// the Entity rule said.
Entity *rigerr.FieldError `json:"entity,omitempty"`
}
NotificationSettingCreateInputError says what was wrong with each field of a NotificationSettingCreateInput.
Its shape is the input's shape, so a client can attach every message to the field it is about without matching on strings. A member is nil when that field was fine, and the whole value is nil when the input was. It is what the 422 carries.
func (*NotificationSettingCreateInputError) Empty ¶
func (e *NotificationSettingCreateInputError) Empty() bool
Empty reports whether anything went wrong. A validator that found nothing returns nil rather than one of these.
func (*NotificationSettingCreateInputError) Error ¶
func (e *NotificationSettingCreateInputError) Error() string
Error implements error. The sentence is for logs and for a person; the structure above is what a client acts on.
func (*NotificationSettingCreateInputError) ErrorCode ¶
func (e *NotificationSettingCreateInputError) ErrorCode() rigerr.Code
ErrorCode implements rigerr.Coder: the request was understood and its content is what is wrong, which is 422 and not 400.
func (*NotificationSettingCreateInputError) ErrorFields ¶
func (e *NotificationSettingCreateInputError) ErrorFields() any
ErrorFields implements rigerr.FieldReporter, which is how the HTTP layer finds this and answers with it rather than with prose.
type NotificationSettingCreateValidator ¶
type NotificationSettingCreateValidator struct {
// Whose preference it is.
AccountID func(ctx context.Context, c *NotificationSettingValidatorContext, value uuid.UUID) error
// Null is the default for the channel, which is the second step of the
// resolution.
Kind func(ctx context.Context, c *NotificationSettingValidatorContext, value *string) error
// Which channel it answers for.
Channel func(ctx context.Context, c *NotificationSettingValidatorContext, value NotificationChannel) error
// False writes no delivery row at all. Not the same as digest Off, which is
// about preferring to look rather than be told.
IsEnabled func(ctx context.Context, c *NotificationSettingValidatorContext, value bool) error
// Immediate sends each on its own; anything else waits for the window and goes
// out as one message.
Digest func(ctx context.Context, c *NotificationSettingValidatorContext, value NotificationDigest) error
// When to start delivering, in the account time zone. Null means all day. A
// window that wraps midnight is ordinary: 22:00 to 06:00 is how somebody says
// "not overnight".
ActiveFrom func(ctx context.Context, c *NotificationSettingValidatorContext, value *time.Time) error
// When to stop. Outside the window a delivery is held, not dropped: the inbox
// line exists either way, and a discarded copy makes the badge and the mailbox
// disagree.
ActiveUntil func(ctx context.Context, c *NotificationSettingValidatorContext, value *time.Time) error
// ISO weekdays, 1 being Monday. Empty means every day. An array rather than a
// bitmask because [1,2,3,4,5] is legible in a settings payload and 62 is not.
ActiveDays func(ctx context.Context, c *NotificationSettingValidatorContext, value []int16) error
// Entity runs after the per-field hooks, for a rule that is about the row
// rather than about one column.
Entity func(ctx context.Context, c *NotificationSettingValidatorContext) error
}
NotificationSettingCreateValidator is the rules for bringing a NotificationSetting into existence: what the schema cannot express.
One optional function per field this operation can set, so the set of fields is the set of rules that could apply — a column an update cannot touch has no hook here to write by mistake. A nil one is skipped. Every configured hook runs, because a rule that fails should not hide the next one, so a request reports everything wrong with it.
A hook returns a FieldError to attach the message to a specific field, or any other error to fail the request outright.
func (NotificationSettingCreateValidator) RunCreate ¶
func (v NotificationSettingCreateValidator) RunCreate(ctx context.Context, claims tenancy.Claims, i *NotificationSettingCreateInput) error
RunCreate implements [dbhook.CreateValidator]: it runs the service's rules against the row this input would produce.
The generated checks are not repeated here. The repository runs Normalize and Validate first, so by the time a hook sees the input it is tidy and the schema is satisfied — which is what lets a hook be about the business rather than about NOT NULL.
type NotificationSettingDeleteInput ¶
NotificationSettingDeleteInput is what deleting a NotificationSetting takes.
type NotificationSettingUpdateInput ¶
type NotificationSettingUpdateInput struct {
// Whose preference it is.
AccountID patch.Optional[uuid.UUID] `json:"accountId"`
// Null is the default for the channel, which is the second step of the
// resolution.
Kind patch.Nullable[string] `json:"kind"`
// Which channel it answers for.
Channel patch.Optional[NotificationChannel] `json:"channel"`
// False writes no delivery row at all. Not the same as digest Off, which is
// about preferring to look rather than be told.
IsEnabled patch.Optional[bool] `json:"isEnabled"`
// Immediate sends each on its own; anything else waits for the window and goes
// out as one message.
Digest patch.Optional[NotificationDigest] `json:"digest"`
// When to start delivering, in the account time zone. Null means all day. A
// window that wraps midnight is ordinary: 22:00 to 06:00 is how somebody says
// "not overnight".
ActiveFrom patch.Nullable[time.Time] `json:"activeFrom"`
// When to stop. Outside the window a delivery is held, not dropped: the inbox
// line exists either way, and a discarded copy makes the badge and the mailbox
// disagree.
ActiveUntil patch.Nullable[time.Time] `json:"activeUntil"`
// ISO weekdays, 1 being Monday. Empty means every day. An array rather than a
// bitmask because [1,2,3,4,5] is legible in a settings payload and 62 is not.
ActiveDays patch.Optional[[]int16] `json:"activeDays"`
}
NotificationSettingUpdateInput is what changing a NotificationSetting takes.
A field left out is untouched. A nullable field set to null is cleared — which is why the two wrappers differ: a column that cannot hold null has no way to be given one, so clearing it is a compile error rather than a rejection at runtime. Immutable fields are not here at all.
func (NotificationSettingUpdateInput) Merged ¶
func (i NotificationSettingUpdateInput) Merged(prev *NotificationSetting) NotificationSetting
Merged is the row as it will be once this update is applied.
It is what validation runs against, and the reason it exists: a rule spanning two fields cannot be checked from a partial request. "Ends after starts" is unanswerable when only one of them was sent.
It returns a copy. The input keeps its patches, so the repository still writes only the columns that were actually given.
func (*NotificationSettingUpdateInput) Normalize ¶
func (i *NotificationSettingUpdateInput) Normalize()
Normalize tidies the fields this request actually carries.
It does not fill in the ones it does not: the repository writes exactly the columns that were sent, and filling them here would turn every update into a write of every column — so two requests changing different fields of one row would start overwriting each other instead of composing.
func (*NotificationSettingUpdateInput) Validate ¶
func (i *NotificationSettingUpdateInput) Validate(prev *NotificationSetting) error
Validate checks the row this update would produce.
Against the merged state, not the request: a length rule on a field nobody sent still has to hold, and a rule about two fields needs both.
What comes back is a *NotificationSettingUpdateInputError, shaped like the input itself.
type NotificationSettingUpdateInputError ¶
type NotificationSettingUpdateInputError struct {
// Whose preference it is.
AccountID *rigerr.FieldError `json:"accountId,omitempty"`
// Null is the default for the channel, which is the second step of the
// resolution.
Kind *rigerr.FieldError `json:"kind,omitempty"`
// Which channel it answers for.
Channel *rigerr.FieldError `json:"channel,omitempty"`
// False writes no delivery row at all. Not the same as digest Off, which is
// about preferring to look rather than be told.
IsEnabled *rigerr.FieldError `json:"isEnabled,omitempty"`
// Immediate sends each on its own; anything else waits for the window and goes
// out as one message.
Digest *rigerr.FieldError `json:"digest,omitempty"`
// When to start delivering, in the account time zone. Null means all day. A
// window that wraps midnight is ordinary: 22:00 to 06:00 is how somebody says
// "not overnight".
ActiveFrom *rigerr.FieldError `json:"activeFrom,omitempty"`
// When to stop. Outside the window a delivery is held, not dropped: the inbox
// line exists either way, and a discarded copy makes the badge and the mailbox
// disagree.
ActiveUntil *rigerr.FieldError `json:"activeUntil,omitempty"`
// ISO weekdays, 1 being Monday. Empty means every day. An array rather than a
// bitmask because [1,2,3,4,5] is legible in a settings payload and 62 is not.
ActiveDays *rigerr.FieldError `json:"activeDays,omitempty"`
// Entity is a problem with the row as a whole rather than with one field: what
// the Entity rule said.
Entity *rigerr.FieldError `json:"entity,omitempty"`
}
NotificationSettingUpdateInputError says what was wrong with each field of a NotificationSettingUpdateInput.
Its shape is the input's shape, so a client can attach every message to the field it is about without matching on strings. A member is nil when that field was fine, and the whole value is nil when the input was. It is what the 422 carries.
func (*NotificationSettingUpdateInputError) Empty ¶
func (e *NotificationSettingUpdateInputError) Empty() bool
Empty reports whether anything went wrong. A validator that found nothing returns nil rather than one of these.
func (*NotificationSettingUpdateInputError) Error ¶
func (e *NotificationSettingUpdateInputError) Error() string
Error implements error. The sentence is for logs and for a person; the structure above is what a client acts on.
func (*NotificationSettingUpdateInputError) ErrorCode ¶
func (e *NotificationSettingUpdateInputError) ErrorCode() rigerr.Code
ErrorCode implements rigerr.Coder: the request was understood and its content is what is wrong, which is 422 and not 400.
func (*NotificationSettingUpdateInputError) ErrorFields ¶
func (e *NotificationSettingUpdateInputError) ErrorFields() any
ErrorFields implements rigerr.FieldReporter, which is how the HTTP layer finds this and answers with it rather than with prose.
type NotificationSettingUpdateValidator ¶
type NotificationSettingUpdateValidator struct {
// Whose preference it is.
AccountID func(ctx context.Context, c *NotificationSettingValidatorContext, value uuid.UUID) error
// Null is the default for the channel, which is the second step of the
// resolution.
Kind func(ctx context.Context, c *NotificationSettingValidatorContext, value *string) error
// Which channel it answers for.
Channel func(ctx context.Context, c *NotificationSettingValidatorContext, value NotificationChannel) error
// False writes no delivery row at all. Not the same as digest Off, which is
// about preferring to look rather than be told.
IsEnabled func(ctx context.Context, c *NotificationSettingValidatorContext, value bool) error
// Immediate sends each on its own; anything else waits for the window and goes
// out as one message.
Digest func(ctx context.Context, c *NotificationSettingValidatorContext, value NotificationDigest) error
// When to start delivering, in the account time zone. Null means all day. A
// window that wraps midnight is ordinary: 22:00 to 06:00 is how somebody says
// "not overnight".
ActiveFrom func(ctx context.Context, c *NotificationSettingValidatorContext, value *time.Time) error
// When to stop. Outside the window a delivery is held, not dropped: the inbox
// line exists either way, and a discarded copy makes the badge and the mailbox
// disagree.
ActiveUntil func(ctx context.Context, c *NotificationSettingValidatorContext, value *time.Time) error
// ISO weekdays, 1 being Monday. Empty means every day. An array rather than a
// bitmask because [1,2,3,4,5] is legible in a settings payload and 62 is not.
ActiveDays func(ctx context.Context, c *NotificationSettingValidatorContext, value []int16) error
// Entity runs after the per-field hooks, for a rule that is about the row
// rather than about one column.
Entity func(ctx context.Context, c *NotificationSettingValidatorContext) error
}
NotificationSettingUpdateValidator is the rules for changing one that already exists: what the schema cannot express.
One optional function per field this operation can set, so the set of fields is the set of rules that could apply — a column an update cannot touch has no hook here to write by mistake. A nil one is skipped. Every configured hook runs, because a rule that fails should not hide the next one, so a request reports everything wrong with it.
A hook returns a FieldError to attach the message to a specific field, or any other error to fail the request outright.
func (NotificationSettingUpdateValidator) RunUpdate ¶
func (v NotificationSettingUpdateValidator) RunUpdate(ctx context.Context, claims tenancy.Claims, i *NotificationSettingUpdateInput, prev *NotificationSetting) error
RunUpdate implements [dbhook.UpdateValidator]: it runs the service's rules against the row this update would produce, with the row as it was available for a rule about the change itself.
type NotificationSettingValidatorContext ¶
type NotificationSettingValidatorContext struct {
// Values is the intended end state.
Values NotificationSetting
// Claims are who is asking. They are a value rather than something to fetch
// from the context because a rule that has to look them up is a rule that can
// forget to, and because there is no case where they are absent: a write
// without a caller is refused by the repository before any rule runs.
Claims tenancy.Claims
// contains filtered or unexported fields
}
NotificationSettingValidatorContext is what a rule sees.
Values is the row as it will be if this goes through — merged from the previous state on an update, so every field is set whether or not the request mentioned it. That is the point: "ends after starts" cannot be answered from a request that only carried one of them.
func (*NotificationSettingValidatorContext) AccountIDChanged ¶
func (c *NotificationSettingValidatorContext) AccountIDChanged() bool
AccountIDChanged reports whether this request set account_id.
func (*NotificationSettingValidatorContext) ActiveDaysChanged ¶
func (c *NotificationSettingValidatorContext) ActiveDaysChanged() bool
ActiveDaysChanged reports whether this request set active_days.
func (*NotificationSettingValidatorContext) ActiveFromChanged ¶
func (c *NotificationSettingValidatorContext) ActiveFromChanged() bool
ActiveFromChanged reports whether this request set active_from.
func (*NotificationSettingValidatorContext) ActiveUntilChanged ¶
func (c *NotificationSettingValidatorContext) ActiveUntilChanged() bool
ActiveUntilChanged reports whether this request set active_until.
func (*NotificationSettingValidatorContext) Changed ¶
func (c *NotificationSettingValidatorContext) Changed(column string) bool
Changed reports whether this request carried a new value for a column.
It is what keeps an expensive rule from running on every update: a check that reaches another service to confirm a reference only needs to run when the reference actually moved. On a create everything is changed, because everything is new.
func (*NotificationSettingValidatorContext) ChannelChanged ¶
func (c *NotificationSettingValidatorContext) ChannelChanged() bool
ChannelChanged reports whether this request set channel.
func (*NotificationSettingValidatorContext) DigestChanged ¶
func (c *NotificationSettingValidatorContext) DigestChanged() bool
DigestChanged reports whether this request set digest.
func (*NotificationSettingValidatorContext) IsEnabledChanged ¶
func (c *NotificationSettingValidatorContext) IsEnabledChanged() bool
IsEnabledChanged reports whether this request set is_enabled.
func (*NotificationSettingValidatorContext) IsUpdate ¶
func (c *NotificationSettingValidatorContext) IsUpdate() bool
IsUpdate reports whether there was a row before this.
func (*NotificationSettingValidatorContext) KindChanged ¶
func (c *NotificationSettingValidatorContext) KindChanged() bool
KindChanged reports whether this request set kind.
func (*NotificationSettingValidatorContext) Previous ¶
func (c *NotificationSettingValidatorContext) Previous() NotificationSetting
Previous is the row as it was, and the zero value on a create. Check IsUpdate before reading it.
type NotificationState ¶
type NotificationState string
Where a notification is in its life. Nothing here says anything was sent: that is the delivery table's business.
const ( // The audience has not been computed yet. Every notification starts here, and // a scheduled one waits here until it is due. NotificationStatePending NotificationState = "Pending" // The audience was computed and the inbox lines exist. It does not mean // anything was sent — a notification with an inbox line and no mail is a // working notification. NotificationStateResolved NotificationState = "Resolved" // It never will be resolved: the row it was about was retired before its time // came. NotificationStateCancelled NotificationState = "Cancelled" )
The values of NotificationState.
func ParseNotificationState ¶
func ParseNotificationState(s string) (NotificationState, bool)
ParseNotificationState reads a value, accepting any casing and surrounding space.
Normalization uses it, so "IN_PROGRESS" from one client and "in_progress" from another mean the same thing rather than one of them being a validation failure nobody can explain. The spelling still has to be the label’s: a value is a name the database knows, not a phrase.
func (NotificationState) String ¶
func (v NotificationState) String() string
String implements fmt.Stringer.
func (NotificationState) Valid ¶
func (v NotificationState) Valid() bool
Valid reports whether the value is one the database will accept.
type NotificationUpdateInput ¶
type NotificationUpdateInput struct {
// What happened, as the application names it. Narrow it to an enum of your own
// to get a switch the compiler can see.
Kind patch.Optional[string] `json:"kind"`
// Resolved means the audience was determined and the inbox lines exist. It
// does not mean anything was sent.
State patch.Optional[NotificationState] `json:"state"`
// When this is due. now() is the ordinary case; a scheduled notification is
// the same row with a later time, which is the only difference between the
// two.
DeliverAt patch.Optional[time.Time] `json:"deliverAt"`
// When the audience was computed. Null while the notification is still pending
// or was cancelled.
ResolvedAt patch.Nullable[time.Time] `json:"resolvedAt"`
// What a template needs beyond the linked row. Give it a Go type with the
// go_type key if it has a shape.
Payload patch.Optional[json.RawMessage] `json:"payload"`
// What collapses several of these into one inbox line, decided when the
// announcement was written and copied onto the line when the audience is
// resolved.
GroupKey patch.Nullable[string] `json:"groupKey"`
// A recipient list captured at write time, and the one exception to computing
// the audience late. Null is the ordinary case; a list here skips the question
// entirely, for an audience that genuinely cannot be re-derived.
AccountIds patch.Nullable[[]uuid.UUID] `json:"accountIds"`
// When a dispatcher took this to resolve. Past notifications.claim_ttl another
// may, which is what makes a crashed process recoverable.
ClaimedAt patch.Nullable[time.Time] `json:"claimedAt"`
// Which process holds the lease, so a stuck one traces to a pod rather than to
// a mystery.
ClaimedBy patch.Nullable[uuid.UUID] `json:"claimedBy"`
// How many times resolving this has been attempted.
Attempts patch.Optional[int] `json:"attempts"`
}
NotificationUpdateInput is what changing a Notification takes.
A field left out is untouched. A nullable field set to null is cleared — which is why the two wrappers differ: a column that cannot hold null has no way to be given one, so clearing it is a compile error rather than a rejection at runtime. Immutable fields are not here at all.
func (NotificationUpdateInput) Merged ¶
func (i NotificationUpdateInput) Merged(prev *Notification) Notification
Merged is the row as it will be once this update is applied.
It is what validation runs against, and the reason it exists: a rule spanning two fields cannot be checked from a partial request. "Ends after starts" is unanswerable when only one of them was sent.
It returns a copy. The input keeps its patches, so the repository still writes only the columns that were actually given.
func (*NotificationUpdateInput) Normalize ¶
func (i *NotificationUpdateInput) Normalize()
Normalize tidies the fields this request actually carries.
It does not fill in the ones it does not: the repository writes exactly the columns that were sent, and filling them here would turn every update into a write of every column — so two requests changing different fields of one row would start overwriting each other instead of composing.
func (*NotificationUpdateInput) Validate ¶
func (i *NotificationUpdateInput) Validate(prev *Notification) error
Validate checks the row this update would produce.
Against the merged state, not the request: a length rule on a field nobody sent still has to hold, and a rule about two fields needs both.
What comes back is a *NotificationUpdateInputError, shaped like the input itself.
type NotificationUpdateInputError ¶
type NotificationUpdateInputError struct {
// What happened, as the application names it. Narrow it to an enum of your own
// to get a switch the compiler can see.
Kind *rigerr.FieldError `json:"kind,omitempty"`
// Resolved means the audience was determined and the inbox lines exist. It
// does not mean anything was sent.
State *rigerr.FieldError `json:"state,omitempty"`
// When this is due. now() is the ordinary case; a scheduled notification is
// the same row with a later time, which is the only difference between the
// two.
DeliverAt *rigerr.FieldError `json:"deliverAt,omitempty"`
// When the audience was computed. Null while the notification is still pending
// or was cancelled.
ResolvedAt *rigerr.FieldError `json:"resolvedAt,omitempty"`
// What a template needs beyond the linked row. Give it a Go type with the
// go_type key if it has a shape.
Payload *rigerr.FieldError `json:"payload,omitempty"`
// What collapses several of these into one inbox line, decided when the
// announcement was written and copied onto the line when the audience is
// resolved.
GroupKey *rigerr.FieldError `json:"groupKey,omitempty"`
// A recipient list captured at write time, and the one exception to computing
// the audience late. Null is the ordinary case; a list here skips the question
// entirely, for an audience that genuinely cannot be re-derived.
AccountIds *rigerr.FieldError `json:"accountIds,omitempty"`
// When a dispatcher took this to resolve. Past notifications.claim_ttl another
// may, which is what makes a crashed process recoverable.
ClaimedAt *rigerr.FieldError `json:"claimedAt,omitempty"`
// Which process holds the lease, so a stuck one traces to a pod rather than to
// a mystery.
ClaimedBy *rigerr.FieldError `json:"claimedBy,omitempty"`
// How many times resolving this has been attempted.
Attempts *rigerr.FieldError `json:"attempts,omitempty"`
// Entity is a problem with the row as a whole rather than with one field: what
// the Entity rule said.
Entity *rigerr.FieldError `json:"entity,omitempty"`
}
NotificationUpdateInputError says what was wrong with each field of a NotificationUpdateInput.
Its shape is the input's shape, so a client can attach every message to the field it is about without matching on strings. A member is nil when that field was fine, and the whole value is nil when the input was. It is what the 422 carries.
func (*NotificationUpdateInputError) Empty ¶
func (e *NotificationUpdateInputError) Empty() bool
Empty reports whether anything went wrong. A validator that found nothing returns nil rather than one of these.
func (*NotificationUpdateInputError) Error ¶
func (e *NotificationUpdateInputError) Error() string
Error implements error. The sentence is for logs and for a person; the structure above is what a client acts on.
func (*NotificationUpdateInputError) ErrorCode ¶
func (e *NotificationUpdateInputError) ErrorCode() rigerr.Code
ErrorCode implements rigerr.Coder: the request was understood and its content is what is wrong, which is 422 and not 400.
func (*NotificationUpdateInputError) ErrorFields ¶
func (e *NotificationUpdateInputError) ErrorFields() any
ErrorFields implements rigerr.FieldReporter, which is how the HTTP layer finds this and answers with it rather than with prose.
type NotificationUpdateValidator ¶
type NotificationUpdateValidator struct {
// What happened, as the application names it. Narrow it to an enum of your own
// to get a switch the compiler can see.
Kind func(ctx context.Context, c *NotificationValidatorContext, value string) error
// Resolved means the audience was determined and the inbox lines exist. It
// does not mean anything was sent.
State func(ctx context.Context, c *NotificationValidatorContext, value NotificationState) error
// When this is due. now() is the ordinary case; a scheduled notification is
// the same row with a later time, which is the only difference between the
// two.
DeliverAt func(ctx context.Context, c *NotificationValidatorContext, value time.Time) error
// When the audience was computed. Null while the notification is still pending
// or was cancelled.
ResolvedAt func(ctx context.Context, c *NotificationValidatorContext, value *time.Time) error
// What a template needs beyond the linked row. Give it a Go type with the
// go_type key if it has a shape.
Payload func(ctx context.Context, c *NotificationValidatorContext, value json.RawMessage) error
// What collapses several of these into one inbox line, decided when the
// announcement was written and copied onto the line when the audience is
// resolved.
GroupKey func(ctx context.Context, c *NotificationValidatorContext, value *string) error
// A recipient list captured at write time, and the one exception to computing
// the audience late. Null is the ordinary case; a list here skips the question
// entirely, for an audience that genuinely cannot be re-derived.
AccountIds func(ctx context.Context, c *NotificationValidatorContext, value []uuid.UUID) error
// When a dispatcher took this to resolve. Past notifications.claim_ttl another
// may, which is what makes a crashed process recoverable.
ClaimedAt func(ctx context.Context, c *NotificationValidatorContext, value *time.Time) error
// Which process holds the lease, so a stuck one traces to a pod rather than to
// a mystery.
ClaimedBy func(ctx context.Context, c *NotificationValidatorContext, value *uuid.UUID) error
// How many times resolving this has been attempted.
Attempts func(ctx context.Context, c *NotificationValidatorContext, value int) error
// Entity runs after the per-field hooks, for a rule that is about the row
// rather than about one column.
Entity func(ctx context.Context, c *NotificationValidatorContext) error
}
NotificationUpdateValidator is the rules for changing one that already exists: what the schema cannot express.
One optional function per field this operation can set, so the set of fields is the set of rules that could apply — a column an update cannot touch has no hook here to write by mistake. A nil one is skipped. Every configured hook runs, because a rule that fails should not hide the next one, so a request reports everything wrong with it.
A hook returns a FieldError to attach the message to a specific field, or any other error to fail the request outright.
func (NotificationUpdateValidator) RunUpdate ¶
func (v NotificationUpdateValidator) RunUpdate(ctx context.Context, claims tenancy.Claims, i *NotificationUpdateInput, prev *Notification) error
RunUpdate implements [dbhook.UpdateValidator]: it runs the service's rules against the row this update would produce, with the row as it was available for a rule about the change itself.
type NotificationValidatorContext ¶
type NotificationValidatorContext struct {
// Values is the intended end state.
Values Notification
// Claims are who is asking. They are a value rather than something to fetch
// from the context because a rule that has to look them up is a rule that can
// forget to, and because there is no case where they are absent: a write
// without a caller is refused by the repository before any rule runs.
Claims tenancy.Claims
// contains filtered or unexported fields
}
NotificationValidatorContext is what a rule sees.
Values is the row as it will be if this goes through — merged from the previous state on an update, so every field is set whether or not the request mentioned it. That is the point: "ends after starts" cannot be answered from a request that only carried one of them.
func (*NotificationValidatorContext) AccountIdsChanged ¶
func (c *NotificationValidatorContext) AccountIdsChanged() bool
AccountIdsChanged reports whether this request set account_ids.
func (*NotificationValidatorContext) AttemptsChanged ¶
func (c *NotificationValidatorContext) AttemptsChanged() bool
AttemptsChanged reports whether this request set attempts.
func (*NotificationValidatorContext) Changed ¶
func (c *NotificationValidatorContext) Changed(column string) bool
Changed reports whether this request carried a new value for a column.
It is what keeps an expensive rule from running on every update: a check that reaches another service to confirm a reference only needs to run when the reference actually moved. On a create everything is changed, because everything is new.
func (*NotificationValidatorContext) ClaimedAtChanged ¶
func (c *NotificationValidatorContext) ClaimedAtChanged() bool
ClaimedAtChanged reports whether this request set claimed_at.
func (*NotificationValidatorContext) ClaimedByChanged ¶
func (c *NotificationValidatorContext) ClaimedByChanged() bool
ClaimedByChanged reports whether this request set claimed_by.
func (*NotificationValidatorContext) DeliverAtChanged ¶
func (c *NotificationValidatorContext) DeliverAtChanged() bool
DeliverAtChanged reports whether this request set deliver_at.
func (*NotificationValidatorContext) GroupKeyChanged ¶
func (c *NotificationValidatorContext) GroupKeyChanged() bool
GroupKeyChanged reports whether this request set group_key.
func (*NotificationValidatorContext) IsUpdate ¶
func (c *NotificationValidatorContext) IsUpdate() bool
IsUpdate reports whether there was a row before this.
func (*NotificationValidatorContext) KindChanged ¶
func (c *NotificationValidatorContext) KindChanged() bool
KindChanged reports whether this request set kind.
func (*NotificationValidatorContext) PayloadChanged ¶
func (c *NotificationValidatorContext) PayloadChanged() bool
PayloadChanged reports whether this request set payload.
func (*NotificationValidatorContext) Previous ¶
func (c *NotificationValidatorContext) Previous() Notification
Previous is the row as it was, and the zero value on a create. Check IsUpdate before reading it.
func (*NotificationValidatorContext) ResolvedAtChanged ¶
func (c *NotificationValidatorContext) ResolvedAtChanged() bool
ResolvedAtChanged reports whether this request set resolved_at.
func (*NotificationValidatorContext) StateChanged ¶
func (c *NotificationValidatorContext) StateChanged() bool
StateChanged reports whether this request set state.