Help us shape the future of Go! Take the 2026 Go Developer Survey

notifymodel

package
v0.6.7 Latest Latest
Warning

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

Go to latest
Published: Oct 3, 2026 License: MIT Imports: 8 Imported by: 0

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

View Source
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.

View Source
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.

View Source
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.

View Source
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.

View Source
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.

View Source
const TableNotification = "rig_notification"

TableNotification is the table this entity is stored in.

View Source
const TableNotificationDelivery = "rig_notification_delivery"

TableNotificationDelivery is the table this entity is stored in.

View Source
const TableNotificationDevice = "rig_notification_device"

TableNotificationDevice is the table this entity is stored in.

View Source
const TableNotificationRecipient = "rig_notification_recipient"

TableNotificationRecipient is the table this entity is stored in.

View Source
const TableNotificationSetting = "rig_notification_setting"

TableNotificationSetting is the table this entity is stored in.

Variables

AllNotificationChannel is every value, in declaration order.

AllNotificationDeliveryState is every value, in declaration order.

AllNotificationDigest is every value, in declaration order.

AllNotificationState is every value, in declaration order.

View Source
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.

View Source
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.

View Source
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.

View Source
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.

View Source
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

Empty reports whether anything went wrong. A validator that found nothing returns nil rather than one of these.

func (*NotificationCreateInputError) Error

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

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

type NotificationDeleteInput struct {
	// ID is the row to delete.
	ID uuid.UUID `json:"id"`
}

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

Empty reports whether anything went wrong. A validator that found nothing returns nil rather than one of these.

func (*NotificationDeliveryCreateInputError) Error

Error implements error. The sentence is for logs and for a person; the structure above is what a client acts on.

func (*NotificationDeliveryCreateInputError) ErrorCode

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

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

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

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

Empty reports whether anything went wrong. A validator that found nothing returns nil rather than one of these.

func (*NotificationDeliveryUpdateInputError) Error

Error implements error. The sentence is for logs and for a person; the structure above is what a client acts on.

func (*NotificationDeliveryUpdateInputError) ErrorCode

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

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

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

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

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

Empty reports whether anything went wrong. A validator that found nothing returns nil rather than one of these.

func (*NotificationDeviceCreateInputError) Error

Error implements error. The sentence is for logs and for a person; the structure above is what a client acts on.

func (*NotificationDeviceCreateInputError) ErrorCode

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

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

type NotificationDeviceDeleteInput struct {
	// ID is the row to delete.
	ID uuid.UUID `json:"id"`
}

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

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

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

Empty reports whether anything went wrong. A validator that found nothing returns nil rather than one of these.

func (*NotificationDeviceUpdateInputError) Error

Error implements error. The sentence is for logs and for a person; the structure above is what a client acts on.

func (*NotificationDeviceUpdateInputError) ErrorCode

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

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

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

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

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

Empty reports whether anything went wrong. A validator that found nothing returns nil rather than one of these.

func (*NotificationRecipientCreateInputError) Error

Error implements error. The sentence is for logs and for a person; the structure above is what a client acts on.

func (*NotificationRecipientCreateInputError) ErrorCode

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

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

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

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

Empty reports whether anything went wrong. A validator that found nothing returns nil rather than one of these.

func (*NotificationRecipientUpdateInputError) Error

Error implements error. The sentence is for logs and for a person; the structure above is what a client acts on.

func (*NotificationRecipientUpdateInputError) ErrorCode

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

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

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

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

IsUpdate reports whether there was a row before this.

func (*NotificationRecipientValidatorContext) KindChanged

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

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

Empty reports whether anything went wrong. A validator that found nothing returns nil rather than one of these.

func (*NotificationSettingCreateInputError) Error

Error implements error. The sentence is for logs and for a person; the structure above is what a client acts on.

func (*NotificationSettingCreateInputError) ErrorCode

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

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

type NotificationSettingDeleteInput struct {
	// ID is the row to delete.
	ID uuid.UUID `json:"id"`
}

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

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

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

Empty reports whether anything went wrong. A validator that found nothing returns nil rather than one of these.

func (*NotificationSettingUpdateInputError) Error

Error implements error. The sentence is for logs and for a person; the structure above is what a client acts on.

func (*NotificationSettingUpdateInputError) ErrorCode

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

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

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

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

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

Empty reports whether anything went wrong. A validator that found nothing returns nil rather than one of these.

func (*NotificationUpdateInputError) Error

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

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

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.

Jump to

Keyboard shortcuts

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