resourcepolicy

package
v0.7.1 Latest Latest
Warning

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

Go to latest
Published: Oct 6, 2026 License: MIT Imports: 7 Imported by: 0

Documentation

Overview

Package resourcepolicy is the foundational slice of the model-first resource generator (ADR-016, issue #352). It defines the `gombit` field-policy tag and resolves, per model field, the API behavior the generator derives DTOs, mappers, and CRUD plumbing from.

Ownership of facts is deliberately split (ADR-016): the GORM schema owns persistence facts (DB-backed / primary key / auto-increment / DB default / nullability / auto timestamp / soft-delete / column name) and policy MUST NOT restate them. This resolver carries those facts losslessly — it never collapses them — because create-source correctness depends on the exact combination (a manual string key is not auto-generated; a NOT NULL column with no default must be supplied on create). The `gombit` tag owns only API behavior:

read     the field appears in the response DTO
write    the field is settable via the create request DTO (create source = request)
server   the field is set server-side (a hook), not from the request (create source = server)
-        the field is hidden from the API (no request, no response)

and the list endpoint's declared query surface (all opt-in, all requiring the field to be response-visible):

filterable    exact-match ?<field>= query param
sortable      ?ordering=<field> (- prefix for DESC)
searchable    matched by ?search=
aggregatable  ?aggregate=sum:<field>,… (numeric columns)

`-` may combine with `server` (hidden from the API but set by a hook); it may not combine with `read`/`write`, and `write`+`server` conflict. A query capability requires the field to be response-visible: a hidden or write-only column with a query surface leaks through result membership, counts, and ordering, so Resolve rejects it. A field with no `gombit` tag defaults by kind: a regular content column and a manual primary key are read+write from the request; an auto-increment key and auto timestamps are read-only; soft-delete is hidden. Resolve rejects a required column (NOT NULL / primary key, no `default:` clause, not DB-generated) that ends up with no create source — the silent zero-fill #352 exists to eliminate. It does not evaluate whether a default's SQL yields a non-null value; the database enforces that and fails loudly, so a `default:` clause defers to the database.

Persistence facts are supplied by the caller (slice 2 reads them from the model); this slice is pure and side-effect free.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func RequestColumns

func RequestColumns(resolved []Resolved) []string

RequestColumns returns the DB column names settable via the create request, sorted — the write surface the generated request DTO must expose.

func ResponseColumns

func ResponseColumns(resolved []Resolved) []string

ResponseColumns returns the DB column names surfaced in responses, sorted.

func ServerColumns

func ServerColumns(resolved []Resolved) []string

ServerColumns returns the DB column names whose create value is set server-side (a hook), sorted — the create-time non-request source.

Types

type CreateSource

type CreateSource int

CreateSource is where a persisted field's create value comes from.

const (
	// CreateSourceNone is for fields the create path does not set (auto-generated,
	// or a nullable/defaulted column left unset, or not DB-backed).
	CreateSourceNone CreateSource = iota
	// CreateSourceRequest: the value comes from the create request body.
	CreateSourceRequest
	// CreateSourceServer: the value is set server-side (a BeforeCreate hook).
	CreateSourceServer
)

func (CreateSource) String

func (s CreateSource) String() string

type Field

type Field struct {
	FieldFacts
	Tag string
}

Field pairs a field's persistence facts with its raw `gombit` tag. It keeps the association 1:1: GORM can flatten two embedded fields to the same GoName with different columns and tags, so policy is never keyed by name.

func FromModel

func FromModel(model any) ([]Field, error)

FromModel parses a GORM model (a pointer to a value, e.g. &Book{}) and returns the resolver input for its effective persisted columns, after validating that no relationship field carries a gombit policy (see FromSchema).

func FromSchema

func FromSchema(sch *schema.Schema) ([]Field, error)

FromSchema maps a parsed GORM schema to resolver input over its EFFECTIVE persisted columns — ordered sch.DBNames → sch.FieldsByDBName — so a duplicate-column or shadowed field is included once, each carrying its own `gombit` tag. A belongs-to FK column is a persisted column and is included; the association object/slice fields (belongs-to/has-one/has-many/many2many) are not columns and are NOT emitted.

Selection and validation are separate. An explicit `gombit` policy on a field that is not an effective persisted column (a relationship, a `gorm:"-"` ignored field, or a column shadowed by another field) would otherwise be silently dropped — it lives outside the emitted set. FromSchema therefore validates EVERY policy-bearing field from GORM's authoritative per-field set (sch.Fields), keyed by field identity, not a name-keyed secondary index that collapses same-named embedded fields. A policy that cannot be honored fails closed. Then it emits the effective persisted columns.

type FieldFacts

type FieldFacts struct {
	GoName string // Go field name, e.g. "Title"
	// Column is the DB column name (e.g. "title"). It is EMPTY exactly for a
	// relationship/association field (has_many / many2many / belongs-to object) —
	// there is no separate "DBBacked" flag to disagree with it; db-backedness is
	// derived from Column != "" so an included field always has a column.
	Column        string
	PrimaryKey    bool
	AutoIncrement bool // database-generated key (serial/identity)
	AutoTime      bool // auto create/update timestamp (autoCreateTime/autoUpdateTime)
	SoftDelete    bool // gorm.DeletedAt
	NotNull       bool // NOT NULL constraint
	HasDefault    bool // a `default:` clause is present. Whether that default's SQL expression actually yields a non-null value is the database's concern (it enforces NOT NULL and rejects a bad default loudly at migrate/insert), NOT something this pure layer evaluates.

	// Creatable / Readable mirror GORM's per-field permissions (schema.Field
	// Creatable/Readable, driven by the `->`/`<-` permission tags), independent of
	// whether the field has a column. GORM OMITS non-creatable fields from INSERT
	// and skips non-readable fields on SELECT, so policy must honor them or the API
	// promises an operation persistence will not perform. Normal columns are both
	// true; a `gorm:"->"` read-only/computed column is Creatable=false, and a
	// `gorm:"->:false;<-:create"` column is Readable=false.
	Creatable bool
	Readable  bool
}

FieldFacts are the persistence facts about a model field, owned by the GORM schema. Policy never restates these; it only layers API behavior on top. The facts are kept independent (not collapsed) because create-source correctness depends on their exact combination.

func FactsFromSchema

func FactsFromSchema(f *schema.Field) FieldFacts

FactsFromSchema is the lossless projection of a parsed GORM field into FieldFacts. Soft-delete is read from the clauses schema.Parse stored on the field's own owning schema (f.Schema, which GORM sets during parse) — its authoritative parsed result — so gorm.DeletedAt, *gorm.DeletedAt, and any wrapper whose DeleteClauses rewrite DELETE into an UPDATE are recognized, in either the value or pointer clause form, while a generic clause provider that hard-deletes is not. Create/read capability comes from GORM's own Creatable/Readable (the `->`/`<-` permission tags); nothing is re-derived by hand. The field carries its own owner, so callers pass only the field.

type Resolved

type Resolved struct {
	FieldFacts
	InRequest    bool         // settable via the create request DTO
	InResponse   bool         // surfaced in the response DTO
	CreateSource CreateSource // where a persisted create value comes from
	// The list endpoint's declared query surface for this column. Each is opt-in
	// via the gombit tag and requires the column to be response-visible.
	Filterable   bool
	Sortable     bool
	Searchable   bool
	Aggregatable bool
}

Resolved is a field's resolved API behavior: its persistence facts plus where it appears in the API and where its create value comes from.

func Resolve

func Resolve(facts FieldFacts, gombitTag string) (Resolved, error)

Resolve combines a field's persistence facts with its `gombit` tag into its API behavior. It errors on a contradictory tag (writing/server-setting a managed or non-creatable column, reading a non-readable one, write+server, tagging a relationship) and on a required column that no create source can satisfy — whether because the policy gives it none or because GORM never writes a valid value for it (a non-creatable, `<-:update` auto-time, or NOT NULL soft-delete column). The two questions "may the API write this?" (managed / capability) and "does a valid value reach the INSERT?" (dbSuppliesCreateValue) are kept apart.

func ResolveAll

func ResolveAll(fields []Field) ([]Resolved, error)

ResolveAll resolves every field, in input order, preserving each field's own facts+tag pairing (never keyed by the possibly-ambiguous Go name). It fails on the first contradictory field so the generator never emits from a bad policy.

func ResolvedFromModel

func ResolvedFromModel(model any) ([]Resolved, error)

ResolvedFromModel parses a model and resolves the API policy for each of its effective persisted columns. It returns the first policy error (a contradictory tag or an unsatisfiable required column), so a bad model fails closed.

func (Resolved) NeedsCreateValue added in v0.4.0

func (r Resolved) NeedsCreateValue() bool

NeedsCreateValue reports whether create must supply this column. Omitting it stores the Go zero value in a required column. Resolve uses the same rule.

Jump to

Keyboard shortcuts

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