forked from eva/focus-flow
996 lines
30 KiB
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;
|
|
}
|