Documentation
¶
Overview ¶
Package authmodel is the Go over rig_account: the row a tenant's membership is, its write inputs, and the two enums its columns are.
It is a module of its own, and that is the whole point of it. The table belongs to rig's authentication schema and the obvious home would be github.com/simonjanss/rig/auth — but `auth.expose: [rig_account]` does not require `auth.enabled`. A project may read account rows without using rig's authentication at all, and for that project a package under the auth module would mean argon2, OAuth and x/crypto in its go.mod for the sake of a struct. So the schema's Go and the machinery over it are separate things to depend on.
Nothing here does anything. There are no queries, no transport and no policy — those are auth's, and a project that wants them imports that module too, alongside this one rather than instead of it.
Only the row and its inputs. The filter grammar stays with the project, because a table of its own that points at an account — an assignee, an author, a reviewer — puts a member on AccountFilter, and that makes the type the project's even though the row is not.
An application does not usually name this package: `model.Account` in a generated project is an alias for Account.
**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 here. That is the trade rig's hand-written routes have always made, and RIG3260 reports it rather than leaving it to be found in a response body.
Index ¶
Constants ¶
const ( ColumnAccountID = "id" ColumnAccountTenantID = "tenant_id" ColumnAccountIdentityID = "identity_id" ColumnAccountCreatedAt = "created_at" ColumnAccountCreatedByAccountID = "created_by_account_id" ColumnAccountUpdatedAt = "updated_at" ColumnAccountUpdatedByAccountID = "updated_by_account_id" ColumnAccountDeletedAt = "deleted_at" ColumnAccountDeletedByAccountID = "deleted_by_account_id" ColumnAccountKind = "kind" ColumnAccountRole = "role" ColumnAccountEmailAddress = "email_address" ColumnAccountDisplayName = "display_name" ColumnAccountTimeZone = "time_zone" ColumnAccountIsActive = "is_active" ColumnAccountCreatedByAPIKeyID = "created_by_api_key_id" ColumnAccountUpdatedByAPIKeyID = "updated_by_api_key_id" ColumnAccountDeletedByAPIKeyID = "deleted_by_api_key_id" )
Column names for rig_account, so nothing has to spell one out.
const TableAccount = "rig_account"
TableAccount is the table this entity is stored in.
Variables ¶
var AccountColumns = []string{"id", "tenant_id", "identity_id", "created_at", "created_by_account_id", "updated_at", "updated_by_account_id", "deleted_at", "deleted_by_account_id", "kind", "role", "email_address", "display_name", "time_zone", "is_active", "created_by_api_key_id", "updated_by_api_key_id", "deleted_by_api_key_id"}
AccountColumns is every column, in the order the row is scanned.
var AllAccountKind = []AccountKind{AccountKindPerson, AccountKindService}
AllAccountKind is every value, in declaration order.
var AllAccountRoleLevel = []AccountRoleLevel{AccountRoleLevelOwner, AccountRoleLevelAdmin, AccountRoleLevelBasic}
AllAccountRoleLevel is every value, in declaration order.
Functions ¶
This section is empty.
Types ¶
type Account ¶
type Account 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 person this account belongs to, or null for a service account, which is
// nobody.
IdentityID *uuid.UUID `json:"identityId,omitempty"`
// 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"`
// When this row was last updated. Set automatically on every update.
UpdatedAt *time.Time `json:"updatedAt,omitempty"`
// Account that last updated this row, taken from the request's claims.
UpdatedByAccountID *uuid.UUID `json:"updatedByAccountId,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"`
// Whether this is a person or a service account an integration acts as.
Kind AccountKind `json:"kind"`
// The coarse level in this tenant: Owner, Admin, or Basic. Somebody can be an
// Owner here and Basic elsewhere.
Role AccountRoleLevel `json:"role"`
// A copy of the identity's address, kept here so listing accounts is one
// query. For a service account it is a label nobody signs in with.
EmailAddress string `json:"emailAddress"`
// What to call the person in this tenant.
DisplayName string `json:"displayName"`
// IANA name, for example Europe/Stockholm. Null means UTC.
TimeZone *string `json:"timeZone,omitempty"`
// Whether the account may be used. A disabled account is refused with 403, not
// 401.
IsActive bool `json:"isActive"`
// API key this row was created through, when it was an integration rather than
// a person.
CreatedByAPIKeyID *uuid.UUID `json:"createdByApiKeyId,omitempty"`
// API key this row was last updated through, when it was an integration rather
// than a person.
UpdatedByAPIKeyID *uuid.UUID `json:"updatedByApiKeyId,omitempty"`
// API key this row was soft-deleted through, when it was an integration rather
// than a person.
DeletedByAPIKeyID *uuid.UUID `json:"deletedByApiKeyId,omitempty"`
}
One person inside one tenant. The person is the identity; this is who they are here.
It is the one definition of a Account: 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 AccountCreateInput ¶
type AccountCreateInput struct {
// What to call the person in this tenant.
DisplayName string `json:"displayName"`
// IANA name, for example Europe/Stockholm. Null means UTC.
TimeZone *string `json:"timeZone"`
}
AccountCreateInput is what creating a Account takes.
The identifier, the tenant, and the audit columns are absent: those are stamped by the repository from the request's claims.
func (*AccountCreateInput) Normalize ¶
func (i *AccountCreateInput) 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 (*AccountCreateInput) Validate ¶
func (i *AccountCreateInput) 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 *AccountCreateInputError, shaped like the input itself.
type AccountCreateInputError ¶
type AccountCreateInputError struct {
// What to call the person in this tenant.
DisplayName *rigerr.FieldError `json:"displayName,omitempty"`
// IANA name, for example Europe/Stockholm. Null means UTC.
TimeZone *rigerr.FieldError `json:"timeZone,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"`
}
AccountCreateInputError says what was wrong with each field of a AccountCreateInput.
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 (*AccountCreateInputError) Empty ¶
func (e *AccountCreateInputError) Empty() bool
Empty reports whether anything went wrong. A validator that found nothing returns nil rather than one of these.
func (*AccountCreateInputError) Error ¶
func (e *AccountCreateInputError) Error() string
Error implements error. The sentence is for logs and for a person; the structure above is what a client acts on.
func (*AccountCreateInputError) ErrorCode ¶
func (e *AccountCreateInputError) 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 (*AccountCreateInputError) ErrorFields ¶
func (e *AccountCreateInputError) ErrorFields() any
ErrorFields implements rigerr.FieldReporter, which is how the HTTP layer finds this and answers with it rather than with prose.
type AccountCreateValidator ¶
type AccountCreateValidator struct {
// What to call the person in this tenant.
DisplayName func(ctx context.Context, c *AccountValidatorContext, value string) error
// IANA name, for example Europe/Stockholm. Null means UTC.
TimeZone func(ctx context.Context, c *AccountValidatorContext, value *string) 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 *AccountValidatorContext) error
}
AccountCreateValidator is the rules for bringing a Account 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 (AccountCreateValidator) RunCreate ¶
func (v AccountCreateValidator) RunCreate(ctx context.Context, claims tenancy.Claims, i *AccountCreateInput) error
RunCreate implements [dbhook.CreateValidator]: it runs the service's rules against the row this input would produce.
The generated checks are not repeated here. The repository runs Normalize and Validate first, so by the time a hook sees the input it is tidy and the schema is satisfied — which is what lets a hook be about the business rather than about NOT NULL.
type AccountDeleteInput ¶
type AccountDeleteInput 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"`
}
AccountDeleteInput is what deleting a Account takes.
type AccountKind ¶
type AccountKind string
What an account is.
const ( // Somebody who signs in. They have an identity, and the identity has the // password. AccountKindPerson AccountKind = "Person" // What an integration's key acts as. It has no identity, so there is nothing // to sign in with. AccountKindService AccountKind = "Service" )
The values of AccountKind.
func ParseAccountKind ¶
func ParseAccountKind(s string) (AccountKind, bool)
ParseAccountKind 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 (AccountKind) Valid ¶
func (v AccountKind) Valid() bool
Valid reports whether the value is one the database will accept.
type AccountRoleLevel ¶
type AccountRoleLevel string
The coarse level an account holds in one tenant.
const ( // May do anything, including the things that end the tenant. AccountRoleLevelOwner AccountRoleLevel = "Owner" // Administers the tenant without the decisions that are the owner's to make. AccountRoleLevelAdmin AccountRoleLevel = "Admin" // Gets on with the work. AccountRoleLevelBasic AccountRoleLevel = "Basic" )
The values of AccountRoleLevel.
func ParseAccountRoleLevel ¶
func ParseAccountRoleLevel(s string) (AccountRoleLevel, bool)
ParseAccountRoleLevel 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 (AccountRoleLevel) String ¶
func (v AccountRoleLevel) String() string
String implements fmt.Stringer.
func (AccountRoleLevel) Valid ¶
func (v AccountRoleLevel) Valid() bool
Valid reports whether the value is one the database will accept.
type AccountUpdateInput ¶
type AccountUpdateInput struct {
// What to call the person in this tenant.
DisplayName patch.Optional[string] `json:"displayName"`
// IANA name, for example Europe/Stockholm. Null means UTC.
TimeZone patch.Nullable[string] `json:"timeZone"`
}
AccountUpdateInput is what changing a Account 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 (AccountUpdateInput) Merged ¶
func (i AccountUpdateInput) Merged(prev *Account) Account
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 (*AccountUpdateInput) Normalize ¶
func (i *AccountUpdateInput) 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 (*AccountUpdateInput) Validate ¶
func (i *AccountUpdateInput) Validate(prev *Account) 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 *AccountUpdateInputError, shaped like the input itself.
type AccountUpdateInputError ¶
type AccountUpdateInputError struct {
// What to call the person in this tenant.
DisplayName *rigerr.FieldError `json:"displayName,omitempty"`
// IANA name, for example Europe/Stockholm. Null means UTC.
TimeZone *rigerr.FieldError `json:"timeZone,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"`
}
AccountUpdateInputError says what was wrong with each field of a AccountUpdateInput.
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 (*AccountUpdateInputError) Empty ¶
func (e *AccountUpdateInputError) Empty() bool
Empty reports whether anything went wrong. A validator that found nothing returns nil rather than one of these.
func (*AccountUpdateInputError) Error ¶
func (e *AccountUpdateInputError) Error() string
Error implements error. The sentence is for logs and for a person; the structure above is what a client acts on.
func (*AccountUpdateInputError) ErrorCode ¶
func (e *AccountUpdateInputError) 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 (*AccountUpdateInputError) ErrorFields ¶
func (e *AccountUpdateInputError) ErrorFields() any
ErrorFields implements rigerr.FieldReporter, which is how the HTTP layer finds this and answers with it rather than with prose.
type AccountUpdateValidator ¶
type AccountUpdateValidator struct {
// What to call the person in this tenant.
DisplayName func(ctx context.Context, c *AccountValidatorContext, value string) error
// IANA name, for example Europe/Stockholm. Null means UTC.
TimeZone func(ctx context.Context, c *AccountValidatorContext, value *string) 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 *AccountValidatorContext) error
}
AccountUpdateValidator 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 (AccountUpdateValidator) RunRestore ¶
func (v AccountUpdateValidator) RunRestore(ctx context.Context, claims tenancy.Claims, i *AccountUpdateInput, prev *Account) error
RunRestore implements [dbhook.RestoreValidator]: it runs the service's rules against the row this restore would bring back.
Every rule runs, not only the ones whose field the request mentioned. The row was not live, so nothing about it has been checked against the world it is returning to.
func (AccountUpdateValidator) RunUpdate ¶
func (v AccountUpdateValidator) RunUpdate(ctx context.Context, claims tenancy.Claims, i *AccountUpdateInput, prev *Account) error
RunUpdate implements [dbhook.UpdateValidator]: it runs the service's rules against the row this update would produce, with the row as it was available for a rule about the change itself.
type AccountValidatorContext ¶
type AccountValidatorContext struct {
// Values is the intended end state.
Values Account
// 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
}
AccountValidatorContext 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 (*AccountValidatorContext) Changed ¶
func (c *AccountValidatorContext) 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 (*AccountValidatorContext) DisplayNameChanged ¶
func (c *AccountValidatorContext) DisplayNameChanged() bool
DisplayNameChanged reports whether this request set display_name.
func (*AccountValidatorContext) IsUpdate ¶
func (c *AccountValidatorContext) IsUpdate() bool
IsUpdate reports whether there was a row before this.
func (*AccountValidatorContext) Previous ¶
func (c *AccountValidatorContext) Previous() Account
Previous is the row as it was, and the zero value on a create. Check IsUpdate before reading it.
func (*AccountValidatorContext) TimeZoneChanged ¶
func (c *AccountValidatorContext) TimeZoneChanged() bool
TimeZoneChanged reports whether this request set time_zone.