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

scaffold

package
v1.0.14 Latest Latest
Warning

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

Go to latest
Published: Nov 2, 2026 License: Apache-2.0 Imports: 14 Imported by: 0

Documentation

Overview

Package scaffold holds forge's BIRTH-TIME, SCAFFOLD-ONCE emitters: code that renders user-owned artifacts exactly once, at an explicit `forge scaffold ...` moment, and never touches them again.

The package exists as an architectural statement, splitting the two writer families that used to share internal/codegen:

  • internal/codegen — Tier-1 DETERMINISTIC PROJECTION. Same inputs, same bytes, regenerated on every `forge generate`, stamped with forge:hash, drift-guarded. Forge owns those files forever.

  • internal/scaffold (this package) — SCAFFOLD-ONCE. Rendered from a truth (the proto descriptor, the applied schema) at an explicit add-time decision, written through checksums.WriteScaffoldIfMissing or a minimal idempotent splice, and USER-OWNED from birth. Deleting the file is the reset; no generate pass ever rewrites it.

Nothing in this package may be called from the generate pipeline: a scaffold-once artifact that regenerates is a contradiction in terms. Conversely, Tier-1 emitters (crud_gen, inject_gen, the ORM, ...) stay in internal/codegen — a projection that stops regenerating is equally wrong. When a helper is genuinely shared (descriptor types, the unwired-stub marker, handler-method scans), it lives in codegen and is imported from here; the dependency is one-directional (scaffold → codegen), never the reverse.

Current residents:

  • entityproto.go — the create-table migration born from an already-authored proto message (`forge scaffold entity --from-proto`), user-owned at birth and never re-derived.

Index

Constants

View Source
const ConfigRootMessage = "AppConfig"

ConfigRootMessage is the config message a scaffolded component's block is composed on: the project-global config every scaffold declares in proto/<project>/config/v1/config.proto, which pkg/config's `Config` aliases and internal/app/compose.go reads blocks off (infra.Cfg).

Variables

View Source
var ErrNoConfigRoot = errors.New("no " + ConfigRootMessage + " message in the config proto directory")

ErrNoConfigRoot reports that the project has no config-directory message named ConfigRootMessage to compose a block on: it has no typed config, or it moved entirely to per-binary configs. The caller refuses before writing adapter files and explains how to declare the required config root.

Functions

func GeneratedColumnRefusal

func GeneratedColumnRefusal(f codegen.SchemaFieldDef, spec EntityFromProtoSpec) string

GeneratedColumnRefusal returns why a `// forge:generated` field cannot be born as a GENERATED column, or "" when it can.

The supported set is every field birth gives ONE single-valued column with a mechanical conversion: a scalar (including a `<x>_id` reference), a same-package enum with known values, and a singular Timestamp. Repeated, map and nested-message fields are JSONB or array columns whose generated form nobody has asked for; refusing them keeps the marker's meaning one sentence long, and the owned migration takes any hand-written generated column after birth. A field birth carries as a TODO has no column to generate at all, and a managed column (id, created_at, …) is the migration's own.

Exported so the CLI refuses the birth BEFORE writing anything, naming the marker's file and line; the renderer applies the same rule as a TODO.

Types

type ConfigBlockLeaf added in v0.1.46

type ConfigBlockLeaf struct {
	Type        string // proto scalar type, e.g. "string"
	Name        string // proto field name, e.g. "base_url"
	EnvVar      string // (forge.v1.config).env_var
	Description string // (forge.v1.config).description
}

ConfigBlockLeaf is one annotated scalar leaf of a block.

type ConfigBlockResult added in v0.1.46

type ConfigBlockResult struct {
	// File is the .proto file that declares the root config (and, when it
	// was added, the block).
	File string
	// AddedMessage / AddedField report the two halves this call wrote.
	// Both false is the idempotent no-op: the block was already declared
	// and composed.
	AddedMessage bool
	AddedField   bool
	// FieldNumber is the root config field number the block was given when
	// AddedField; zero otherwise.
	FieldNumber int
}

ConfigBlockResult reports what EnsureConfigBlock changed.

func EnsureConfigBlock added in v0.1.46

func EnsureConfigBlock(configDir string, spec ConfigBlockSpec) (ConfigBlockResult, error)

EnsureConfigBlock makes the proto config in configDir (proto/<project>/config/v1) declare spec's block and compose it on ConfigRootMessage, writing only the half that is missing. It is a birth-time splice into a file the author owns, held to the same invariants as the managed-field injection (entity_managed_fields.go in internal/cli/scaffold):

  • IDEMPOTENT. A block message that is already declared anywhere in the package is never re-declared, and a root that already composes it is never given a second field: re-running the scaffold changes nothing.
  • NEVER RENUMBERS. The root field takes the root message's high-water mark + 1, and that mark counts `reserved` numbers, so it can neither collide with nor reuse a number the author wrote or retired.
  • REFUSES RATHER THAN GUESSES. A root field already named spec.Field with another type, an existing block message lacking a leaf the scaffold's code reads, or a leaf env var another field already binds is an error naming the conflict; the file is left untouched.

It returns ErrNoConfigRoot when there is no root message to compose on.

func PlanConfigBlock added in v1.0.0

func PlanConfigBlock(configDir string, spec ConfigBlockSpec) (ConfigBlockResult, error)

PlanConfigBlock is EnsureConfigBlock without the write: the same checks and refusals, and a result reporting what EnsureConfigBlock WOULD add (and the field number it would take). A scaffold's --dry-run reports it.

type ConfigBlockSpec added in v0.1.46

type ConfigBlockSpec struct {
	// Message is the block's message name, e.g. "AlpacaConfig".
	Message string
	// Field is the root config field holding it, e.g. "alpaca" (its Go
	// getter is GetAlpaca).
	Field string
	// Doc is the comment written above the message, one line per element.
	Doc []string
	// FieldDoc is the one-line comment written above the root field.
	FieldDoc string
	// Leaves are the block's fields, in declaration order.
	Leaves []ConfigBlockLeaf
}

ConfigBlockSpec is one component config block: a `message <Message>` of annotated leaves, composed on the root config as `<Message> <Field> = N;`.

type EntityFromProtoMigration

type EntityFromProtoMigration struct {
	UpSQL string
	Notes []string
	// PendingRefColumns are this table's `<x>_id` columns that resolve to a
	// known entity whose table does not exist YET, so no constraint could be
	// emitted here. The caller carries them forward as this table's
	// ExistingTable.UnconstrainedRefColumns, and the referenced table's own
	// birth migration back-fills them.
	PendingRefColumns []string
	// BackfilledRefColumns names, per earlier table, the reference columns
	// THIS migration constrained on its behalf. The caller strikes them off
	// that table's pending list so a third birth cannot propose the same
	// constraint twice.
	BackfilledRefColumns map[string][]string
}

EntityFromProtoMigration is the rendered forward migration (forge writes no down migrations — it rolls forward only) plus the per-field notes (skips, TODOs) the CLI prints.

func RenderEntityMigrationFromProto

func RenderEntityMigrationFromProto(spec EntityFromProtoSpec) EntityFromProtoMigration

RenderEntityMigrationFromProto renders the create-table migration pair for one already-authored proto message, per the mapping table in the file header. Pure: no filesystem, no descriptor loading — the CLI owns resolution and file placement.

type EntityFromProtoSpec

type EntityFromProtoSpec struct {
	// Table is the target table name (already pluralized/overridden).
	Table string
	// MessageFQ is the fully-qualified proto message the entity is born
	// from (recorded in the migration header for provenance).
	MessageFQ string
	// ProtoPkg is the service's own proto package — the boundary for
	// cross-package detection.
	ProtoPkg string
	// Fields is the message's descriptor schema, in declaration order.
	Fields []codegen.SchemaFieldDef
	// Enums maps fully-qualified enum names to their value names (the
	// CHECK-constraint vocabulary).
	Enums map[string][]string
	// SoftDelete adds the deleted_at column (set by the flag, the
	// `// forge:soft-delete` marker, or the message itself carrying a
	// deleted_at field).
	SoftDelete bool
	// Timestamps adds the managed created_at/updated_at columns.
	Timestamps bool
	// AppendOnly adds a DB guard (a trigger raising an exception) that
	// rejects every UPDATE/DELETE on the table — the storage half of the
	// `// forge:append-only` marker (the wire half omits Update/Delete
	// RPCs). An audit/ledger table: rows are immutable once written.
	AppendOnly bool
	// KnownTables is the set of table names an `<x>_id` FK may reference:
	// the applied schema UNION every table born in this run (descriptor
	// CRUD entities + `// forge:entity` messages). A `<x>_id` column whose
	// stem names no table in this set is polymorphic/auth
	// (user_id/actor_user_id/entity_id in a project with no such entity)
	// and gets NO foreign key — a REFERENCES to a table that doesn't exist
	// is worse than silence. Empty/nil suppresses every foreign key.
	KnownTables map[string]bool
	// ExistingTables are the tables that ALREADY EXIST at the moment this
	// migration runs — the applied schema plus everything born earlier in
	// the same sweep. It is what makes the emitted constraints applyable:
	// a REFERENCES may only name a table an EARLIER migration created.
	//
	// It also carries the reverse direction. A child born before its parent
	// could not constrain its own column; the parent's birth migration
	// back-fills that constraint, because the parent's migration is the
	// first point at which it can be applied. Between the two directions
	// every resolvable reference becomes a real constraint exactly once, in
	// whatever order the entities are born.
	//
	// Empty/nil means "nothing exists yet", which suppresses forward
	// constraints — the drift renderer runs this way.
	ExistingTables []ExistingTable
}

EntityFromProtoSpec is the resolved input to RenderEntityMigrationFromProto. The add command resolves identities (descriptor service, message, table name, flags); this renderer owns the mapping table above and the SQL text.

type ExistingTable

type ExistingTable struct {
	// Name is the table name.
	Name string
	// UnconstrainedRefColumns are its TEXT `<x>_id` columns that carry no
	// FOREIGN KEY yet — the columns a later table's birth may still be
	// able to constrain, once it creates the table they point at.
	UnconstrainedRefColumns []string
}

ExistingTable is one already-created table, as seen by a migration being born after it.

type ProtoSQLMapping

type ProtoSQLMapping struct {
	// Proto is the field shape as declared in the message ("string",
	// "optional <scalar>", "repeated enum", "map<K, scalar>").
	Proto string
	// SQL is the column the birth migration emits for it.
	SQL string
	// Notes is the one-line qualification, or "" when there is none.
	Notes string
}

ProtoSQLMapping is one row of the proto→column mapping this renderer applies: the proto field shape as an author writes it, the column it is born as, and the one-line reason where the column is not simply the scalar type.

func ProtoSQLMappings

func ProtoSQLMappings() []ProtoSQLMapping

ProtoSQLMappings returns the full proto→column mapping the birth renderer applies, scalar rows first (iterated from the real scalarSQL function, so adding a kind there shows up here for free) then the structural rows.

It exists so `forge project annotations` can DUMP the mapping rather than transcribe it. The scalar half is genuinely derived; the structural half is a literal projection of this file's own field switch, pinned by TestProtoSQLMappings_MatchTheRenderer.

The kinds come from codegen.ProtoScalarKinds() — the key set of the one table forge writes the fifteen names down in — rather than from a copy kept here. A copy is a guard that cannot fail: a kind missing from it is simply never dumped, and a mapping absent from the dump reads exactly like a mapping that does not exist.

They are GROUPED BY COLUMN rather than listed alphabetically, so the ten integer widths read as the one BIGINT family they are; alphabetical order scatters them through the float and text rows. The grouping is derived from each kind's own mapping, so it needs no second list to stay correct.

Jump to

Keyboard shortcuts

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