1374 lines
44 KiB
Dart
1374 lines
44 KiB
Dart
/// SQLite repository implementations backed by [SchedulerDb].
|
|
library;
|
|
|
|
import 'dart:convert';
|
|
|
|
import 'package:drift/drift.dart' as drift;
|
|
import 'package:scheduler_core/scheduler_core.dart' as core;
|
|
import 'package:scheduler_persistence/persistence.dart';
|
|
|
|
import 'scheduler_db.dart';
|
|
|
|
const _snapshotRetention = Duration(days: 30);
|
|
|
|
final _epoch = DateTime.fromMillisecondsSinceEpoch(0, isUtc: true);
|
|
|
|
/// Drift-backed task repository.
|
|
final class SqliteTaskRepository implements TaskRepository {
|
|
/// Creates a task repository using [db].
|
|
SqliteTaskRepository(this.db);
|
|
|
|
/// Shared SQLite database handle.
|
|
final SchedulerDb db;
|
|
|
|
@override
|
|
Future<RepoResult<core.Task?>> findById({
|
|
required core.OwnerId ownerId,
|
|
required String taskId,
|
|
}) async {
|
|
final row = await _taskById(taskId);
|
|
if (row == null || row.ownerId != ownerId.value) {
|
|
return const Right(null);
|
|
}
|
|
return Right(_taskFromRow(row));
|
|
}
|
|
|
|
@override
|
|
Future<RepoResult<core.Page<core.Task>>> findByOwner({
|
|
required core.OwnerId ownerId,
|
|
core.PageRequest page = const core.PageRequest(),
|
|
}) async {
|
|
return Right(await _pageTaskRows(
|
|
page: page,
|
|
where: (table) => table.ownerId.equals(ownerId.value),
|
|
));
|
|
}
|
|
|
|
@override
|
|
Future<RepoResult<core.Page<core.Task>>> findByParentTaskId({
|
|
required core.OwnerId ownerId,
|
|
required String parentTaskId,
|
|
core.PageRequest page = const core.PageRequest(),
|
|
}) async {
|
|
return Right(await _pageTaskRows(
|
|
page: page,
|
|
where: (table) =>
|
|
table.ownerId.equals(ownerId.value) &
|
|
table.parentId.equals(parentTaskId),
|
|
));
|
|
}
|
|
|
|
@override
|
|
Future<RepoResult<core.Page<core.Task>>> findByProjectId({
|
|
required core.OwnerId ownerId,
|
|
required String projectId,
|
|
core.PageRequest page = const core.PageRequest(),
|
|
}) async {
|
|
return Right(await _pageTaskRows(
|
|
page: page,
|
|
where: (table) =>
|
|
table.ownerId.equals(ownerId.value) &
|
|
table.projectId.equals(projectId),
|
|
));
|
|
}
|
|
|
|
@override
|
|
Future<RepoResult<core.Page<core.Task>>> findByStatus({
|
|
required core.OwnerId ownerId,
|
|
required core.TaskStatus status,
|
|
core.PageRequest page = const core.PageRequest(),
|
|
}) async {
|
|
return Right(await _pageTaskRows(
|
|
page: page,
|
|
where: (table) =>
|
|
table.ownerId.equals(ownerId.value) &
|
|
table.status.equals(core.PersistenceEnumMapping.encodeTaskStatus(
|
|
status,
|
|
)),
|
|
));
|
|
}
|
|
|
|
@override
|
|
Future<RepoResult<core.Revision>> insert({
|
|
required core.OwnerId ownerId,
|
|
required core.Task task,
|
|
}) async {
|
|
if (await _taskById(task.id) != null) {
|
|
return Left(RepositoryDuplicateId(entityId: task.id));
|
|
}
|
|
await db.into(db.tasks).insert(_taskCompanion(
|
|
task,
|
|
ownerId: ownerId,
|
|
revision: core.Revision.initial,
|
|
));
|
|
return const Right(core.Revision.initial);
|
|
}
|
|
|
|
@override
|
|
Future<RepoResult<core.Revision>> save({
|
|
required core.OwnerId ownerId,
|
|
required core.Task task,
|
|
required core.Revision expectedRevision,
|
|
}) async {
|
|
final checked = await _checkTaskWrite(
|
|
task.id,
|
|
ownerId,
|
|
expectedRevision,
|
|
);
|
|
if (checked is Left<RepositoryFailure, TaskRow>) {
|
|
return Left(checked.value);
|
|
}
|
|
final nextRevision = checked.right.revision.next();
|
|
final updates = _taskCompanion(
|
|
task,
|
|
ownerId: ownerId,
|
|
revision: nextRevision,
|
|
);
|
|
final updated = await (db.update(db.tasks)
|
|
..where(
|
|
(table) =>
|
|
table.id.equals(task.id) &
|
|
table.ownerId.equals(ownerId.value) &
|
|
table.revision.equals(expectedRevision.value),
|
|
))
|
|
.write(updates);
|
|
if (updated != 1) {
|
|
return Left(await _staleTaskFailure(task.id, expectedRevision));
|
|
}
|
|
return Right(nextRevision);
|
|
}
|
|
|
|
@override
|
|
Future<RepoResult<core.Revision>> delete({
|
|
required core.OwnerId ownerId,
|
|
required String taskId,
|
|
required core.Revision expectedRevision,
|
|
}) async {
|
|
final checked = await _checkTaskWrite(taskId, ownerId, expectedRevision);
|
|
if (checked is Left<RepositoryFailure, TaskRow>) {
|
|
return Left(checked.value);
|
|
}
|
|
final nextRevision = checked.right.revision.next();
|
|
final deleted = await (db.delete(db.tasks)
|
|
..where(
|
|
(table) =>
|
|
table.id.equals(taskId) &
|
|
table.ownerId.equals(ownerId.value) &
|
|
table.revision.equals(expectedRevision.value),
|
|
))
|
|
.go();
|
|
if (deleted != 1) {
|
|
return Left(await _staleTaskFailure(taskId, expectedRevision));
|
|
}
|
|
return Right(nextRevision);
|
|
}
|
|
|
|
Future<core.Page<core.Task>> _pageTaskRows({
|
|
required core.PageRequest page,
|
|
required drift.Expression<bool> Function($TasksTable table) where,
|
|
}) async {
|
|
final offset = _cursorOffset(page.cursor);
|
|
final query = db.select(db.tasks)
|
|
..where(where)
|
|
..orderBy([(table) => drift.OrderingTerm.asc(table.id)])
|
|
..limit(page.limit + 1, offset: offset);
|
|
final rows = await query.get();
|
|
final pageRows = rows.take(page.limit).toList(growable: false);
|
|
return core.Page(
|
|
items: pageRows.map(_taskFromRow),
|
|
nextCursor: rows.length > page.limit ? '${offset + page.limit}' : null,
|
|
);
|
|
}
|
|
|
|
Future<TaskRow?> _taskById(String id) {
|
|
return (db.select(db.tasks)..where((table) => table.id.equals(id)))
|
|
.getSingleOrNull();
|
|
}
|
|
|
|
Future<Either<RepositoryFailure, TaskRow>> _checkTaskWrite(
|
|
String id,
|
|
core.OwnerId ownerId,
|
|
core.Revision expectedRevision,
|
|
) async {
|
|
if (expectedRevision.value <= 0) {
|
|
return Left(RepositoryInvalidRevision(
|
|
entityId: id,
|
|
expectedRevision: expectedRevision,
|
|
));
|
|
}
|
|
final row = await _taskById(id);
|
|
if (row == null) {
|
|
return Left(RepositoryNotFound(entityId: id));
|
|
}
|
|
if (row.ownerId != ownerId.value) {
|
|
return Left(RepositoryOwnerMismatch(entityId: id));
|
|
}
|
|
if (row.revision != expectedRevision.value) {
|
|
return Left(RepositoryStaleRevision(
|
|
entityId: id,
|
|
expectedRevision: expectedRevision,
|
|
actualRevision: core.Revision(row.revision),
|
|
));
|
|
}
|
|
return Right(row);
|
|
}
|
|
|
|
Future<RepositoryStaleRevision> _staleTaskFailure(
|
|
String id,
|
|
core.Revision expectedRevision,
|
|
) async {
|
|
final row = await _taskById(id);
|
|
return RepositoryStaleRevision(
|
|
entityId: id,
|
|
expectedRevision: expectedRevision,
|
|
actualRevision:
|
|
row == null ? expectedRevision.next() : core.Revision(row.revision),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Drift-backed project repository.
|
|
final class SqliteProjectRepository implements ProjectRepository {
|
|
/// Creates a project repository using [db].
|
|
SqliteProjectRepository(this.db);
|
|
|
|
/// Shared SQLite database handle.
|
|
final SchedulerDb db;
|
|
|
|
@override
|
|
Future<RepoResult<core.ProjectProfile?>> findById({
|
|
required core.OwnerId ownerId,
|
|
required String projectId,
|
|
}) async {
|
|
final row = await _projectById(projectId);
|
|
if (row == null || row.ownerId != ownerId.value) {
|
|
return const Right(null);
|
|
}
|
|
return Right(_projectFromRow(row));
|
|
}
|
|
|
|
@override
|
|
Future<RepoResult<core.Page<core.ProjectProfile>>> findByOwner({
|
|
required core.OwnerId ownerId,
|
|
bool includeArchived = false,
|
|
core.PageRequest page = const core.PageRequest(),
|
|
}) async {
|
|
final offset = _cursorOffset(page.cursor);
|
|
final query = db.select(db.projects)
|
|
..where((table) {
|
|
final ownerClause = table.ownerId.equals(ownerId.value);
|
|
return includeArchived
|
|
? ownerClause
|
|
: ownerClause & table.archivedAtUtc.isNull();
|
|
})
|
|
..orderBy([(table) => drift.OrderingTerm.asc(table.id)])
|
|
..limit(page.limit + 1, offset: offset);
|
|
final rows = await query.get();
|
|
final pageRows = rows.take(page.limit).toList(growable: false);
|
|
return Right(core.Page(
|
|
items: pageRows.map(_projectFromRow),
|
|
nextCursor: rows.length > page.limit ? '${offset + page.limit}' : null,
|
|
));
|
|
}
|
|
|
|
@override
|
|
Future<RepoResult<core.Revision>> insert({
|
|
required core.OwnerId ownerId,
|
|
required core.ProjectProfile project,
|
|
}) async {
|
|
if (await _projectById(project.id) != null) {
|
|
return Left(RepositoryDuplicateId(entityId: project.id));
|
|
}
|
|
await db.into(db.projects).insert(_projectCompanion(
|
|
project,
|
|
ownerId: ownerId,
|
|
revision: core.Revision.initial,
|
|
createdAt: _epoch,
|
|
updatedAt: _epoch,
|
|
));
|
|
return const Right(core.Revision.initial);
|
|
}
|
|
|
|
@override
|
|
Future<RepoResult<core.Revision>> save({
|
|
required core.OwnerId ownerId,
|
|
required core.ProjectProfile project,
|
|
required core.Revision expectedRevision,
|
|
}) async {
|
|
final checked = await _checkProjectWrite(
|
|
project.id,
|
|
ownerId,
|
|
expectedRevision,
|
|
);
|
|
if (checked is Left<RepositoryFailure, ProjectRow>) {
|
|
return Left(checked.value);
|
|
}
|
|
final row = checked.right;
|
|
final nextRevision = core.Revision(row.revision).next();
|
|
final updated = await (db.update(db.projects)
|
|
..where(
|
|
(table) =>
|
|
table.id.equals(project.id) &
|
|
table.ownerId.equals(ownerId.value) &
|
|
table.revision.equals(expectedRevision.value),
|
|
))
|
|
.write(_projectCompanion(
|
|
project,
|
|
ownerId: ownerId,
|
|
revision: nextRevision,
|
|
createdAt: row.createdAtUtc,
|
|
updatedAt: _epoch,
|
|
));
|
|
if (updated != 1) {
|
|
return Left(await _staleProjectFailure(project.id, expectedRevision));
|
|
}
|
|
return Right(nextRevision);
|
|
}
|
|
|
|
@override
|
|
Future<RepoResult<core.Revision>> archive({
|
|
required core.OwnerId ownerId,
|
|
required String projectId,
|
|
required core.Revision expectedRevision,
|
|
required DateTime archivedAtUtc,
|
|
}) async {
|
|
final checked = await _checkProjectWrite(
|
|
projectId,
|
|
ownerId,
|
|
expectedRevision,
|
|
);
|
|
if (checked is Left<RepositoryFailure, ProjectRow>) {
|
|
return Left(checked.value);
|
|
}
|
|
final row = checked.right;
|
|
final nextRevision = core.Revision(row.revision).next();
|
|
final archived = _projectFromRow(row).copyWith(archivedAt: archivedAtUtc);
|
|
final updated = await (db.update(db.projects)
|
|
..where(
|
|
(table) =>
|
|
table.id.equals(projectId) &
|
|
table.ownerId.equals(ownerId.value) &
|
|
table.revision.equals(expectedRevision.value),
|
|
))
|
|
.write(_projectCompanion(
|
|
archived,
|
|
ownerId: ownerId,
|
|
revision: nextRevision,
|
|
createdAt: row.createdAtUtc,
|
|
updatedAt: archivedAtUtc,
|
|
));
|
|
if (updated != 1) {
|
|
return Left(await _staleProjectFailure(projectId, expectedRevision));
|
|
}
|
|
return Right(nextRevision);
|
|
}
|
|
|
|
Future<ProjectRow?> _projectById(String id) {
|
|
return (db.select(db.projects)..where((table) => table.id.equals(id)))
|
|
.getSingleOrNull();
|
|
}
|
|
|
|
Future<Either<RepositoryFailure, ProjectRow>> _checkProjectWrite(
|
|
String id,
|
|
core.OwnerId ownerId,
|
|
core.Revision expectedRevision,
|
|
) async {
|
|
if (expectedRevision.value <= 0) {
|
|
return Left(RepositoryInvalidRevision(
|
|
entityId: id,
|
|
expectedRevision: expectedRevision,
|
|
));
|
|
}
|
|
final row = await _projectById(id);
|
|
if (row == null) {
|
|
return Left(RepositoryNotFound(entityId: id));
|
|
}
|
|
if (row.ownerId != ownerId.value) {
|
|
return Left(RepositoryOwnerMismatch(entityId: id));
|
|
}
|
|
if (row.revision != expectedRevision.value) {
|
|
return Left(RepositoryStaleRevision(
|
|
entityId: id,
|
|
expectedRevision: expectedRevision,
|
|
actualRevision: core.Revision(row.revision),
|
|
));
|
|
}
|
|
return Right(row);
|
|
}
|
|
|
|
Future<RepositoryStaleRevision> _staleProjectFailure(
|
|
String id,
|
|
core.Revision expectedRevision,
|
|
) async {
|
|
final row = await _projectById(id);
|
|
return RepositoryStaleRevision(
|
|
entityId: id,
|
|
expectedRevision: expectedRevision,
|
|
actualRevision:
|
|
row == null ? expectedRevision.next() : core.Revision(row.revision),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Drift-backed locked-block and override repository.
|
|
final class SqliteLockedBlockRepository implements LockedBlockRepository {
|
|
/// Creates a locked-time repository using [db].
|
|
SqliteLockedBlockRepository(this.db);
|
|
|
|
/// Shared SQLite database handle.
|
|
final SchedulerDb db;
|
|
|
|
@override
|
|
Future<RepoResult<core.LockedBlock?>> findBlockById({
|
|
required core.OwnerId ownerId,
|
|
required String blockId,
|
|
}) async {
|
|
final row = await _blockById(blockId);
|
|
if (row == null || row.ownerId != ownerId.value) {
|
|
return const Right(null);
|
|
}
|
|
return Right(_lockedBlockFromRow(row));
|
|
}
|
|
|
|
@override
|
|
Future<RepoResult<core.Page<core.LockedBlock>>> findBlocksByOwner({
|
|
required core.OwnerId ownerId,
|
|
bool includeArchived = false,
|
|
core.PageRequest page = const core.PageRequest(),
|
|
}) async {
|
|
final offset = _cursorOffset(page.cursor);
|
|
final query = db.select(db.lockedBlocks)
|
|
..where((table) {
|
|
final ownerClause = table.ownerId.equals(ownerId.value);
|
|
return includeArchived
|
|
? ownerClause
|
|
: ownerClause & table.archivedAtUtc.isNull();
|
|
})
|
|
..orderBy([(table) => drift.OrderingTerm.asc(table.id)])
|
|
..limit(page.limit + 1, offset: offset);
|
|
final rows = await query.get();
|
|
final pageRows = rows.take(page.limit).toList(growable: false);
|
|
return Right(core.Page(
|
|
items: pageRows.map(_lockedBlockFromRow),
|
|
nextCursor: rows.length > page.limit ? '${offset + page.limit}' : null,
|
|
));
|
|
}
|
|
|
|
@override
|
|
Future<RepoResult<core.LockedBlockOverride?>> findOverrideById({
|
|
required core.OwnerId ownerId,
|
|
required String overrideId,
|
|
}) async {
|
|
final row = await _overrideById(overrideId);
|
|
if (row == null || row.ownerId != ownerId.value) {
|
|
return const Right(null);
|
|
}
|
|
return Right(_lockedOverrideFromRow(row));
|
|
}
|
|
|
|
@override
|
|
Future<RepoResult<core.Page<core.LockedBlockOverride>>> findOverridesForDate({
|
|
required core.OwnerId ownerId,
|
|
required core.CivilDate date,
|
|
core.PageRequest page = const core.PageRequest(),
|
|
}) async {
|
|
final offset = _cursorOffset(page.cursor);
|
|
final query = db.select(db.lockedOverrides)
|
|
..where(
|
|
(table) =>
|
|
table.ownerId.equals(ownerId.value) &
|
|
table.date.equals(date.toIsoString()),
|
|
)
|
|
..orderBy([(table) => drift.OrderingTerm.asc(table.id)])
|
|
..limit(page.limit + 1, offset: offset);
|
|
final rows = await query.get();
|
|
final pageRows = rows.take(page.limit).toList(growable: false);
|
|
return Right(core.Page(
|
|
items: pageRows.map(_lockedOverrideFromRow),
|
|
nextCursor: rows.length > page.limit ? '${offset + page.limit}' : null,
|
|
));
|
|
}
|
|
|
|
@override
|
|
Future<RepoResult<core.Revision>> insertBlock({
|
|
required core.OwnerId ownerId,
|
|
required core.LockedBlock block,
|
|
}) async {
|
|
if (await _blockById(block.id) != null) {
|
|
return Left(RepositoryDuplicateId(entityId: block.id));
|
|
}
|
|
await db.into(db.lockedBlocks).insert(_lockedBlockCompanion(
|
|
block,
|
|
ownerId: ownerId,
|
|
revision: core.Revision.initial,
|
|
));
|
|
return const Right(core.Revision.initial);
|
|
}
|
|
|
|
@override
|
|
Future<RepoResult<core.Revision>> saveBlock({
|
|
required core.OwnerId ownerId,
|
|
required core.LockedBlock block,
|
|
required core.Revision expectedRevision,
|
|
}) async {
|
|
final checked = await _checkBlockWrite(
|
|
block.id,
|
|
ownerId,
|
|
expectedRevision,
|
|
);
|
|
if (checked is Left<RepositoryFailure, LockedBlockRow>) {
|
|
return Left(checked.value);
|
|
}
|
|
final nextRevision = core.Revision(checked.right.revision).next();
|
|
final updated = await (db.update(db.lockedBlocks)
|
|
..where(
|
|
(table) =>
|
|
table.id.equals(block.id) &
|
|
table.ownerId.equals(ownerId.value) &
|
|
table.revision.equals(expectedRevision.value),
|
|
))
|
|
.write(_lockedBlockCompanion(
|
|
block,
|
|
ownerId: ownerId,
|
|
revision: nextRevision,
|
|
));
|
|
if (updated != 1) {
|
|
return Left(await _staleBlockFailure(block.id, expectedRevision));
|
|
}
|
|
return Right(nextRevision);
|
|
}
|
|
|
|
@override
|
|
Future<RepoResult<core.Revision>> archiveBlock({
|
|
required core.OwnerId ownerId,
|
|
required String blockId,
|
|
required core.Revision expectedRevision,
|
|
required DateTime archivedAtUtc,
|
|
}) async {
|
|
final checked = await _checkBlockWrite(blockId, ownerId, expectedRevision);
|
|
if (checked is Left<RepositoryFailure, LockedBlockRow>) {
|
|
return Left(checked.value);
|
|
}
|
|
final row = checked.right;
|
|
final nextRevision = core.Revision(row.revision).next();
|
|
final archived =
|
|
_lockedBlockFromRow(row).copyWith(archivedAt: archivedAtUtc);
|
|
final updated = await (db.update(db.lockedBlocks)
|
|
..where(
|
|
(table) =>
|
|
table.id.equals(blockId) &
|
|
table.ownerId.equals(ownerId.value) &
|
|
table.revision.equals(expectedRevision.value),
|
|
))
|
|
.write(_lockedBlockCompanion(
|
|
archived,
|
|
ownerId: ownerId,
|
|
revision: nextRevision,
|
|
));
|
|
if (updated != 1) {
|
|
return Left(await _staleBlockFailure(blockId, expectedRevision));
|
|
}
|
|
return Right(nextRevision);
|
|
}
|
|
|
|
@override
|
|
Future<RepoResult<core.Revision>> insertOverride({
|
|
required core.OwnerId ownerId,
|
|
required core.LockedBlockOverride override,
|
|
}) async {
|
|
if (await _overrideById(override.id) != null) {
|
|
return Left(RepositoryDuplicateId(entityId: override.id));
|
|
}
|
|
await db.into(db.lockedOverrides).insert(_lockedOverrideCompanion(
|
|
override,
|
|
ownerId: ownerId,
|
|
revision: core.Revision.initial,
|
|
));
|
|
return const Right(core.Revision.initial);
|
|
}
|
|
|
|
@override
|
|
Future<RepoResult<core.Revision>> saveOverride({
|
|
required core.OwnerId ownerId,
|
|
required core.LockedBlockOverride override,
|
|
required core.Revision expectedRevision,
|
|
}) async {
|
|
final checked = await _checkOverrideWrite(
|
|
override.id,
|
|
ownerId,
|
|
expectedRevision,
|
|
);
|
|
if (checked is Left<RepositoryFailure, LockedOverrideRow>) {
|
|
return Left(checked.value);
|
|
}
|
|
final nextRevision = core.Revision(checked.right.revision).next();
|
|
final updated = await (db.update(db.lockedOverrides)
|
|
..where(
|
|
(table) =>
|
|
table.id.equals(override.id) &
|
|
table.ownerId.equals(ownerId.value) &
|
|
table.revision.equals(expectedRevision.value),
|
|
))
|
|
.write(_lockedOverrideCompanion(
|
|
override,
|
|
ownerId: ownerId,
|
|
revision: nextRevision,
|
|
));
|
|
if (updated != 1) {
|
|
return Left(await _staleOverrideFailure(
|
|
override.id,
|
|
expectedRevision,
|
|
));
|
|
}
|
|
return Right(nextRevision);
|
|
}
|
|
|
|
Future<LockedBlockRow?> _blockById(String id) {
|
|
return (db.select(db.lockedBlocks)..where((table) => table.id.equals(id)))
|
|
.getSingleOrNull();
|
|
}
|
|
|
|
Future<LockedOverrideRow?> _overrideById(String id) {
|
|
return (db.select(db.lockedOverrides)
|
|
..where((table) => table.id.equals(id)))
|
|
.getSingleOrNull();
|
|
}
|
|
|
|
Future<Either<RepositoryFailure, LockedBlockRow>> _checkBlockWrite(
|
|
String id,
|
|
core.OwnerId ownerId,
|
|
core.Revision expectedRevision,
|
|
) async {
|
|
if (expectedRevision.value <= 0) {
|
|
return Left(RepositoryInvalidRevision(
|
|
entityId: id,
|
|
expectedRevision: expectedRevision,
|
|
));
|
|
}
|
|
final row = await _blockById(id);
|
|
if (row == null) return Left(RepositoryNotFound(entityId: id));
|
|
if (row.ownerId != ownerId.value) {
|
|
return Left(RepositoryOwnerMismatch(entityId: id));
|
|
}
|
|
if (row.revision != expectedRevision.value) {
|
|
return Left(RepositoryStaleRevision(
|
|
entityId: id,
|
|
expectedRevision: expectedRevision,
|
|
actualRevision: core.Revision(row.revision),
|
|
));
|
|
}
|
|
return Right(row);
|
|
}
|
|
|
|
Future<Either<RepositoryFailure, LockedOverrideRow>> _checkOverrideWrite(
|
|
String id,
|
|
core.OwnerId ownerId,
|
|
core.Revision expectedRevision,
|
|
) async {
|
|
if (expectedRevision.value <= 0) {
|
|
return Left(RepositoryInvalidRevision(
|
|
entityId: id,
|
|
expectedRevision: expectedRevision,
|
|
));
|
|
}
|
|
final row = await _overrideById(id);
|
|
if (row == null) return Left(RepositoryNotFound(entityId: id));
|
|
if (row.ownerId != ownerId.value) {
|
|
return Left(RepositoryOwnerMismatch(entityId: id));
|
|
}
|
|
if (row.revision != expectedRevision.value) {
|
|
return Left(RepositoryStaleRevision(
|
|
entityId: id,
|
|
expectedRevision: expectedRevision,
|
|
actualRevision: core.Revision(row.revision),
|
|
));
|
|
}
|
|
return Right(row);
|
|
}
|
|
|
|
Future<RepositoryStaleRevision> _staleBlockFailure(
|
|
String id,
|
|
core.Revision expectedRevision,
|
|
) async {
|
|
final row = await _blockById(id);
|
|
return RepositoryStaleRevision(
|
|
entityId: id,
|
|
expectedRevision: expectedRevision,
|
|
actualRevision:
|
|
row == null ? expectedRevision.next() : core.Revision(row.revision),
|
|
);
|
|
}
|
|
|
|
Future<RepositoryStaleRevision> _staleOverrideFailure(
|
|
String id,
|
|
core.Revision expectedRevision,
|
|
) async {
|
|
final row = await _overrideById(id);
|
|
return RepositoryStaleRevision(
|
|
entityId: id,
|
|
expectedRevision: expectedRevision,
|
|
actualRevision:
|
|
row == null ? expectedRevision.next() : core.Revision(row.revision),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Drift-backed owner settings repository.
|
|
final class SqliteSettingsRepository implements SettingsRepository {
|
|
/// Creates a settings repository using [db].
|
|
SqliteSettingsRepository(this.db);
|
|
|
|
/// Shared SQLite database handle.
|
|
final SchedulerDb db;
|
|
|
|
@override
|
|
Future<RepoResult<core.OwnerSettings?>> findByOwner({
|
|
required core.OwnerId ownerId,
|
|
}) async {
|
|
final row = await _settingsByOwner(ownerId.value);
|
|
if (row == null) return const Right(null);
|
|
return Right(_settingsFromRow(row));
|
|
}
|
|
|
|
@override
|
|
Future<RepoResult<core.Revision>> insert({
|
|
required core.OwnerId ownerId,
|
|
required core.OwnerSettings settings,
|
|
}) async {
|
|
if (await _settingsByOwner(ownerId.value) != null) {
|
|
return Left(RepositoryDuplicateId(entityId: ownerId.value));
|
|
}
|
|
final ownedSettings = settings.ownerId == ownerId.value
|
|
? settings
|
|
: settings.copyWith(ownerId: ownerId.value);
|
|
await db.into(db.settingsTable).insert(_settingsCompanion(
|
|
ownedSettings,
|
|
revision: core.Revision.initial,
|
|
createdAt: _epoch,
|
|
updatedAt: _epoch,
|
|
));
|
|
return const Right(core.Revision.initial);
|
|
}
|
|
|
|
@override
|
|
Future<RepoResult<core.Revision>> save({
|
|
required core.OwnerId ownerId,
|
|
required core.OwnerSettings settings,
|
|
required core.Revision expectedRevision,
|
|
}) async {
|
|
final checked = await _checkSettingsWrite(ownerId, expectedRevision);
|
|
if (checked is Left<RepositoryFailure, SettingsRow>) {
|
|
return Left(checked.value);
|
|
}
|
|
final row = checked.right;
|
|
final nextRevision = core.Revision(row.revision).next();
|
|
final ownedSettings = settings.ownerId == ownerId.value
|
|
? settings
|
|
: settings.copyWith(ownerId: ownerId.value);
|
|
final updated = await (db.update(db.settingsTable)
|
|
..where(
|
|
(table) =>
|
|
table.ownerId.equals(ownerId.value) &
|
|
table.revision.equals(expectedRevision.value),
|
|
))
|
|
.write(_settingsCompanion(
|
|
ownedSettings,
|
|
revision: nextRevision,
|
|
createdAt: row.createdAtUtc,
|
|
updatedAt: _epoch,
|
|
));
|
|
if (updated != 1) {
|
|
return Left(await _staleSettingsFailure(ownerId, expectedRevision));
|
|
}
|
|
return Right(nextRevision);
|
|
}
|
|
|
|
Future<SettingsRow?> _settingsByOwner(String ownerId) {
|
|
return (db.select(db.settingsTable)
|
|
..where((table) => table.ownerId.equals(ownerId)))
|
|
.getSingleOrNull();
|
|
}
|
|
|
|
Future<Either<RepositoryFailure, SettingsRow>> _checkSettingsWrite(
|
|
core.OwnerId ownerId,
|
|
core.Revision expectedRevision,
|
|
) async {
|
|
if (expectedRevision.value <= 0) {
|
|
return Left(RepositoryInvalidRevision(
|
|
entityId: ownerId.value,
|
|
expectedRevision: expectedRevision,
|
|
));
|
|
}
|
|
final row = await _settingsByOwner(ownerId.value);
|
|
if (row == null) return Left(RepositoryNotFound(entityId: ownerId.value));
|
|
if (row.revision != expectedRevision.value) {
|
|
return Left(RepositoryStaleRevision(
|
|
entityId: ownerId.value,
|
|
expectedRevision: expectedRevision,
|
|
actualRevision: core.Revision(row.revision),
|
|
));
|
|
}
|
|
return Right(row);
|
|
}
|
|
|
|
Future<RepositoryStaleRevision> _staleSettingsFailure(
|
|
core.OwnerId ownerId,
|
|
core.Revision expectedRevision,
|
|
) async {
|
|
final row = await _settingsByOwner(ownerId.value);
|
|
return RepositoryStaleRevision(
|
|
entityId: ownerId.value,
|
|
expectedRevision: expectedRevision,
|
|
actualRevision:
|
|
row == null ? expectedRevision.next() : core.Revision(row.revision),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Drift-backed schedule snapshot repository.
|
|
final class SqliteScheduleSnapshotRepository
|
|
implements ScheduleSnapshotRepository {
|
|
/// Creates a snapshot repository using [db].
|
|
SqliteScheduleSnapshotRepository(this.db);
|
|
|
|
/// Shared SQLite database handle.
|
|
final SchedulerDb db;
|
|
|
|
@override
|
|
Future<RepoResult<core.SchedulingStateSnapshot?>> findById({
|
|
required core.OwnerId ownerId,
|
|
required String snapshotId,
|
|
}) async {
|
|
final row = await _snapshotById(snapshotId);
|
|
if (row == null || row.ownerId != ownerId.value) {
|
|
return const Right(null);
|
|
}
|
|
return Right(_snapshotFromRow(row));
|
|
}
|
|
|
|
@override
|
|
Future<RepoResult<core.Page<core.SchedulingStateSnapshot>>> findInWindow({
|
|
required core.OwnerId ownerId,
|
|
required core.SchedulingWindow window,
|
|
core.PageRequest page = const core.PageRequest(),
|
|
}) async {
|
|
final rows = await (db.select(db.snapshots)
|
|
..where((table) => table.ownerId.equals(ownerId.value))
|
|
..orderBy([(table) => drift.OrderingTerm.asc(table.id)]))
|
|
.get();
|
|
final matching = rows
|
|
.map(_snapshotFromRow)
|
|
.where((snapshot) => _windowsOverlap(snapshot.window, window))
|
|
.toList(growable: false);
|
|
final offset = _cursorOffset(page.cursor);
|
|
final pageItems = matching.skip(offset).take(page.limit).toList();
|
|
final nextOffset = offset + page.limit;
|
|
return Right(core.Page(
|
|
items: pageItems,
|
|
nextCursor: nextOffset < matching.length ? '$nextOffset' : null,
|
|
));
|
|
}
|
|
|
|
@override
|
|
Future<RepoResult<core.Revision>> insert({
|
|
required core.OwnerId ownerId,
|
|
required core.SchedulingStateSnapshot snapshot,
|
|
}) async {
|
|
if (await _snapshotById(snapshot.id) != null) {
|
|
return Left(RepositoryDuplicateId(entityId: snapshot.id));
|
|
}
|
|
await db.into(db.snapshots).insert(_snapshotCompanion(
|
|
snapshot,
|
|
ownerId: ownerId,
|
|
revision: core.Revision.initial,
|
|
));
|
|
return const Right(core.Revision.initial);
|
|
}
|
|
|
|
@override
|
|
Future<RepoResult<int>> deleteExpired({
|
|
required core.OwnerId ownerId,
|
|
required DateTime nowUtc,
|
|
}) async {
|
|
final deleted = await (db.delete(db.snapshots)
|
|
..where(
|
|
(table) =>
|
|
table.ownerId.equals(ownerId.value) &
|
|
table.retentionExpiresUtc.isSmallerThanValue(nowUtc.toUtc()),
|
|
))
|
|
.go();
|
|
return Right(deleted);
|
|
}
|
|
|
|
Future<SnapshotRow?> _snapshotById(String id) {
|
|
return (db.select(db.snapshots)..where((table) => table.id.equals(id)))
|
|
.getSingleOrNull();
|
|
}
|
|
}
|
|
|
|
TasksCompanion _taskCompanion(
|
|
core.Task task, {
|
|
required core.OwnerId ownerId,
|
|
required core.Revision revision,
|
|
}) {
|
|
final doc = core.TaskDocumentMapping.toDocument(
|
|
task,
|
|
ownerId: ownerId.value,
|
|
revision: revision.value,
|
|
);
|
|
return TasksCompanion.insert(
|
|
id: task.id,
|
|
ownerId: ownerId.value,
|
|
title: _string(doc, core.TaskDocumentFields.title),
|
|
projectId: _string(doc, core.TaskDocumentFields.projectId),
|
|
parentId: drift.Value(_nullableString(
|
|
doc,
|
|
core.TaskDocumentFields.parentTaskId,
|
|
)),
|
|
type: _string(doc, core.TaskDocumentFields.type),
|
|
status: _string(doc, core.TaskDocumentFields.status),
|
|
priority: drift.Value(_nullableString(
|
|
doc,
|
|
core.TaskDocumentFields.priority,
|
|
)),
|
|
reward: _string(doc, core.TaskDocumentFields.reward),
|
|
difficulty: _string(doc, core.TaskDocumentFields.difficulty),
|
|
durationMinutes: drift.Value(_nullableInt(
|
|
doc,
|
|
core.TaskDocumentFields.durationMinutes,
|
|
)),
|
|
scheduledStartUtc: drift.Value(_nullableDateTime(
|
|
doc,
|
|
core.TaskDocumentFields.scheduledStart,
|
|
)),
|
|
scheduledEndUtc: drift.Value(_nullableDateTime(
|
|
doc,
|
|
core.TaskDocumentFields.scheduledEnd,
|
|
)),
|
|
actualStartUtc: drift.Value(_nullableDateTime(
|
|
doc,
|
|
core.TaskDocumentFields.actualStart,
|
|
)),
|
|
actualEndUtc: drift.Value(_nullableDateTime(
|
|
doc,
|
|
core.TaskDocumentFields.actualEnd,
|
|
)),
|
|
completedAtUtc: drift.Value(_nullableDateTime(
|
|
doc,
|
|
core.TaskDocumentFields.completedAt,
|
|
)),
|
|
backlogTagsJson: jsonEncode(doc[core.TaskDocumentFields.backlogTags]),
|
|
reminderOverride: drift.Value(_nullableString(
|
|
doc,
|
|
core.TaskDocumentFields.reminderOverride,
|
|
)),
|
|
statsJson: jsonEncode(doc[core.TaskDocumentFields.stats]),
|
|
backlogEnteredAtUtc: drift.Value(_nullableDateTime(
|
|
doc,
|
|
core.TaskDocumentFields.backlogEnteredAt,
|
|
)),
|
|
backlogEnteredProvenance: drift.Value(_nullableString(
|
|
doc,
|
|
core.TaskDocumentFields.backlogEnteredAtProvenance,
|
|
)),
|
|
revision: revision.value,
|
|
createdAtUtc: task.createdAt.toUtc(),
|
|
updatedAtUtc: task.updatedAt.toUtc(),
|
|
);
|
|
}
|
|
|
|
core.Task _taskFromRow(TaskRow row) {
|
|
return core.TaskDocumentMapping.fromDocument({
|
|
..._commonDocumentFields(
|
|
row.id, row.ownerId, row.revision, row.createdAtUtc, row.updatedAtUtc),
|
|
core.TaskDocumentFields.title: row.title,
|
|
core.TaskDocumentFields.projectId: row.projectId,
|
|
core.TaskDocumentFields.type: row.type,
|
|
core.TaskDocumentFields.status: row.status,
|
|
core.TaskDocumentFields.priority: row.priority,
|
|
core.TaskDocumentFields.reward: row.reward,
|
|
core.TaskDocumentFields.difficulty: row.difficulty,
|
|
core.TaskDocumentFields.durationMinutes: row.durationMinutes,
|
|
core.TaskDocumentFields.scheduledStart:
|
|
_storedDateTimeOrNull(row.scheduledStartUtc),
|
|
core.TaskDocumentFields.scheduledEnd:
|
|
_storedDateTimeOrNull(row.scheduledEndUtc),
|
|
core.TaskDocumentFields.actualStart:
|
|
_storedDateTimeOrNull(row.actualStartUtc),
|
|
core.TaskDocumentFields.actualEnd: _storedDateTimeOrNull(row.actualEndUtc),
|
|
core.TaskDocumentFields.completedAt:
|
|
_storedDateTimeOrNull(row.completedAtUtc),
|
|
core.TaskDocumentFields.parentTaskId: row.parentId,
|
|
core.TaskDocumentFields.backlogTags: _jsonList(row.backlogTagsJson),
|
|
core.TaskDocumentFields.reminderOverride: row.reminderOverride,
|
|
core.TaskDocumentFields.stats: _jsonMap(row.statsJson),
|
|
core.TaskDocumentFields.backlogEnteredAt:
|
|
_storedDateTimeOrNull(row.backlogEnteredAtUtc),
|
|
core.TaskDocumentFields.backlogEnteredAtProvenance:
|
|
row.backlogEnteredProvenance,
|
|
});
|
|
}
|
|
|
|
ProjectsCompanion _projectCompanion(
|
|
core.ProjectProfile project, {
|
|
required core.OwnerId ownerId,
|
|
required core.Revision revision,
|
|
required DateTime createdAt,
|
|
required DateTime updatedAt,
|
|
}) {
|
|
final doc = core.ProjectDocumentMapping.toDocument(
|
|
project,
|
|
ownerId: ownerId.value,
|
|
revision: revision.value,
|
|
createdAt: createdAt,
|
|
updatedAt: updatedAt,
|
|
);
|
|
return ProjectsCompanion.insert(
|
|
id: project.id,
|
|
ownerId: ownerId.value,
|
|
name: _string(doc, core.ProjectDocumentFields.name),
|
|
colorKey: _string(doc, core.ProjectDocumentFields.colorKey),
|
|
defaultPriority: _string(doc, core.ProjectDocumentFields.defaultPriority),
|
|
defaultReward: _string(doc, core.ProjectDocumentFields.defaultReward),
|
|
defaultDifficulty:
|
|
_string(doc, core.ProjectDocumentFields.defaultDifficulty),
|
|
defaultReminderProfile:
|
|
_string(doc, core.ProjectDocumentFields.defaultReminderProfile),
|
|
defaultDurationMinutes: drift.Value(_nullableInt(
|
|
doc,
|
|
core.ProjectDocumentFields.defaultDurationMinutes,
|
|
)),
|
|
archivedAtUtc: drift.Value(_nullableDateTime(
|
|
doc,
|
|
core.ProjectDocumentFields.archivedAt,
|
|
)),
|
|
revision: revision.value,
|
|
createdAtUtc: createdAt.toUtc(),
|
|
updatedAtUtc: updatedAt.toUtc(),
|
|
);
|
|
}
|
|
|
|
core.ProjectProfile _projectFromRow(ProjectRow row) {
|
|
return core.ProjectDocumentMapping.fromDocument({
|
|
..._commonDocumentFields(
|
|
row.id, row.ownerId, row.revision, row.createdAtUtc, row.updatedAtUtc),
|
|
core.ProjectDocumentFields.name: row.name,
|
|
core.ProjectDocumentFields.colorKey: row.colorKey,
|
|
core.ProjectDocumentFields.defaultPriority: row.defaultPriority,
|
|
core.ProjectDocumentFields.defaultReward: row.defaultReward,
|
|
core.ProjectDocumentFields.defaultDifficulty: row.defaultDifficulty,
|
|
core.ProjectDocumentFields.defaultReminderProfile:
|
|
row.defaultReminderProfile,
|
|
core.ProjectDocumentFields.defaultDurationMinutes:
|
|
row.defaultDurationMinutes,
|
|
core.ProjectDocumentFields.archivedAt: _storedDateTimeOrNull(
|
|
row.archivedAtUtc,
|
|
),
|
|
});
|
|
}
|
|
|
|
LockedBlocksCompanion _lockedBlockCompanion(
|
|
core.LockedBlock block, {
|
|
required core.OwnerId ownerId,
|
|
required core.Revision revision,
|
|
}) {
|
|
final doc = core.LockedBlockDocumentMapping.toDocument(
|
|
block,
|
|
ownerId: ownerId.value,
|
|
revision: revision.value,
|
|
);
|
|
return LockedBlocksCompanion.insert(
|
|
id: block.id,
|
|
ownerId: ownerId.value,
|
|
name: _string(doc, core.LockedBlockDocumentFields.name),
|
|
date:
|
|
drift.Value(_nullableString(doc, core.LockedBlockDocumentFields.date)),
|
|
startTime: _string(doc, core.LockedBlockDocumentFields.startTime),
|
|
endTime: _string(doc, core.LockedBlockDocumentFields.endTime),
|
|
recurrenceJson: drift.Value(_jsonEncodeNullable(
|
|
doc[core.LockedBlockDocumentFields.recurrence],
|
|
)),
|
|
hiddenByDefault: _bool(doc, core.LockedBlockDocumentFields.hiddenByDefault),
|
|
projectId: drift.Value(_nullableString(
|
|
doc,
|
|
core.LockedBlockDocumentFields.projectId,
|
|
)),
|
|
archivedAtUtc: drift.Value(_nullableDateTime(
|
|
doc,
|
|
core.LockedBlockDocumentFields.archivedAt,
|
|
)),
|
|
revision: revision.value,
|
|
createdAtUtc: block.createdAt.toUtc(),
|
|
updatedAtUtc: block.updatedAt.toUtc(),
|
|
);
|
|
}
|
|
|
|
core.LockedBlock _lockedBlockFromRow(LockedBlockRow row) {
|
|
return core.LockedBlockDocumentMapping.fromDocument({
|
|
..._commonDocumentFields(
|
|
row.id, row.ownerId, row.revision, row.createdAtUtc, row.updatedAtUtc),
|
|
core.LockedBlockDocumentFields.name: row.name,
|
|
core.LockedBlockDocumentFields.startTime: row.startTime,
|
|
core.LockedBlockDocumentFields.endTime: row.endTime,
|
|
core.LockedBlockDocumentFields.date: row.date,
|
|
core.LockedBlockDocumentFields.recurrence:
|
|
row.recurrenceJson == null ? null : _jsonMap(row.recurrenceJson!),
|
|
core.LockedBlockDocumentFields.hiddenByDefault: row.hiddenByDefault,
|
|
core.LockedBlockDocumentFields.projectId: row.projectId,
|
|
core.LockedBlockDocumentFields.archivedAt:
|
|
_storedDateTimeOrNull(row.archivedAtUtc),
|
|
});
|
|
}
|
|
|
|
LockedOverridesCompanion _lockedOverrideCompanion(
|
|
core.LockedBlockOverride override, {
|
|
required core.OwnerId ownerId,
|
|
required core.Revision revision,
|
|
}) {
|
|
final doc = core.LockedBlockOverrideDocumentMapping.toDocument(
|
|
override,
|
|
ownerId: ownerId.value,
|
|
revision: revision.value,
|
|
);
|
|
return LockedOverridesCompanion.insert(
|
|
id: override.id,
|
|
ownerId: ownerId.value,
|
|
lockedBlockId: drift.Value(_nullableString(
|
|
doc,
|
|
core.LockedBlockOverrideDocumentFields.lockedBlockId,
|
|
)),
|
|
date: _string(doc, core.LockedBlockOverrideDocumentFields.date),
|
|
type: _string(doc, core.LockedBlockOverrideDocumentFields.type),
|
|
name: drift.Value(
|
|
_nullableString(doc, core.LockedBlockOverrideDocumentFields.name),
|
|
),
|
|
startTime: drift.Value(_nullableString(
|
|
doc,
|
|
core.LockedBlockOverrideDocumentFields.startTime,
|
|
)),
|
|
endTime: drift.Value(_nullableString(
|
|
doc,
|
|
core.LockedBlockOverrideDocumentFields.endTime,
|
|
)),
|
|
hiddenByDefault:
|
|
_bool(doc, core.LockedBlockOverrideDocumentFields.hiddenByDefault),
|
|
projectId: drift.Value(_nullableString(
|
|
doc,
|
|
core.LockedBlockOverrideDocumentFields.projectId,
|
|
)),
|
|
revision: revision.value,
|
|
createdAtUtc: override.createdAt.toUtc(),
|
|
updatedAtUtc: override.updatedAt.toUtc(),
|
|
);
|
|
}
|
|
|
|
core.LockedBlockOverride _lockedOverrideFromRow(LockedOverrideRow row) {
|
|
return core.LockedBlockOverrideDocumentMapping.fromDocument({
|
|
..._commonDocumentFields(
|
|
row.id, row.ownerId, row.revision, row.createdAtUtc, row.updatedAtUtc),
|
|
core.LockedBlockOverrideDocumentFields.lockedBlockId: row.lockedBlockId,
|
|
core.LockedBlockOverrideDocumentFields.date: row.date,
|
|
core.LockedBlockOverrideDocumentFields.type: row.type,
|
|
core.LockedBlockOverrideDocumentFields.name: row.name,
|
|
core.LockedBlockOverrideDocumentFields.startTime: row.startTime,
|
|
core.LockedBlockOverrideDocumentFields.endTime: row.endTime,
|
|
core.LockedBlockOverrideDocumentFields.hiddenByDefault: row.hiddenByDefault,
|
|
core.LockedBlockOverrideDocumentFields.projectId: row.projectId,
|
|
});
|
|
}
|
|
|
|
SettingsTableCompanion _settingsCompanion(
|
|
core.OwnerSettings settings, {
|
|
required core.Revision revision,
|
|
required DateTime createdAt,
|
|
required DateTime updatedAt,
|
|
}) {
|
|
final doc = core.OwnerSettingsDocumentMapping.toDocument(
|
|
settings,
|
|
revision: revision.value,
|
|
createdAt: createdAt,
|
|
updatedAt: updatedAt,
|
|
);
|
|
return SettingsTableCompanion.insert(
|
|
ownerId: settings.ownerId,
|
|
timeZoneId: _string(doc, core.OwnerSettingsDocumentFields.timeZoneId),
|
|
dayStartMinutes: settings.dayStart.minutesSinceMidnight,
|
|
dayEndMinutes: settings.dayEnd.minutesSinceMidnight,
|
|
compactMode:
|
|
_bool(doc, core.OwnerSettingsDocumentFields.compactModeEnabled),
|
|
backlogStalenessJson:
|
|
jsonEncode(doc[core.OwnerSettingsDocumentFields.backlogStaleness]),
|
|
revision: revision.value,
|
|
createdAtUtc: createdAt.toUtc(),
|
|
updatedAtUtc: updatedAt.toUtc(),
|
|
);
|
|
}
|
|
|
|
core.OwnerSettings _settingsFromRow(SettingsRow row) {
|
|
return core.OwnerSettingsDocumentMapping.fromDocument({
|
|
..._commonDocumentFields(row.ownerId, row.ownerId, row.revision,
|
|
row.createdAtUtc, row.updatedAtUtc),
|
|
core.OwnerSettingsDocumentFields.timeZoneId: row.timeZoneId,
|
|
core.OwnerSettingsDocumentFields.dayStart:
|
|
_wallTimeFromMinutes(row.dayStartMinutes).toIsoString(),
|
|
core.OwnerSettingsDocumentFields.dayEnd:
|
|
_wallTimeFromMinutes(row.dayEndMinutes).toIsoString(),
|
|
core.OwnerSettingsDocumentFields.compactModeEnabled: row.compactMode,
|
|
core.OwnerSettingsDocumentFields.backlogStaleness:
|
|
_jsonMap(row.backlogStalenessJson),
|
|
});
|
|
}
|
|
|
|
SnapshotsCompanion _snapshotCompanion(
|
|
core.SchedulingStateSnapshot snapshot, {
|
|
required core.OwnerId ownerId,
|
|
required core.Revision revision,
|
|
}) {
|
|
final retentionExpiresAt = snapshot.capturedAt.add(_snapshotRetention);
|
|
final doc = core.SchedulingSnapshotDocumentMapping.toDocument(
|
|
snapshot,
|
|
ownerId: ownerId.value,
|
|
revision: revision.value,
|
|
retentionExpiresAt: retentionExpiresAt,
|
|
);
|
|
return SnapshotsCompanion.insert(
|
|
id: snapshot.id,
|
|
ownerId: ownerId.value,
|
|
capturedAtUtc: snapshot.capturedAt.toUtc(),
|
|
operationName: drift.Value(_nullableString(
|
|
doc,
|
|
core.SchedulingSnapshotDocumentFields.operationName,
|
|
)),
|
|
sourceDate: drift.Value(_nullableString(
|
|
doc,
|
|
core.SchedulingSnapshotDocumentFields.sourceDate,
|
|
)),
|
|
targetDate: drift.Value(_nullableString(
|
|
doc,
|
|
core.SchedulingSnapshotDocumentFields.targetDate,
|
|
)),
|
|
windowJson: jsonEncode(doc[core.SchedulingSnapshotDocumentFields.window]),
|
|
tasksJson: jsonEncode(doc[core.SchedulingSnapshotDocumentFields.tasks]),
|
|
lockedIntervalsJson:
|
|
jsonEncode(doc[core.SchedulingSnapshotDocumentFields.lockedIntervals]),
|
|
requiredVisibleIntervalsJson: jsonEncode(
|
|
doc[core.SchedulingSnapshotDocumentFields.requiredVisibleIntervals],
|
|
),
|
|
noticesJson: jsonEncode(doc[core.SchedulingSnapshotDocumentFields.notices]),
|
|
changesJson: jsonEncode(doc[core.SchedulingSnapshotDocumentFields.changes]),
|
|
overlapsJson:
|
|
jsonEncode(doc[core.SchedulingSnapshotDocumentFields.overlaps]),
|
|
retentionExpiresUtc: retentionExpiresAt.toUtc(),
|
|
truncated: _bool(doc, core.SchedulingSnapshotDocumentFields.truncated),
|
|
revision: revision.value,
|
|
createdAtUtc: snapshot.capturedAt.toUtc(),
|
|
updatedAtUtc: snapshot.capturedAt.toUtc(),
|
|
);
|
|
}
|
|
|
|
core.SchedulingStateSnapshot _snapshotFromRow(SnapshotRow row) {
|
|
return core.SchedulingSnapshotDocumentMapping.fromDocument({
|
|
..._commonDocumentFields(
|
|
row.id, row.ownerId, row.revision, row.createdAtUtc, row.updatedAtUtc),
|
|
core.SchedulingSnapshotDocumentFields.capturedAt:
|
|
core.PersistenceDateTimeConvention.toStoredString(row.capturedAtUtc),
|
|
core.SchedulingSnapshotDocumentFields.sourceDate: row.sourceDate,
|
|
core.SchedulingSnapshotDocumentFields.targetDate: row.targetDate,
|
|
core.SchedulingSnapshotDocumentFields.window: _jsonMap(row.windowJson),
|
|
core.SchedulingSnapshotDocumentFields.tasks: _jsonList(row.tasksJson),
|
|
core.SchedulingSnapshotDocumentFields.lockedIntervals:
|
|
_jsonList(row.lockedIntervalsJson),
|
|
core.SchedulingSnapshotDocumentFields.requiredVisibleIntervals:
|
|
_jsonList(row.requiredVisibleIntervalsJson),
|
|
core.SchedulingSnapshotDocumentFields.notices: _jsonList(row.noticesJson),
|
|
core.SchedulingSnapshotDocumentFields.changes: _jsonList(row.changesJson),
|
|
core.SchedulingSnapshotDocumentFields.overlaps: _jsonList(row.overlapsJson),
|
|
core.SchedulingSnapshotDocumentFields.operationName: row.operationName,
|
|
core.SchedulingSnapshotDocumentFields.retentionExpiresAt:
|
|
core.PersistenceDateTimeConvention.toStoredString(
|
|
row.retentionExpiresUtc,
|
|
),
|
|
core.SchedulingSnapshotDocumentFields.truncated: row.truncated,
|
|
});
|
|
}
|
|
|
|
Map<String, Object?> _commonDocumentFields(
|
|
String id,
|
|
String ownerId,
|
|
int revision,
|
|
DateTime createdAt,
|
|
DateTime updatedAt,
|
|
) {
|
|
return <String, Object?>{
|
|
core.DocumentFields.schemaVersion: core.v1SchemaVersion,
|
|
core.DocumentFields.id: id,
|
|
core.DocumentFields.ownerId: ownerId,
|
|
core.DocumentFields.revision: revision,
|
|
core.DocumentFields.createdAt:
|
|
core.PersistenceDateTimeConvention.toStoredString(createdAt),
|
|
core.DocumentFields.updatedAt:
|
|
core.PersistenceDateTimeConvention.toStoredString(updatedAt),
|
|
};
|
|
}
|
|
|
|
String _string(Map<String, Object?> doc, String fieldName) {
|
|
return doc[fieldName] as String;
|
|
}
|
|
|
|
String? _nullableString(Map<String, Object?> doc, String fieldName) {
|
|
return doc[fieldName] as String?;
|
|
}
|
|
|
|
int? _nullableInt(Map<String, Object?> doc, String fieldName) {
|
|
return doc[fieldName] as int?;
|
|
}
|
|
|
|
bool _bool(Map<String, Object?> doc, String fieldName) {
|
|
return doc[fieldName] as bool;
|
|
}
|
|
|
|
DateTime? _nullableDateTime(Map<String, Object?> doc, String fieldName) {
|
|
final value = doc[fieldName] as String?;
|
|
return value == null
|
|
? null
|
|
: core.PersistenceDateTimeConvention.fromStoredString(value);
|
|
}
|
|
|
|
String? _storedDateTimeOrNull(DateTime? value) {
|
|
return value == null
|
|
? null
|
|
: core.PersistenceDateTimeConvention.toStoredString(value);
|
|
}
|
|
|
|
String? _jsonEncodeNullable(Object? value) {
|
|
return value == null ? null : jsonEncode(value);
|
|
}
|
|
|
|
Map<String, Object?> _jsonMap(String value) {
|
|
return Map<String, Object?>.from(
|
|
jsonDecode(value) as Map<dynamic, dynamic>,
|
|
);
|
|
}
|
|
|
|
List<Object?> _jsonList(String value) {
|
|
return List<Object?>.from(jsonDecode(value) as List<dynamic>);
|
|
}
|
|
|
|
core.WallTime _wallTimeFromMinutes(int minutes) {
|
|
return core.WallTime(hour: minutes ~/ 60, minute: minutes % 60);
|
|
}
|
|
|
|
int _cursorOffset(String? cursor) {
|
|
if (cursor == null) return 0;
|
|
final parsed = int.tryParse(cursor);
|
|
if (parsed == null || parsed < 0) return 0;
|
|
return parsed;
|
|
}
|
|
|
|
extension on int {
|
|
core.Revision next() => core.Revision(this).next();
|
|
}
|
|
|
|
bool _windowsOverlap(core.SchedulingWindow left, core.SchedulingWindow right) {
|
|
return left.start.isBefore(right.end) && right.start.isBefore(left.end);
|
|
}
|