feat(todo,app): files attached to tasks and comments
Halkyon's ADR 0043, D1, D3, D5 and D8: any file up to 100 MB, attached to a task's description or to a comment, sealed as everything of Todo's. Server: the attachment table, synced and sealed as the others, with the sealed file's hash readable for the store; todoFiles hands halkyon_base its rows, readable by their space's members, and takes sealed files alone; the files no attachment names go daily. Its tests hold downloads to the row's space, refuse a readable file and prune what no live row names. Migration attachments: CREATE TABLE "attachment" with its indexes and the foreign key to its space, cascading as every synced table's. App: AttachmentRepository seals each file for its row, keeps it sealed and opened in the person's folder, uploads it after each sync and fetches it as it shows, both held back by Without files on mobile data but for a file tapped. Under the description and a comment's text, a row of them: pictures as thumbnails opening full-size, other files as chips opening in the system's app, else saved; the paperclip beside the description and in the comment's composer adds them, the composer holding them until it is sent. A task deleted takes its files and its comments' along, a comment its own. halkyon_app opens a file in the system's app (FileOpener): on Android through its plugin and a FileProvider of its own class, sharing the cache's halkyon_opened/ folder alone; on the desktop as a file: URI. Whats-New: Attach files to a task's description or a comment with the paperclip: pictures show as thumbnails, other files open in their app. Whats-New-De: Hänge mit der Büroklammer Dateien an die Beschreibung einer Aufgabe oder an einen Kommentar: Bilder erscheinen als Vorschau, andere Dateien öffnen sich in ihrer App. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
5dd7575dd6
commit
3ca4b4308d
19 changed files with 23300 additions and 62 deletions
|
|
@ -0,0 +1,29 @@
|
|||
### A file attached to a task's description or to a comment (Halkyon's ADR
|
||||
### 0043, D8): its name, media type and size, and what it is attached to, in
|
||||
### its sealed payload (ADR 0033, D5); the file itself is sealed too, kept
|
||||
### in halkyon_base's store (D2) and fetched when shown. The instance sees
|
||||
### that it exists, its size and the sealed file's hash, not what it is
|
||||
### attached to. Never changed: deleted with what it is attached to, or on
|
||||
### its own. Synced.
|
||||
class: Attachment
|
||||
table: attachment
|
||||
database: sync
|
||||
fields:
|
||||
### The sealed file's SHA-256, in lowercase hex: its name in the store,
|
||||
### which the server reads to serve it and to count and prune the files
|
||||
### (`todo_files.dart`). The hash of sealed bytes says nothing of the file.
|
||||
sha256: String
|
||||
### The key it is sealed with (ADR 0002's envelope); never null in Todo,
|
||||
### where everything is sealed.
|
||||
keyId: String?
|
||||
### The content, sealed: UTF-8 JSON with a `schemaVersion` once opened.
|
||||
payload: ByteData
|
||||
### The sealing's nonce (ADR 0002's envelope).
|
||||
nonce: ByteData?
|
||||
indexes:
|
||||
# Deleting a space finds its rows through this index (see content_key).
|
||||
attachment_space_idx:
|
||||
fields: spaceId
|
||||
# A space's live files, for the room counted and the pruning.
|
||||
attachment_hash_idx:
|
||||
fields: spaceId, sha256
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import 'package:clock/clock.dart';
|
||||
import 'package:halkyon_base_server/base.dart';
|
||||
import 'package:serverpod/serverpod.dart';
|
||||
|
||||
/// Deletes the stored files nobody can reach any more (Halkyon's ADR 0043):
|
||||
/// those no live attachment names, a week after they were last claimed; the
|
||||
/// server schedules it daily when it starts (`schedulePruning`).
|
||||
class FilePruningFutureCall extends FutureCall {
|
||||
/// Prunes one batch and logs how many files went.
|
||||
Future<void> pruneFiles(Session session) async {
|
||||
final pruned = await pruneUnreferencedFiles(session, now: clock.now());
|
||||
session.log('Pruned $pruned unreferenced files.');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
import 'package:halkyon_base_server/base.dart';
|
||||
import 'package:halkyon_todo_server/src/generated/protocol.dart';
|
||||
import 'package:serverpod/serverpod.dart';
|
||||
import 'package:serverpod_offline_sync_server/serverpod_offline_sync_server.dart';
|
||||
|
||||
/// Todo's files in halkyon_base's store (Halkyon's ADR 0043, D6): the
|
||||
/// `attachment` rows name them, a file is read by the members of the space
|
||||
/// its row is in, and every file is sealed, as every row is (ADR 0033, D5);
|
||||
/// up to 100 MB, the module's default (D3).
|
||||
const todoFiles = FileStore(
|
||||
fileOfRow: _fileOfAttachment,
|
||||
liveHashes: _liveAttachmentHashes,
|
||||
accepts: FileStore.acceptsSealedOnly,
|
||||
);
|
||||
|
||||
/// The file the attachment row [attachmentId] names, where the caller may
|
||||
/// read its space: their own, or one they are a member of (ADR 0044); null
|
||||
/// for a deleted row (the sync engine hides tombstones) and any other.
|
||||
Future<StoredFileRef?> _fileOfAttachment(
|
||||
Session session,
|
||||
UuidValue attachmentId,
|
||||
) async {
|
||||
// SELECT * FROM "attachment" WHERE "id" = $1 (tombstones hidden)
|
||||
final attachment = await Attachment.db.findById(session, attachmentId);
|
||||
final spaceId = attachment?.spaceId;
|
||||
// SELECT * FROM "offline_sync_spaces" WHERE "id" = $1
|
||||
final space = spaceId == null
|
||||
? null
|
||||
: await OfflineSyncSpace.db.findById(session, spaceId);
|
||||
final caller = UuidValue.withValidation(
|
||||
session.authenticated!.userIdentifier,
|
||||
);
|
||||
if (attachment == null ||
|
||||
space == null ||
|
||||
!(space.uuidSpaceId == caller ||
|
||||
await OfflineSyncSpaces(
|
||||
session,
|
||||
).roleOf(user: caller, space: space.uuidSpaceId) !=
|
||||
null)) {
|
||||
return null;
|
||||
}
|
||||
return (space: space.uuidSpaceId, sha256: attachment.sha256);
|
||||
}
|
||||
|
||||
/// Which of [hashes] (every one, where null) the live attachment rows of
|
||||
/// [space] name.
|
||||
Future<Set<String>> _liveAttachmentHashes(
|
||||
Session session,
|
||||
UuidValue space,
|
||||
Set<String>? hashes,
|
||||
) async {
|
||||
// SELECT "id" FROM "offline_sync_spaces" WHERE "uuidSpaceId" = $1
|
||||
final spaceId = (await OfflineSyncSpace.db.findFirstRow(
|
||||
session,
|
||||
where: (t) => t.uuidSpaceId.equals(space),
|
||||
))?.id;
|
||||
if (spaceId == null) {
|
||||
return const {};
|
||||
}
|
||||
// SELECT * FROM "attachment" WHERE "spaceId" = $1 [AND "sha256" IN …]
|
||||
// (tombstones hidden)
|
||||
return {
|
||||
for (final attachment in await Attachment.db.find(
|
||||
session,
|
||||
where: (t) => hashes == null
|
||||
? t.spaceId.equals(spaceId)
|
||||
: t.spaceId.equals(spaceId) & t.sha256.inSet(hashes),
|
||||
))
|
||||
attachment.sha256,
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,702 @@
|
|||
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
|
||||
/* To generate run: "serverpod generate" */
|
||||
|
||||
// ignore_for_file: implementation_imports
|
||||
// ignore_for_file: library_private_types_in_public_api
|
||||
// ignore_for_file: non_constant_identifier_names
|
||||
// ignore_for_file: public_member_api_docs
|
||||
// ignore_for_file: type_literal_in_constant_pattern
|
||||
// ignore_for_file: use_super_parameters
|
||||
// ignore_for_file: invalid_use_of_internal_member
|
||||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'dart:typed_data' as _idt;
|
||||
import 'package:serverpod/serverpod.dart' as _is;
|
||||
|
||||
/// A file attached to a task's description or to a comment (Halkyon's ADR
|
||||
/// 0043, D8): its name, media type and size, and what it is attached to, in
|
||||
/// its sealed payload (ADR 0033, D5); the file itself is sealed too, kept
|
||||
/// in halkyon_base's store (D2) and fetched when shown. The instance sees
|
||||
/// that it exists, its size and the sealed file's hash, not what it is
|
||||
/// attached to. Never changed: deleted with what it is attached to, or on
|
||||
/// its own. Synced.
|
||||
abstract class Attachment
|
||||
implements _is.TableRow<_is.UuidValue?>, _is.ProtocolSerialization {
|
||||
Attachment._({
|
||||
this.id,
|
||||
this.spaceId,
|
||||
required this.sha256,
|
||||
this.keyId,
|
||||
required this.payload,
|
||||
this.nonce,
|
||||
});
|
||||
|
||||
factory Attachment({
|
||||
_is.UuidValue? id,
|
||||
int? spaceId,
|
||||
required String sha256,
|
||||
String? keyId,
|
||||
required _idt.ByteData payload,
|
||||
_idt.ByteData? nonce,
|
||||
}) = _AttachmentImpl;
|
||||
|
||||
factory Attachment.fromJson(Map<String, dynamic> jsonSerialization) {
|
||||
return Attachment(
|
||||
id: jsonSerialization['id'] == null
|
||||
? null
|
||||
: _is.UuidValueJsonExtension.fromJson(jsonSerialization['id']),
|
||||
spaceId: jsonSerialization['spaceId'] as int?,
|
||||
sha256: jsonSerialization['sha256'] as String,
|
||||
keyId: jsonSerialization['keyId'] as String?,
|
||||
payload: _is.ByteDataJsonExtension.fromJson(jsonSerialization['payload']),
|
||||
nonce: jsonSerialization['nonce'] == null
|
||||
? null
|
||||
: _is.ByteDataJsonExtension.fromJson(jsonSerialization['nonce']),
|
||||
);
|
||||
}
|
||||
|
||||
static final t = AttachmentTable();
|
||||
|
||||
static const db = AttachmentRepository._();
|
||||
|
||||
@override
|
||||
_is.UuidValue? id;
|
||||
|
||||
/// The space owning this row. Maintained by the sync engine.
|
||||
int? spaceId;
|
||||
|
||||
/// The sealed file's SHA-256, in lowercase hex: its name in the store,
|
||||
/// which the server reads to serve it and to count and prune the files
|
||||
/// (`todo_files.dart`). The hash of sealed bytes says nothing of the file.
|
||||
String sha256;
|
||||
|
||||
/// The key it is sealed with (ADR 0002's envelope); never null in Todo,
|
||||
/// where everything is sealed.
|
||||
String? keyId;
|
||||
|
||||
/// The content, sealed: UTF-8 JSON with a `schemaVersion` once opened.
|
||||
_idt.ByteData payload;
|
||||
|
||||
/// The sealing's nonce (ADR 0002's envelope).
|
||||
_idt.ByteData? nonce;
|
||||
|
||||
@override
|
||||
_is.Table<_is.UuidValue?> get table => t;
|
||||
|
||||
/// Returns a shallow copy of this [Attachment]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_is.useResult
|
||||
Attachment copyWith({
|
||||
_is.UuidValue? id,
|
||||
int? spaceId,
|
||||
String? sha256,
|
||||
String? keyId,
|
||||
_idt.ByteData? payload,
|
||||
_idt.ByteData? nonce,
|
||||
});
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'__className__': 'Attachment',
|
||||
if (id != null) 'id': id?.toJson(),
|
||||
if (spaceId != null) 'spaceId': spaceId,
|
||||
'sha256': sha256,
|
||||
if (keyId != null) 'keyId': keyId,
|
||||
'payload': payload.toJson(),
|
||||
if (nonce != null) 'nonce': nonce?.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJsonForProtocol() {
|
||||
return {
|
||||
'__className__': 'Attachment',
|
||||
if (id != null) 'id': id?.toJson(),
|
||||
if (spaceId != null) 'spaceId': spaceId,
|
||||
'sha256': sha256,
|
||||
if (keyId != null) 'keyId': keyId,
|
||||
'payload': payload.toJson(),
|
||||
if (nonce != null) 'nonce': nonce?.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
static AttachmentInclude include() {
|
||||
return AttachmentInclude._();
|
||||
}
|
||||
|
||||
static AttachmentIncludeList includeList({
|
||||
_is.WhereExpressionBuilder<AttachmentTable>? where,
|
||||
int? limit,
|
||||
int? offset,
|
||||
_is.OrderByBuilder<AttachmentTable>? orderBy,
|
||||
_is.OrderByListBuilder<AttachmentTable>? orderByList,
|
||||
AttachmentInclude? include,
|
||||
}) {
|
||||
return AttachmentIncludeList._(
|
||||
where: where,
|
||||
limit: limit,
|
||||
offset: offset,
|
||||
orderBy: orderBy?.call(Attachment.t),
|
||||
orderByList: orderByList?.call(Attachment.t),
|
||||
include: include,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return _is.SerializationManager.encode(this);
|
||||
}
|
||||
}
|
||||
|
||||
class _Undefined {}
|
||||
|
||||
class _AttachmentImpl extends Attachment {
|
||||
_AttachmentImpl({
|
||||
_is.UuidValue? id,
|
||||
int? spaceId,
|
||||
required String sha256,
|
||||
String? keyId,
|
||||
required _idt.ByteData payload,
|
||||
_idt.ByteData? nonce,
|
||||
}) : super._(
|
||||
id: id,
|
||||
spaceId: spaceId,
|
||||
sha256: sha256,
|
||||
keyId: keyId,
|
||||
payload: payload,
|
||||
nonce: nonce,
|
||||
);
|
||||
|
||||
/// Returns a shallow copy of this [Attachment]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_is.useResult
|
||||
@override
|
||||
Attachment copyWith({
|
||||
Object? id = _Undefined,
|
||||
Object? spaceId = _Undefined,
|
||||
String? sha256,
|
||||
Object? keyId = _Undefined,
|
||||
_idt.ByteData? payload,
|
||||
Object? nonce = _Undefined,
|
||||
}) {
|
||||
return Attachment(
|
||||
id: id is _is.UuidValue? ? id : this.id,
|
||||
spaceId: spaceId is int? ? spaceId : this.spaceId,
|
||||
sha256: sha256 ?? this.sha256,
|
||||
keyId: keyId is String? ? keyId : this.keyId,
|
||||
payload: payload ?? this.payload.clone(),
|
||||
nonce: nonce is _idt.ByteData? ? nonce : this.nonce?.clone(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AttachmentUpdateTable extends _is.UpdateTable<AttachmentTable> {
|
||||
AttachmentUpdateTable(super.table);
|
||||
|
||||
_is.ColumnValue<int, int> spaceId(int? value) => _is.ColumnValue(
|
||||
table.spaceId,
|
||||
value,
|
||||
);
|
||||
|
||||
_is.ColumnValue<String, String> sha256(String value) => _is.ColumnValue(
|
||||
table.sha256,
|
||||
value,
|
||||
);
|
||||
|
||||
_is.ColumnValue<String, String> keyId(String? value) => _is.ColumnValue(
|
||||
table.keyId,
|
||||
value,
|
||||
);
|
||||
|
||||
_is.ColumnValue<_idt.ByteData, _idt.ByteData> payload(_idt.ByteData value) =>
|
||||
_is.ColumnValue(
|
||||
table.payload,
|
||||
value,
|
||||
);
|
||||
|
||||
_is.ColumnValue<_idt.ByteData, _idt.ByteData> nonce(_idt.ByteData? value) =>
|
||||
_is.ColumnValue(
|
||||
table.nonce,
|
||||
value,
|
||||
);
|
||||
}
|
||||
|
||||
class AttachmentTable extends _is.Table<_is.UuidValue?> {
|
||||
AttachmentTable({super.tableRelation}) : super(tableName: 'attachment') {
|
||||
updateTable = AttachmentUpdateTable(this);
|
||||
spaceId = _is.ColumnInt(
|
||||
'spaceId',
|
||||
this,
|
||||
);
|
||||
sha256 = _is.ColumnString(
|
||||
'sha256',
|
||||
this,
|
||||
);
|
||||
keyId = _is.ColumnString(
|
||||
'keyId',
|
||||
this,
|
||||
);
|
||||
payload = _is.ColumnByteData(
|
||||
'payload',
|
||||
this,
|
||||
);
|
||||
nonce = _is.ColumnByteData(
|
||||
'nonce',
|
||||
this,
|
||||
);
|
||||
}
|
||||
|
||||
late final AttachmentUpdateTable updateTable;
|
||||
|
||||
/// The space owning this row. Maintained by the sync engine.
|
||||
late final _is.ColumnInt spaceId;
|
||||
|
||||
/// The sealed file's SHA-256, in lowercase hex: its name in the store,
|
||||
/// which the server reads to serve it and to count and prune the files
|
||||
/// (`todo_files.dart`). The hash of sealed bytes says nothing of the file.
|
||||
late final _is.ColumnString sha256;
|
||||
|
||||
/// The key it is sealed with (ADR 0002's envelope); never null in Todo,
|
||||
/// where everything is sealed.
|
||||
late final _is.ColumnString keyId;
|
||||
|
||||
/// The content, sealed: UTF-8 JSON with a `schemaVersion` once opened.
|
||||
late final _is.ColumnByteData payload;
|
||||
|
||||
/// The sealing's nonce (ADR 0002's envelope).
|
||||
late final _is.ColumnByteData nonce;
|
||||
|
||||
@override
|
||||
List<_is.Column> get columns => [
|
||||
id,
|
||||
spaceId,
|
||||
sha256,
|
||||
keyId,
|
||||
payload,
|
||||
nonce,
|
||||
];
|
||||
}
|
||||
|
||||
class AttachmentInclude extends _is.IncludeObject {
|
||||
AttachmentInclude._();
|
||||
|
||||
@override
|
||||
Map<String, _is.Include?> get includes => {};
|
||||
|
||||
@override
|
||||
_is.Table<_is.UuidValue?> get table => Attachment.t;
|
||||
}
|
||||
|
||||
class AttachmentIncludeList extends _is.IncludeList {
|
||||
AttachmentIncludeList._({
|
||||
_is.WhereExpressionBuilder<AttachmentTable>? where,
|
||||
super.limit,
|
||||
super.offset,
|
||||
super.orderBy,
|
||||
super.orderByList,
|
||||
super.include,
|
||||
}) {
|
||||
super.where = where?.call(Attachment.t);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, _is.Include?> get includes => include?.includes ?? {};
|
||||
|
||||
@override
|
||||
_is.Table<_is.UuidValue?> get table => Attachment.t;
|
||||
}
|
||||
|
||||
class AttachmentRepository {
|
||||
const AttachmentRepository._();
|
||||
|
||||
/// Returns a list of [Attachment]s matching the given query parameters.
|
||||
///
|
||||
/// Use [where] to specify which items to include in the return value.
|
||||
/// If none is specified, all items will be returned.
|
||||
///
|
||||
/// To specify the order of the items use [orderBy] or [orderByList]
|
||||
/// when sorting by multiple columns.
|
||||
///
|
||||
/// The maximum number of items can be set by [limit]. If no limit is set,
|
||||
/// all items matching the query will be returned.
|
||||
///
|
||||
/// [offset] defines how many items to skip, after which [limit] (or all)
|
||||
/// items are read from the database.
|
||||
///
|
||||
/// ```dart
|
||||
/// var persons = await Persons.db.find(
|
||||
/// session,
|
||||
/// where: (t) => t.lastName.equals('Jones'),
|
||||
/// orderBy: (t) => t.firstName,
|
||||
/// limit: 100,
|
||||
/// );
|
||||
/// ```
|
||||
Future<List<Attachment>> find(
|
||||
_is.DatabaseSession session, {
|
||||
_is.WhereExpressionBuilder<AttachmentTable>? where,
|
||||
int? limit,
|
||||
int? offset,
|
||||
_is.OrderByBuilder<AttachmentTable>? orderBy,
|
||||
_is.OrderByListBuilder<AttachmentTable>? orderByList,
|
||||
_is.Transaction? transaction,
|
||||
_is.LockMode? lockMode,
|
||||
_is.LockBehavior? lockBehavior,
|
||||
}) async {
|
||||
return session.db.find<Attachment>(
|
||||
where: where?.call(Attachment.t),
|
||||
orderBy: orderBy?.call(Attachment.t),
|
||||
orderByList: orderByList?.call(Attachment.t),
|
||||
limit: limit,
|
||||
offset: offset,
|
||||
transaction: transaction,
|
||||
lockMode: lockMode,
|
||||
lockBehavior: lockBehavior,
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns the first matching [Attachment] matching the given query parameters.
|
||||
///
|
||||
/// Use [where] to specify which items to include in the return value.
|
||||
/// If none is specified, all items will be returned.
|
||||
///
|
||||
/// To specify the order use [orderBy] or [orderByList]
|
||||
/// when sorting by multiple columns.
|
||||
///
|
||||
/// [offset] defines how many items to skip, after which the next one will be picked.
|
||||
///
|
||||
/// ```dart
|
||||
/// var youngestPerson = await Persons.db.findFirstRow(
|
||||
/// session,
|
||||
/// where: (t) => t.lastName.equals('Jones'),
|
||||
/// orderBy: (t) => t.age,
|
||||
/// );
|
||||
/// ```
|
||||
Future<Attachment?> findFirstRow(
|
||||
_is.DatabaseSession session, {
|
||||
_is.WhereExpressionBuilder<AttachmentTable>? where,
|
||||
int? offset,
|
||||
_is.OrderByBuilder<AttachmentTable>? orderBy,
|
||||
_is.OrderByListBuilder<AttachmentTable>? orderByList,
|
||||
_is.Transaction? transaction,
|
||||
_is.LockMode? lockMode,
|
||||
_is.LockBehavior? lockBehavior,
|
||||
}) async {
|
||||
return session.db.findFirstRow<Attachment>(
|
||||
where: where?.call(Attachment.t),
|
||||
orderBy: orderBy?.call(Attachment.t),
|
||||
orderByList: orderByList?.call(Attachment.t),
|
||||
offset: offset,
|
||||
transaction: transaction,
|
||||
lockMode: lockMode,
|
||||
lockBehavior: lockBehavior,
|
||||
);
|
||||
}
|
||||
|
||||
/// Finds a single [Attachment] by its [id] or null if no such row exists.
|
||||
Future<Attachment?> findById(
|
||||
_is.DatabaseSession session,
|
||||
_is.UuidValue id, {
|
||||
_is.Transaction? transaction,
|
||||
_is.LockMode? lockMode,
|
||||
_is.LockBehavior? lockBehavior,
|
||||
}) async {
|
||||
return session.db.findById<Attachment>(
|
||||
id,
|
||||
transaction: transaction,
|
||||
lockMode: lockMode,
|
||||
lockBehavior: lockBehavior,
|
||||
);
|
||||
}
|
||||
|
||||
/// Inserts all [Attachment]s in the list and returns the inserted rows.
|
||||
///
|
||||
/// The returned [Attachment]s will have their `id` fields set.
|
||||
///
|
||||
/// This is an atomic operation, meaning that if one of the rows fails to
|
||||
/// insert, none of the rows will be inserted.
|
||||
///
|
||||
/// If [ignoreConflicts] is set to `true`, rows that conflict with existing
|
||||
/// rows are silently skipped, and only the successfully inserted rows are
|
||||
/// returned.
|
||||
///
|
||||
/// If [noReturn] is set to `true`, the inserted rows are not read back from
|
||||
/// the database and an empty list is returned. This avoids the overhead of
|
||||
/// transferring and deserializing the rows when the result is not needed.
|
||||
Future<List<Attachment>> insert(
|
||||
_is.DatabaseSession session,
|
||||
List<Attachment> rows, {
|
||||
_is.Transaction? transaction,
|
||||
bool ignoreConflicts = false,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.insert<Attachment>(
|
||||
rows,
|
||||
transaction: transaction,
|
||||
ignoreConflicts: ignoreConflicts,
|
||||
noReturn: noReturn,
|
||||
);
|
||||
}
|
||||
|
||||
/// Inserts a single [Attachment] and returns the inserted row.
|
||||
///
|
||||
/// The returned [Attachment] will have its `id` field set.
|
||||
Future<Attachment> insertRow(
|
||||
_is.DatabaseSession session,
|
||||
Attachment row, {
|
||||
_is.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.insertRow<Attachment>(
|
||||
row,
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
|
||||
/// Upserts all [Attachment]s in the list and returns the resulting rows.
|
||||
///
|
||||
/// If a row conflicts on the given [conflictColumns], the existing row is
|
||||
/// updated with the new values. Otherwise, a new row is inserted.
|
||||
///
|
||||
/// If [updateColumns] is provided, only those columns will be updated on
|
||||
/// conflict. If null, all non-conflict, non-id columns are updated.
|
||||
///
|
||||
/// If [updateWhere] is provided, the update only applies to rows matching the
|
||||
/// given expression. Conflicting rows that don't match are skipped and not
|
||||
/// returned, so the resulting list may be shorter than [rows].
|
||||
///
|
||||
/// The returned [Attachment]s will have their `id` fields set.
|
||||
///
|
||||
/// This is an atomic operation, meaning that if one of the rows fails,
|
||||
/// none of the rows will be affected.
|
||||
///
|
||||
/// If [noReturn] is set to `true`, the resulting rows are not read back from
|
||||
/// the database and an empty list is returned. This avoids the overhead of
|
||||
/// transferring and deserializing the rows when the result is not needed.
|
||||
Future<List<Attachment>> upsert(
|
||||
_is.DatabaseSession session,
|
||||
List<Attachment> rows, {
|
||||
required _is.ColumnSelections<AttachmentTable> conflictColumns,
|
||||
_is.ColumnSelections<AttachmentTable>? updateColumns,
|
||||
_is.WhereExpressionBuilder<AttachmentTable>? updateWhere,
|
||||
_is.Transaction? transaction,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.upsert<Attachment>(
|
||||
rows,
|
||||
conflictColumns: conflictColumns(Attachment.t),
|
||||
updateColumns: updateColumns?.call(Attachment.t),
|
||||
updateWhere: updateWhere?.call(Attachment.t),
|
||||
transaction: transaction,
|
||||
noReturn: noReturn,
|
||||
);
|
||||
}
|
||||
|
||||
/// Upserts a single [Attachment] and returns the resulting row.
|
||||
///
|
||||
/// If the row conflicts on the given [conflictColumns], the existing row is
|
||||
/// updated. Otherwise, a new row is inserted.
|
||||
///
|
||||
/// If [updateColumns] is provided, only those columns will be updated on
|
||||
/// conflict. If null, all non-conflict, non-id columns are updated.
|
||||
///
|
||||
/// If [updateWhere] is provided, the update only applies when the existing
|
||||
/// row matches the expression. Returns `null` if no row was affected — for
|
||||
/// example when [updateWhere] does not match the conflicting row.
|
||||
///
|
||||
/// The returned [Attachment] will have its `id` field set.
|
||||
Future<Attachment?> upsertRow(
|
||||
_is.DatabaseSession session,
|
||||
Attachment row, {
|
||||
required _is.ColumnSelections<AttachmentTable> conflictColumns,
|
||||
_is.ColumnSelections<AttachmentTable>? updateColumns,
|
||||
_is.WhereExpressionBuilder<AttachmentTable>? updateWhere,
|
||||
_is.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.upsertRow<Attachment>(
|
||||
row,
|
||||
conflictColumns: conflictColumns(Attachment.t),
|
||||
updateColumns: updateColumns?.call(Attachment.t),
|
||||
updateWhere: updateWhere?.call(Attachment.t),
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
|
||||
/// Updates all [Attachment]s in the list and returns the updated rows. If
|
||||
/// [columns] is provided, only those columns will be updated. Defaults to
|
||||
/// all columns.
|
||||
/// This is an atomic operation, meaning that if one of the rows fails to
|
||||
/// update, none of the rows will be updated.
|
||||
///
|
||||
/// If [noReturn] is set to `true`, the updated rows are not read back from
|
||||
/// the database and an empty list is returned. This avoids the overhead of
|
||||
/// transferring and deserializing the rows when the result is not needed.
|
||||
Future<List<Attachment>> update(
|
||||
_is.DatabaseSession session,
|
||||
List<Attachment> rows, {
|
||||
_is.ColumnSelections<AttachmentTable>? columns,
|
||||
_is.Transaction? transaction,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.update<Attachment>(
|
||||
rows,
|
||||
columns: columns?.call(Attachment.t),
|
||||
transaction: transaction,
|
||||
noReturn: noReturn,
|
||||
);
|
||||
}
|
||||
|
||||
/// Updates a single [Attachment]. The row needs to have its id set.
|
||||
/// Optionally, a list of [columns] can be provided to only update those
|
||||
/// columns. Defaults to all columns.
|
||||
Future<Attachment> updateRow(
|
||||
_is.DatabaseSession session,
|
||||
Attachment row, {
|
||||
_is.ColumnSelections<AttachmentTable>? columns,
|
||||
_is.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.updateRow<Attachment>(
|
||||
row,
|
||||
columns: columns?.call(Attachment.t),
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
|
||||
/// Updates a single [Attachment] by its [id] with the specified [columnValues].
|
||||
/// Returns the updated row or null if no row with the given id exists.
|
||||
Future<Attachment?> updateById(
|
||||
_is.DatabaseSession session,
|
||||
_is.UuidValue id, {
|
||||
required _is.ColumnValueListBuilder<AttachmentUpdateTable> columnValues,
|
||||
_is.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.updateById<Attachment>(
|
||||
id,
|
||||
columnValues: columnValues(Attachment.t.updateTable),
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
|
||||
/// Updates all [Attachment]s matching the [where] expression with the specified [columnValues].
|
||||
/// Returns the list of updated rows.
|
||||
///
|
||||
/// If [noReturn] is set to `true`, the updated rows are not read back from
|
||||
/// the database and an empty list is returned. This avoids the overhead of
|
||||
/// transferring and deserializing the rows when the result is not needed.
|
||||
Future<List<Attachment>> updateWhere(
|
||||
_is.DatabaseSession session, {
|
||||
required _is.ColumnValueListBuilder<AttachmentUpdateTable> columnValues,
|
||||
required _is.WhereExpressionBuilder<AttachmentTable> where,
|
||||
int? limit,
|
||||
int? offset,
|
||||
_is.OrderByBuilder<AttachmentTable>? orderBy,
|
||||
_is.OrderByListBuilder<AttachmentTable>? orderByList,
|
||||
_is.Transaction? transaction,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.updateWhere<Attachment>(
|
||||
columnValues: columnValues(Attachment.t.updateTable),
|
||||
where: where(Attachment.t),
|
||||
limit: limit,
|
||||
offset: offset,
|
||||
orderBy: orderBy?.call(Attachment.t),
|
||||
orderByList: orderByList?.call(Attachment.t),
|
||||
transaction: transaction,
|
||||
noReturn: noReturn,
|
||||
);
|
||||
}
|
||||
|
||||
/// Deletes all [Attachment]s in the list and returns the deleted rows.
|
||||
///
|
||||
/// To specify the order of the returned rows use [orderBy] or [orderByList]
|
||||
/// when sorting by multiple columns.
|
||||
///
|
||||
/// This is an atomic operation, meaning that if one of the rows fail to
|
||||
/// be deleted, none of the rows will be deleted.
|
||||
///
|
||||
/// If [noReturn] is set to `true`, the deleted rows are not read back from
|
||||
/// the database and an empty list is returned. This avoids the overhead of
|
||||
/// transferring and deserializing the rows when the result is not needed.
|
||||
Future<List<Attachment>> delete(
|
||||
_is.DatabaseSession session,
|
||||
List<Attachment> rows, {
|
||||
_is.OrderByBuilder<AttachmentTable>? orderBy,
|
||||
_is.OrderByListBuilder<AttachmentTable>? orderByList,
|
||||
_is.Transaction? transaction,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.delete<Attachment>(
|
||||
rows,
|
||||
orderBy: orderBy?.call(Attachment.t),
|
||||
orderByList: orderByList?.call(Attachment.t),
|
||||
transaction: transaction,
|
||||
noReturn: noReturn,
|
||||
);
|
||||
}
|
||||
|
||||
/// Deletes a single [Attachment].
|
||||
Future<Attachment> deleteRow(
|
||||
_is.DatabaseSession session,
|
||||
Attachment row, {
|
||||
_is.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.deleteRow<Attachment>(
|
||||
row,
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
|
||||
/// Deletes all rows matching the [where] expression.
|
||||
///
|
||||
/// To specify the order of the returned rows use [orderBy] or [orderByList]
|
||||
/// when sorting by multiple columns.
|
||||
///
|
||||
/// If [noReturn] is set to `true`, the deleted rows are not read back from
|
||||
/// the database and an empty list is returned. This avoids the overhead of
|
||||
/// transferring and deserializing the rows when the result is not needed.
|
||||
Future<List<Attachment>> deleteWhere(
|
||||
_is.DatabaseSession session, {
|
||||
required _is.WhereExpressionBuilder<AttachmentTable> where,
|
||||
_is.OrderByBuilder<AttachmentTable>? orderBy,
|
||||
_is.OrderByListBuilder<AttachmentTable>? orderByList,
|
||||
_is.Transaction? transaction,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.deleteWhere<Attachment>(
|
||||
where: where(Attachment.t),
|
||||
orderBy: orderBy?.call(Attachment.t),
|
||||
orderByList: orderByList?.call(Attachment.t),
|
||||
transaction: transaction,
|
||||
noReturn: noReturn,
|
||||
);
|
||||
}
|
||||
|
||||
/// Counts the number of rows matching the [where] expression. If omitted,
|
||||
/// will return the count of all rows in the table.
|
||||
Future<int> count(
|
||||
_is.DatabaseSession session, {
|
||||
_is.WhereExpressionBuilder<AttachmentTable>? where,
|
||||
int? limit,
|
||||
_is.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.count<Attachment>(
|
||||
where: where?.call(Attachment.t),
|
||||
limit: limit,
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
|
||||
/// Acquires row-level locks on [Attachment] rows matching the [where] expression.
|
||||
Future<void> lockRows(
|
||||
_is.DatabaseSession session, {
|
||||
required _is.WhereExpressionBuilder<AttachmentTable> where,
|
||||
required _is.LockMode lockMode,
|
||||
required _is.Transaction transaction,
|
||||
_is.LockBehavior lockBehavior = _is.LockBehavior.wait,
|
||||
}) async {
|
||||
return session.db.lockRows<Attachment>(
|
||||
where: where(Attachment.t),
|
||||
lockMode: lockMode,
|
||||
lockBehavior: lockBehavior,
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -14,6 +14,7 @@
|
|||
import 'dart:async' as _ida;
|
||||
import 'package:clock/clock.dart' as _io0w16m8;
|
||||
import 'package:serverpod/serverpod.dart' as _is;
|
||||
import '../attachment/file_pruning_future_call.dart' as _irygz6s8;
|
||||
import '../pruning_future_call.dart' as _ibrplims;
|
||||
|
||||
/// Invokes a future call.
|
||||
|
|
@ -58,6 +59,7 @@ class FutureCalls extends _is.FutureCallDispatch<_FutureCallRef> {
|
|||
String serverId,
|
||||
) {
|
||||
var registeredFutureCalls = <String, _is.InvokableFutureCall>{
|
||||
'FilePruningPruneFilesFutureCall': FilePruningPruneFilesFutureCall(),
|
||||
'PruningPruneAccountsFutureCall': PruningPruneAccountsFutureCall(),
|
||||
'PruningForgetSignUpAttemptsFutureCall':
|
||||
PruningForgetSignUpAttemptsFutureCall(),
|
||||
|
|
@ -185,9 +187,24 @@ class _FutureCallRef {
|
|||
|
||||
final _InvokeFutureCall _invokeFutureCall;
|
||||
|
||||
late final filePruning = _FilePruningFutureCallDispatcher(_invokeFutureCall);
|
||||
|
||||
late final pruning = _PruningFutureCallDispatcher(_invokeFutureCall);
|
||||
}
|
||||
|
||||
class _FilePruningFutureCallDispatcher {
|
||||
_FilePruningFutureCallDispatcher(this._invokeFutureCall);
|
||||
|
||||
final _InvokeFutureCall _invokeFutureCall;
|
||||
|
||||
Future<void> pruneFiles() {
|
||||
return _invokeFutureCall(
|
||||
'FilePruningPruneFilesFutureCall',
|
||||
null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PruningFutureCallDispatcher {
|
||||
_PruningFutureCallDispatcher(this._invokeFutureCall);
|
||||
|
||||
|
|
@ -222,6 +239,18 @@ class _PruningFutureCallDispatcher {
|
|||
}
|
||||
}
|
||||
|
||||
/// Prunes one batch and logs how many files went.
|
||||
class FilePruningPruneFilesFutureCall extends _is.FutureCall
|
||||
implements _is.InvokableFutureCall {
|
||||
@override
|
||||
_ida.Future<void> invoke(
|
||||
_is.Session session,
|
||||
_is.SerializableModel? object,
|
||||
) async {
|
||||
await _irygz6s8.FilePruningFutureCall().pruneFiles(session);
|
||||
}
|
||||
}
|
||||
|
||||
/// Prunes one batch of anonymous accounts and logs how many people went,
|
||||
/// forgets the resets that lapsed (ADR 0017) and the assistants' figures
|
||||
/// past their 3 months (Learn's ADR 0021, D41), and deletes the new accounts left
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import 'package:serverpod_auth_idp_server/serverpod_auth_idp_server.dart'
|
|||
as _iais;
|
||||
import 'package:serverpod_offline_sync_server/serverpod_offline_sync_server.dart'
|
||||
as _izehhkf5;
|
||||
import 'attachment/attachment.dart' as _imhw6pbz;
|
||||
import 'comment/task_comment.dart' as _i68ws957;
|
||||
import 'filter/saved_filter.dart' as _i9q9fhid;
|
||||
import 'label/label.dart' as _ivak08z4;
|
||||
|
|
@ -39,6 +40,7 @@ import 'settings/todo_settings.dart' as _icak60vd;
|
|||
import 'task/task.dart' as _ib4f8uyw;
|
||||
import 'weather/forecast_hour.dart' as _i1l0g3o2;
|
||||
import 'weather/weather_refused.dart' as _ieu1vbq0;
|
||||
export 'attachment/attachment.dart';
|
||||
export 'comment/task_comment.dart';
|
||||
export 'filter/saved_filter.dart';
|
||||
export 'label/label.dart';
|
||||
|
|
@ -63,6 +65,96 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
static final Protocol _instance = Protocol._().._registerHostProtocols();
|
||||
|
||||
static List<_isp.TableDefinition> get targetTableDefinitions => [
|
||||
_isp.TableDefinition(
|
||||
name: 'attachment',
|
||||
dartName: 'Attachment',
|
||||
schema: 'public',
|
||||
module: 'halkyon_todo',
|
||||
columns: [
|
||||
_isp.ColumnDefinition(
|
||||
name: 'id',
|
||||
columnType: _isp.ColumnType.uuid,
|
||||
isNullable: false,
|
||||
dartType: 'UuidValue?',
|
||||
columnDefault: 'random_v7',
|
||||
),
|
||||
_isp.ColumnDefinition(
|
||||
name: 'spaceId',
|
||||
columnType: _isp.ColumnType.bigint,
|
||||
isNullable: true,
|
||||
dartType: 'int?',
|
||||
),
|
||||
_isp.ColumnDefinition(
|
||||
name: 'sha256',
|
||||
columnType: _isp.ColumnType.text,
|
||||
isNullable: false,
|
||||
dartType: 'String',
|
||||
),
|
||||
_isp.ColumnDefinition(
|
||||
name: 'keyId',
|
||||
columnType: _isp.ColumnType.text,
|
||||
isNullable: true,
|
||||
dartType: 'String?',
|
||||
),
|
||||
_isp.ColumnDefinition(
|
||||
name: 'payload',
|
||||
columnType: _isp.ColumnType.bytea,
|
||||
isNullable: false,
|
||||
dartType: 'dart:typed_data:ByteData',
|
||||
),
|
||||
_isp.ColumnDefinition(
|
||||
name: 'nonce',
|
||||
columnType: _isp.ColumnType.bytea,
|
||||
isNullable: true,
|
||||
dartType: 'dart:typed_data:ByteData?',
|
||||
),
|
||||
],
|
||||
foreignKeys: [
|
||||
_isp.ForeignKeyDefinition(
|
||||
constraintName: 'attachment_fk_0',
|
||||
columns: ['spaceId'],
|
||||
referenceTable: 'offline_sync_spaces',
|
||||
referenceTableSchema: 'public',
|
||||
referenceColumns: ['id'],
|
||||
onUpdate: _isp.ForeignKeyAction.noAction,
|
||||
onDelete: _isp.ForeignKeyAction.cascade,
|
||||
matchType: null,
|
||||
),
|
||||
],
|
||||
indexes: [
|
||||
_isp.IndexDefinition(
|
||||
indexName: 'attachment_space_idx',
|
||||
tableSpace: null,
|
||||
elements: [
|
||||
_isp.IndexElementDefinition(
|
||||
type: _isp.IndexElementDefinitionType.column,
|
||||
definition: 'spaceId',
|
||||
),
|
||||
],
|
||||
type: 'btree',
|
||||
isUnique: false,
|
||||
isPrimary: false,
|
||||
),
|
||||
_isp.IndexDefinition(
|
||||
indexName: 'attachment_hash_idx',
|
||||
tableSpace: null,
|
||||
elements: [
|
||||
_isp.IndexElementDefinition(
|
||||
type: _isp.IndexElementDefinitionType.column,
|
||||
definition: 'spaceId',
|
||||
),
|
||||
_isp.IndexElementDefinition(
|
||||
type: _isp.IndexElementDefinitionType.column,
|
||||
definition: 'sha256',
|
||||
),
|
||||
],
|
||||
type: 'btree',
|
||||
isUnique: false,
|
||||
isPrimary: false,
|
||||
),
|
||||
],
|
||||
managed: true,
|
||||
),
|
||||
_isp.TableDefinition(
|
||||
name: 'content_key',
|
||||
dartName: 'ContentKey',
|
||||
|
|
@ -628,6 +720,9 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
}
|
||||
}
|
||||
|
||||
if (t == _imhw6pbz.Attachment) {
|
||||
return _imhw6pbz.Attachment.fromJson(data) as T;
|
||||
}
|
||||
if (t == _i68ws957.TaskComment) {
|
||||
return _i68ws957.TaskComment.fromJson(data) as T;
|
||||
}
|
||||
|
|
@ -670,6 +765,9 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
if (t == _ieu1vbq0.WeatherRefused) {
|
||||
return _ieu1vbq0.WeatherRefused.fromJson(data) as T;
|
||||
}
|
||||
if (t == _is.getType<_imhw6pbz.Attachment?>()) {
|
||||
return (data != null ? _imhw6pbz.Attachment.fromJson(data) : null) as T;
|
||||
}
|
||||
if (t == _is.getType<_i68ws957.TaskComment?>()) {
|
||||
return (data != null ? _i68ws957.TaskComment.fromJson(data) : null) as T;
|
||||
}
|
||||
|
|
@ -751,6 +849,7 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
|
||||
static String? getClassNameForType(Type type) {
|
||||
return switch (type) {
|
||||
_imhw6pbz.Attachment => 'Attachment',
|
||||
_i68ws957.TaskComment => 'TaskComment',
|
||||
_i9q9fhid.SavedFilter => 'SavedFilter',
|
||||
_ivak08z4.Label => 'Label',
|
||||
|
|
@ -782,6 +881,8 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
}
|
||||
|
||||
switch (data) {
|
||||
case _imhw6pbz.Attachment():
|
||||
return 'Attachment';
|
||||
case _i68ws957.TaskComment():
|
||||
return 'TaskComment';
|
||||
case _i9q9fhid.SavedFilter():
|
||||
|
|
@ -850,6 +951,9 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
if (dataClassName is! String) {
|
||||
return super.deserializeByClassName(data);
|
||||
}
|
||||
if (dataClassName == 'Attachment') {
|
||||
return deserialize<_imhw6pbz.Attachment>(data['data']);
|
||||
}
|
||||
if (dataClassName == 'TaskComment') {
|
||||
return deserialize<_i68ws957.TaskComment>(data['data']);
|
||||
}
|
||||
|
|
@ -966,6 +1070,8 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
}
|
||||
}
|
||||
switch (t) {
|
||||
case _imhw6pbz.Attachment:
|
||||
return _imhw6pbz.Attachment.t;
|
||||
case _i68ws957.TaskComment:
|
||||
return _i68ws957.TaskComment.t;
|
||||
case _i9q9fhid.SavedFilter:
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'package:serverpod/serverpod.dart' as _is;
|
||||
import 'attachment/attachment.dart' as _imhw6pbz;
|
||||
import 'comment/task_comment.dart' as _i68ws957;
|
||||
import 'filter/saved_filter.dart' as _i9q9fhid;
|
||||
import 'label/label.dart' as _ivak08z4;
|
||||
|
|
@ -24,6 +25,7 @@ import 'task/task.dart' as _ib4f8uyw;
|
|||
/// `serverpod_offline_sync` package, including the ones owned by modules and
|
||||
/// shared packages.
|
||||
final List<_is.Table> syncTables = [
|
||||
_imhw6pbz.Attachment.t,
|
||||
_imed3x5d.ContentKey.t,
|
||||
_ivak08z4.Label.t,
|
||||
_i9c44spp.Project.t,
|
||||
|
|
|
|||
|
|
@ -16,19 +16,24 @@ const _deletionNotices = 'tell-members-of-deletions';
|
|||
/// The identifier of the daily deleting of accounts the register forgot.
|
||||
const _deletedAccounts = 'forget-deleted-accounts';
|
||||
|
||||
/// The identifier of the daily pruning of files (Halkyon's ADR 0043).
|
||||
const _filePruning = 'prune-unreferenced-files';
|
||||
|
||||
/// The future calls [schedulePruning] keeps scheduled, by their identifiers.
|
||||
const pruningIdentifiers = [
|
||||
_pruning,
|
||||
_attemptPruning,
|
||||
_deletionNotices,
|
||||
_deletedAccounts,
|
||||
_filePruning,
|
||||
];
|
||||
|
||||
/// Schedules the pruning (`PruningFutureCall`): the anonymous accounts left
|
||||
/// from before daily, sign-up attempts and expired email codes hourly, and the instance's
|
||||
/// accounts (ADR 0016): the register's deletions its members haven't
|
||||
/// confirmed hourly, a member's people the register forgot daily (each does
|
||||
/// nothing in the other mode). Scheduling always adds an entry, so the
|
||||
/// nothing in the other mode), and the files no attachment names any more
|
||||
/// daily (`FilePruningFutureCall`). Scheduling always adds an entry, so the
|
||||
/// previous one goes first, and starting the server again leaves one of
|
||||
/// each; the one scheduled under the pruning's earlier name, which would
|
||||
/// call nothing, goes too.
|
||||
|
|
@ -61,4 +66,11 @@ Future<void> schedulePruning(Serverpod pod) async {
|
|||
.cron('37 3 * * *')
|
||||
.pruning
|
||||
.forgetDeletedAccounts();
|
||||
|
||||
await pod.futureCalls.cancel(_filePruning);
|
||||
await pod.futureCalls
|
||||
.callRecurring(identifier: _filePruning)
|
||||
.cron('47 3 * * *')
|
||||
.filePruning
|
||||
.pruneFiles();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:halkyon_base_server/base.dart';
|
||||
import 'package:halkyon_todo_server/src/attachment/todo_files.dart';
|
||||
import 'package:halkyon_todo_server/src/todo_data.dart';
|
||||
|
||||
/// Where this project's source is published, which the instance names
|
||||
|
|
@ -21,6 +22,7 @@ const minimumAppBuild = 1;
|
|||
/// with them ([deleteTodoData]). It holds the instance's
|
||||
/// register, or is a member of the one [accounts] names (ADR 0016; the
|
||||
/// server reads it from the operator's environment, `ACCOUNT_REGISTER_URL`).
|
||||
/// It keeps the files attached to tasks and comments ([todoFiles], ADR 0043).
|
||||
/// The server sets it up before it starts (`server.dart`), and so do its
|
||||
/// tests.
|
||||
HalkyonBase todoBase({AccountsMode accounts = const HoldsRegister()}) =>
|
||||
|
|
@ -31,4 +33,5 @@ HalkyonBase todoBase({AccountsMode accounts = const HoldsRegister()}) =>
|
|||
minimumAppBuild: minimumAppBuild,
|
||||
deletePersonData: const [deleteTodoData],
|
||||
accounts: accounts,
|
||||
files: todoFiles,
|
||||
);
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,806 @@
|
|||
{
|
||||
"__className__": "serverpod.DatabaseDefinition",
|
||||
"schemaVersion": 2,
|
||||
"moduleName": "halkyon_todo",
|
||||
"tables": [
|
||||
{
|
||||
"__className__": "serverpod.TableDefinition",
|
||||
"name": "attachment",
|
||||
"dartName": "Attachment",
|
||||
"module": "halkyon_todo",
|
||||
"schema": "public",
|
||||
"columns": [
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "id",
|
||||
"fieldName": "id",
|
||||
"columnType": 7,
|
||||
"isNullable": false,
|
||||
"columnDefault": "random_v7",
|
||||
"dartType": "UuidValue?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "spaceId",
|
||||
"fieldName": "spaceId",
|
||||
"columnType": 6,
|
||||
"isNullable": true,
|
||||
"dartType": "int?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "sha256",
|
||||
"fieldName": "sha256",
|
||||
"columnType": 0,
|
||||
"isNullable": false,
|
||||
"dartType": "String"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "keyId",
|
||||
"fieldName": "keyId",
|
||||
"columnType": 0,
|
||||
"isNullable": true,
|
||||
"dartType": "String?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "payload",
|
||||
"fieldName": "payload",
|
||||
"columnType": 5,
|
||||
"isNullable": false,
|
||||
"dartType": "dart:typed_data:ByteData"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "nonce",
|
||||
"fieldName": "nonce",
|
||||
"columnType": 5,
|
||||
"isNullable": true,
|
||||
"dartType": "dart:typed_data:ByteData?"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"__className__": "serverpod.ForeignKeyDefinition",
|
||||
"constraintName": "attachment_fk_0",
|
||||
"columns": [
|
||||
"spaceId"
|
||||
],
|
||||
"referenceTable": "offline_sync_spaces",
|
||||
"referenceTableSchema": "public",
|
||||
"referenceColumns": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": 3,
|
||||
"onDelete": 4
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
{
|
||||
"__className__": "serverpod.IndexDefinition",
|
||||
"indexName": "attachment_space_idx",
|
||||
"elements": [
|
||||
{
|
||||
"__className__": "serverpod.IndexElementDefinition",
|
||||
"type": 0,
|
||||
"definition": "spaceId"
|
||||
}
|
||||
],
|
||||
"type": "btree",
|
||||
"isUnique": false,
|
||||
"isPrimary": false
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.IndexDefinition",
|
||||
"indexName": "attachment_hash_idx",
|
||||
"elements": [
|
||||
{
|
||||
"__className__": "serverpod.IndexElementDefinition",
|
||||
"type": 0,
|
||||
"definition": "spaceId"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.IndexElementDefinition",
|
||||
"type": 0,
|
||||
"definition": "sha256"
|
||||
}
|
||||
],
|
||||
"type": "btree",
|
||||
"isUnique": false,
|
||||
"isPrimary": false
|
||||
}
|
||||
],
|
||||
"managed": true
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.TableDefinition",
|
||||
"name": "content_key",
|
||||
"dartName": "ContentKey",
|
||||
"module": "halkyon_todo",
|
||||
"schema": "public",
|
||||
"columns": [
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "id",
|
||||
"fieldName": "id",
|
||||
"columnType": 7,
|
||||
"isNullable": false,
|
||||
"columnDefault": "random_v7",
|
||||
"dartType": "UuidValue?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "spaceId",
|
||||
"fieldName": "spaceId",
|
||||
"columnType": 6,
|
||||
"isNullable": true,
|
||||
"dartType": "int?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "dataKeyId",
|
||||
"fieldName": "dataKeyId",
|
||||
"columnType": 0,
|
||||
"isNullable": false,
|
||||
"dartType": "String"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "sealedKey",
|
||||
"fieldName": "sealedKey",
|
||||
"columnType": 5,
|
||||
"isNullable": false,
|
||||
"dartType": "dart:typed_data:ByteData"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"__className__": "serverpod.ForeignKeyDefinition",
|
||||
"constraintName": "content_key_fk_0",
|
||||
"columns": [
|
||||
"spaceId"
|
||||
],
|
||||
"referenceTable": "offline_sync_spaces",
|
||||
"referenceTableSchema": "public",
|
||||
"referenceColumns": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": 3,
|
||||
"onDelete": 4
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
{
|
||||
"__className__": "serverpod.IndexDefinition",
|
||||
"indexName": "content_key_space_idx",
|
||||
"elements": [
|
||||
{
|
||||
"__className__": "serverpod.IndexElementDefinition",
|
||||
"type": 0,
|
||||
"definition": "spaceId"
|
||||
}
|
||||
],
|
||||
"type": "btree",
|
||||
"isUnique": false,
|
||||
"isPrimary": false
|
||||
}
|
||||
],
|
||||
"managed": true
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.TableDefinition",
|
||||
"name": "label",
|
||||
"dartName": "Label",
|
||||
"module": "halkyon_todo",
|
||||
"schema": "public",
|
||||
"columns": [
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "id",
|
||||
"fieldName": "id",
|
||||
"columnType": 7,
|
||||
"isNullable": false,
|
||||
"columnDefault": "random_v7",
|
||||
"dartType": "UuidValue?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "spaceId",
|
||||
"fieldName": "spaceId",
|
||||
"columnType": 6,
|
||||
"isNullable": true,
|
||||
"dartType": "int?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "keyId",
|
||||
"fieldName": "keyId",
|
||||
"columnType": 0,
|
||||
"isNullable": true,
|
||||
"dartType": "String?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "payload",
|
||||
"fieldName": "payload",
|
||||
"columnType": 5,
|
||||
"isNullable": false,
|
||||
"dartType": "dart:typed_data:ByteData"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "nonce",
|
||||
"fieldName": "nonce",
|
||||
"columnType": 5,
|
||||
"isNullable": true,
|
||||
"dartType": "dart:typed_data:ByteData?"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"__className__": "serverpod.ForeignKeyDefinition",
|
||||
"constraintName": "label_fk_0",
|
||||
"columns": [
|
||||
"spaceId"
|
||||
],
|
||||
"referenceTable": "offline_sync_spaces",
|
||||
"referenceTableSchema": "public",
|
||||
"referenceColumns": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": 3,
|
||||
"onDelete": 4
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
{
|
||||
"__className__": "serverpod.IndexDefinition",
|
||||
"indexName": "label_space_idx",
|
||||
"elements": [
|
||||
{
|
||||
"__className__": "serverpod.IndexElementDefinition",
|
||||
"type": 0,
|
||||
"definition": "spaceId"
|
||||
}
|
||||
],
|
||||
"type": "btree",
|
||||
"isUnique": false,
|
||||
"isPrimary": false
|
||||
}
|
||||
],
|
||||
"managed": true
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.TableDefinition",
|
||||
"name": "project",
|
||||
"dartName": "Project",
|
||||
"module": "halkyon_todo",
|
||||
"schema": "public",
|
||||
"columns": [
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "id",
|
||||
"fieldName": "id",
|
||||
"columnType": 7,
|
||||
"isNullable": false,
|
||||
"columnDefault": "random_v7",
|
||||
"dartType": "UuidValue?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "spaceId",
|
||||
"fieldName": "spaceId",
|
||||
"columnType": 6,
|
||||
"isNullable": true,
|
||||
"dartType": "int?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "keyId",
|
||||
"fieldName": "keyId",
|
||||
"columnType": 0,
|
||||
"isNullable": true,
|
||||
"dartType": "String?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "payload",
|
||||
"fieldName": "payload",
|
||||
"columnType": 5,
|
||||
"isNullable": false,
|
||||
"dartType": "dart:typed_data:ByteData"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "nonce",
|
||||
"fieldName": "nonce",
|
||||
"columnType": 5,
|
||||
"isNullable": true,
|
||||
"dartType": "dart:typed_data:ByteData?"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"__className__": "serverpod.ForeignKeyDefinition",
|
||||
"constraintName": "project_fk_0",
|
||||
"columns": [
|
||||
"spaceId"
|
||||
],
|
||||
"referenceTable": "offline_sync_spaces",
|
||||
"referenceTableSchema": "public",
|
||||
"referenceColumns": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": 3,
|
||||
"onDelete": 4
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
{
|
||||
"__className__": "serverpod.IndexDefinition",
|
||||
"indexName": "project_space_idx",
|
||||
"elements": [
|
||||
{
|
||||
"__className__": "serverpod.IndexElementDefinition",
|
||||
"type": 0,
|
||||
"definition": "spaceId"
|
||||
}
|
||||
],
|
||||
"type": "btree",
|
||||
"isUnique": false,
|
||||
"isPrimary": false
|
||||
}
|
||||
],
|
||||
"managed": true
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.TableDefinition",
|
||||
"name": "saved_filter",
|
||||
"dartName": "SavedFilter",
|
||||
"module": "halkyon_todo",
|
||||
"schema": "public",
|
||||
"columns": [
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "id",
|
||||
"fieldName": "id",
|
||||
"columnType": 7,
|
||||
"isNullable": false,
|
||||
"columnDefault": "random_v7",
|
||||
"dartType": "UuidValue?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "spaceId",
|
||||
"fieldName": "spaceId",
|
||||
"columnType": 6,
|
||||
"isNullable": true,
|
||||
"dartType": "int?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "keyId",
|
||||
"fieldName": "keyId",
|
||||
"columnType": 0,
|
||||
"isNullable": true,
|
||||
"dartType": "String?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "payload",
|
||||
"fieldName": "payload",
|
||||
"columnType": 5,
|
||||
"isNullable": false,
|
||||
"dartType": "dart:typed_data:ByteData"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "nonce",
|
||||
"fieldName": "nonce",
|
||||
"columnType": 5,
|
||||
"isNullable": true,
|
||||
"dartType": "dart:typed_data:ByteData?"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"__className__": "serverpod.ForeignKeyDefinition",
|
||||
"constraintName": "saved_filter_fk_0",
|
||||
"columns": [
|
||||
"spaceId"
|
||||
],
|
||||
"referenceTable": "offline_sync_spaces",
|
||||
"referenceTableSchema": "public",
|
||||
"referenceColumns": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": 3,
|
||||
"onDelete": 4
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
{
|
||||
"__className__": "serverpod.IndexDefinition",
|
||||
"indexName": "saved_filter_space_idx",
|
||||
"elements": [
|
||||
{
|
||||
"__className__": "serverpod.IndexElementDefinition",
|
||||
"type": 0,
|
||||
"definition": "spaceId"
|
||||
}
|
||||
],
|
||||
"type": "btree",
|
||||
"isUnique": false,
|
||||
"isPrimary": false
|
||||
}
|
||||
],
|
||||
"managed": true
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.TableDefinition",
|
||||
"name": "section",
|
||||
"dartName": "Section",
|
||||
"module": "halkyon_todo",
|
||||
"schema": "public",
|
||||
"columns": [
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "id",
|
||||
"fieldName": "id",
|
||||
"columnType": 7,
|
||||
"isNullable": false,
|
||||
"columnDefault": "random_v7",
|
||||
"dartType": "UuidValue?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "spaceId",
|
||||
"fieldName": "spaceId",
|
||||
"columnType": 6,
|
||||
"isNullable": true,
|
||||
"dartType": "int?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "keyId",
|
||||
"fieldName": "keyId",
|
||||
"columnType": 0,
|
||||
"isNullable": true,
|
||||
"dartType": "String?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "payload",
|
||||
"fieldName": "payload",
|
||||
"columnType": 5,
|
||||
"isNullable": false,
|
||||
"dartType": "dart:typed_data:ByteData"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "nonce",
|
||||
"fieldName": "nonce",
|
||||
"columnType": 5,
|
||||
"isNullable": true,
|
||||
"dartType": "dart:typed_data:ByteData?"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"__className__": "serverpod.ForeignKeyDefinition",
|
||||
"constraintName": "section_fk_0",
|
||||
"columns": [
|
||||
"spaceId"
|
||||
],
|
||||
"referenceTable": "offline_sync_spaces",
|
||||
"referenceTableSchema": "public",
|
||||
"referenceColumns": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": 3,
|
||||
"onDelete": 4
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
{
|
||||
"__className__": "serverpod.IndexDefinition",
|
||||
"indexName": "section_space_idx",
|
||||
"elements": [
|
||||
{
|
||||
"__className__": "serverpod.IndexElementDefinition",
|
||||
"type": 0,
|
||||
"definition": "spaceId"
|
||||
}
|
||||
],
|
||||
"type": "btree",
|
||||
"isUnique": false,
|
||||
"isPrimary": false
|
||||
}
|
||||
],
|
||||
"managed": true
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.TableDefinition",
|
||||
"name": "task",
|
||||
"dartName": "Task",
|
||||
"module": "halkyon_todo",
|
||||
"schema": "public",
|
||||
"columns": [
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "id",
|
||||
"fieldName": "id",
|
||||
"columnType": 7,
|
||||
"isNullable": false,
|
||||
"columnDefault": "random_v7",
|
||||
"dartType": "UuidValue?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "spaceId",
|
||||
"fieldName": "spaceId",
|
||||
"columnType": 6,
|
||||
"isNullable": true,
|
||||
"dartType": "int?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "keyId",
|
||||
"fieldName": "keyId",
|
||||
"columnType": 0,
|
||||
"isNullable": true,
|
||||
"dartType": "String?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "payload",
|
||||
"fieldName": "payload",
|
||||
"columnType": 5,
|
||||
"isNullable": false,
|
||||
"dartType": "dart:typed_data:ByteData"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "nonce",
|
||||
"fieldName": "nonce",
|
||||
"columnType": 5,
|
||||
"isNullable": true,
|
||||
"dartType": "dart:typed_data:ByteData?"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"__className__": "serverpod.ForeignKeyDefinition",
|
||||
"constraintName": "task_fk_0",
|
||||
"columns": [
|
||||
"spaceId"
|
||||
],
|
||||
"referenceTable": "offline_sync_spaces",
|
||||
"referenceTableSchema": "public",
|
||||
"referenceColumns": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": 3,
|
||||
"onDelete": 4
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
{
|
||||
"__className__": "serverpod.IndexDefinition",
|
||||
"indexName": "task_space_idx",
|
||||
"elements": [
|
||||
{
|
||||
"__className__": "serverpod.IndexElementDefinition",
|
||||
"type": 0,
|
||||
"definition": "spaceId"
|
||||
}
|
||||
],
|
||||
"type": "btree",
|
||||
"isUnique": false,
|
||||
"isPrimary": false
|
||||
}
|
||||
],
|
||||
"managed": true
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.TableDefinition",
|
||||
"name": "task_comment",
|
||||
"dartName": "TaskComment",
|
||||
"module": "halkyon_todo",
|
||||
"schema": "public",
|
||||
"columns": [
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "id",
|
||||
"fieldName": "id",
|
||||
"columnType": 7,
|
||||
"isNullable": false,
|
||||
"columnDefault": "random_v7",
|
||||
"dartType": "UuidValue?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "spaceId",
|
||||
"fieldName": "spaceId",
|
||||
"columnType": 6,
|
||||
"isNullable": true,
|
||||
"dartType": "int?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "keyId",
|
||||
"fieldName": "keyId",
|
||||
"columnType": 0,
|
||||
"isNullable": true,
|
||||
"dartType": "String?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "payload",
|
||||
"fieldName": "payload",
|
||||
"columnType": 5,
|
||||
"isNullable": false,
|
||||
"dartType": "dart:typed_data:ByteData"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "nonce",
|
||||
"fieldName": "nonce",
|
||||
"columnType": 5,
|
||||
"isNullable": true,
|
||||
"dartType": "dart:typed_data:ByteData?"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"__className__": "serverpod.ForeignKeyDefinition",
|
||||
"constraintName": "task_comment_fk_0",
|
||||
"columns": [
|
||||
"spaceId"
|
||||
],
|
||||
"referenceTable": "offline_sync_spaces",
|
||||
"referenceTableSchema": "public",
|
||||
"referenceColumns": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": 3,
|
||||
"onDelete": 4
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
{
|
||||
"__className__": "serverpod.IndexDefinition",
|
||||
"indexName": "task_comment_space_idx",
|
||||
"elements": [
|
||||
{
|
||||
"__className__": "serverpod.IndexElementDefinition",
|
||||
"type": 0,
|
||||
"definition": "spaceId"
|
||||
}
|
||||
],
|
||||
"type": "btree",
|
||||
"isUnique": false,
|
||||
"isPrimary": false
|
||||
}
|
||||
],
|
||||
"managed": true
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.TableDefinition",
|
||||
"name": "todo_settings",
|
||||
"dartName": "TodoSettings",
|
||||
"module": "halkyon_todo",
|
||||
"schema": "public",
|
||||
"columns": [
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "id",
|
||||
"fieldName": "id",
|
||||
"columnType": 7,
|
||||
"isNullable": false,
|
||||
"columnDefault": "random_v7",
|
||||
"dartType": "UuidValue?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "spaceId",
|
||||
"fieldName": "spaceId",
|
||||
"columnType": 6,
|
||||
"isNullable": true,
|
||||
"dartType": "int?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "keyId",
|
||||
"fieldName": "keyId",
|
||||
"columnType": 0,
|
||||
"isNullable": true,
|
||||
"dartType": "String?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "payload",
|
||||
"fieldName": "payload",
|
||||
"columnType": 5,
|
||||
"isNullable": false,
|
||||
"dartType": "dart:typed_data:ByteData"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "nonce",
|
||||
"fieldName": "nonce",
|
||||
"columnType": 5,
|
||||
"isNullable": true,
|
||||
"dartType": "dart:typed_data:ByteData?"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"__className__": "serverpod.ForeignKeyDefinition",
|
||||
"constraintName": "todo_settings_fk_0",
|
||||
"columns": [
|
||||
"spaceId"
|
||||
],
|
||||
"referenceTable": "offline_sync_spaces",
|
||||
"referenceTableSchema": "public",
|
||||
"referenceColumns": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": 3,
|
||||
"onDelete": 4
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
{
|
||||
"__className__": "serverpod.IndexDefinition",
|
||||
"indexName": "todo_settings_space_idx",
|
||||
"elements": [
|
||||
{
|
||||
"__className__": "serverpod.IndexElementDefinition",
|
||||
"type": 0,
|
||||
"definition": "spaceId"
|
||||
}
|
||||
],
|
||||
"type": "btree",
|
||||
"isUnique": false,
|
||||
"isPrimary": false
|
||||
}
|
||||
],
|
||||
"managed": true
|
||||
}
|
||||
],
|
||||
"installedModules": [
|
||||
{
|
||||
"__className__": "serverpod.DatabaseMigrationVersion",
|
||||
"module": "halkyon_base",
|
||||
"version": "20261110225132890-file-store"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.DatabaseMigrationVersion",
|
||||
"module": "serverpod",
|
||||
"version": "20260824182259319"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.DatabaseMigrationVersion",
|
||||
"module": "serverpod_auth_idp",
|
||||
"version": "20260924105404509"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.DatabaseMigrationVersion",
|
||||
"module": "serverpod_offline_sync",
|
||||
"version": "20260914143806119"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.DatabaseMigrationVersion",
|
||||
"module": "halkyon_oidc",
|
||||
"version": "20261023180644988"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.DatabaseMigrationVersion",
|
||||
"module": "serverpod_auth_core",
|
||||
"version": "20260924105232991"
|
||||
}
|
||||
],
|
||||
"migrationApiVersion": 1
|
||||
}
|
||||
|
|
@ -0,0 +1,121 @@
|
|||
{
|
||||
"__className__": "serverpod.DatabaseMigration",
|
||||
"actions": [
|
||||
{
|
||||
"__className__": "serverpod.DatabaseMigrationAction",
|
||||
"type": "createTable",
|
||||
"createTable": {
|
||||
"__className__": "serverpod.TableDefinition",
|
||||
"name": "attachment",
|
||||
"dartName": "Attachment",
|
||||
"module": "halkyon_todo",
|
||||
"schema": "public",
|
||||
"columns": [
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "id",
|
||||
"fieldName": "id",
|
||||
"columnType": 7,
|
||||
"isNullable": false,
|
||||
"columnDefault": "random_v7",
|
||||
"dartType": "UuidValue?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "spaceId",
|
||||
"fieldName": "spaceId",
|
||||
"columnType": 6,
|
||||
"isNullable": true,
|
||||
"dartType": "int?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "sha256",
|
||||
"fieldName": "sha256",
|
||||
"columnType": 0,
|
||||
"isNullable": false,
|
||||
"dartType": "String"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "keyId",
|
||||
"fieldName": "keyId",
|
||||
"columnType": 0,
|
||||
"isNullable": true,
|
||||
"dartType": "String?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "payload",
|
||||
"fieldName": "payload",
|
||||
"columnType": 5,
|
||||
"isNullable": false,
|
||||
"dartType": "dart:typed_data:ByteData"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "nonce",
|
||||
"fieldName": "nonce",
|
||||
"columnType": 5,
|
||||
"isNullable": true,
|
||||
"dartType": "dart:typed_data:ByteData?"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"__className__": "serverpod.ForeignKeyDefinition",
|
||||
"constraintName": "attachment_fk_0",
|
||||
"columns": [
|
||||
"spaceId"
|
||||
],
|
||||
"referenceTable": "offline_sync_spaces",
|
||||
"referenceTableSchema": "public",
|
||||
"referenceColumns": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": 3,
|
||||
"onDelete": 4
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
{
|
||||
"__className__": "serverpod.IndexDefinition",
|
||||
"indexName": "attachment_space_idx",
|
||||
"elements": [
|
||||
{
|
||||
"__className__": "serverpod.IndexElementDefinition",
|
||||
"type": 0,
|
||||
"definition": "spaceId"
|
||||
}
|
||||
],
|
||||
"type": "btree",
|
||||
"isUnique": false,
|
||||
"isPrimary": false
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.IndexDefinition",
|
||||
"indexName": "attachment_hash_idx",
|
||||
"elements": [
|
||||
{
|
||||
"__className__": "serverpod.IndexElementDefinition",
|
||||
"type": 0,
|
||||
"definition": "spaceId"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.IndexElementDefinition",
|
||||
"type": 0,
|
||||
"definition": "sha256"
|
||||
}
|
||||
],
|
||||
"type": "btree",
|
||||
"isUnique": false,
|
||||
"isPrimary": false
|
||||
}
|
||||
],
|
||||
"managed": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"warnings": [],
|
||||
"migrationApiVersion": 1
|
||||
}
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
BEGIN;
|
||||
|
||||
--
|
||||
-- Function: gen_random_uuid_v7()
|
||||
-- Source: https://gist.github.com/kjmph/5bd772b2c2df145aa645b837da7eca74
|
||||
-- License: MIT (copyright notice included on the generator source code).
|
||||
--
|
||||
create or replace function gen_random_uuid_v7()
|
||||
returns uuid
|
||||
as $$
|
||||
begin
|
||||
-- use random v4 uuid as starting point (which has the same variant we need)
|
||||
-- then overlay timestamp
|
||||
-- then set version 7 by flipping the 2 and 1 bit in the version 4 string
|
||||
return encode(
|
||||
set_bit(
|
||||
set_bit(
|
||||
overlay(uuid_send(gen_random_uuid())
|
||||
placing substring(int8send(floor(extract(epoch from clock_timestamp()) * 1000)::bigint) from 3)
|
||||
from 1 for 6
|
||||
),
|
||||
52, 1
|
||||
),
|
||||
53, 1
|
||||
),
|
||||
'hex')::uuid;
|
||||
end
|
||||
$$
|
||||
language plpgsql
|
||||
volatile;
|
||||
|
||||
--
|
||||
-- ACTION CREATE TABLE
|
||||
--
|
||||
CREATE TABLE "attachment" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid_v7(),
|
||||
"spaceId" bigint,
|
||||
"sha256" text NOT NULL,
|
||||
"keyId" text,
|
||||
"payload" bytea NOT NULL,
|
||||
"nonce" bytea
|
||||
);
|
||||
|
||||
-- Indexes
|
||||
CREATE INDEX "attachment_space_idx" ON "attachment" USING btree ("spaceId");
|
||||
CREATE INDEX "attachment_hash_idx" ON "attachment" USING btree ("spaceId", "sha256");
|
||||
|
||||
--
|
||||
-- ACTION CREATE FOREIGN KEY
|
||||
--
|
||||
ALTER TABLE ONLY "attachment"
|
||||
ADD CONSTRAINT "attachment_fk_0"
|
||||
FOREIGN KEY("spaceId")
|
||||
REFERENCES "offline_sync_spaces"("id")
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE NO ACTION;
|
||||
|
||||
|
||||
--
|
||||
-- MIGRATION VERSION FOR halkyon_todo
|
||||
--
|
||||
INSERT INTO "serverpod_migrations" ("module", "version", "timestamp")
|
||||
VALUES ('halkyon_todo', '20261110231607673-attachments', now())
|
||||
ON CONFLICT ("module")
|
||||
DO UPDATE SET "version" = '20261110231607673-attachments', "timestamp" = now();
|
||||
|
||||
--
|
||||
-- MIGRATION VERSION FOR halkyon_base
|
||||
--
|
||||
INSERT INTO "serverpod_migrations" ("module", "version", "timestamp")
|
||||
VALUES ('halkyon_base', '20261110225132890-file-store', now())
|
||||
ON CONFLICT ("module")
|
||||
DO UPDATE SET "version" = '20261110225132890-file-store', "timestamp" = now();
|
||||
|
||||
--
|
||||
-- MIGRATION VERSION FOR serverpod
|
||||
--
|
||||
INSERT INTO "serverpod_migrations" ("module", "version", "timestamp")
|
||||
VALUES ('serverpod', '20260824182259319', now())
|
||||
ON CONFLICT ("module")
|
||||
DO UPDATE SET "version" = '20260824182259319', "timestamp" = now();
|
||||
|
||||
--
|
||||
-- MIGRATION VERSION FOR serverpod_auth_idp
|
||||
--
|
||||
INSERT INTO "serverpod_migrations" ("module", "version", "timestamp")
|
||||
VALUES ('serverpod_auth_idp', '20260924105404509', now())
|
||||
ON CONFLICT ("module")
|
||||
DO UPDATE SET "version" = '20260924105404509', "timestamp" = now();
|
||||
|
||||
--
|
||||
-- MIGRATION VERSION FOR serverpod_offline_sync
|
||||
--
|
||||
INSERT INTO "serverpod_migrations" ("module", "version", "timestamp")
|
||||
VALUES ('serverpod_offline_sync', '20260914143806119', now())
|
||||
ON CONFLICT ("module")
|
||||
DO UPDATE SET "version" = '20260914143806119', "timestamp" = now();
|
||||
|
||||
--
|
||||
-- MIGRATION VERSION FOR halkyon_oidc
|
||||
--
|
||||
INSERT INTO "serverpod_migrations" ("module", "version", "timestamp")
|
||||
VALUES ('halkyon_oidc', '20261023180644988', now())
|
||||
ON CONFLICT ("module")
|
||||
DO UPDATE SET "version" = '20261023180644988', "timestamp" = now();
|
||||
|
||||
--
|
||||
-- MIGRATION VERSION FOR serverpod_auth_core
|
||||
--
|
||||
INSERT INTO "serverpod_migrations" ("module", "version", "timestamp")
|
||||
VALUES ('serverpod_auth_core', '20260924105232991', now())
|
||||
ON CONFLICT ("module")
|
||||
DO UPDATE SET "version" = '20260924105232991', "timestamp" = now();
|
||||
|
||||
|
||||
COMMIT;
|
||||
|
|
@ -27,3 +27,4 @@
|
|||
20261106183802735-filters
|
||||
20261106184947570-comments
|
||||
20261110225635732-file-store
|
||||
20261110231607673-attachments
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ dependencies:
|
|||
serverpod_offline_sync_server: 0.0.10
|
||||
|
||||
dev_dependencies:
|
||||
crypto: ^3.0.7
|
||||
serverpod_test: 4.0.4
|
||||
test: ^1.25.5
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,278 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:halkyon_base_server/base.dart';
|
||||
import 'package:halkyon_todo_server/src/attachment/todo_files.dart';
|
||||
import 'package:halkyon_todo_server/src/generated/protocol.dart';
|
||||
import 'package:halkyon_todo_server/src/todo_base.dart';
|
||||
import 'package:serverpod/protocol.dart' show CloudStorageEntry;
|
||||
import 'package:serverpod/serverpod.dart';
|
||||
import 'package:serverpod_offline_sync_server/serverpod_offline_sync_server.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import '../support/todo_fixtures.dart';
|
||||
import '../test_tools/serverpod_test_tools.dart';
|
||||
|
||||
/// Todo's files (Halkyon's ADR 0043): the module's store, which its own
|
||||
/// tests hold to its rules, with Todo's attachment rows naming the files.
|
||||
/// These call the module as its `files` endpoint does, on a signed-in
|
||||
/// session.
|
||||
void main() {
|
||||
withServerpod(
|
||||
'Attached files',
|
||||
(sessionBuilder, endpoints) {
|
||||
late Session session;
|
||||
late UuidValue alice;
|
||||
late UuidValue bob;
|
||||
|
||||
/// A sealed file, as the app seals every file: `HLKF`, version 1, and
|
||||
/// what can't be looked into.
|
||||
final sealed = [...ascii.encode('HLKF'), 1, 4, 2];
|
||||
final sealedHash = sha256.convert(sealed).toString();
|
||||
|
||||
/// The spaces a test made, which it deletes again with their files.
|
||||
final spaces = <UuidValue>{};
|
||||
|
||||
setUp(() {
|
||||
// The test pod is built without the generated Serverpod class, which
|
||||
// sets up sync for the running server.
|
||||
Serverpod.instance.initializeOfflineSync(syncTables: syncTables);
|
||||
useTodoBase(sessionBuilder);
|
||||
session = sessionBuilder.build();
|
||||
alice = const Uuid().v7obj();
|
||||
bob = const Uuid().v7obj();
|
||||
spaces.addAll([alice, bob]);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
for (final space in spaces) {
|
||||
await session.db.unsafeExecute(
|
||||
'DELETE FROM "serverpod_cloud_storage" WHERE "path" LIKE @prefix',
|
||||
parameters: QueryParameters.named({
|
||||
'prefix': '${storedFilePath(space, '')}%',
|
||||
}),
|
||||
);
|
||||
await session.db.unsafeExecute(
|
||||
'DELETE FROM "offline_sync_spaces" WHERE "uuidSpaceId" = @space',
|
||||
parameters: QueryParameters.named({'space': space.uuid}),
|
||||
);
|
||||
}
|
||||
spaces.clear();
|
||||
});
|
||||
|
||||
/// [person]'s session, `identified`.
|
||||
Session signedIn(UuidValue person) => sessionBuilder
|
||||
.copyWith(
|
||||
authentication: AuthenticationOverride.authenticationInfo(
|
||||
person.uuid,
|
||||
{identified},
|
||||
),
|
||||
)
|
||||
.build();
|
||||
|
||||
/// Adds [bytes] as [person], as the app does.
|
||||
Future<bool> add(
|
||||
UuidValue person,
|
||||
List<int> bytes, {
|
||||
required bool encrypted,
|
||||
}) async {
|
||||
final hash = sha256.convert(bytes).toString();
|
||||
final description = await startFileUpload(
|
||||
signedIn(person),
|
||||
hash,
|
||||
bytes.length,
|
||||
encrypted: encrypted,
|
||||
);
|
||||
if (description != null) {
|
||||
await _upload(session, description, bytes);
|
||||
}
|
||||
return await finishFileUpload(
|
||||
signedIn(person),
|
||||
hash,
|
||||
encrypted: encrypted,
|
||||
);
|
||||
}
|
||||
|
||||
final notFound = throwsA(isA<StoredFileNotFoundException>());
|
||||
|
||||
test('Todo keeps files, in its own store', () {
|
||||
expect(todoBase().files, same(todoFiles));
|
||||
});
|
||||
|
||||
test('keeps sealed files alone', () async {
|
||||
expect(
|
||||
await add(alice, utf8.encode('a photo'), encrypted: false),
|
||||
isFalse,
|
||||
);
|
||||
expect(await add(alice, sealed, encrypted: true), isTrue);
|
||||
});
|
||||
|
||||
test("serves a file by its attachment to its space's owner, and nobody "
|
||||
'else', () async {
|
||||
final attachment = await _attachmentIn(session, alice, sealedHash);
|
||||
await add(alice, sealed, encrypted: true);
|
||||
|
||||
expect(
|
||||
Uint8List.sublistView(
|
||||
await downloadFile(signedIn(alice), attachment),
|
||||
),
|
||||
sealed,
|
||||
);
|
||||
await expectLater(downloadFile(signedIn(bob), attachment), notFound);
|
||||
await expectLater(
|
||||
downloadFile(signedIn(alice), const Uuid().v7obj()),
|
||||
notFound,
|
||||
);
|
||||
});
|
||||
|
||||
test("serves a shared space's files to its members, and only them "
|
||||
'(ADR 0044)', () async {
|
||||
final carol = const Uuid().v7obj();
|
||||
spaces.add(carol);
|
||||
final syncSpaces = OfflineSyncSpaces(session);
|
||||
final shared = await syncSpaces.createFor(carol);
|
||||
spaces.add(shared);
|
||||
await syncSpaces.grant(
|
||||
user: alice,
|
||||
space: shared,
|
||||
role: OfflineSyncSpaceRole.readOnly,
|
||||
);
|
||||
final attachment = await _attachmentIn(
|
||||
session,
|
||||
carol,
|
||||
sealedHash,
|
||||
space: shared,
|
||||
);
|
||||
await session.storage.storeFile(
|
||||
storageId: fileStorage,
|
||||
path: storedFilePath(shared, sealedHash),
|
||||
byteData: ByteData.sublistView(Uint8List.fromList(sealed)),
|
||||
);
|
||||
|
||||
expect(
|
||||
Uint8List.sublistView(
|
||||
await downloadFile(signedIn(alice), attachment),
|
||||
),
|
||||
sealed,
|
||||
);
|
||||
await expectLater(downloadFile(signedIn(bob), attachment), notFound);
|
||||
});
|
||||
|
||||
test('serves no deleted attachment', () async {
|
||||
final attachment = await _attachmentIn(session, alice, sealedHash);
|
||||
await add(alice, sealed, encrypted: true);
|
||||
|
||||
await _deleteAttachment(session, alice, attachment);
|
||||
|
||||
await expectLater(downloadFile(signedIn(alice), attachment), notFound);
|
||||
});
|
||||
|
||||
test('counts and keeps the files live attachments name; prunes the '
|
||||
'rest', () async {
|
||||
final other = [...ascii.encode('HLKF'), 1, 7, 7];
|
||||
final otherHash = sha256.convert(other).toString();
|
||||
await add(alice, sealed, encrypted: true);
|
||||
await add(alice, other, encrypted: true);
|
||||
final kept = await _attachmentIn(session, alice, sealedHash);
|
||||
final gone = await _attachmentIn(session, alice, otherHash);
|
||||
await _deleteAttachment(session, alice, gone);
|
||||
final longAgo = DateTime.now().toUtc().subtract(
|
||||
const Duration(days: 8),
|
||||
);
|
||||
for (final hash in [sealedHash, otherHash]) {
|
||||
await _claimed(session, alice, hash, longAgo);
|
||||
}
|
||||
|
||||
expect((await fileUsageOf(signedIn(alice))).usedBytes, sealed.length);
|
||||
expect(await pruneUnreferencedFiles(session, now: DateTime.now()), 1);
|
||||
expect(
|
||||
Uint8List.sublistView(await downloadFile(signedIn(alice), kept)),
|
||||
sealed,
|
||||
);
|
||||
});
|
||||
},
|
||||
// The sync engine's database, as the running server has it. Writing
|
||||
// through it takes transactions of its own, outside the one a test would
|
||||
// be rolled back in, so the tests delete what they made instead.
|
||||
databaseInterceptor: (session, inner) {
|
||||
final database = offlineSyncDatabaseInterceptor(session, inner);
|
||||
if (database is OfflineSyncDatabase) {
|
||||
_syncDatabases[session] = database;
|
||||
}
|
||||
return database;
|
||||
},
|
||||
rollbackDatabase: RollbackDatabase.disabled,
|
||||
);
|
||||
}
|
||||
|
||||
/// The sync engine's database of each session, as the test pod's own
|
||||
/// database wraps it.
|
||||
final _syncDatabases = Expando<OfflineSyncDatabase>();
|
||||
|
||||
/// An attachment naming the file with [hash] in [person]'s personal space,
|
||||
/// or in [space], written as their device would through the sync engine
|
||||
/// (the payload stands in for a sealed one: the server never reads it).
|
||||
Future<UuidValue> _attachmentIn(
|
||||
Session session,
|
||||
UuidValue person,
|
||||
String hash, {
|
||||
UuidValue? space,
|
||||
}) => _syncDatabases[session]!.transactionForUser(
|
||||
person,
|
||||
spaceId: space,
|
||||
(transaction) async => (await Attachment.db.insertRow(
|
||||
session,
|
||||
Attachment(sha256: hash, keyId: 'todo1.0.key', payload: ByteData(3)),
|
||||
transaction: transaction,
|
||||
)).id!,
|
||||
);
|
||||
|
||||
/// Deletes the attachment [attachment] as [person] would through the sync
|
||||
/// engine.
|
||||
Future<void> _deleteAttachment(
|
||||
Session session,
|
||||
UuidValue person,
|
||||
UuidValue attachment,
|
||||
) => _syncDatabases[session]!.transactionForUser(
|
||||
person,
|
||||
(transaction) => Attachment.db.deleteWhere(
|
||||
session,
|
||||
where: (t) => t.id.equals(attachment),
|
||||
transaction: transaction,
|
||||
),
|
||||
);
|
||||
|
||||
/// Uploads [bytes] as the upload [description] says, standing in for the
|
||||
/// app's HTTP upload.
|
||||
Future<void> _upload(
|
||||
Session session,
|
||||
String description,
|
||||
List<int> bytes,
|
||||
) async {
|
||||
final url = Uri.parse(
|
||||
(jsonDecode(description) as Map<String, Object?>)['url']! as String,
|
||||
);
|
||||
final storage =
|
||||
Serverpod.instance.storage[url.queryParameters['storage']!]!
|
||||
as DatabaseCloudStorage;
|
||||
await storage.storeUnverifiedFile(
|
||||
session: session,
|
||||
path: url.queryParameters['path']!,
|
||||
byteData: ByteData.sublistView(Uint8List.fromList(bytes)),
|
||||
);
|
||||
}
|
||||
|
||||
/// Makes the stored file with [hash] of [space] last claimed [at].
|
||||
Future<void> _claimed(
|
||||
Session session,
|
||||
UuidValue space,
|
||||
String hash,
|
||||
DateTime at,
|
||||
) => CloudStorageEntry.db.updateWhere(
|
||||
session,
|
||||
columnValues: (t) => [t.addedTime(at)],
|
||||
where: (t) =>
|
||||
t.storageId.equals(fileStorage) &
|
||||
t.path.equals(storedFilePath(space, hash)),
|
||||
);
|
||||
|
|
@ -53,7 +53,7 @@ void main() {
|
|||
|
||||
test(
|
||||
'deleting a person deletes their space with every key, project, '
|
||||
"section, task, label, filter, comment and settings in it, and nobody else's",
|
||||
"section, task, label, filter, comment, settings and attachment in it, and nobody else's",
|
||||
() async {
|
||||
for (final person in [alice, bob]) {
|
||||
await _projectOf(session, person);
|
||||
|
|
@ -109,6 +109,7 @@ const _tables = [
|
|||
'saved_filter',
|
||||
'task_comment',
|
||||
'todo_settings',
|
||||
'attachment',
|
||||
];
|
||||
|
||||
/// A key, a project, a section, a task, a label, a filter and the settings in
|
||||
|
|
@ -158,4 +159,14 @@ Future<void> _projectOf(Session session, UuidValue person) =>
|
|||
TaskComment(keyId: 'todo1.0.key', payload: sealed, nonce: sealed),
|
||||
transaction: transaction,
|
||||
);
|
||||
await Attachment.db.insertRow(
|
||||
session,
|
||||
Attachment(
|
||||
sha256: '0' * 64,
|
||||
keyId: 'todo1.0.key',
|
||||
payload: sealed,
|
||||
nonce: sealed,
|
||||
),
|
||||
transaction: transaction,
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -184,6 +184,8 @@ class _InternalTestEndpoints extends TestEndpoints
|
|||
}
|
||||
|
||||
class _FutureCalls {
|
||||
late final filePruning = _FilePruningFutureCall();
|
||||
|
||||
late final pruning = _PruningFutureCall();
|
||||
}
|
||||
|
||||
|
|
@ -372,6 +374,21 @@ class _WeatherEndpoint {
|
|||
}
|
||||
}
|
||||
|
||||
class _FilePruningFutureCall {
|
||||
Future<void> pruneFiles(_ist.TestSessionBuilder sessionBuilder) async {
|
||||
var _localUniqueSession =
|
||||
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild();
|
||||
try {
|
||||
await _i772x17y.FilePruningPruneFilesFutureCall().invoke(
|
||||
_localUniqueSession,
|
||||
null,
|
||||
);
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _PruningFutureCall {
|
||||
Future<void> pruneAccounts(_ist.TestSessionBuilder sessionBuilder) async {
|
||||
var _localUniqueSession =
|
||||
|
|
|
|||
Loading…
Reference in a new issue