focus-flow/lib/src/application_layer.dart

996 lines
30 KiB
Dart

// Application orchestration contracts for UI-facing use cases.
//
// This layer sits above the pure domain services and repository interfaces. It
// gives future Flutter/use-case code one atomic boundary per user command
// without importing widgets, MongoDB clients, or platform notification APIs.
import 'backlog.dart';
import 'locked_time.dart';
import 'models.dart';
import 'project_statistics.dart';
import 'repositories.dart';
import 'scheduling_engine.dart';
import 'task_lifecycle.dart';
import 'time_contracts.dart';
/// Owner-local time context supplied to application operations.
class OwnerTimeZoneContext {
OwnerTimeZoneContext({
required String ownerId,
required String timeZoneId,
}) : ownerId = _requiredTrimmed(ownerId, 'ownerId'),
timeZoneId = _requiredTrimmed(timeZoneId, 'timeZoneId');
/// Authorization-neutral owner scope. V1 does not implement accounts.
final String ownerId;
/// IANA-style time-zone identifier chosen by the app boundary.
final String timeZoneId;
}
/// Per-operation application context.
class ApplicationOperationContext {
ApplicationOperationContext({
required String operationId,
required this.now,
required this.ownerTimeZone,
required this.clock,
required this.idGenerator,
}) : operationId = _requiredTrimmed(operationId, 'operationId');
/// Convenience constructor that captures [Clock.now] once.
factory ApplicationOperationContext.start({
required String operationId,
required OwnerTimeZoneContext ownerTimeZone,
required Clock clock,
required IdGenerator idGenerator,
}) {
return ApplicationOperationContext(
operationId: operationId,
now: clock.now(),
ownerTimeZone: ownerTimeZone,
clock: clock,
idGenerator: idGenerator,
);
}
/// Exactly-once key for this user command.
final String operationId;
/// Instant used by the operation. Use this instead of repeatedly reading the
/// clock during one command.
final DateTime now;
/// Owner scope and time-zone data for date-sensitive rules.
final OwnerTimeZoneContext ownerTimeZone;
/// Injected clock available to lower-level services that need one.
final Clock clock;
/// Injected ID generator for records created by the operation.
final IdGenerator idGenerator;
}
/// Stable application-layer result categories.
enum ApplicationFailureCode {
validation,
notFound,
conflict,
staleRevision,
noSlot,
duplicateOperation,
persistenceFailure,
unexpected,
}
/// Typed failure returned by application commands and queries.
class ApplicationFailure {
const ApplicationFailure({
required this.code,
this.entityId,
this.detailCode,
});
/// Stable machine-readable category.
final ApplicationFailureCode code;
/// Optional stable entity id related to the failure.
final String? entityId;
/// Optional narrower stable reason, such as a domain validation code name.
final String? detailCode;
}
/// Result wrapper for expected application success/failure paths.
class ApplicationResult<T> {
const ApplicationResult._({
this.value,
this.failure,
});
/// Successful result carrying [value].
factory ApplicationResult.success(T value) {
return ApplicationResult._(value: value);
}
/// Expected failure result.
factory ApplicationResult.failure(ApplicationFailure failure) {
return ApplicationResult._(failure: failure);
}
/// Successful value, if any.
final T? value;
/// Typed failure, if the operation did not succeed.
final ApplicationFailure? failure;
/// Whether the operation succeeded.
bool get isSuccess => failure == null;
/// Whether the operation failed with a typed application failure.
bool get isFailure => failure != null;
/// Return the value or throw if this result is a failure.
T get requireValue {
if (failure != null) {
throw StateError(
'Application result is a failure: ${failure!.code.name}');
}
return value as T;
}
}
/// Exception for expected failures raised inside a unit-of-work callback.
class ApplicationFailureException implements Exception {
const ApplicationFailureException(this.failure);
/// Failure to return from the application boundary.
final ApplicationFailure failure;
}
/// Exception representing persistence/driver failures at repository boundaries.
class ApplicationPersistenceException implements Exception {
const ApplicationPersistenceException();
}
/// Persisted exactly-once operation record.
class ApplicationOperationRecord {
ApplicationOperationRecord({
required String operationId,
required String ownerId,
required String operationName,
required this.committedAt,
}) : operationId = _requiredTrimmed(operationId, 'operationId'),
ownerId = _requiredTrimmed(ownerId, 'ownerId'),
operationName = _requiredTrimmed(operationName, 'operationName');
/// Exactly-once operation id.
final String operationId;
/// Authorization-neutral owner scope.
final String ownerId;
/// Stable use-case/command label.
final String operationName;
/// Commit instant.
final DateTime committedAt;
}
/// Owner-scoped acknowledgement for a one-time scheduling notice.
class NoticeAcknowledgementRecord {
NoticeAcknowledgementRecord({
required String noticeId,
required String ownerId,
required this.acknowledgedAt,
}) : noticeId = _requiredTrimmed(noticeId, 'noticeId'),
ownerId = _requiredTrimmed(ownerId, 'ownerId');
/// Stable notice id generated by the read/use-case boundary.
final String noticeId;
/// Authorization-neutral owner scope.
final String ownerId;
/// Instant the notice was consumed.
final DateTime acknowledgedAt;
}
/// Owner-level settings needed by application use cases.
class OwnerSettings {
OwnerSettings({
required String ownerId,
required String timeZoneId,
WallTime? dayStart,
WallTime? dayEnd,
this.compactModeEnabled = false,
this.backlogStalenessSettings = const BacklogStalenessSettings(),
}) : ownerId = _requiredTrimmed(ownerId, 'ownerId'),
timeZoneId = _requiredTrimmed(timeZoneId, 'timeZoneId'),
dayStart = dayStart ?? WallTime(hour: 0, minute: 0),
dayEnd = dayEnd ?? WallTime(hour: 23, minute: 59) {
if (this.dayStart.compareTo(this.dayEnd) >= 0) {
throw ArgumentError.value(
{'dayStart': this.dayStart, 'dayEnd': this.dayEnd},
'dayEnd',
'Day end must be after day start.',
);
}
}
/// Authorization-neutral owner scope.
final String ownerId;
/// Owner time-zone id used by date-sensitive application use cases.
final String timeZoneId;
/// Default local day start.
final WallTime dayStart;
/// Default local day end.
final WallTime dayEnd;
/// Persisted compact-mode preference.
final bool compactModeEnabled;
/// Configurable backlog age thresholds.
final BacklogStalenessSettings backlogStalenessSettings;
/// Return a copy with selected settings changed.
OwnerSettings copyWith({
String? ownerId,
String? timeZoneId,
WallTime? dayStart,
WallTime? dayEnd,
bool? compactModeEnabled,
BacklogStalenessSettings? backlogStalenessSettings,
}) {
return OwnerSettings(
ownerId: ownerId ?? this.ownerId,
timeZoneId: timeZoneId ?? this.timeZoneId,
dayStart: dayStart ?? this.dayStart,
dayEnd: dayEnd ?? this.dayEnd,
compactModeEnabled: compactModeEnabled ?? this.compactModeEnabled,
backlogStalenessSettings:
backlogStalenessSettings ?? this.backlogStalenessSettings,
);
}
}
/// Repository contract for internal task activities.
abstract interface class TaskActivityRepository {
Future<TaskActivity?> findById(String id);
Future<List<TaskActivity>> findByOperationId(String operationId);
Future<List<TaskActivity>> findByTaskId(String taskId);
Future<List<TaskActivity>> findAll();
Future<void> save(TaskActivity activity);
Future<void> saveAll(Iterable<TaskActivity> activities);
}
/// Repository contract for project statistics aggregates.
abstract interface class ProjectStatisticsRepository {
Future<ProjectStatistics?> findByProjectId(String projectId);
Future<List<ProjectStatistics>> findAll();
Future<void> save(ProjectStatistics statistics);
}
/// Repository contract for owner settings.
abstract interface class OwnerSettingsRepository {
Future<OwnerSettings?> findByOwnerId(String ownerId);
Future<void> save(OwnerSettings settings);
}
/// Repository contract for consumed one-time notices.
abstract interface class NoticeAcknowledgementRepository {
Future<NoticeAcknowledgementRecord?> findById({
required String ownerId,
required String noticeId,
});
Future<List<NoticeAcknowledgementRecord>> findByOwnerId(String ownerId);
Future<void> save(NoticeAcknowledgementRecord record);
}
/// Repository contract for exactly-once operation records.
abstract interface class ApplicationOperationRepository {
Future<ApplicationOperationRecord?> findById(String operationId);
Future<void> save(ApplicationOperationRecord operation);
}
/// Repository set available inside one application unit of work.
abstract interface class ApplicationUnitOfWorkRepositories {
TaskRepository get tasks;
ProjectRepository get projects;
LockedBlockRepository get lockedBlocks;
SchedulingSnapshotRepository get schedulingSnapshots;
TaskActivityRepository get taskActivities;
ProjectStatisticsRepository get projectStatistics;
OwnerSettingsRepository get ownerSettings;
NoticeAcknowledgementRepository get noticeAcknowledgements;
ApplicationOperationRepository get operations;
}
/// Atomic application transaction boundary.
abstract interface class ApplicationUnitOfWork {
Future<ApplicationResult<T>> run<T>({
required ApplicationOperationContext context,
required String operationName,
required Future<ApplicationResult<T>> Function(
ApplicationUnitOfWorkRepositories repositories,
) action,
});
Future<ApplicationResult<T>> read<T>({
required Future<ApplicationResult<T>> Function(
ApplicationUnitOfWorkRepositories repositories,
) action,
});
}
/// Repository loader for common scheduler input assembly.
class ApplicationSchedulingLoader {
const ApplicationSchedulingLoader(this.repositories);
/// Repositories for the current application operation.
final ApplicationUnitOfWorkRepositories repositories;
/// Load scheduler input for [window] from the repository boundary.
///
/// UI code should not persist partial [SchedulingResult] objects. Commands
/// should load authoritative state here, invoke domain services, then commit
/// resulting entities through a unit of work.
Future<SchedulingInput> loadInputForWindow({
required SchedulingWindow window,
List<TimeInterval> lockedIntervals = const <TimeInterval>[],
List<TimeInterval> requiredVisibleIntervals = const <TimeInterval>[],
}) async {
final scheduledTasks = await repositories.tasks.findScheduledInWindow(
window,
);
return SchedulingInput(
tasks: scheduledTasks,
window: window,
lockedIntervals: lockedIntervals,
requiredVisibleIntervals: requiredVisibleIntervals,
);
}
}
/// In-memory unit-of-work implementation for deterministic application tests.
class InMemoryApplicationUnitOfWork implements ApplicationUnitOfWork {
InMemoryApplicationUnitOfWork({
Iterable<Task> initialTasks = const <Task>[],
Iterable<ProjectProfile> initialProjects = const <ProjectProfile>[],
Iterable<LockedBlock> initialLockedBlocks = const <LockedBlock>[],
Iterable<LockedBlockOverride> initialLockedOverrides =
const <LockedBlockOverride>[],
Iterable<SchedulingStateSnapshot> initialSchedulingSnapshots =
const <SchedulingStateSnapshot>[],
Iterable<TaskActivity> initialTaskActivities = const <TaskActivity>[],
Iterable<ProjectStatistics> initialProjectStatistics =
const <ProjectStatistics>[],
Iterable<OwnerSettings> initialOwnerSettings = const <OwnerSettings>[],
Iterable<NoticeAcknowledgementRecord> initialNoticeAcknowledgements =
const <NoticeAcknowledgementRecord>[],
Iterable<ApplicationOperationRecord> initialOperations =
const <ApplicationOperationRecord>[],
}) : _state = _InMemoryApplicationState(
tasksById: {
for (final task in initialTasks) task.id: task,
},
projectsById: {
for (final project in initialProjects) project.id: project,
},
lockedBlocksById: {
for (final block in initialLockedBlocks) block.id: block,
},
lockedOverridesById: {
for (final override in initialLockedOverrides)
override.id: override,
},
schedulingSnapshotsById: {
for (final snapshot in initialSchedulingSnapshots)
snapshot.id: snapshot,
},
taskActivitiesById: {
for (final activity in initialTaskActivities) activity.id: activity,
},
projectStatisticsByProjectId: {
for (final statistics in initialProjectStatistics)
statistics.projectId: statistics,
},
ownerSettingsByOwnerId: {
for (final settings in initialOwnerSettings)
settings.ownerId: settings,
},
noticeAcknowledgementsByScopedId: {
for (final acknowledgement in initialNoticeAcknowledgements)
_noticeAcknowledgementKey(
ownerId: acknowledgement.ownerId,
noticeId: acknowledgement.noticeId,
): acknowledgement,
},
operationsById: {
for (final operation in initialOperations)
operation.operationId: operation,
},
);
_InMemoryApplicationState _state;
bool _failNextCommit = false;
/// Current committed tasks, useful for contract tests.
List<Task> get currentTasks =>
List<Task>.unmodifiable(_state.tasksById.values);
/// Current committed projects, useful for contract tests.
List<ProjectProfile> get currentProjects {
return List<ProjectProfile>.unmodifiable(_state.projectsById.values);
}
/// Current committed locked blocks, useful for contract tests.
List<LockedBlock> get currentLockedBlocks {
return List<LockedBlock>.unmodifiable(_state.lockedBlocksById.values);
}
/// Current committed locked overrides, useful for contract tests.
List<LockedBlockOverride> get currentLockedOverrides {
return List<LockedBlockOverride>.unmodifiable(
_state.lockedOverridesById.values,
);
}
/// Current committed scheduling snapshots, useful for contract tests.
List<SchedulingStateSnapshot> get currentSchedulingSnapshots {
return List<SchedulingStateSnapshot>.unmodifiable(
_state.schedulingSnapshotsById.values,
);
}
/// Current committed activities, useful for contract tests.
List<TaskActivity> get currentTaskActivities {
return List<TaskActivity>.unmodifiable(_state.taskActivitiesById.values);
}
/// Current committed project statistics, useful for contract tests.
List<ProjectStatistics> get currentProjectStatistics {
return List<ProjectStatistics>.unmodifiable(
_state.projectStatisticsByProjectId.values,
);
}
/// Current committed owner settings, useful for contract tests.
List<OwnerSettings> get currentOwnerSettings {
return List<OwnerSettings>.unmodifiable(
_state.ownerSettingsByOwnerId.values);
}
/// Current committed notice acknowledgements, useful for contract tests.
List<NoticeAcknowledgementRecord> get currentNoticeAcknowledgements {
return List<NoticeAcknowledgementRecord>.unmodifiable(
_state.noticeAcknowledgementsByScopedId.values,
);
}
/// Current committed operation records, useful for contract tests.
List<ApplicationOperationRecord> get currentOperations {
return List<ApplicationOperationRecord>.unmodifiable(
_state.operationsById.values,
);
}
/// Cause the next successful transaction to fail at commit time.
void failNextCommitForTesting() {
_failNextCommit = true;
}
@override
Future<ApplicationResult<T>> run<T>({
required ApplicationOperationContext context,
required String operationName,
required Future<ApplicationResult<T>> Function(
ApplicationUnitOfWorkRepositories repositories,
) action,
}) async {
if (_state.operationsById.containsKey(context.operationId)) {
return ApplicationResult.failure(
ApplicationFailure(
code: ApplicationFailureCode.duplicateOperation,
entityId: context.operationId,
),
);
}
final staged = _state.copy();
final repositories = _InMemoryApplicationRepositories(staged);
try {
final result = await action(repositories);
if (result.isFailure) {
return result;
}
staged.operationsById[context.operationId] = ApplicationOperationRecord(
operationId: context.operationId,
ownerId: context.ownerTimeZone.ownerId,
operationName: operationName,
committedAt: context.now,
);
if (_failNextCommit) {
_failNextCommit = false;
throw const ApplicationPersistenceException();
}
_state = staged;
return result;
} on ApplicationFailureException catch (error) {
return ApplicationResult.failure(error.failure);
} on DomainValidationException catch (error) {
return ApplicationResult.failure(
ApplicationFailure(
code: ApplicationFailureCode.validation,
detailCode: error.code.name,
),
);
} on ArgumentError catch (error) {
return ApplicationResult.failure(
ApplicationFailure(
code: ApplicationFailureCode.validation,
detailCode: error.name,
),
);
} on ApplicationPersistenceException {
return ApplicationResult.failure(
const ApplicationFailure(
code: ApplicationFailureCode.persistenceFailure,
),
);
} catch (_) {
return ApplicationResult.failure(
const ApplicationFailure(code: ApplicationFailureCode.unexpected),
);
}
}
@override
Future<ApplicationResult<T>> read<T>({
required Future<ApplicationResult<T>> Function(
ApplicationUnitOfWorkRepositories repositories,
) action,
}) async {
final staged = _state.copy();
final repositories = _InMemoryApplicationRepositories(staged);
try {
return await action(repositories);
} on ApplicationFailureException catch (error) {
return ApplicationResult.failure(error.failure);
} on DomainValidationException catch (error) {
return ApplicationResult.failure(
ApplicationFailure(
code: ApplicationFailureCode.validation,
detailCode: error.code.name,
),
);
} on ArgumentError catch (error) {
return ApplicationResult.failure(
ApplicationFailure(
code: ApplicationFailureCode.validation,
detailCode: error.name,
),
);
} on ApplicationPersistenceException {
return ApplicationResult.failure(
const ApplicationFailure(
code: ApplicationFailureCode.persistenceFailure,
),
);
} catch (_) {
return ApplicationResult.failure(
const ApplicationFailure(code: ApplicationFailureCode.unexpected),
);
}
}
}
class _InMemoryApplicationState {
_InMemoryApplicationState({
required this.tasksById,
required this.projectsById,
required this.lockedBlocksById,
required this.lockedOverridesById,
required this.schedulingSnapshotsById,
required this.taskActivitiesById,
required this.projectStatisticsByProjectId,
required this.ownerSettingsByOwnerId,
required this.noticeAcknowledgementsByScopedId,
required this.operationsById,
});
final Map<String, Task> tasksById;
final Map<String, ProjectProfile> projectsById;
final Map<String, LockedBlock> lockedBlocksById;
final Map<String, LockedBlockOverride> lockedOverridesById;
final Map<String, SchedulingStateSnapshot> schedulingSnapshotsById;
final Map<String, TaskActivity> taskActivitiesById;
final Map<String, ProjectStatistics> projectStatisticsByProjectId;
final Map<String, OwnerSettings> ownerSettingsByOwnerId;
final Map<String, NoticeAcknowledgementRecord>
noticeAcknowledgementsByScopedId;
final Map<String, ApplicationOperationRecord> operationsById;
_InMemoryApplicationState copy() {
return _InMemoryApplicationState(
tasksById: Map<String, Task>.of(tasksById),
projectsById: Map<String, ProjectProfile>.of(projectsById),
lockedBlocksById: Map<String, LockedBlock>.of(lockedBlocksById),
lockedOverridesById:
Map<String, LockedBlockOverride>.of(lockedOverridesById),
schedulingSnapshotsById:
Map<String, SchedulingStateSnapshot>.of(schedulingSnapshotsById),
taskActivitiesById: Map<String, TaskActivity>.of(taskActivitiesById),
projectStatisticsByProjectId:
Map<String, ProjectStatistics>.of(projectStatisticsByProjectId),
ownerSettingsByOwnerId: Map<String, OwnerSettings>.of(
ownerSettingsByOwnerId,
),
noticeAcknowledgementsByScopedId:
Map<String, NoticeAcknowledgementRecord>.of(
noticeAcknowledgementsByScopedId,
),
operationsById: Map<String, ApplicationOperationRecord>.of(
operationsById,
),
);
}
}
class _InMemoryApplicationRepositories
implements ApplicationUnitOfWorkRepositories {
_InMemoryApplicationRepositories(_InMemoryApplicationState state)
: tasks = _UnitOfWorkTaskRepository(state.tasksById),
projects = _UnitOfWorkProjectRepository(state.projectsById),
lockedBlocks = _UnitOfWorkLockedBlockRepository(
blocksById: state.lockedBlocksById,
overridesById: state.lockedOverridesById,
),
schedulingSnapshots = _UnitOfWorkSchedulingSnapshotRepository(
state.schedulingSnapshotsById,
),
taskActivities = _UnitOfWorkTaskActivityRepository(
state.taskActivitiesById,
),
projectStatistics = _UnitOfWorkProjectStatisticsRepository(
state.projectStatisticsByProjectId,
),
ownerSettings = _UnitOfWorkOwnerSettingsRepository(
state.ownerSettingsByOwnerId,
),
noticeAcknowledgements = _UnitOfWorkNoticeAcknowledgementRepository(
state.noticeAcknowledgementsByScopedId,
),
operations = _UnitOfWorkOperationRepository(state.operationsById);
@override
final TaskRepository tasks;
@override
final ProjectRepository projects;
@override
final LockedBlockRepository lockedBlocks;
@override
final SchedulingSnapshotRepository schedulingSnapshots;
@override
final TaskActivityRepository taskActivities;
@override
final ProjectStatisticsRepository projectStatistics;
@override
final OwnerSettingsRepository ownerSettings;
@override
final NoticeAcknowledgementRepository noticeAcknowledgements;
@override
final ApplicationOperationRepository operations;
}
class _UnitOfWorkTaskRepository implements TaskRepository {
_UnitOfWorkTaskRepository(this._tasksById);
final Map<String, Task> _tasksById;
@override
Future<Task?> findById(String id) async => _tasksById[id];
@override
Future<List<Task>> findAll() async {
return List<Task>.unmodifiable(_tasksById.values);
}
@override
Future<List<Task>> findByStatus(TaskStatus status) async {
return List<Task>.unmodifiable(
_tasksById.values.where((task) => task.status == status),
);
}
@override
Future<List<Task>> findScheduledInWindow(SchedulingWindow window) async {
return List<Task>.unmodifiable(
_tasksById.values.where((task) {
final start = task.scheduledStart;
final end = task.scheduledEnd;
if (start == null || end == null) {
return false;
}
return TimeInterval(start: start, end: end).overlaps(window.interval);
}),
);
}
@override
Future<void> save(Task task) async {
_tasksById[task.id] = task;
}
@override
Future<void> saveAll(Iterable<Task> tasks) async {
for (final task in tasks) {
_tasksById[task.id] = task;
}
}
}
class _UnitOfWorkProjectRepository implements ProjectRepository {
_UnitOfWorkProjectRepository(this._projectsById);
final Map<String, ProjectProfile> _projectsById;
@override
Future<ProjectProfile?> findById(String id) async => _projectsById[id];
@override
Future<List<ProjectProfile>> findAll() async {
return List<ProjectProfile>.unmodifiable(_projectsById.values);
}
@override
Future<void> save(ProjectProfile project) async {
_projectsById[project.id] = project;
}
}
class _UnitOfWorkLockedBlockRepository implements LockedBlockRepository {
_UnitOfWorkLockedBlockRepository({
required Map<String, LockedBlock> blocksById,
required Map<String, LockedBlockOverride> overridesById,
}) : _blocksById = blocksById,
_overridesById = overridesById;
final Map<String, LockedBlock> _blocksById;
final Map<String, LockedBlockOverride> _overridesById;
@override
Future<LockedBlock?> findBlockById(String id) async => _blocksById[id];
@override
Future<List<LockedBlock>> findAllBlocks() async {
return List<LockedBlock>.unmodifiable(_blocksById.values);
}
@override
Future<LockedBlockOverride?> findOverrideById(String id) async {
return _overridesById[id];
}
@override
Future<List<LockedBlockOverride>> findAllOverrides() async {
return List<LockedBlockOverride>.unmodifiable(_overridesById.values);
}
@override
Future<void> saveBlock(LockedBlock block) async {
_blocksById[block.id] = block;
}
@override
Future<void> saveOverride(LockedBlockOverride override) async {
_overridesById[override.id] = override;
}
}
class _UnitOfWorkSchedulingSnapshotRepository
implements SchedulingSnapshotRepository {
_UnitOfWorkSchedulingSnapshotRepository(this._snapshotsById);
final Map<String, SchedulingStateSnapshot> _snapshotsById;
@override
Future<SchedulingStateSnapshot?> findById(String id) async {
return _snapshotsById[id];
}
@override
Future<List<SchedulingStateSnapshot>> findInWindow(
SchedulingWindow window,
) async {
return List<SchedulingStateSnapshot>.unmodifiable(
_snapshotsById.values.where(
(snapshot) => snapshot.window.interval.overlaps(window.interval),
),
);
}
@override
Future<void> save(SchedulingStateSnapshot snapshot) async {
_snapshotsById[snapshot.id] = snapshot;
}
}
class _UnitOfWorkTaskActivityRepository implements TaskActivityRepository {
_UnitOfWorkTaskActivityRepository(this._activitiesById);
final Map<String, TaskActivity> _activitiesById;
@override
Future<TaskActivity?> findById(String id) async => _activitiesById[id];
@override
Future<List<TaskActivity>> findByOperationId(String operationId) async {
return List<TaskActivity>.unmodifiable(
_activitiesById.values.where(
(activity) => activity.operationId == operationId,
),
);
}
@override
Future<List<TaskActivity>> findByTaskId(String taskId) async {
return List<TaskActivity>.unmodifiable(
_activitiesById.values.where((activity) => activity.taskId == taskId),
);
}
@override
Future<List<TaskActivity>> findAll() async {
return List<TaskActivity>.unmodifiable(_activitiesById.values);
}
@override
Future<void> save(TaskActivity activity) async {
_activitiesById[activity.id] = activity;
}
@override
Future<void> saveAll(Iterable<TaskActivity> activities) async {
for (final activity in activities) {
_activitiesById[activity.id] = activity;
}
}
}
class _UnitOfWorkProjectStatisticsRepository
implements ProjectStatisticsRepository {
_UnitOfWorkProjectStatisticsRepository(this._statisticsByProjectId);
final Map<String, ProjectStatistics> _statisticsByProjectId;
@override
Future<ProjectStatistics?> findByProjectId(String projectId) async {
return _statisticsByProjectId[projectId];
}
@override
Future<List<ProjectStatistics>> findAll() async {
return List<ProjectStatistics>.unmodifiable(_statisticsByProjectId.values);
}
@override
Future<void> save(ProjectStatistics statistics) async {
_statisticsByProjectId[statistics.projectId] = statistics;
}
}
class _UnitOfWorkOwnerSettingsRepository implements OwnerSettingsRepository {
_UnitOfWorkOwnerSettingsRepository(this._settingsByOwnerId);
final Map<String, OwnerSettings> _settingsByOwnerId;
@override
Future<OwnerSettings?> findByOwnerId(String ownerId) async {
return _settingsByOwnerId[ownerId];
}
@override
Future<void> save(OwnerSettings settings) async {
_settingsByOwnerId[settings.ownerId] = settings;
}
}
class _UnitOfWorkNoticeAcknowledgementRepository
implements NoticeAcknowledgementRepository {
_UnitOfWorkNoticeAcknowledgementRepository(this._recordsByScopedId);
final Map<String, NoticeAcknowledgementRecord> _recordsByScopedId;
@override
Future<NoticeAcknowledgementRecord?> findById({
required String ownerId,
required String noticeId,
}) async {
return _recordsByScopedId[
_noticeAcknowledgementKey(ownerId: ownerId, noticeId: noticeId)];
}
@override
Future<List<NoticeAcknowledgementRecord>> findByOwnerId(
String ownerId,
) async {
return List<NoticeAcknowledgementRecord>.unmodifiable(
_recordsByScopedId.values.where((record) => record.ownerId == ownerId),
);
}
@override
Future<void> save(NoticeAcknowledgementRecord record) async {
_recordsByScopedId[_noticeAcknowledgementKey(
ownerId: record.ownerId,
noticeId: record.noticeId,
)] = record;
}
}
class _UnitOfWorkOperationRepository implements ApplicationOperationRepository {
_UnitOfWorkOperationRepository(this._operationsById);
final Map<String, ApplicationOperationRecord> _operationsById;
@override
Future<ApplicationOperationRecord?> findById(String operationId) async {
return _operationsById[operationId];
}
@override
Future<void> save(ApplicationOperationRecord operation) async {
_operationsById[operation.operationId] = operation;
}
}
String _noticeAcknowledgementKey({
required String ownerId,
required String noticeId,
}) {
return '$ownerId::$noticeId';
}
String _requiredTrimmed(String value, String name) {
final trimmed = value.trim();
if (trimmed.isEmpty) {
throw ArgumentError.value(value, name, 'Value is required.');
}
return trimmed;
}