2. Fields, Types, and Rows
A FieldType describes value interpretation. A Field<T> is a named, reusable token that carries that type plus storage, UI, seed, defaulting, and reference metadata.
Prefer semantic field tokens
abstract final class AreaFields {
static const code = CodeField();
static const name = NameField();
static const description = DescriptionField();
static const tenantId = TenantIdField();
static const siteId = SiteIdField();
}CodeField, NameField, and TenantIdField encode shared conventions. Use primitive tokens when the semantics are genuinely local:
static const capacity = IntegerField(
'capacity',
title: 'Capacity',
nullable: true,
);
static const roomNumber = TextField(
'room_number',
title: 'Room Number',
maxLength: 80,
);
static const calibratedAt = TimestampField(
'calibrated_at',
title: 'Calibrated At',
withTimeZone: true,
);Closed field types
The sealed interpretation core covers text, integers, big integers, doubles, exact decimals, booleans, timestamps, dates, wall-clock times, intervals, UUIDs, JSONB, binary values, PostGIS points, native ranges, network addresses, pgvector values, enums, and decisions. Higher-level tokens such as quantities, money, files, identifiers, and related fields compose those primitives.
Use DecimalField for regulated or financial precision. Its Dart boundary is a string so a binary floating-point conversion cannot silently change it.
Use one GeoPointField rather than independent latitude/longitude columns when the value is a location. BinaryField is for small bounded byte values; large documents remain object-storage records represented by FileField metadata. A FileField names the StorageBucket that holds its files with bucket:; the module declares the bucket with Module(buckets: [...]). The field's size limit and MIME types must fit inside the bucket's, and bucket names are unique across the Blueprint. See File storage for the routes, the client and the runtime tables. ObjectField<T> stores a registered Dart value as JSONB through an explicit ObjectFieldCodec<T>; it never turns an arbitrary runtime map into an executable Dart type. PostGIS fields require the postgis module extension and vector fields require vector.
| Value family | Field token | Dart boundary | PostgreSQL lowering |
|---|---|---|---|
| wall-clock time | TimeField | LocalTime | time / timetz |
| duration | IntervalField | Duration | interval |
| bytes | BinaryField | BinaryValue | bytea |
| point | GeoPointField | GeoPoint | geometry / geography(Point,srid) |
| scalar interval | range field family | ValueRange<T> | native range type |
| network | InetField, CidrField, MacAddressField | validated string | native network type |
| embedding | VectorField | fixed-length List<double> | vector(n) |
| typed JSON | ObjectField<T> | registered T | jsonb |
The value wrappers have explicit portable codecs:
final time = LocalTime.parse('08:30:00+05:30');
final point = GeoPoint(latitude: 17.385, longitude: 78.487);
final range = ValueRange<DateTime>(
lower: DateTime.utc(2026, 1, 1),
upper: DateTime.utc(2027, 1, 1),
);
print(time.toIso8601String());
print(point.toGeoJson());
print(range.toJson((value) => value.toIso8601String()));These types flow through declaration, definition serialization, runtime validation, execution-plan casting, DDL, Explorer metadata, and field-registry mapping. Dedicated spatial-distance, nearest-neighbor, range-containment, and vector-similarity operators are not part of the current query vocabulary.
Enums keep wire values stable and labels human
enum AreaType { room, suite, zone, corridor, airlock, wash_bay }
static const areaType = EnumField(
'area_type',
AreaType.values,
dbEnumName: 'area_type',
title: 'Area Type',
ui: FieldUI(
valueLabels: {'wash_bay': 'Wash Bay'},
),
);The persisted value is wash_bay; the UI renders Wash Bay. Filters, tables, forms, chips, and details all consume the same label map.
Defaults belong at the correct layer
static const id = UuidField(
'id',
nullable: false,
defaultExpr: FieldDefaultExpr.uuidV4,
);
static const createdAt = TimestampField(
'created_at',
nullable: false,
defaultExpr: FieldDefaultExpr.now,
);
static const createdBy = ActorField(
'created_by',
defaultExpr: FieldDefaultExpr.actorId,
);Postgres can own uuidV4 and now. The authenticated runtime injects actorId and siteId; they cannot be honest SQL defaults.
Reference fields carry picker semantics
static const areaCategoryId = UuidField(
'area_category_id',
title: 'Area Category',
indexed: true,
db: FieldDb(
queryPatterns: [QueryPattern.filter, QueryPattern.aggregate],
),
ui: FieldUI(
widget: UIFieldControl.referencePicker,
filterable: true,
),
reference: UIReferenceHint(
target: EntityRef(module: ModuleRef('ops'), name: 'area_category'),
projection: 'picker',
),
);The storage value remains a UUID. The generated editor uses an entity picker and the generated detail surface uses an entity link. A user should not have to type or interpret the raw ID.
Typed row access
The same token reads and writes a loose storage row safely:
final row = Row(<String, Object?>{
'code': 'WASH-01',
'capacity': 12,
});
final code = row.require(AreaFields.code); // String
final capacity = row.maybe(AreaFields.capacity); // int?
row.set(AreaFields.name, 'Equipment Wash Bay');This is why field tokens must be reused rather than recreated from matching strings.
DB and UI hints do not change domain meaning
static const name = TextField(
'name',
maxLength: 240,
nullable: false,
db: FieldDb(
indexed: true,
queryPatterns: [QueryPattern.search, QueryPattern.sort],
),
ui: FieldUI(
searchable: true,
sortable: true,
order: 10,
),
);Hints guide interpreters. They do not create a second field definition.
Checkpoint
Create tokens for a controlled equipment record:
- code and name;
- serial number;
- category reference;
- decimal contact-surface area;
- multiple document attachments;
- lifecycle status enum.
Compare your result with the real Ops equipment.dart, then continue to Facets and entities.