Skip to content

EntityApi

The abstract CRUD interface for entity operations. Every entity type requires an EntityApi<T> implementation that handles backend communication.

Class Signature

Constructor Parameters

ParameterTypeRequiredDescription
entityTypeStringYesEntity type identifier (e.g., 'equipment')
schemaTypeStringYesSchema-qualified type (e.g., 'lms.equipment')
defaultSortFieldString?NoDefault field for list sorting
hasDraftsboolNoWhether entity supports drafts (uses Draftable mixin). When true, getDrafts/countDrafts are wired in HttpEntityApi.

Read Operations

  • getOne
    Future<T?> getOne(String id)

    Get single entity by ID.

  • getMany
    Future<List<T>> getMany({int page, int pageSize, String? query, String? sortField, bool sortAscending, Map<String, dynamic>? filters})

    Paginated list. Returns only approved entities.

  • count
    Future<int> count({String? query, Map<String, dynamic>? filters})

    Total entity count.

  • findPosition
    Future<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

  • create
    Future<T> create(T entity, {String? remarks})

    Create a new entity. Remarks are stored in the audit trail.

  • update
    Future<T> update(String id, T entity, {String? remarks})

    Update existing entity.

  • delete
    Future<bool> delete(String id)

    Delete an entity.

Draft Operations (default no-op when hasDrafts=false)

  • getDrafts
    Future<List<T>> getDrafts({int page, int pageSize, String? query, String? sortField, bool sortAscending, Map<String, dynamic>? filters})

    List pending drafts.

  • countDrafts
    Future<int> countDrafts({String? query, Map<String, dynamic>? filters})

    Count pending drafts.

Serialization

  • fromJson
    T fromJson(Map<String, dynamic> json)

    Create entity from JSON.


HttpEntityApi

Class Signature

Constructor Parameters

ParameterTypeRequiredDescription
entityTypeStringYesEntity type identifier
schemaTypeStringYesSchema-qualified type name
pathBuilderEndpointBuilderYesAPI endpoint path builder
fromJsonT Function(Map<String, dynamic>)YesJSON deserialization factory
defaultSortFieldString?NoDefault sort field
hasDraftsboolNoDraft support flag

Overridable Getters

PropertyTypeDefaultDescription
cachingPolicyCachingPolicyCachingPolicy() (5 min stale)Controls caching behavior
keyBuilderCacheKeyBuilderCacheKeyBuilder(entityType)Generates cache keys

Overridable Methods

  • toCreateJson
    Future<Map<String, dynamic>> toCreateJson(T entity)

    Transform entity to create payload (default strips id, sets createdBy/updatedBy/At from current user).

  • toUpdateJson
    Future<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)

  • post
    Future<Map<String, dynamic>> post(String path, {Object? body, Set<Mutation> announces})

    Custom POST. Fans out announces on 2xx; never on error.

  • put
    Future<Map<String, dynamic>> put(String path, {Object? body, Set<Mutation> announces})

    Custom PUT.

  • patch
    Future<Map<String, dynamic>> patch(String path, {Object? body, Set<Mutation> announces})

    Custom PATCH.

  • mutate
    Future<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.

  • announceAll
    void announceAll(Iterable<Mutation> mutations)

    Fan out a batch of mutations without an HTTP call (SSE triggers, batch workflows, post-hoc reconciliation).

Cache helpers (@protected)

  • cachedGet
    Future<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.

  • announce
    void announce(MutationType type, {String? entityId})

    Notify cache of a mutation for this entityType.

Bulk import

  • bulkImport
    Future<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:

dart
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

dart
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

ParameterTypeRequiredDescription
prefixString?NoEntity endpoint path (e.g., 'lms/equipment')
baseUrlString?NoOptional base URL override (defaults to plugin's baseUrl)

Methods

  • getMany returns Uri
    Uri getMany({Map<String, dynamic>? queryParameters})

    GET /{prefix}

  • count returns Uri
    Uri count({Map<String, dynamic>? queryParameters, bool drafts = false})

    GET /{prefix}/count (drafts=true counts drafts)

  • getOne returns Uri
    Uri getOne(String id, {bool drafts = false})

    GET /{prefix}/{id}; drafts=true fetches entity_drafts.id from the draft partition

  • create returns Uri
    Uri create()

    POST /{prefix}

  • update returns Uri
    Uri update(String id)

    PUT /{prefix}/{id}

  • delete returns Uri
    Uri delete(String id)

    DELETE /{prefix}/{id}

  • findPosition returns Uri
    Uri findPosition(String id, {Map<String, dynamic>? queryParameters})

    GET /{prefix}/{id}/position

  • buildUri returns Uri
    Uri 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}'.

MethodDefault 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

PropertyTypeDefaultDescription
staleDurationDurationDuration(minutes: 5)Time before cached data is stale
excludedOperationsSet<String>{}Operations to exclude from caching

Methods

  • shouldCache
    bool shouldCache(String operation)

    Whether to cache for this operation.

Static Constants

PropertyTypeDefaultDescription
CachingPolicy.noneCachingPolicy--Disables all caching

Controls caching behavior for an entity API.

Example

dart
// 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:params

Examples

dart
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

dart
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.

dart
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

ParameterTypeRequiredDescription
parentIdentifierStringYesParent entity segment (e.g., 'user_groups')
relationSegmentStringYesRelation path segment (e.g., 'users')
requestBodyKeyStringYesJSON key for IDs in POST/DELETE body (e.g., 'user_ids')

Methods

  • list
    Future<List<Map<String, dynamic>>> list(String parentId)

    List linked entities.

  • listAvailable
    Future<List<Map<String, dynamic>>> listAvailable(String parentId, {String? query})

    List available (unlinked) entities, optionally filtered by query.

  • link
    Future<void> link(String parentId, List<String> childIds)

    Link entities to parent.

  • unlink
    Future<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

dart
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