feat(studyset): deleted studysets wait 14 days in Recently deleted
Deleting a studyset blanked it with everything inside it once a four-second Undo was over. It now only marks it (`studyset.deletedAt`, a column as `archivedAt`): it leaves every list, count and session with what is inside it and waits in the library's Recently deleted for 14 days, saying how many are left, with Restore and Delete now. Deleting no longer asks first and offers Undo; Delete now asks. Once the 14 days are over, whichever of the person's devices looks next (as their data opens, after every sync) deletes it for good, blanking first as before; the server can't blank a payload and never writes a person's space (ADR 0007). The connector, publishing and the learner count leave deleted studysets out too. The migration adds one nullable column: app versions before it stop syncing until they update. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
11a16f5686
commit
4644125cc4
7 changed files with 26969 additions and 313 deletions
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
"__className__": "serverpod.DatabaseMigration",
|
||||
"actions": [
|
||||
{
|
||||
"__className__": "serverpod.DatabaseMigrationAction",
|
||||
"type": "alterTable",
|
||||
"alterTable": {
|
||||
"__className__": "serverpod.TableMigration",
|
||||
"name": "studyset",
|
||||
"schema": "public",
|
||||
"addColumns": [
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "deletedAt",
|
||||
"fieldName": "deletedAt",
|
||||
"columnType": 4,
|
||||
"isNullable": true,
|
||||
"dartType": "DateTime?"
|
||||
}
|
||||
],
|
||||
"deleteColumns": [],
|
||||
"modifyColumns": [],
|
||||
"addIndexes": [],
|
||||
"deleteIndexes": [],
|
||||
"addForeignKeys": [],
|
||||
"deleteForeignKeys": [],
|
||||
"warnings": []
|
||||
}
|
||||
}
|
||||
],
|
||||
"warnings": [],
|
||||
"migrationApiVersion": 1
|
||||
}
|
||||
|
|
@ -14,6 +14,7 @@ part '20261003110855698-pdf-sources/migration.dart';
|
|||
part '20261007205440868-card-buried-at/migration.dart';
|
||||
part '20261008165051407-pdf-position/migration.dart';
|
||||
part '20261009224606274-feedback/migration.dart';
|
||||
part '20261012162411835-studyset-deleted-at/migration.dart';
|
||||
|
||||
/// Migration registry for the client-side database.
|
||||
class MigrationRegistry {
|
||||
|
|
@ -30,6 +31,7 @@ class MigrationRegistry {
|
|||
_Migration20261007205440868_card_buried_at(),
|
||||
_Migration20261008165051407_pdf_position(),
|
||||
_Migration20261009224606274_feedback(),
|
||||
_Migration20261012162411835_studyset_deleted_at(),
|
||||
];
|
||||
|
||||
/// List of all client-side database migration versions.
|
||||
|
|
|
|||
|
|
@ -2075,6 +2075,12 @@ class Protocol extends _isd.DatabaseSerializationManager {
|
|||
isNullable: true,
|
||||
dartType: 'DateTime?',
|
||||
),
|
||||
_isd.ColumnDefinition(
|
||||
name: 'deletedAt',
|
||||
columnType: _isd.ColumnType.timestampWithoutTimeZone,
|
||||
isNullable: true,
|
||||
dartType: 'DateTime?',
|
||||
),
|
||||
_isd.ColumnDefinition(
|
||||
name: 'starredAt',
|
||||
columnType: _isd.ColumnType.timestampWithoutTimeZone,
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ abstract class Studyset
|
|||
this.parent,
|
||||
String? position,
|
||||
this.archivedAt,
|
||||
this.deletedAt,
|
||||
this.starredAt,
|
||||
this.updatedAt,
|
||||
this.keyId,
|
||||
|
|
@ -46,6 +47,7 @@ abstract class Studyset
|
|||
_ie5k1jc2.Studyset? parent,
|
||||
String? position,
|
||||
DateTime? archivedAt,
|
||||
DateTime? deletedAt,
|
||||
DateTime? starredAt,
|
||||
DateTime? updatedAt,
|
||||
String? keyId,
|
||||
|
|
@ -73,6 +75,9 @@ abstract class Studyset
|
|||
: _isc.DateTimeJsonExtension.fromJson(
|
||||
jsonSerialization['archivedAt'],
|
||||
),
|
||||
deletedAt: jsonSerialization['deletedAt'] == null
|
||||
? null
|
||||
: _isc.DateTimeJsonExtension.fromJson(jsonSerialization['deletedAt']),
|
||||
starredAt: jsonSerialization['starredAt'] == null
|
||||
? null
|
||||
: _isc.DateTimeJsonExtension.fromJson(jsonSerialization['starredAt']),
|
||||
|
|
@ -118,6 +123,15 @@ abstract class Studyset
|
|||
/// archiving on one device and editing on another don't overwrite each other.
|
||||
DateTime? archivedAt;
|
||||
|
||||
/// When the person deleted it (decided 2026-10-12); null while it isn't. A
|
||||
/// deleted studyset waits in the library's Recently deleted for 14 days,
|
||||
/// together with everything below it, out of every list and of studying, and
|
||||
/// can be restored until then; after that the person's devices delete it for
|
||||
/// good, its content blanked first (ADR 0002, rule 2). A column of its own,
|
||||
/// like archivedAt, so that deleting on one device and editing on another
|
||||
/// don't overwrite each other.
|
||||
DateTime? deletedAt;
|
||||
|
||||
/// When the person starred it, to keep it at the top of the library; null when
|
||||
/// not starred. A column of its own, like archivedAt.
|
||||
DateTime? starredAt;
|
||||
|
|
@ -152,6 +166,7 @@ abstract class Studyset
|
|||
_ie5k1jc2.Studyset? parent,
|
||||
String? position,
|
||||
DateTime? archivedAt,
|
||||
DateTime? deletedAt,
|
||||
DateTime? starredAt,
|
||||
DateTime? updatedAt,
|
||||
String? keyId,
|
||||
|
|
@ -168,6 +183,7 @@ abstract class Studyset
|
|||
if (parent != null) 'parent': parent?.toJson(),
|
||||
'position': position,
|
||||
if (archivedAt != null) 'archivedAt': archivedAt?.toJson(),
|
||||
if (deletedAt != null) 'deletedAt': deletedAt?.toJson(),
|
||||
if (starredAt != null) 'starredAt': starredAt?.toJson(),
|
||||
if (updatedAt != null) 'updatedAt': updatedAt?.toJson(),
|
||||
if (keyId != null) 'keyId': keyId,
|
||||
|
|
@ -186,6 +202,7 @@ abstract class Studyset
|
|||
if (parent != null) 'parent': parent?.toJsonForProtocol(),
|
||||
'position': position,
|
||||
if (archivedAt != null) 'archivedAt': archivedAt?.toJson(),
|
||||
if (deletedAt != null) 'deletedAt': deletedAt?.toJson(),
|
||||
if (starredAt != null) 'starredAt': starredAt?.toJson(),
|
||||
if (updatedAt != null) 'updatedAt': updatedAt?.toJson(),
|
||||
if (keyId != null) 'keyId': keyId,
|
||||
|
|
@ -232,6 +249,7 @@ class _StudysetImpl extends Studyset {
|
|||
_ie5k1jc2.Studyset? parent,
|
||||
String? position,
|
||||
DateTime? archivedAt,
|
||||
DateTime? deletedAt,
|
||||
DateTime? starredAt,
|
||||
DateTime? updatedAt,
|
||||
String? keyId,
|
||||
|
|
@ -244,6 +262,7 @@ class _StudysetImpl extends Studyset {
|
|||
parent: parent,
|
||||
position: position,
|
||||
archivedAt: archivedAt,
|
||||
deletedAt: deletedAt,
|
||||
starredAt: starredAt,
|
||||
updatedAt: updatedAt,
|
||||
keyId: keyId,
|
||||
|
|
@ -262,6 +281,7 @@ class _StudysetImpl extends Studyset {
|
|||
Object? parent = _Undefined,
|
||||
String? position,
|
||||
Object? archivedAt = _Undefined,
|
||||
Object? deletedAt = _Undefined,
|
||||
Object? starredAt = _Undefined,
|
||||
Object? updatedAt = _Undefined,
|
||||
Object? keyId = _Undefined,
|
||||
|
|
@ -275,6 +295,7 @@ class _StudysetImpl extends Studyset {
|
|||
parent: parent is _ie5k1jc2.Studyset? ? parent : this.parent?.copyWith(),
|
||||
position: position ?? this.position,
|
||||
archivedAt: archivedAt is DateTime? ? archivedAt : this.archivedAt,
|
||||
deletedAt: deletedAt is DateTime? ? deletedAt : this.deletedAt,
|
||||
starredAt: starredAt is DateTime? ? starredAt : this.starredAt,
|
||||
updatedAt: updatedAt is DateTime? ? updatedAt : this.updatedAt,
|
||||
keyId: keyId is String? ? keyId : this.keyId,
|
||||
|
|
@ -310,6 +331,12 @@ class StudysetUpdateTable extends _isd.UpdateTable<StudysetTable> {
|
|||
value,
|
||||
);
|
||||
|
||||
_isd.ColumnValue<DateTime, DateTime> deletedAt(DateTime? value) =>
|
||||
_isd.ColumnValue(
|
||||
table.deletedAt,
|
||||
value,
|
||||
);
|
||||
|
||||
_isd.ColumnValue<DateTime, DateTime> starredAt(DateTime? value) =>
|
||||
_isd.ColumnValue(
|
||||
table.starredAt,
|
||||
|
|
@ -360,6 +387,10 @@ class StudysetTable extends _isd.Table<_isc.UuidValue?> {
|
|||
'archivedAt',
|
||||
this,
|
||||
);
|
||||
deletedAt = _isd.ColumnDateTime(
|
||||
'deletedAt',
|
||||
this,
|
||||
);
|
||||
starredAt = _isd.ColumnDateTime(
|
||||
'starredAt',
|
||||
this,
|
||||
|
|
@ -406,6 +437,15 @@ class StudysetTable extends _isd.Table<_isc.UuidValue?> {
|
|||
/// archiving on one device and editing on another don't overwrite each other.
|
||||
late final _isd.ColumnDateTime archivedAt;
|
||||
|
||||
/// When the person deleted it (decided 2026-10-12); null while it isn't. A
|
||||
/// deleted studyset waits in the library's Recently deleted for 14 days,
|
||||
/// together with everything below it, out of every list and of studying, and
|
||||
/// can be restored until then; after that the person's devices delete it for
|
||||
/// good, its content blanked first (ADR 0002, rule 2). A column of its own,
|
||||
/// like archivedAt, so that deleting on one device and editing on another
|
||||
/// don't overwrite each other.
|
||||
late final _isd.ColumnDateTime deletedAt;
|
||||
|
||||
/// When the person starred it, to keep it at the top of the library; null when
|
||||
/// not starred. A column of its own, like archivedAt.
|
||||
late final _isd.ColumnDateTime starredAt;
|
||||
|
|
@ -447,6 +487,7 @@ class StudysetTable extends _isd.Table<_isc.UuidValue?> {
|
|||
parentId,
|
||||
position,
|
||||
archivedAt,
|
||||
deletedAt,
|
||||
starredAt,
|
||||
updatedAt,
|
||||
keyId,
|
||||
|
|
|
|||
Loading…
Reference in a new issue