EntityApi
The abstract CRUD interface for entity operations. Every entity type requires an EntityApi<T> implementation that handles backend communication.
Class Signature
Constructor Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
entityType | String | Yes | Entity type identifier (e.g., 'equipment') |
schemaType | String | Yes | Schema-qualified type (e.g., 'lms.equipment') |
defaultSortField | String? | No | Default field for list sorting |
hasDrafts | bool | No | Whether entity supports drafts (uses Draftable mixin). When true, getDrafts/countDrafts are wired in HttpEntityApi. |
Read Operations
getOneFuture<T?> getOne(String id)Get single entity by ID.
getManyFuture<List<T>> getMany({int page, int pageSize, String? query, String? sortField, bool sortAscending, Map<String, dynamic>? filters})Paginated list. Returns only approved entities.
countFuture<int> count({String? query, Map<String, dynamic>? filters})Total entity count.
findPositionFuture<int?> findPosition(String entityId, {String? query, Map<String, dynamic>? filters, bool drafts, String? sortField, bool sortAscending})Find entity position in sorted dataset. Default returns null.
Write Operations
createFuture<T> create(T entity, {String? remarks})Create a new entity. Remarks are stored in the audit trail.
updateFuture<T> update(String id, T entity, {String? remarks})Update existing entity.
deleteFuture<bool> delete(String id)Delete an entity.
Draft Operations (default no-op when hasDrafts=false)
getDraftsFuture<List<T>> getDrafts({int page, int pageSize, String? query, String? sortField, bool sortAscending, Map<String, dynamic>? filters})List pending drafts.
countDraftsFuture<int> countDrafts({String? query, Map<String, dynamic>? filters})Count pending drafts.
Serialization
fromJsonT fromJson(Map<String, dynamic> json)Create entity from JSON.
HttpEntityApi
Class Signature
Constructor Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
entityType | String | Yes | Entity type identifier |
schemaType | String | Yes | Schema-qualified type name |
pathBuilder | EndpointBuilder | Yes | API endpoint path builder |
fromJson | T Function(Map<String, dynamic>) | Yes | JSON deserialization factory |
defaultSortField | String? | No | Default sort field |
hasDrafts | bool | No | Draft support flag |
Overridable Getters
| Property | Type | Default | Description |
|---|---|---|---|
cachingPolicy | CachingPolicy | CachingPolicy() (5 min stale) | Controls caching behavior |
keyBuilder | CacheKeyBuilder | CacheKeyBuilder(entityType) | Generates cache keys |
Overridable Methods
toCreateJsonFuture<Map<String, dynamic>> toCreateJson(T entity)Transform entity to create payload (default strips id, sets createdBy/updatedBy/At from current user).
toUpdateJsonFuture<Map<String, dynamic>> toUpdateJson(T entity)Transform entity to update payload (default strips id/createdAt/createdBy/version_number, sets updatedBy/updatedAt).
Custom mutation endpoints (@protected)
postFuture<Map<String, dynamic>> post(String path, {Object? body, Set<Mutation> announces})Custom POST. Fans out announces on 2xx; never on error.
putFuture<Map<String, dynamic>> put(String path, {Object? body, Set<Mutation> announces})Custom PUT.
patchFuture<Map<String, dynamic>> patch(String path, {Object? body, Set<Mutation> announces})Custom PATCH.
mutateFuture<Map<String, dynamic>> mutate({required Uri uri, required HttpMutationMethod method, Object? body, Set<Mutation> announces})Core dispatch (POST/PUT/PATCH/DELETE). Use directly for custom DELETEs or pre-built URIs.
announceAllvoid announceAll(Iterable<Mutation> mutations)Fan out a batch of mutations without an HTTP call (SSE triggers, batch workflows, post-hoc reconciliation).
Cache helpers (@protected)
cachedGetFuture<R> cachedGet<R>({required String operation, required Future<R> Function() fetch, required R fallback, Object? params, Set<Mutation> invalidateWhen})Stale-while-revalidate fetch with in-flight dedup.
announcevoid announce(MutationType type, {String? entityId})Notify cache of a mutation for this entityType.
Bulk import
bulkImportFuture<BatchResult> bulkImport(List<T> entities, {String? conflictKey, ConflictStrategy conflictStrategy})Bulk POST to /{entity}/import. Announces a single create mutation on completion.
class HttpEntityApi<T extends EntityBase> extends EntityApi<T> with ApiHeadersMixin — standard HTTP/REST implementation. Provides default implementations for all CRUD operations using REST patterns and integrates with QueryCacheService for stale-while-revalidate caching.
WARNING
pathBuilder is required. Always provide it via EndpointBuilder(prefix: ...) or a subclass like VersionedEndpointBuilder.
Mutation announcements
Every successful create / update / delete automatically calls announce(MutationType.x, entityId: ...), which wakes up subscribed cache keys via QueryCacheService.subscribe. Subclasses with custom POST/PUT/PATCH endpoints declare their own announces inline:
Set<Mutation> _instructionMutation(String id) => {
Mutation('checklist_usage_instructions', MutationType.update),
Mutation(entityType, MutationType.update, entityId: id),
};
Future<Map<String, dynamic>> submitInstructionResponse(String id, ...) =>
post('$id/submit_instruction_response', body: {...},
announces: _instructionMutation(id));HttpMutationMethod
HttpMutationMethod
Class Signature
The verb passed to mutate — kept as a first-class enum so subclasses get type safety and exhaustiveness checking.
Usage Example
class EquipmentApi extends HttpEntityApi<Equipment> {
const EquipmentApi()
: super(
entityType: 'equipment',
schemaType: 'lms.equipment',
pathBuilder: const EndpointBuilder(prefix: 'lms/equipment'),
fromJson: Equipment.fromJson,
defaultSortField: 'name',
hasDrafts: true,
);
// Optional: customize caching
@override
CachingPolicy get cachingPolicy => const CachingPolicy(
staleDuration: Duration(minutes: 3),
excludedOperations: {'activity_heatmap'},
);
}EndpointBuilder
Class Signature
Constructor Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
prefix | String? | No | Entity endpoint path (e.g., 'lms/equipment') |
baseUrl | String? | No | Optional base URL override (defaults to plugin's baseUrl) |
Methods
getManyreturnsUriUri getMany({Map<String, dynamic>? queryParameters})GET /{prefix}
countreturnsUriUri count({Map<String, dynamic>? queryParameters, bool drafts = false})GET /{prefix}/count (drafts=true counts drafts)
getOnereturnsUriUri getOne(String id, {bool drafts = false})GET /{prefix}/{id}; drafts=true fetches entity_drafts.id from the draft partition
createreturnsUriUri create()POST /{prefix}
updatereturnsUriUri update(String id)PUT /{prefix}/{id}
deletereturnsUriUri delete(String id)DELETE /{prefix}/{id}
findPositionreturnsUriUri findPosition(String id, {Map<String, dynamic>? queryParameters})GET /{prefix}/{id}/position
buildUrireturnsUriUri buildUri(String path, {Map<String, dynamic>? queryParameters, String? prefix})Generic URI builder with optional query params and prefix override.
Constructs REST API endpoint URIs from a prefix path. All methods accept optional queryParameters (Map<String, dynamic>).
VersionedEndpointBuilder
class VersionedEndpointBuilder extends EndpointBuilder — extends with versioning and draft operations. Constructor accepts an optional draftsPrefix; falls back to 'drafts/${prefix.split('/').last}'.
| Method | Default Pattern |
|---|---|
versions(entityId, {queryParameters}) | GET /{prefix}/{id}/versions |
audit(entityId, {queryParameters}) | GET /{prefix}/{id}/audit |
byVersion(entityId, ver) | GET /{prefix}/{id}/versions/{ver} |
restoreVersion(entityId, ver) | POST /{prefix}/{id}/restore/{ver} |
compareVersions(id, from, to) | GET /{prefix}/{id}/compare/{from}/{to} |
deactivate(entityId) | POST /{prefix}/{id}/deactivate |
activate(entityId) | POST /{prefix}/{id}/activate |
getDraft(draftId) | GET /{draftsPrefix}/{draftId} |
updateDraft(draftId) | PUT /{draftsPrefix}/{draftId} |
submitDraft(draftId) | POST /{draftsPrefix}/{draftId}/submit |
cancelDraft(draftId) | POST /{draftsPrefix}/{draftId}/cancel |
resubmitDraft(draftId) | POST /{draftsPrefix}/{draftId}/resubmit |
draftVersions(draftId, {queryParameters}) | GET /{draftsPrefix}/{draftId}/versions |
draftAudit(draftId, {queryParameters}) | GET /{draftsPrefix}/{draftId}/audit |
getPendingDraftForEntity(entityId) | GET /{draftsPrefix}/{entityId}/pending |
reactivateDraft(draftId) | POST /{draftsPrefix}/{draftId}/restart |
SingletonVersionedEndpointBuilder
class SingletonVersionedEndpointBuilder extends VersionedEndpointBuilder — overrides getOne / update / delete to use the bare prefix (no /id), since singletons don't carry IDs in their CRUD URLs.
CachingPolicy
Class Signature
Properties
| Property | Type | Default | Description |
|---|---|---|---|
staleDuration | Duration | Duration(minutes: 5) | Time before cached data is stale |
excludedOperations | Set<String> | {} | Operations to exclude from caching |
Methods
shouldCachebool shouldCache(String operation)Whether to cache for this operation.
Static Constants
| Property | Type | Default | Description |
|---|---|---|---|
CachingPolicy.none | CachingPolicy | -- | Disables all caching |
Controls caching behavior for an entity API.
Example
// Disable caching entirely
@override
CachingPolicy get cachingPolicy => CachingPolicy.none;
// Custom stale duration, exclude analytics
@override
CachingPolicy get cachingPolicy => const CachingPolicy(
staleDuration: Duration(minutes: 3),
excludedOperations: {'activity_type_distribution'},
);CacheKeyBuilder
class CacheKeyBuilder — builds hierarchical cache keys for entity queries.
Key Format
entityType:operation:paramsExamples
final kb = CacheKeyBuilder('areas');
kb.key() // 'areas:' (entity prefix)
kb.key('list') // 'areas:list:' (operation prefix)
kb.key('byId', 'abc') // 'areas:byId:abc' (specific key)
kb.key('list', {'page': 0}) // 'areas:list:{"page":0}' (parameterized)StandardCacheOperations
abstract final class StandardCacheOperations {
static const list = 'list';
static const count = 'count';
static const byId = 'byId';
static const versions = 'versions';
static const audits = 'audits';
static const version = 'version';
static const compare = 'compare';
static const pendingDraft = 'pending_draft';
static const draftVersions = 'draft_versions';
static const draftAudits = 'draft_audits';
}Mutation
class Mutation (in api/cache/mutation.dart) — describes a change to declare via announce / subscribe to via subscribe.
final class Mutation {
final String entityType;
final MutationType type;
final String? entityId;
const Mutation(this.entityType, this.type, {this.entityId});
bool matches(Mutation other);
}
enum MutationType { create, update, delete }The matcher treats entityId == null as a wildcard — a subscription on "any update of equipment" matches every per-entity update mutation.
RelatedEntityApi
Class Signature
Constructor Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
parentIdentifier | String | Yes | Parent entity segment (e.g., 'user_groups') |
relationSegment | String | Yes | Relation path segment (e.g., 'users') |
requestBodyKey | String | Yes | JSON key for IDs in POST/DELETE body (e.g., 'user_ids') |
Methods
listFuture<List<Map<String, dynamic>>> list(String parentId)List linked entities.
listAvailableFuture<List<Map<String, dynamic>>> listAvailable(String parentId, {String? query})List available (unlinked) entities, optionally filtered by query.
linkFuture<void> link(String parentId, List<String> childIds)Link entities to parent.
unlinkFuture<void> unlink(String parentId, List<String> childIds)Unlink entities from parent.
class RelatedEntityApi with ApiHeadersMixin — minimal API for managing many-to-many relationships using a conventional REST route. The base URL is taken from EntitySystemPlugin.baseUrl.
Route convention
GET /{baseUrl}/{parentIdentifier}/{parentId}/{relationSegment}
POST /{baseUrl}/{parentIdentifier}/{parentId}/{relationSegment}
DELETE /{baseUrl}/{parentIdentifier}/{parentId}/{relationSegment}Body for POST / DELETE: { [requestBodyKey]: ['id1', 'id2', ...] }
Response convention: { success: true, data: [...] }. Raw arrays are tolerated (with a dev-warning) for backwards compatibility.
Usage
const relatedApi = RelatedEntityApi(
parentIdentifier: 'user_groups',
relationSegment: 'users',
requestBodyKey: 'user_ids',
);
// List users in group
final users = await relatedApi.list(groupId);
// List users available to add (with optional search)
final available = await relatedApi.listAvailable(groupId, query: 'jane');
// Add users to group
await relatedApi.link(groupId, ['user1', 'user2']);
// Remove user from group
await relatedApi.unlink(groupId, ['user1']);See Also
- EntityConfiguration — Where the API is registered
- Services — Query caching infrastructure
- Glossary — Term definitions