forked from eva/focus-flow
325 lines
11 KiB
Dart
325 lines
11 KiB
Dart
/// Domain-only repository contracts for scheduler persistence adapters.
|
|
///
|
|
/// This package defines the stable boundary that SQLite, memory, and future
|
|
/// adapters must implement. Interfaces accept and return domain objects from
|
|
/// `scheduler_core`; storage rows, Drift classes, SQL clients, and platform
|
|
/// details stay behind adapter packages.
|
|
library;
|
|
|
|
import 'package:scheduler_core/scheduler_core.dart' as core;
|
|
|
|
/// Adapter-neutral repository failure.
|
|
sealed class RepositoryFailure {
|
|
const RepositoryFailure({
|
|
this.entityId,
|
|
this.expectedRevision,
|
|
this.actualRevision,
|
|
this.message,
|
|
});
|
|
|
|
/// Stable id of the entity related to the failure, when available.
|
|
final String? entityId;
|
|
|
|
/// Revision the caller expected for a compare-and-set operation.
|
|
final core.Revision? expectedRevision;
|
|
|
|
/// Revision currently stored by the adapter.
|
|
final core.Revision? actualRevision;
|
|
|
|
/// Optional non-localized diagnostic text for logs and tests.
|
|
final String? message;
|
|
}
|
|
|
|
/// The requested entity was not found.
|
|
final class RepositoryNotFound extends RepositoryFailure {
|
|
/// Creates a not-found failure for [entityId].
|
|
const RepositoryNotFound({super.entityId, super.message});
|
|
}
|
|
|
|
/// A record with the requested id already exists.
|
|
final class RepositoryDuplicateId extends RepositoryFailure {
|
|
/// Creates a duplicate-id failure for [entityId].
|
|
const RepositoryDuplicateId({super.entityId, super.message});
|
|
}
|
|
|
|
/// The caller supplied an invalid revision value.
|
|
final class RepositoryInvalidRevision extends RepositoryFailure {
|
|
/// Creates an invalid-revision failure.
|
|
const RepositoryInvalidRevision({
|
|
super.entityId,
|
|
super.expectedRevision,
|
|
super.message,
|
|
});
|
|
}
|
|
|
|
/// The caller attempted to write using a stale revision.
|
|
final class RepositoryStaleRevision extends RepositoryFailure {
|
|
/// Creates a stale-revision failure.
|
|
const RepositoryStaleRevision({
|
|
super.entityId,
|
|
required super.expectedRevision,
|
|
required super.actualRevision,
|
|
super.message,
|
|
});
|
|
}
|
|
|
|
/// The requested record exists under a different owner scope.
|
|
final class RepositoryOwnerMismatch extends RepositoryFailure {
|
|
/// Creates an owner-mismatch failure for [entityId].
|
|
const RepositoryOwnerMismatch({super.entityId, super.message});
|
|
}
|
|
|
|
/// The adapter could not complete the operation due to storage failure.
|
|
final class RepositoryStorageFailure extends RepositoryFailure {
|
|
/// Creates a storage failure with optional [message].
|
|
const RepositoryStorageFailure({super.entityId, super.message});
|
|
}
|
|
|
|
/// Minimal Either type for expected repository success/failure paths.
|
|
sealed class Either<L, R> {
|
|
const Either();
|
|
|
|
/// Whether this value is a left/failure value.
|
|
bool get isLeft => this is Left<L, R>;
|
|
|
|
/// Whether this value is a right/success value.
|
|
bool get isRight => this is Right<L, R>;
|
|
|
|
/// Return the left value or throw when this is right.
|
|
L get left => switch (this) {
|
|
Left<L, R>(:final value) => value,
|
|
Right<L, R>() => throw StateError('Either does not contain left.'),
|
|
};
|
|
|
|
/// Return the right value or throw when this is left.
|
|
R get right => switch (this) {
|
|
Left<L, R>() => throw StateError('Either does not contain right.'),
|
|
Right<L, R>(:final value) => value,
|
|
};
|
|
}
|
|
|
|
/// Failure side of [Either].
|
|
final class Left<L, R> extends Either<L, R> {
|
|
/// Creates a left/failure value.
|
|
const Left(this.value);
|
|
|
|
/// Wrapped failure value.
|
|
final L value;
|
|
}
|
|
|
|
/// Success side of [Either].
|
|
final class Right<L, R> extends Either<L, R> {
|
|
/// Creates a right/success value.
|
|
const Right(this.value);
|
|
|
|
/// Wrapped success value.
|
|
final R value;
|
|
}
|
|
|
|
/// Repository result wrapper for expected adapter outcomes.
|
|
typedef RepoResult<T> = Either<RepositoryFailure, T>;
|
|
|
|
/// Repository contract for task records.
|
|
abstract interface class TaskRepository {
|
|
/// Return the task with [taskId] in [ownerId], or null when it is absent.
|
|
Future<RepoResult<core.Task?>> findById({
|
|
required core.OwnerId ownerId,
|
|
required String taskId,
|
|
});
|
|
|
|
/// Return owner-scoped tasks in deterministic adapter order.
|
|
Future<RepoResult<core.Page<core.Task>>> findByOwner({
|
|
required core.OwnerId ownerId,
|
|
core.PageRequest page = const core.PageRequest(),
|
|
});
|
|
|
|
/// Return owner-scoped tasks assigned to [projectId].
|
|
Future<RepoResult<core.Page<core.Task>>> findByProjectId({
|
|
required core.OwnerId ownerId,
|
|
required String projectId,
|
|
core.PageRequest page = const core.PageRequest(),
|
|
});
|
|
|
|
/// Return owner-scoped direct child tasks for [parentTaskId].
|
|
Future<RepoResult<core.Page<core.Task>>> findByParentTaskId({
|
|
required core.OwnerId ownerId,
|
|
required String parentTaskId,
|
|
core.PageRequest page = const core.PageRequest(),
|
|
});
|
|
|
|
/// Return owner-scoped tasks with [status].
|
|
Future<RepoResult<core.Page<core.Task>>> findByStatus({
|
|
required core.OwnerId ownerId,
|
|
required core.TaskStatus status,
|
|
core.PageRequest page = const core.PageRequest(),
|
|
});
|
|
|
|
/// Insert [task] for [ownerId] and return the assigned revision.
|
|
Future<RepoResult<core.Revision>> insert({
|
|
required core.OwnerId ownerId,
|
|
required core.Task task,
|
|
});
|
|
|
|
/// Save [task] if [expectedRevision] still matches the stored record.
|
|
Future<RepoResult<core.Revision>> save({
|
|
required core.OwnerId ownerId,
|
|
required core.Task task,
|
|
required core.Revision expectedRevision,
|
|
});
|
|
|
|
/// Remove [taskId] if [expectedRevision] still matches the stored record.
|
|
Future<RepoResult<core.Revision>> delete({
|
|
required core.OwnerId ownerId,
|
|
required String taskId,
|
|
required core.Revision expectedRevision,
|
|
});
|
|
}
|
|
|
|
/// Repository contract for project profile records.
|
|
abstract interface class ProjectRepository {
|
|
/// Return the project with [projectId] in [ownerId], or null when absent.
|
|
Future<RepoResult<core.ProjectProfile?>> findById({
|
|
required core.OwnerId ownerId,
|
|
required String projectId,
|
|
});
|
|
|
|
/// Return owner-scoped projects, optionally including archived profiles.
|
|
Future<RepoResult<core.Page<core.ProjectProfile>>> findByOwner({
|
|
required core.OwnerId ownerId,
|
|
bool includeArchived = false,
|
|
core.PageRequest page = const core.PageRequest(),
|
|
});
|
|
|
|
/// Insert [project] for [ownerId] and return the assigned revision.
|
|
Future<RepoResult<core.Revision>> insert({
|
|
required core.OwnerId ownerId,
|
|
required core.ProjectProfile project,
|
|
});
|
|
|
|
/// Save [project] if [expectedRevision] still matches the stored record.
|
|
Future<RepoResult<core.Revision>> save({
|
|
required core.OwnerId ownerId,
|
|
required core.ProjectProfile project,
|
|
required core.Revision expectedRevision,
|
|
});
|
|
|
|
/// Archive [projectId] without deleting task history.
|
|
Future<RepoResult<core.Revision>> archive({
|
|
required core.OwnerId ownerId,
|
|
required String projectId,
|
|
required core.Revision expectedRevision,
|
|
required DateTime archivedAtUtc,
|
|
});
|
|
}
|
|
|
|
/// Repository contract for locked blocks and date-scoped overrides.
|
|
abstract interface class LockedBlockRepository {
|
|
/// Return the locked block with [blockId] in [ownerId], or null when absent.
|
|
Future<RepoResult<core.LockedBlock?>> findBlockById({
|
|
required core.OwnerId ownerId,
|
|
required String blockId,
|
|
});
|
|
|
|
/// Return owner-scoped locked blocks.
|
|
Future<RepoResult<core.Page<core.LockedBlock>>> findBlocksByOwner({
|
|
required core.OwnerId ownerId,
|
|
bool includeArchived = false,
|
|
core.PageRequest page = const core.PageRequest(),
|
|
});
|
|
|
|
/// Return the override with [overrideId] in [ownerId], or null when absent.
|
|
Future<RepoResult<core.LockedBlockOverride?>> findOverrideById({
|
|
required core.OwnerId ownerId,
|
|
required String overrideId,
|
|
});
|
|
|
|
/// Return owner-scoped overrides for [date].
|
|
Future<RepoResult<core.Page<core.LockedBlockOverride>>> findOverridesForDate({
|
|
required core.OwnerId ownerId,
|
|
required core.CivilDate date,
|
|
core.PageRequest page = const core.PageRequest(),
|
|
});
|
|
|
|
/// Insert [block] for [ownerId] and return the assigned revision.
|
|
Future<RepoResult<core.Revision>> insertBlock({
|
|
required core.OwnerId ownerId,
|
|
required core.LockedBlock block,
|
|
});
|
|
|
|
/// Save [block] if [expectedRevision] still matches the stored record.
|
|
Future<RepoResult<core.Revision>> saveBlock({
|
|
required core.OwnerId ownerId,
|
|
required core.LockedBlock block,
|
|
required core.Revision expectedRevision,
|
|
});
|
|
|
|
/// Archive [blockId] without deleting historical schedule context.
|
|
Future<RepoResult<core.Revision>> archiveBlock({
|
|
required core.OwnerId ownerId,
|
|
required String blockId,
|
|
required core.Revision expectedRevision,
|
|
required DateTime archivedAtUtc,
|
|
});
|
|
|
|
/// Insert [override] for [ownerId] and return the assigned revision.
|
|
Future<RepoResult<core.Revision>> insertOverride({
|
|
required core.OwnerId ownerId,
|
|
required core.LockedBlockOverride override,
|
|
});
|
|
|
|
/// Save [override] if [expectedRevision] still matches the stored record.
|
|
Future<RepoResult<core.Revision>> saveOverride({
|
|
required core.OwnerId ownerId,
|
|
required core.LockedBlockOverride override,
|
|
required core.Revision expectedRevision,
|
|
});
|
|
}
|
|
|
|
/// Repository contract for owner settings.
|
|
abstract interface class SettingsRepository {
|
|
/// Return settings for [ownerId], or null when no settings have been saved.
|
|
Future<RepoResult<core.OwnerSettings?>> findByOwner({
|
|
required core.OwnerId ownerId,
|
|
});
|
|
|
|
/// Insert [settings] for [ownerId] and return the assigned revision.
|
|
Future<RepoResult<core.Revision>> insert({
|
|
required core.OwnerId ownerId,
|
|
required core.OwnerSettings settings,
|
|
});
|
|
|
|
/// Save [settings] if [expectedRevision] still matches the stored record.
|
|
Future<RepoResult<core.Revision>> save({
|
|
required core.OwnerId ownerId,
|
|
required core.OwnerSettings settings,
|
|
required core.Revision expectedRevision,
|
|
});
|
|
}
|
|
|
|
/// Repository contract for diagnostic schedule snapshots.
|
|
abstract interface class ScheduleSnapshotRepository {
|
|
/// Return the snapshot with [snapshotId] in [ownerId], or null when absent.
|
|
Future<RepoResult<core.SchedulingStateSnapshot?>> findById({
|
|
required core.OwnerId ownerId,
|
|
required String snapshotId,
|
|
});
|
|
|
|
/// Return snapshots captured for [ownerId] that overlap [window].
|
|
Future<RepoResult<core.Page<core.SchedulingStateSnapshot>>> findInWindow({
|
|
required core.OwnerId ownerId,
|
|
required core.SchedulingWindow window,
|
|
core.PageRequest page = const core.PageRequest(),
|
|
});
|
|
|
|
/// Insert [snapshot] for [ownerId] and return the assigned revision.
|
|
Future<RepoResult<core.Revision>> insert({
|
|
required core.OwnerId ownerId,
|
|
required core.SchedulingStateSnapshot snapshot,
|
|
});
|
|
|
|
/// Delete snapshots whose retention expiry is before [nowUtc].
|
|
Future<RepoResult<int>> deleteExpired({
|
|
required core.OwnerId ownerId,
|
|
required DateTime nowUtc,
|
|
});
|
|
}
|