Documentation
¶
Overview ¶
Package compile turns a Postgres schema and its table configuration into the frozen document every generator reads.
The pipeline is a sequence of pure functions. Introspection — the one impure step — happens elsewhere and hands its result in by value, so the whole compiler runs from a JSON fixture with no database and no container. That is what makes the bulk of rig's test suite finish in under a second, and it is the reason Compile takes a schema rather than a connection.
Every stage appends to one diagnostic list rather than returning on the first problem, so a developer who has made five mistakes learns about all five from one run.
Index ¶
- Constants
- Variables
- func ApplyConfig(api ir.API, schema ir.Schema, set *tableconf.Set, opt ConfigOptions) (ir.API, ir.Schema, diag.List)
- func Bookkeeping(p *project.Project) []string
- func Compile(raw ir.Schema, set *tableconf.Set, opt Options) (*ir.Document, diag.List)
- func Expand(api ir.API, opt ExpandOptions) (ir.API, diag.List)
- func FileRole(column string) (role string, ok bool)
- func Freeze(api ir.API, schema ir.Schema, meta Meta) (*ir.Document, diag.List)
- func IsManagedColumn(table, name string) bool
- func Normalize(raw ir.Schema, opt NormalizeOptions) (ir.Schema, diag.List)
- func NotificationTables() []string
- func Project(schema ir.Schema, opt ProjectOptions) (ir.API, diag.List)
- func Reserved(p *project.Project, foundation []string, table string) (why string, escapable bool)
- func ShippedModelTables() []string
- func SnapshotFromAtColumn(table string) string
- func SnapshotFromIDColumn(table string) string
- func SyncServicePublication(name string) bool
- func Validate(doc *ir.Document, set *tableconf.Set, p *project.Project) diag.List
- type ConfigOptions
- type ErrorCode
- type ExpandOptions
- type Meta
- type NormalizeOptions
- type Options
- type ProjectOptions
Constants ¶
const ( EnumErrorCode = "ErrorCode" ObjectError = "Error" ObjectPagination = "Pagination" // ObjectRigFile is what an upload answers with and what a file column points // at. It is spelled the way projecting [FileTable] would spell it, because // with `files.expose` set the projection produces exactly this name and one // of the two has to win — an exposed project with two structs for one row // would leave the upload method picking between them. ObjectRigFile = "RigFile" // ObjectAuthLogEntry is one recorded authentication event, as GET /auth/audit // answers it. // // Named after the log rather than after "audit", which the endpoint's path // uses: the audit log and its tables were removed in favour of snapshots, and // a builtin `AuditEntry` in every model package would read as that coming // back. This is authentication only — what happened to a credential, never // what happened to a row. ObjectAuthLogEntry = "AuthLogEntry" )
Names rig reserves for the shapes it injects. A resource or field that collides with one of these is renamed rather than silently overwriting it.
const ( DefaultLimit = 50 MaxLimit = 500 )
Pagination defaults. A limit is always applied: an unbounded list is a production incident waiting for the table to grow.
const ( // ColID is the primary key of every table rig exposes. ColID = "id" // ColTenantID scopes every generated query. ColTenantID = "tenant_id" ColCreatedAt = "created_at" ColCreatedBy = "created_by_account_id" ColUpdatedAt = "updated_at" ColUpdatedBy = "updated_by_account_id" // ColDeletedAt makes a table soft-deletable. ColDeletedAt = "deleted_at" ColDeletedBy = "deleted_by_account_id" // The key columns say which credential a change came through, where the // account columns say whose it was. // // They are worth having separately because an integration's account answers // "who" with a service account that may be shared between several keys: the // account tells you the import did it, and the key tells you which // integration and which credential to revoke. A change made by a person // signed in normally leaves them null. ColCreatedByKey = "created_by_api_key_id" ColUpdatedByKey = "updated_by_api_key_id" ColDeletedByKey = "deleted_by_api_key_id" // ColVersionType is one third of the snapshot triple. ColVersionType = "version_type" )
The columns rig recognizes by name. A table opts into a behavior by having the column: there is no configuration key for "this table is soft-deletable", because the database already answers that question and two sources of truth would eventually disagree.
const ( VersionOriginal = "Original" VersionSnapshot = "Snapshot" )
The two labels the version_type enum must carry.
const ( NotificationTable = "rig_notification" NotificationRecipientTable = "rig_notification_recipient" // And the three delivery carries: where a push can reach somebody, what // they want told to them, and one copy on its way. NotificationDeviceTable = "rig_notification_device" NotificationSettingTable = "rig_notification_setting" NotificationDeliveryTable = "rig_notification_delivery" )
The two managed tables the inbox is made of.
NotificationTable is what happened; NotificationRecipientTable is one person's line about it. They are separate because one person clearing their inbox must not change what anybody else sees, and because ten events collapse into one line on the second and never on the first.
const ( PresenceScopeColumn = "scope" PresenceTargetIDColumn = "target_id" )
PresenceScopeColumn and PresenceTargetIDColumn are the two the shape lets a subscriber narrow by.
A subscriber chooses how much presence traffic its screen pays for, and these are the two questions worth asking: which part of the application, and which row. A tenant-wide presence stream sends every heartbeat of every tab to every tab, which is quadratic in people; narrowed to one board it is linear in the people on it.
const ( MediaJSON = ir.MediaJSON MediaMultipart = ir.MediaMultipart MediaOctet = ir.MediaOctet )
The media types rig's own endpoints speak, spelled here for the compiler's own convenience.
They are ir.MediaJSON and its neighbours, and they live there because a generator reads them back out of the document and no generator imports this package. Writing them and reading them from two different constants is how the two halves of one fact drift apart.
const ( AccessScopeOwn = "own" AccessScopeAll = "all" )
The values of a table's `access.scope`, and of the reserved request parameter that widens it.
The parameter's own vocabulary lives in [tenancy.Scope], where the handler and the client both read it. These are here because the compiler has to name them in a diagnostic before any of that exists — and the two the parameter accepts are exported, because a specification has to list them as an enumeration and deriving that list a second time is how a document ends up offering a value the handler refuses.
const AccountTable = "rig_account"
AccountTable is the managed table an owner column has to point at. A tenant's membership row, and what every claim carries the identifier of.
const FileTable = "rig_file"
FileTable is the managed table every uploaded file has a row in.
const NotificationRecipientOwner = "account_id"
NotificationRecipientOwner is the column an inbox read narrows to.
Not the created-by audit column every other owner-scoped table filters on: an inbox line is not owned by whoever caused it, it is owned by the person it is addressed to. That distinction is what `access.owner` exists for, and this is the table it was written against.
const PresenceTable = "rig_presence"
PresenceTable is who is here, and what they are looking at.
One table, unlike the inbox's five, because a presence row is complete on its own: nothing collapses into it and nothing is pending about it.
Variables ¶
var ErrorCodes = []ErrorCode{
{"BadRequest", 400, "The request was malformed or a parameter could not be parsed."},
{"Unauthorized", 401, "No valid session or API key was presented."},
{"Forbidden", 403, "The caller is known but not permitted to do this."},
{"NotFound", 404, "No such resource, or it belongs to another tenant."},
{"Conflict", 409, "The request conflicts with the current state of the resource."},
{"UnprocessableEntity", 422, "The request was well-formed but failed validation."},
{"RateLimited", 429, "Too many requests. Retry after the interval in the Retry-After header."},
{"TooLarge", 413, "The request body is larger than this endpoint accepts."},
{"UnsupportedMediaType", 415, "The body's content type is not one this endpoint accepts."},
{"UpgradeRequired", 426, "The client was built against an API revision this server no longer serves."},
{"Unavailable", 503, "Something the server depends on did not answer in time. Retry."},
{"Internal", 500, "Something went wrong on the server."},
}
ErrorCodes is every failure rig can produce. The set is closed: a generated handler cannot invent a status the client does not already know about.
Functions ¶
func ApplyConfig ¶
func ApplyConfig(api ir.API, schema ir.Schema, set *tableconf.Set, opt ConfigOptions) (ir.API, ir.Schema, diag.List)
ApplyConfig merges the per-table YAML onto the projected API.
Configuration wins wherever it speaks, and speaks about nothing physical: there is no key for a column's type or nullability, so a file can never claim something the database contradicts.
Drift is handled asymmetrically on purpose. A column the configuration does not mention is a warning, because a migration must never stop `rig generate` from running. A configuration entry naming a column that no longer exists is an error, because dropping it silently is how a rename quietly loses the comment and the immutability that went with it.
func Bookkeeping ¶
Bookkeeping are the tables no project ever gets a resource for: the migration histories.
The project's own is named in rig.yaml. The rest belong to the foundation's sets, one per module that can carry its own schema, and they exist only in a project whose `migrations.foundation` is `embedded` — but they are listed in every mode, because a table that used to be there after somebody switched off embedded is still not an API.
Introspection returns all of them like any other table, so both the ignore list and the reserved-prefix rule have to know the names. A resource over a migration history would be a generated PATCH on `is_applied`.
func Compile ¶
Compile runs every stage after introspection.
A document is always returned, even when validation failed. It is marked invalid and generators refuse to run against it, but it can still be inspected — which is what lets `rig sync` repair a project that does not yet validate. Refusing to produce anything would make a broken project harder to fix than it needs to be.
func Expand ¶
Expand fills in everything that follows from what the schema and the configuration already said: the built-in shapes, one object per resource, the filter shapes Search needs, and the CRUD endpoints themselves.
Two properties matter and are tested directly. It is idempotent, so running it twice produces the same API as running it once. And it does not modify its input: every slice it touches is rebuilt rather than appended to, because appending into a slice the caller still holds is a bug that only shows up when capacity happens to allow it.
Everything is guarded by name. A hand-written endpoint called Get suppresses the generated one, and a hand-declared object called Error replaces the built-in — which is what makes the configuration an escape hatch rather than a suggestion.
func FileRole ¶
FileRole reports the role a file column names, and whether it is one.
The column is the declaration. `profile_image_file_id` says there is one file attached to this row in the role "profile image", and the path segment, the endpoint names, the permission keys and the Go field all follow from it. This is the same kind of fact as `deleted_at` meaning soft-deletable: the migration says what the thing is, and no configuration key can disagree with it.
A bare `file_id` has no role to name, and two of them on one table would be indistinguishable, so it is not one of these.
func Freeze ¶
Freeze produces the final document.
Three things happen here and nowhere else. Types are resolved once, so no generator has to work out whether "LessonStatus" is an enum or an object. Routes are computed once, so the router, the specification, and the client cannot disagree about a path. And every column reference is checked against the schema it claims to describe — the check that makes the document's two views structurally unable to drift apart.
A failure from the last of those is a bug in rig, not in the project, and says so.
func IsManagedColumn ¶
IsManagedColumn reports whether a column is set by the framework rather than by a client. Such columns never appear in a create or update input, and they are not configurable, so `rig sync` leaves them out of the files it writes.
func Normalize ¶
Normalize canonicalizes an introspected schema.
It sorts everything into a stable order, links each column to the enum type or foreign key behind it, classifies pure join tables, and checks that every column has a type rig can map. It changes no facts: what comes out describes exactly the database that went in.
The input is not modified.
func NotificationTables ¶
func NotificationTables() []string
NotificationTables are all five, in dependency order.
func Project ¶
Project turns a normalized schema into a naked API surface.
Every base table that is not a join table becomes a resource; every column becomes a field; every foreign key and join table becomes a relation. No opinions are applied yet — no CRUD endpoints, no filters, no defaults. Those come from Expand, after the configuration has had its say, so that a hand-written endpoint can shadow a generated one.
func Reserved ¶
Reserved reports why rig will not let a project have a table by this name, or "" when nothing stands in the way. `escapable` is whether a `resource:` key still answers it.
It asks the two questions [checkReserved] asks, from a name alone: the commands that write a file — `rig migration new --table`, `rig sync` — call it before they write, so the answer arrives before the DDL exists rather than from the next `rig validate`. The compile stage deliberately does not call it, because by then the resource name has been through configuration and the frozen document is the better source.
The two halves of the rule are not equally final, and a caller that could not tell them apart would have to refuse both. The `rig_` prefix is rig's and nothing moves a table off it. A reserved resource name is only what the table projects to *by default*, so a `resource:` of its own answers it — which is what diag.CodeReservedResource's hint offers, and a caller here must not turn that into a refusal: a table rig's own rules allow would then be one rig cannot scaffold.
func ShippedModelTables ¶
func ShippedModelTables() []string
ShippedModelTables are the tables whose Go — the row, its write inputs and its enums — is compiled into a module rig ships rather than generated into the project. Exposing one gets an alias to that declaration.
It is here rather than in the generator that writes the aliases because two unrelated places have to agree about the list: that generator, and the check that warns the tags it cannot reach are camelCase whatever `naming.json_case` says. Splitting them is how a table gets one and not the other.
Keyed by physical table name, which is the one thing a configuration file cannot move.
func SnapshotFromAtColumn ¶
SnapshotFromAtColumn records the source row's version identity at copy time.
func SnapshotFromIDColumn ¶
SnapshotFromIDColumn is the self-referencing column pointing at the row a snapshot was copied from.
func SyncServicePublication ¶ added in v0.3.0
SyncServicePublication reports whether a publication carries the name the sync service uses for its own.
The name is `electric_publication_` followed by the replication stream id, which is "default" unless a deployment sets one. Matching the prefix rather than the whole name is what keeps this true under a deployment that does.
The name alone does not say who made it, which is the whole of [migrationOwned]: the sync service is not the only thing that may create this publication, and a migration that creates it first is the arrangement that makes live sync work for a role owning no tables.
Exported because two things have to agree about it and they are in different packages: this package reports a table as unpublished, and `rig migration new --publish-shapes` writes the migration that publishes it. A copy of the rule beside the fix is a way for the fix to write nothing where the rule fires.
func Validate ¶
Validate checks the frozen document against rig's rules.
There are two kinds. Structural rules are always errors: the generators cannot produce correct code without them, so there is nothing to configure. Convention rules take their severity from the project's validate block, because "every column must have a comment" is a policy a team adopts rather than a fact about the code.
Every rule reports at the position in the table's configuration that a reader would go to in order to fix it.
Types ¶
type ConfigOptions ¶
type ConfigOptions struct {
Namer *naming.Namer
// UnmentionedColumn is the severity for a column the database has and the
// configuration does not mention. Empty reports nothing.
UnmentionedColumn diag.Severity
// Ignored are tables the database has and this document deliberately does
// not, so a configuration file naming one can be told apart from a file
// naming a table that was renamed or dropped. The two need different advice,
// and "no such table" would send somebody looking for a migration that is
// not the problem.
Ignored []string
// Notifications is the resolved notifications block, or nil for a project
// with no inbox.
//
// It arrives here for the reason FileRestoreWindowDays does: rig's two
// notification tables have no table configuration in an ordinary project,
// and the two things that have to be true of them — that an inbox read
// narrows to the caller and that the inbox is the only one of them with a
// live-sync shape — are the same decision in every project. A copy of them
// in a YAML file could only ever disagree with this one, and the way it
// would disagree is by streaming somebody else's inbox.
Notifications *ir.Notifications
// Presence is the resolved presence block, or nil for a project that does
// not track who is here.
//
// It arrives here for the same reason Notifications does: rig_presence has no
// table configuration in an ordinary project, and the three things that have
// to be true of it are the same decision everywhere. One of them is a hole
// somebody could otherwise open — see applyPresenceTable on why the owner
// column stays nil.
Presence *ir.Presence
// FileRestoreWindowDays is how long a deleted rig_file row stays
// restorable, resolved from `files.restore_window` in rig.yaml.
//
// It arrives here rather than through a table configuration because
// rig_file has no `restore_window_days` key: the sweeper reads one number
// for every file, and a second copy of it in services/rig_file would be a
// number that could disagree with the one the bytes are kept for. Zero
// means the project has no files block, and rig_file — if it is projected
// at all — is an ordinary table subject to the ordinary rule.
FileRestoreWindowDays int
// Cache is the resolved project `cache:` block, or nil for a project that
// caches nothing.
//
// It arrives here to answer one question: whether a table's `cache: true` has
// a channel to be withdrawn over. The block and the tables are separate files
// and neither can see the other, so this is where a table asking for
// something the project did not turn on is caught — and where the flag is
// dropped rather than carried into a document nothing would honour.
Cache *ir.Cache
}
ConfigOptions tune how configuration is merged.
type ErrorCode ¶
ErrorCode pairs an HTTP status with the machine-readable code clients switch on. Statuses alone are too coarse — three different failures all return 400 — and codes alone leave clients guessing at retry behavior.
type ExpandOptions ¶
type ExpandOptions struct {
Namer *naming.Namer
// SearchMethod selects how Search is exposed.
SearchMethod string
// BasePath prefixes every route.
BasePath string
}
ExpandOptions tune the defaulting pass.
type Meta ¶
type Meta struct {
// Tool identifies the version of rig that generated it.
Tool string
// Permissions is whether authorization checks are derived. The zero value
// derives them, so a caller that forgets to pass it gets the protected shape
// rather than the open one.
Permissions project.PermissionMode
}
Meta describes the run that produced a document.
type NormalizeOptions ¶
type NormalizeOptions struct {
// IgnoreTables are never projected. Migration bookkeeping lives here.
IgnoreTables []string
}
NormalizeOptions tune the first pure stage.
type Options ¶
type Options struct {
// Project supplies naming, layout, API shape, and rule severities.
Project *project.Project
// Tool identifies the rig version in the produced document.
Tool string
// IgnoreTables are never projected; migration bookkeeping lives here.
IgnoreTables []string
// Foundation names the rig_-prefixed tables this project's own migrations
// show rig created, exposed or not.
//
// Deliberately not [Options.IgnoreTables]. Exposing a foundation table takes
// it out of that list while leaving it rig's, and `auth.own` empties the
// list entirely — which is also exactly what a project that never scaffolded
// looks like. [checkReserved] has to tell those two apart.
Foundation []string
}
Options carry everything the pure stages need from the project.
type ProjectOptions ¶
type ProjectOptions struct {
Name string
Version string
Description string
BasePath string
// RevisionHeader is the header the API revision travels in. The revision
// itself is not here: it is decided from the finished document's hash, so it
// is stamped on afterwards.
RevisionHeader string
Namer *naming.Namer
// Auth is the authentication foundation this API is served with, already
// resolved by the project configuration, or nil for a project with none.
Auth *ir.Auth
// Files is the resolved file handling, or nil for a project that accepts no
// uploads.
Files *ir.Files
// Notifications is the resolved inbox, or nil for a project with none.
Notifications *ir.Notifications
// Presence is the resolved presence block, or nil for a project that does
// not track who is here.
Presence *ir.Presence
// Throttle is the resolved throttle block, or nil for a project that does
// not limit API calls.
Throttle *ir.Throttle
// Cache is the resolved cache block, or nil for a project that caches none
// of the reads rig makes on its own behalf.
Cache *ir.Cache
// Monitoring is the resolved monitoring block, or nil for a project with no
// page. Never set without Tracing; the config check is what guarantees it.
Monitoring *ir.Monitoring
// Servers are the deployments this API is served on, in the order the
// project named them, or nil for a project that has named none.
Servers []ir.Server
// Web is where the browser front end is served, or nil for a project whose
// front end is this API's own origin — or which has none.
Web *ir.Web
// OpenAPI is where the generated document is served from — the package it is
// embedded in — or nil for a project that keeps it a file.
//
// The paths are not here: they are computed at freeze, against the same base
// path every other route is and into the same namespace.
OpenAPI *ir.OpenAPI
// Tracing is the resolved tracing block, or nil for a project that emits no
// spans.
Tracing *ir.Tracing
// EmbeddedFoundation is `migrations.foundation: embedded` — rig's own
// migrations stay in the modules that own them.
EmbeddedFoundation bool
}
ProjectOptions describe the API being projected.