---
name: arandu-module
description: The model and data of an Arandu (Go) application -- an entity, its table, the generated query, migrations, factories, seeders, scopes, relations, the rules of the entity itself and the service that orders a use case over them. Use when the request is to "create a model", "add a table", "add a column", "scaffold CRUD", "add invoices", "a record under another record", "add a state transition", "write a seeder", or when a query, a migration or `model:build` is involved. Covers aru make:module (with --tenant and --parent), aru generate and its specification, make:model, make:migration, make:service, model:build and the custom blocks that survive regeneration.
license: MIT
---

# The model and its data

## When to use

A new kind of record, a column, a transition of a record's state, a query the
generated builder says, a factory or a seeder, or the service method that
reads and writes them. Who may run the method is `arandu-policy`; what calls
the service over HTTP is `arandu-http`.

## Before you start

- Read the example's model, `app/Models/Note.go`, and its service,
  `app/Services/NoteService.go`. `app/Models/Comment.go` is the same shape
  generated under a parent.
- Answer the ownership questions in `arandu-ecosystem` first: a balance, a
  role, a tag or rendered Markdown already has an owner and gets no table here.

## Contracts and imports

| piece | where | contract |
| --- | --- | --- |
| entity | `app/Models/<Entity>.go` | a struct embedding `model.Model` (`github.com/arandu-io/hesape/database/model`) with `db:` tags, and `var <entity>Table = model.NewTable(model.TableSpec{...})` beside it |
| query | `app/Models/<Entity>Query.go` | generated by `aru model:build`: `models.<Entities>(db)`, `*<Entity>Query`, `<Entity>Collection`. Every terminal -- `Get`, `First`, `FindOrFail`, `SimplePaginate`, `Count`, `Exists` -- takes an `auth.Grant` |
| write | the entity | `Save(ctx, g)` and `Delete(ctx, g)` on a row that came from a query or from `models.<Entities>(db).New()` |
| rule of the entity | the custom block of `<Entity>.go` | a method that changes only this row's fields; no database, network, clock or Grant |
| scope | the custom block of `<Entity>.go` | a method on `*<Entity>Query` |
| relation | an `init` in `<Entity>.go` | `<entity>Table.Relate(name, func(*model.Model) model.Relation)` |
| migration | `database/migrations/<id>.go` | registers itself in `init`; `Up` and `Down` over `schema.Blueprint` |
| factory, seeder | `database/factories`, `database/seeders` | `factories.<Entities>(db).Count(n).Create(ctx, g)`; a seeder runs twice safely |
| service | `app/Services/<Entity>Service.go` | `New<Entity>Service(db *database.DB)`; methods `(ctx, actor auth.Subject, in requests.X | id string)`; a transaction is `database.Transaction(ctx, db, fn)` |

## Procedure

1. **Generate the resource.** `aru make:module note --fields
   "title:string!,body:text,pinned:bool" --tenant` writes the migration, the
   entity and its query, the factory, the policy, the request, the service, the
   controller, the four screens and the tests, and prints the wiring. Under a
   parent, add `--parent=notes`: the service then loads the parent through the
   parent's own service and filters by the row it loaded, which is how
   `CommentService` reads a note.
2. **Or write a specification** when the module needs permissions or a
   description the flags cannot say: `aru schema` prints the schema,
   `aru generate invoice.yaml --check` reports every problem at once, and
   `aru generate invoice.yaml` writes the tree and keeps the specification in
   `database/specs/`.
3. **Change a table with a new migration**, never by editing one that ran:
   `aru make:migration add_published_at_to_notes --table=notes --fields
   "published_at:timestamp"`. A column added to a table that has rows is
   nullable or has a default, because the previous binary is still inserting
   during a rollout.
4. **Put the entity's rules in the entity.** A transition is a method on the
   entity that takes what it needs -- the time included -- and refuses a state
   it does not accept with an error that carries `HTTPStatus() int`, as
   `Note.Publish` does with `ErrNoteAlreadyPublished` (409).
5. **Order the use case in the service**: validate the request, read the row
   through the Grant, authorize the row (`arandu-policy`), apply the entity's
   rule, save with the Grant, and store the events of the write in the same
   transaction (`arandu-async`). `NoteService.Publish` is that order, in full.
6. **Run `aru model:build`** after any change to an entity, and never edit what
   it writes.

## Commands

- `aru make:module <name> --fields "..." --tenant [--parent=<resource>] [--force] [--dry-run]`
- `aru generate <spec.yaml> --check`, `aru generate <spec.yaml>`, `aru schema`
- `aru make:model <Name> --fields "..." [--tenant] [--migration] [--factory] [--seed] [--policy] [--requests] [--controller] [--all]`
- `aru make:migration <name> --create=<table>` or `--table=<table>`, with `--fields`
- `aru make:service <Name>`, `aru make:factory <Name>`, `aru make:seeder <Name>`, `aru make:enum <Name> --values draft,sent`
- `aru model:build`, and `aru model:build --check` in a pipeline
- `aru migrate`, `aru migrate:rollback`, `aru migrate:status`, `aru migrate:fresh` (development only), `aru db:seed`

The column types are a closed set -- `string` `text` `int` `decimal` `money`
`bool` `date` `timestamp` `uuid` `email` -- and so are a specification's actions:
`view` `create` `update` `delete` `list`. `money` is an integer of cents.

## Example

A read through the generated query and a write through the entity's rule, the
two halves of every service method:

```go compile
package example

import (
	"context"
	"time"

	"github.com/arandu-io/hesape/auth"
	"github.com/arandu-io/hesape/database"

	models "<module>/app/Models"
	policies "<module>/app/Policies"
)

// PinnedDrafts reads the tenant's pinned drafts, newest first, one page at a
// time: the generated query, finished by a terminal that takes the Grant.
func PinnedDrafts(ctx context.Context, db *database.DB, g auth.Grant) (models.NoteCollection, error) {
	if err := g.Check(policies.NoteList); err != nil {
		return nil, err
	}
	return models.Notes(db).Where("pinned", true).WhereNull("published_at").
		Latest().Limit(25).Get(ctx, g)
}

// PublishAt applies the entity's transition and saves the row with the Grant
// the policy issued for it.
func PublishAt(ctx context.Context, g auth.Grant, note *models.Note, at time.Time) error {
	if err := note.Publish(at); err != nil {
		return err
	}
	_, err := note.Save(ctx, g)
	return err
}
```

## Do not

- Query without a Grant, or use the model core -- `model.NewTable`, a
  `*model.Builder`, what `Base()` returns -- outside `app/Models`. It answers
  untyped rows and is a second way to reach the table; `aru doctor` reports it
  as `model-core-outside-models`.
- Read the clock, the database or the network in a rule of the entity:
  `aru doctor` reports it as `model-rule-touches-io`.
- Assign a whole struct over a row from a query. A struct literal has no
  connection, and its `Save` returns `model.ErrUnwired`; set the fields.
- Edit `<Entity>Query.go` or a factory outside its custom block: the next
  `aru model:build` writes over it, and `aru doctor` reports a stale one as
  `model-query-stale`.
- Open a subpackage under `app/Services`, or grow a service past what one
  aggregate needs: `service-subpackage` and `service-file-too-large`.

## Extending it

Between `// arandu:begin custom` and `// arandu:end custom`: table settings
inside the `TableSpec` (`Hidden`, `PerPage`, `SoftDeletes`, `Scopes`, `Events`),
rules, scopes and relations at the end of the entity's file, named states in a
factory, use cases beyond CRUD at the end of a service. `--force` rewrites
everything outside the blocks; the example's files were finished by hand
outside them and are changed by hand.

## Wiring

`aru make:module` prints three lines and the claim:

```go
// routes/web.go, the field on Deps
Note *controllers.NoteController
// bootstrap/app.go, in the routes.Deps literal
Note: controllers.NewNoteController(services.NewNoteService(db)),
```

The route is in `arandu-http`. A migration needs nothing: the blank import of
`database/migrations` in `bootstrap/app.go` links every one. A seeder goes in
the registry of `database/seeders/seeders.go`.

## Acceptance test

- `tests/Feature/<Entity>TenantScope_test.go`, which `--tenant` generates, and
  the claim of the table in `tests/Feature/TenantScope_test.go`, written after
  reading every query of it.
- A cross-tenant read and write through the HTTP path that answers 404, in the
  shape of `TestANoteOfAnotherTenantIsNotFound`.
- The entity's rules tested alone, with no database and the time passed in, as
  `TestANoteIsPublishedOnceAtTheTimeItIsGiven` does.

## Limits

The generator knows columns, not meaning: it writes no author, no ownership
rule and no transition. A table with no tenant column -- a pivot keyed by two
ids -- is invisible to the tenant test, and its isolation rests on the Grant of
every query that reads it. The query builder covers what one aggregate needs; a
report or a join belongs in a repository (`app/Repositories`) with its own
policy, never in a second query path.

## Gates

Run them all, in this order, as `AGENTS.md` lists them:

```sh
export GOWORK=off
aru model:build --check
aru view:build
gofmt -l $(find . -name '*.go' -not -path '*/testdata/*' -not -name '*.kyse.go')
go vet ./...
bash tests/test-layout-guard.sh
go test -race ./...
go build ./...
aru doctor
```

<!-- arandu:begin custom -->
<!-- arandu:end custom -->
