Models
Models are application-owned Go types. They receive storage.Connection through constructors, wrap a narsilc client, and register with Fx via models.Module. Controllers and services call model APIs. They never import models/internal/queries directly.
Entity versus query client
Two layers sit in the same package:
| Layer | What it is | Who uses it |
|---|---|---|
| Entity | Domain row type (Product, User) with andurel tags |
Controllers map to payloads; factories sync fields |
| Plural API | Products, Users holding *queries.Queries |
Controllers, services, seeds |
| narsilc client | Generated under models/internal/queries |
Only the owning model package |
Hand-written SQL lives in models/queries/*.sql. andurel sync queries compiles it into the internal client. The plural API constructs that client with queries.New(db) or queries.New(tx).
Mark the table above the imports so generators and factory sync can find the Entity:
1// andurel:table products
2
3type Product struct {
4 ID uuid.UUID `andurel:"id"`
5 Name string `andurel:"name"`
6 PriceCents int32 `andurel:"price_cents"`
7 CreatedAt pgtype.Timestamptz `andurel:"created_at"`
8 UpdatedAt pgtype.Timestamptz `andurel:"updated_at"`
9}
Nullable columns use pgtype.* when database.nullType is pgtype.Null (the default), or pointers when set to pointer in andurel.toml.
Construct and inject
Generated plural APIs look like this:
1type Products struct {
2 queries *queries.Queries
3}
4
5func NewProducts(db storage.Connection) Products {
6 return Products{queries: queries.New(db)}
7}
8
9func (p Products) WithTx(tx storage.Transaction) Products {
10 return Products{queries: queries.New(tx)}
11}
NewProducts binds the pool. WithTx returns a value copy whose narsilc client uses the same PostgreSQL transaction. Persistence methods on Products call generated query methods and map rows into Product entities.
Register constructors in models.Module so Fx can supply them to controllers and services:
1var Module = fx.Module(
2 "models",
3 fx.Provide(
4 NewUsers,
5 NewProducts,
6 ),
7)
Keep entities, validation, relationships, and persistence methods together in the owning model file.
Generate from migrations
1andurel generate migration create_products_table
2andurel db migrate up
3andurel generate model Product
4andurel generate model Product --mode read-only
5andurel generate model Product --update --yes
| Flag | Meaning |
|---|---|
--mode |
crud (default), read-only, or create-only |
--table-name |
Override the default table name |
--primary-key |
Skip interactive primary-key detection |
--skip-factory |
Do not create or update models/factories |
--update |
Refresh an existing model from migration changes |
--dry-run / --diff / --json |
Preview structured mutations |
Model generation reads migration history, writes (or updates) the model API and matching models/queries/*.sql, and updates models.Module. You own the resulting Go afterward. After editing SQL by hand, run andurel sync queries --json so the internal client matches.
Transactions
For multi-step work, use storage.RunInTransaction and WithTx so every statement shares one PostgreSQL transaction:
1err := storage.RunInTransaction(ctx, db,
2 func(ctx context.Context, tx storage.Transaction) error {
3 products := models.NewProducts(db).WithTx(tx)
4 _, err := products.Create(ctx, data)
5 return err
6 },
7)
Prefer injecting models.Products and calling products.WithTx(tx) over constructing a new API mid-handler when Fx already supplied one.
See Getting Started and Queries.