focus-flow/packages/scheduler_persistence/lib/persistence.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,
});
}