feat: PDF sources: read, take notes and cards from pages, deadlines
PDFs live in a studyset's new Files tab: uploaded from Add material (a file the studyset holds already asks first), each row with its pages, cards, notes and size, how far it is read and its reading deadline; renamed, given a deadline (a page or a chapter by a day), marked as read, started afresh, deleted with a moment to undo. The reader (/pdfs/:id) opens where reading left off: on wide windows a rail of tools (search, fit, zoom, fullscreen), a page counter with the chapter, and a sidebar; on phones a top bar, a page scrubber and a sheet. The sidebar holds the PDF's flashcards, by page or chapter, made from selected text or written there, each tied to its page and tagged with its chapter; and the page's note, an ordinary note tied to the page. Screenshots of a region become images that know their page. Cards and screenshots lead back to their page; reading on past a chapter with cards suggests studying it. Home shows the deadlines running and to come, and a timeline of them. Server: pdf_source rows (content in the payload, last writer wins) and an append-only pdf_reading log, the furthest page derived from it so devices never conflict. Files up to 50 MB for PDFs (20 MB for others; a larger file that isn't a PDF is deleted once stored). A deleted PDF takes its file's asset row along unless another PDF has the same file. Card payload 4 and note payload 2 keep the page. The reader draws with pdfrx (PDFium) behind PdfEngine in Dependencies; widget tests use a fake, and integration_test/pdf_engine_test.dart checks the real engine on a device or the desktop. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
ec34bb2334
commit
b96db731c1
14 changed files with 34926 additions and 58 deletions
|
|
@ -9,8 +9,8 @@ import 'package:serverpod/protocol.dart' show CloudStorageEntry;
|
|||
import 'package:serverpod/serverpod.dart';
|
||||
import 'package:serverpod_offline_sync_server/serverpod_offline_sync_server.dart';
|
||||
|
||||
/// The files of people's content (ADR 0004, rule 5): images and sounds,
|
||||
/// stored in the server's private storage at a path derived from the space
|
||||
/// The files of people's content (ADR 0004, rule 5): images, sounds and
|
||||
/// PDFs, stored in the server's private storage at a path derived from the space
|
||||
/// and the file's SHA-256, so a file added twice is stored once.
|
||||
///
|
||||
/// Uploading goes into the caller's own space, by the file's hash, before or
|
||||
|
|
@ -23,8 +23,12 @@ class AssetEndpoint extends Endpoint {
|
|||
@override
|
||||
bool get requireLogin => true;
|
||||
|
||||
/// The largest file accepted.
|
||||
static const maxBytes = 20 * 1024 * 1024;
|
||||
/// The largest file accepted: a PDF (decided 2026-10-03).
|
||||
static const maxBytes = 50 * 1024 * 1024;
|
||||
|
||||
/// The largest file of any other kind, an image or a sound. A larger one
|
||||
/// is checked to be a PDF once stored, and deleted otherwise.
|
||||
static const maxOtherBytes = 20 * 1024 * 1024;
|
||||
|
||||
/// How to upload the caller's file with [sha256] (lowercase hex) of
|
||||
/// [byteSize] bytes; null when the server has it already. Throws a
|
||||
|
|
@ -62,7 +66,8 @@ class AssetEndpoint extends Endpoint {
|
|||
|
||||
/// Completes the upload of the caller's file with [sha256]; whether it is
|
||||
/// stored now. A file whose content has another hash is deleted, and so is
|
||||
/// one that no longer fits, with a [FileLimitExceededException].
|
||||
/// one over [maxOtherBytes] that isn't a PDF, and one that no longer fits,
|
||||
/// with a [FileLimitExceededException].
|
||||
Future<bool> finishUpload(Session session, String sha256) async {
|
||||
_checkHash(sha256);
|
||||
final path = assetPath(_caller(session), sha256);
|
||||
|
|
@ -76,7 +81,8 @@ class AssetEndpoint extends Endpoint {
|
|||
storageId: assetStorage,
|
||||
path: path,
|
||||
);
|
||||
if (_hashOf(bytes) != sha256) {
|
||||
if (_hashOf(bytes) != sha256 ||
|
||||
(bytes.lengthInBytes > maxOtherBytes && !_isPdf(bytes))) {
|
||||
await session.storage.deleteFile(storageId: assetStorage, path: path);
|
||||
return false;
|
||||
}
|
||||
|
|
@ -150,5 +156,15 @@ void _checkHash(String sha256) {
|
|||
}
|
||||
}
|
||||
|
||||
/// Whether [bytes] are a PDF's: they start as every PDF does.
|
||||
bool _isPdf(ByteData bytes) {
|
||||
const signature = '%PDF-';
|
||||
return bytes.lengthInBytes >= signature.length &&
|
||||
String.fromCharCodes(
|
||||
Uint8List.sublistView(bytes, 0, signature.length),
|
||||
) ==
|
||||
signature;
|
||||
}
|
||||
|
||||
String _hashOf(ByteData bytes) =>
|
||||
sha256.convert(Uint8List.sublistView(bytes)).toString();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,780 @@
|
|||
/* 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: dead_code, unnecessary_null_comparison
|
||||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'package:halkyon_learn_server/src/generated/protocol.dart' as _i5lsenq5;
|
||||
import 'package:serverpod/serverpod.dart' as _is;
|
||||
import '../pdf_source/pdf_source.dart' as _ijdmmu1w;
|
||||
|
||||
/// How far a PDF has been read, as an entry that is never changed once written:
|
||||
/// the furthest page isn't stored but derived from these, as a card's schedule
|
||||
/// is from its reviews, so reading on two devices never conflicts. An entry is
|
||||
/// written only when the furthest page grows, so a PDF has at most an entry per
|
||||
/// page per fresh start. Synced; the entry goes with its PDF.
|
||||
abstract class PdfReading
|
||||
implements _is.TableRow<_is.UuidValue?>, _is.ProtocolSerialization {
|
||||
PdfReading._({
|
||||
this.id,
|
||||
this.spaceId,
|
||||
required this.pdfSourceId,
|
||||
this.pdfSource,
|
||||
required this.readAt,
|
||||
required this.kind,
|
||||
required this.page,
|
||||
});
|
||||
|
||||
factory PdfReading({
|
||||
_is.UuidValue? id,
|
||||
int? spaceId,
|
||||
required _is.UuidValue pdfSourceId,
|
||||
_ijdmmu1w.PdfSource? pdfSource,
|
||||
required DateTime readAt,
|
||||
required String kind,
|
||||
required int page,
|
||||
}) = _PdfReadingImpl;
|
||||
|
||||
factory PdfReading.fromJson(Map<String, dynamic> jsonSerialization) {
|
||||
return PdfReading(
|
||||
id: jsonSerialization['id'] == null
|
||||
? null
|
||||
: _is.UuidValueJsonExtension.fromJson(jsonSerialization['id']),
|
||||
spaceId: jsonSerialization['spaceId'] as int?,
|
||||
pdfSourceId: _is.UuidValueJsonExtension.fromJson(
|
||||
jsonSerialization['pdfSourceId'],
|
||||
),
|
||||
pdfSource: jsonSerialization['pdfSource'] == null
|
||||
? null
|
||||
: _i5lsenq5.Protocol().deserialize<_ijdmmu1w.PdfSource>(
|
||||
jsonSerialization['pdfSource'],
|
||||
),
|
||||
readAt: _is.DateTimeJsonExtension.fromJson(jsonSerialization['readAt']),
|
||||
kind: jsonSerialization['kind'] as String,
|
||||
page: jsonSerialization['page'] as int,
|
||||
);
|
||||
}
|
||||
|
||||
static final t = PdfReadingTable();
|
||||
|
||||
static const db = PdfReadingRepository._();
|
||||
|
||||
@override
|
||||
_is.UuidValue? id;
|
||||
|
||||
/// The space owning this row. Maintained by the sync engine.
|
||||
int? spaceId;
|
||||
|
||||
_is.UuidValue pdfSourceId;
|
||||
|
||||
/// The PDF read.
|
||||
_ijdmmu1w.PdfSource? pdfSource;
|
||||
|
||||
/// When, in UTC.
|
||||
DateTime readAt;
|
||||
|
||||
/// 'reached' for a page reached (marking the PDF finished reaches its last
|
||||
/// page); 'reset' for a fresh start: the furthest page is the furthest
|
||||
/// reached since the last one.
|
||||
String kind;
|
||||
|
||||
/// The page reached, from 1; 0 for 'reset'.
|
||||
int page;
|
||||
|
||||
@override
|
||||
_is.Table<_is.UuidValue?> get table => t;
|
||||
|
||||
/// Returns a shallow copy of this [PdfReading]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_is.useResult
|
||||
PdfReading copyWith({
|
||||
_is.UuidValue? id,
|
||||
int? spaceId,
|
||||
_is.UuidValue? pdfSourceId,
|
||||
_ijdmmu1w.PdfSource? pdfSource,
|
||||
DateTime? readAt,
|
||||
String? kind,
|
||||
int? page,
|
||||
});
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'__className__': 'PdfReading',
|
||||
if (id != null) 'id': id?.toJson(),
|
||||
if (spaceId != null) 'spaceId': spaceId,
|
||||
'pdfSourceId': pdfSourceId.toJson(),
|
||||
if (pdfSource != null) 'pdfSource': pdfSource?.toJson(),
|
||||
'readAt': readAt.toJson(),
|
||||
'kind': kind,
|
||||
'page': page,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJsonForProtocol() {
|
||||
return {
|
||||
'__className__': 'PdfReading',
|
||||
if (id != null) 'id': id?.toJson(),
|
||||
if (spaceId != null) 'spaceId': spaceId,
|
||||
'pdfSourceId': pdfSourceId.toJson(),
|
||||
if (pdfSource != null) 'pdfSource': pdfSource?.toJsonForProtocol(),
|
||||
'readAt': readAt.toJson(),
|
||||
'kind': kind,
|
||||
'page': page,
|
||||
};
|
||||
}
|
||||
|
||||
static PdfReadingInclude include({_ijdmmu1w.PdfSourceInclude? pdfSource}) {
|
||||
return PdfReadingInclude._(pdfSource: pdfSource);
|
||||
}
|
||||
|
||||
static PdfReadingIncludeList includeList({
|
||||
_is.WhereExpressionBuilder<PdfReadingTable>? where,
|
||||
int? limit,
|
||||
int? offset,
|
||||
_is.OrderByBuilder<PdfReadingTable>? orderBy,
|
||||
_is.OrderByListBuilder<PdfReadingTable>? orderByList,
|
||||
PdfReadingInclude? include,
|
||||
}) {
|
||||
return PdfReadingIncludeList._(
|
||||
where: where,
|
||||
limit: limit,
|
||||
offset: offset,
|
||||
orderBy: orderBy?.call(PdfReading.t),
|
||||
orderByList: orderByList?.call(PdfReading.t),
|
||||
include: include,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return _is.SerializationManager.encode(this);
|
||||
}
|
||||
}
|
||||
|
||||
class _Undefined {}
|
||||
|
||||
class _PdfReadingImpl extends PdfReading {
|
||||
_PdfReadingImpl({
|
||||
_is.UuidValue? id,
|
||||
int? spaceId,
|
||||
required _is.UuidValue pdfSourceId,
|
||||
_ijdmmu1w.PdfSource? pdfSource,
|
||||
required DateTime readAt,
|
||||
required String kind,
|
||||
required int page,
|
||||
}) : super._(
|
||||
id: id,
|
||||
spaceId: spaceId,
|
||||
pdfSourceId: pdfSourceId,
|
||||
pdfSource: pdfSource,
|
||||
readAt: readAt,
|
||||
kind: kind,
|
||||
page: page,
|
||||
);
|
||||
|
||||
/// Returns a shallow copy of this [PdfReading]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_is.useResult
|
||||
@override
|
||||
PdfReading copyWith({
|
||||
Object? id = _Undefined,
|
||||
Object? spaceId = _Undefined,
|
||||
_is.UuidValue? pdfSourceId,
|
||||
Object? pdfSource = _Undefined,
|
||||
DateTime? readAt,
|
||||
String? kind,
|
||||
int? page,
|
||||
}) {
|
||||
return PdfReading(
|
||||
id: id is _is.UuidValue? ? id : this.id,
|
||||
spaceId: spaceId is int? ? spaceId : this.spaceId,
|
||||
pdfSourceId: pdfSourceId ?? this.pdfSourceId,
|
||||
pdfSource: pdfSource is _ijdmmu1w.PdfSource?
|
||||
? pdfSource
|
||||
: this.pdfSource?.copyWith(),
|
||||
readAt: readAt ?? this.readAt,
|
||||
kind: kind ?? this.kind,
|
||||
page: page ?? this.page,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PdfReadingUpdateTable extends _is.UpdateTable<PdfReadingTable> {
|
||||
PdfReadingUpdateTable(super.table);
|
||||
|
||||
_is.ColumnValue<int, int> spaceId(int? value) => _is.ColumnValue(
|
||||
table.spaceId,
|
||||
value,
|
||||
);
|
||||
|
||||
_is.ColumnValue<_is.UuidValue, _is.UuidValue> pdfSourceId(
|
||||
_is.UuidValue value,
|
||||
) => _is.ColumnValue(
|
||||
table.pdfSourceId,
|
||||
value,
|
||||
);
|
||||
|
||||
_is.ColumnValue<DateTime, DateTime> readAt(DateTime value) => _is.ColumnValue(
|
||||
table.readAt,
|
||||
value,
|
||||
);
|
||||
|
||||
_is.ColumnValue<String, String> kind(String value) => _is.ColumnValue(
|
||||
table.kind,
|
||||
value,
|
||||
);
|
||||
|
||||
_is.ColumnValue<int, int> page(int value) => _is.ColumnValue(
|
||||
table.page,
|
||||
value,
|
||||
);
|
||||
}
|
||||
|
||||
class PdfReadingTable extends _is.Table<_is.UuidValue?> {
|
||||
PdfReadingTable({super.tableRelation}) : super(tableName: 'pdf_reading') {
|
||||
updateTable = PdfReadingUpdateTable(this);
|
||||
spaceId = _is.ColumnInt(
|
||||
'spaceId',
|
||||
this,
|
||||
);
|
||||
pdfSourceId = _is.ColumnUuid(
|
||||
'pdfSourceId',
|
||||
this,
|
||||
);
|
||||
readAt = _is.ColumnDateTime(
|
||||
'readAt',
|
||||
this,
|
||||
);
|
||||
kind = _is.ColumnString(
|
||||
'kind',
|
||||
this,
|
||||
);
|
||||
page = _is.ColumnInt(
|
||||
'page',
|
||||
this,
|
||||
);
|
||||
}
|
||||
|
||||
late final PdfReadingUpdateTable updateTable;
|
||||
|
||||
/// The space owning this row. Maintained by the sync engine.
|
||||
late final _is.ColumnInt spaceId;
|
||||
|
||||
late final _is.ColumnUuid pdfSourceId;
|
||||
|
||||
/// The PDF read.
|
||||
_ijdmmu1w.PdfSourceTable? _pdfSource;
|
||||
|
||||
/// When, in UTC.
|
||||
late final _is.ColumnDateTime readAt;
|
||||
|
||||
/// 'reached' for a page reached (marking the PDF finished reaches its last
|
||||
/// page); 'reset' for a fresh start: the furthest page is the furthest
|
||||
/// reached since the last one.
|
||||
late final _is.ColumnString kind;
|
||||
|
||||
/// The page reached, from 1; 0 for 'reset'.
|
||||
late final _is.ColumnInt page;
|
||||
|
||||
_ijdmmu1w.PdfSourceTable get pdfSource {
|
||||
if (_pdfSource != null) return _pdfSource!;
|
||||
_pdfSource = _is.createRelationTable(
|
||||
relationFieldName: 'pdfSource',
|
||||
field: PdfReading.t.pdfSourceId,
|
||||
foreignField: _ijdmmu1w.PdfSource.t.id,
|
||||
tableRelation: tableRelation,
|
||||
createTable: (foreignTableRelation) =>
|
||||
_ijdmmu1w.PdfSourceTable(tableRelation: foreignTableRelation),
|
||||
);
|
||||
return _pdfSource!;
|
||||
}
|
||||
|
||||
@override
|
||||
List<_is.Column> get columns => [
|
||||
id,
|
||||
spaceId,
|
||||
pdfSourceId,
|
||||
readAt,
|
||||
kind,
|
||||
page,
|
||||
];
|
||||
|
||||
@override
|
||||
_is.Table? getRelationTable(String relationField) {
|
||||
if (relationField == 'pdfSource') {
|
||||
return pdfSource;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class PdfReadingInclude extends _is.IncludeObject {
|
||||
PdfReadingInclude._({_ijdmmu1w.PdfSourceInclude? pdfSource}) {
|
||||
_pdfSource = pdfSource;
|
||||
}
|
||||
|
||||
_ijdmmu1w.PdfSourceInclude? _pdfSource;
|
||||
|
||||
@override
|
||||
Map<String, _is.Include?> get includes => {'pdfSource': _pdfSource};
|
||||
|
||||
@override
|
||||
_is.Table<_is.UuidValue?> get table => PdfReading.t;
|
||||
}
|
||||
|
||||
class PdfReadingIncludeList extends _is.IncludeList {
|
||||
PdfReadingIncludeList._({
|
||||
_is.WhereExpressionBuilder<PdfReadingTable>? where,
|
||||
super.limit,
|
||||
super.offset,
|
||||
super.orderBy,
|
||||
super.orderByList,
|
||||
super.include,
|
||||
}) {
|
||||
super.where = where?.call(PdfReading.t);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, _is.Include?> get includes => include?.includes ?? {};
|
||||
|
||||
@override
|
||||
_is.Table<_is.UuidValue?> get table => PdfReading.t;
|
||||
}
|
||||
|
||||
class PdfReadingRepository {
|
||||
const PdfReadingRepository._();
|
||||
|
||||
final attachRow = const PdfReadingAttachRowRepository._();
|
||||
|
||||
/// Returns a list of [PdfReading]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<PdfReading>> find(
|
||||
_is.DatabaseSession session, {
|
||||
_is.WhereExpressionBuilder<PdfReadingTable>? where,
|
||||
int? limit,
|
||||
int? offset,
|
||||
_is.OrderByBuilder<PdfReadingTable>? orderBy,
|
||||
_is.OrderByListBuilder<PdfReadingTable>? orderByList,
|
||||
_is.Transaction? transaction,
|
||||
PdfReadingInclude? include,
|
||||
_is.LockMode? lockMode,
|
||||
_is.LockBehavior? lockBehavior,
|
||||
}) async {
|
||||
return session.db.find<PdfReading>(
|
||||
where: where?.call(PdfReading.t),
|
||||
orderBy: orderBy?.call(PdfReading.t),
|
||||
orderByList: orderByList?.call(PdfReading.t),
|
||||
limit: limit,
|
||||
offset: offset,
|
||||
transaction: transaction,
|
||||
include: include,
|
||||
lockMode: lockMode,
|
||||
lockBehavior: lockBehavior,
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns the first matching [PdfReading] 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<PdfReading?> findFirstRow(
|
||||
_is.DatabaseSession session, {
|
||||
_is.WhereExpressionBuilder<PdfReadingTable>? where,
|
||||
int? offset,
|
||||
_is.OrderByBuilder<PdfReadingTable>? orderBy,
|
||||
_is.OrderByListBuilder<PdfReadingTable>? orderByList,
|
||||
_is.Transaction? transaction,
|
||||
PdfReadingInclude? include,
|
||||
_is.LockMode? lockMode,
|
||||
_is.LockBehavior? lockBehavior,
|
||||
}) async {
|
||||
return session.db.findFirstRow<PdfReading>(
|
||||
where: where?.call(PdfReading.t),
|
||||
orderBy: orderBy?.call(PdfReading.t),
|
||||
orderByList: orderByList?.call(PdfReading.t),
|
||||
offset: offset,
|
||||
transaction: transaction,
|
||||
include: include,
|
||||
lockMode: lockMode,
|
||||
lockBehavior: lockBehavior,
|
||||
);
|
||||
}
|
||||
|
||||
/// Finds a single [PdfReading] by its [id] or null if no such row exists.
|
||||
Future<PdfReading?> findById(
|
||||
_is.DatabaseSession session,
|
||||
_is.UuidValue id, {
|
||||
_is.Transaction? transaction,
|
||||
PdfReadingInclude? include,
|
||||
_is.LockMode? lockMode,
|
||||
_is.LockBehavior? lockBehavior,
|
||||
}) async {
|
||||
return session.db.findById<PdfReading>(
|
||||
id,
|
||||
transaction: transaction,
|
||||
include: include,
|
||||
lockMode: lockMode,
|
||||
lockBehavior: lockBehavior,
|
||||
);
|
||||
}
|
||||
|
||||
/// Inserts all [PdfReading]s in the list and returns the inserted rows.
|
||||
///
|
||||
/// The returned [PdfReading]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<PdfReading>> insert(
|
||||
_is.DatabaseSession session,
|
||||
List<PdfReading> rows, {
|
||||
_is.Transaction? transaction,
|
||||
bool ignoreConflicts = false,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.insert<PdfReading>(
|
||||
rows,
|
||||
transaction: transaction,
|
||||
ignoreConflicts: ignoreConflicts,
|
||||
noReturn: noReturn,
|
||||
);
|
||||
}
|
||||
|
||||
/// Inserts a single [PdfReading] and returns the inserted row.
|
||||
///
|
||||
/// The returned [PdfReading] will have its `id` field set.
|
||||
Future<PdfReading> insertRow(
|
||||
_is.DatabaseSession session,
|
||||
PdfReading row, {
|
||||
_is.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.insertRow<PdfReading>(
|
||||
row,
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
|
||||
/// Upserts all [PdfReading]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 [PdfReading]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<PdfReading>> upsert(
|
||||
_is.DatabaseSession session,
|
||||
List<PdfReading> rows, {
|
||||
required _is.ColumnSelections<PdfReadingTable> conflictColumns,
|
||||
_is.ColumnSelections<PdfReadingTable>? updateColumns,
|
||||
_is.WhereExpressionBuilder<PdfReadingTable>? updateWhere,
|
||||
_is.Transaction? transaction,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.upsert<PdfReading>(
|
||||
rows,
|
||||
conflictColumns: conflictColumns(PdfReading.t),
|
||||
updateColumns: updateColumns?.call(PdfReading.t),
|
||||
updateWhere: updateWhere?.call(PdfReading.t),
|
||||
transaction: transaction,
|
||||
noReturn: noReturn,
|
||||
);
|
||||
}
|
||||
|
||||
/// Upserts a single [PdfReading] 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 [PdfReading] will have its `id` field set.
|
||||
Future<PdfReading?> upsertRow(
|
||||
_is.DatabaseSession session,
|
||||
PdfReading row, {
|
||||
required _is.ColumnSelections<PdfReadingTable> conflictColumns,
|
||||
_is.ColumnSelections<PdfReadingTable>? updateColumns,
|
||||
_is.WhereExpressionBuilder<PdfReadingTable>? updateWhere,
|
||||
_is.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.upsertRow<PdfReading>(
|
||||
row,
|
||||
conflictColumns: conflictColumns(PdfReading.t),
|
||||
updateColumns: updateColumns?.call(PdfReading.t),
|
||||
updateWhere: updateWhere?.call(PdfReading.t),
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
|
||||
/// Updates all [PdfReading]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<PdfReading>> update(
|
||||
_is.DatabaseSession session,
|
||||
List<PdfReading> rows, {
|
||||
_is.ColumnSelections<PdfReadingTable>? columns,
|
||||
_is.Transaction? transaction,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.update<PdfReading>(
|
||||
rows,
|
||||
columns: columns?.call(PdfReading.t),
|
||||
transaction: transaction,
|
||||
noReturn: noReturn,
|
||||
);
|
||||
}
|
||||
|
||||
/// Updates a single [PdfReading]. 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<PdfReading> updateRow(
|
||||
_is.DatabaseSession session,
|
||||
PdfReading row, {
|
||||
_is.ColumnSelections<PdfReadingTable>? columns,
|
||||
_is.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.updateRow<PdfReading>(
|
||||
row,
|
||||
columns: columns?.call(PdfReading.t),
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
|
||||
/// Updates a single [PdfReading] by its [id] with the specified [columnValues].
|
||||
/// Returns the updated row or null if no row with the given id exists.
|
||||
Future<PdfReading?> updateById(
|
||||
_is.DatabaseSession session,
|
||||
_is.UuidValue id, {
|
||||
required _is.ColumnValueListBuilder<PdfReadingUpdateTable> columnValues,
|
||||
_is.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.updateById<PdfReading>(
|
||||
id,
|
||||
columnValues: columnValues(PdfReading.t.updateTable),
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
|
||||
/// Updates all [PdfReading]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<PdfReading>> updateWhere(
|
||||
_is.DatabaseSession session, {
|
||||
required _is.ColumnValueListBuilder<PdfReadingUpdateTable> columnValues,
|
||||
required _is.WhereExpressionBuilder<PdfReadingTable> where,
|
||||
int? limit,
|
||||
int? offset,
|
||||
_is.OrderByBuilder<PdfReadingTable>? orderBy,
|
||||
_is.OrderByListBuilder<PdfReadingTable>? orderByList,
|
||||
_is.Transaction? transaction,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.updateWhere<PdfReading>(
|
||||
columnValues: columnValues(PdfReading.t.updateTable),
|
||||
where: where(PdfReading.t),
|
||||
limit: limit,
|
||||
offset: offset,
|
||||
orderBy: orderBy?.call(PdfReading.t),
|
||||
orderByList: orderByList?.call(PdfReading.t),
|
||||
transaction: transaction,
|
||||
noReturn: noReturn,
|
||||
);
|
||||
}
|
||||
|
||||
/// Deletes all [PdfReading]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<PdfReading>> delete(
|
||||
_is.DatabaseSession session,
|
||||
List<PdfReading> rows, {
|
||||
_is.OrderByBuilder<PdfReadingTable>? orderBy,
|
||||
_is.OrderByListBuilder<PdfReadingTable>? orderByList,
|
||||
_is.Transaction? transaction,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.delete<PdfReading>(
|
||||
rows,
|
||||
orderBy: orderBy?.call(PdfReading.t),
|
||||
orderByList: orderByList?.call(PdfReading.t),
|
||||
transaction: transaction,
|
||||
noReturn: noReturn,
|
||||
);
|
||||
}
|
||||
|
||||
/// Deletes a single [PdfReading].
|
||||
Future<PdfReading> deleteRow(
|
||||
_is.DatabaseSession session,
|
||||
PdfReading row, {
|
||||
_is.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.deleteRow<PdfReading>(
|
||||
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<PdfReading>> deleteWhere(
|
||||
_is.DatabaseSession session, {
|
||||
required _is.WhereExpressionBuilder<PdfReadingTable> where,
|
||||
_is.OrderByBuilder<PdfReadingTable>? orderBy,
|
||||
_is.OrderByListBuilder<PdfReadingTable>? orderByList,
|
||||
_is.Transaction? transaction,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.deleteWhere<PdfReading>(
|
||||
where: where(PdfReading.t),
|
||||
orderBy: orderBy?.call(PdfReading.t),
|
||||
orderByList: orderByList?.call(PdfReading.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<PdfReadingTable>? where,
|
||||
int? limit,
|
||||
_is.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.count<PdfReading>(
|
||||
where: where?.call(PdfReading.t),
|
||||
limit: limit,
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
|
||||
/// Acquires row-level locks on [PdfReading] rows matching the [where] expression.
|
||||
Future<void> lockRows(
|
||||
_is.DatabaseSession session, {
|
||||
required _is.WhereExpressionBuilder<PdfReadingTable> where,
|
||||
required _is.LockMode lockMode,
|
||||
required _is.Transaction transaction,
|
||||
_is.LockBehavior lockBehavior = _is.LockBehavior.wait,
|
||||
}) async {
|
||||
return session.db.lockRows<PdfReading>(
|
||||
where: where(PdfReading.t),
|
||||
lockMode: lockMode,
|
||||
lockBehavior: lockBehavior,
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PdfReadingAttachRowRepository {
|
||||
const PdfReadingAttachRowRepository._();
|
||||
|
||||
/// Creates a relation between the given [PdfReading] and [PdfSource]
|
||||
/// by setting the [PdfReading]'s foreign key `pdfSourceId` to refer to the [PdfSource].
|
||||
Future<void> pdfSource(
|
||||
_is.DatabaseSession session,
|
||||
PdfReading pdfReading,
|
||||
_ijdmmu1w.PdfSource pdfSource, {
|
||||
_is.Transaction? transaction,
|
||||
}) async {
|
||||
if (pdfReading.id == null) {
|
||||
throw ArgumentError.notNull('pdfReading.id');
|
||||
}
|
||||
if (pdfSource.id == null) {
|
||||
throw ArgumentError.notNull('pdfSource.id');
|
||||
}
|
||||
|
||||
var $pdfReading = pdfReading.copyWith(pdfSourceId: pdfSource.id);
|
||||
await session.db.updateRow<PdfReading>(
|
||||
$pdfReading,
|
||||
columns: [PdfReading.t.pdfSourceId],
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,787 @@
|
|||
/* 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: dead_code, unnecessary_null_comparison
|
||||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'dart:typed_data' as _idt;
|
||||
import 'package:halkyon_learn_server/src/generated/protocol.dart' as _i5lsenq5;
|
||||
import 'package:serverpod/serverpod.dart' as _is;
|
||||
import '../studyset/studyset.dart' as _ie5k1jc2;
|
||||
|
||||
/// A PDF in a studyset, to read and learn from: its cards and notes can be tied
|
||||
/// to its pages. Synced; its space owns it.
|
||||
///
|
||||
/// Its content (title, the name the file came with, the file, its page count
|
||||
/// once known, a reading deadline) lives in the payload, last writer wins, as a
|
||||
/// learning path's: rarely edited on two devices at once. The file itself is
|
||||
/// an asset (ADR 0004), named in the payload by the asset row's id.
|
||||
abstract class PdfSource
|
||||
implements _is.TableRow<_is.UuidValue?>, _is.ProtocolSerialization {
|
||||
PdfSource._({
|
||||
this.id,
|
||||
this.spaceId,
|
||||
required this.studysetId,
|
||||
this.studyset,
|
||||
this.keyId,
|
||||
required this.payload,
|
||||
this.nonce,
|
||||
});
|
||||
|
||||
factory PdfSource({
|
||||
_is.UuidValue? id,
|
||||
int? spaceId,
|
||||
required _is.UuidValue studysetId,
|
||||
_ie5k1jc2.Studyset? studyset,
|
||||
String? keyId,
|
||||
required _idt.ByteData payload,
|
||||
_idt.ByteData? nonce,
|
||||
}) = _PdfSourceImpl;
|
||||
|
||||
factory PdfSource.fromJson(Map<String, dynamic> jsonSerialization) {
|
||||
return PdfSource(
|
||||
id: jsonSerialization['id'] == null
|
||||
? null
|
||||
: _is.UuidValueJsonExtension.fromJson(jsonSerialization['id']),
|
||||
spaceId: jsonSerialization['spaceId'] as int?,
|
||||
studysetId: _is.UuidValueJsonExtension.fromJson(
|
||||
jsonSerialization['studysetId'],
|
||||
),
|
||||
studyset: jsonSerialization['studyset'] == null
|
||||
? null
|
||||
: _i5lsenq5.Protocol().deserialize<_ie5k1jc2.Studyset>(
|
||||
jsonSerialization['studyset'],
|
||||
),
|
||||
keyId: jsonSerialization['keyId'] as String?,
|
||||
payload: _is.ByteDataJsonExtension.fromJson(jsonSerialization['payload']),
|
||||
nonce: jsonSerialization['nonce'] == null
|
||||
? null
|
||||
: _is.ByteDataJsonExtension.fromJson(jsonSerialization['nonce']),
|
||||
);
|
||||
}
|
||||
|
||||
static final t = PdfSourceTable();
|
||||
|
||||
static const db = PdfSourceRepository._();
|
||||
|
||||
@override
|
||||
_is.UuidValue? id;
|
||||
|
||||
/// The space owning this row. Maintained by the sync engine.
|
||||
int? spaceId;
|
||||
|
||||
_is.UuidValue studysetId;
|
||||
|
||||
/// The studyset the PDF belongs to; the PDF goes with it.
|
||||
_ie5k1jc2.Studyset? studyset;
|
||||
|
||||
/// The encryption key's id; always null (ADR 0002, rule 6).
|
||||
String? keyId;
|
||||
|
||||
/// The content as UTF-8 JSON: {"schemaVersion", "title", "fileName", "asset",
|
||||
/// "pageCount", "deadline"}; the app's pdf_source_payload.dart describes each
|
||||
/// schema.
|
||||
_idt.ByteData payload;
|
||||
|
||||
/// The encryption nonce; always null, like keyId.
|
||||
_idt.ByteData? nonce;
|
||||
|
||||
@override
|
||||
_is.Table<_is.UuidValue?> get table => t;
|
||||
|
||||
/// Returns a shallow copy of this [PdfSource]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_is.useResult
|
||||
PdfSource copyWith({
|
||||
_is.UuidValue? id,
|
||||
int? spaceId,
|
||||
_is.UuidValue? studysetId,
|
||||
_ie5k1jc2.Studyset? studyset,
|
||||
String? keyId,
|
||||
_idt.ByteData? payload,
|
||||
_idt.ByteData? nonce,
|
||||
});
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'__className__': 'PdfSource',
|
||||
if (id != null) 'id': id?.toJson(),
|
||||
if (spaceId != null) 'spaceId': spaceId,
|
||||
'studysetId': studysetId.toJson(),
|
||||
if (studyset != null) 'studyset': studyset?.toJson(),
|
||||
if (keyId != null) 'keyId': keyId,
|
||||
'payload': payload.toJson(),
|
||||
if (nonce != null) 'nonce': nonce?.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJsonForProtocol() {
|
||||
return {
|
||||
'__className__': 'PdfSource',
|
||||
if (id != null) 'id': id?.toJson(),
|
||||
if (spaceId != null) 'spaceId': spaceId,
|
||||
'studysetId': studysetId.toJson(),
|
||||
if (studyset != null) 'studyset': studyset?.toJsonForProtocol(),
|
||||
if (keyId != null) 'keyId': keyId,
|
||||
'payload': payload.toJson(),
|
||||
if (nonce != null) 'nonce': nonce?.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
static PdfSourceInclude include({_ie5k1jc2.StudysetInclude? studyset}) {
|
||||
return PdfSourceInclude._(studyset: studyset);
|
||||
}
|
||||
|
||||
static PdfSourceIncludeList includeList({
|
||||
_is.WhereExpressionBuilder<PdfSourceTable>? where,
|
||||
int? limit,
|
||||
int? offset,
|
||||
_is.OrderByBuilder<PdfSourceTable>? orderBy,
|
||||
_is.OrderByListBuilder<PdfSourceTable>? orderByList,
|
||||
PdfSourceInclude? include,
|
||||
}) {
|
||||
return PdfSourceIncludeList._(
|
||||
where: where,
|
||||
limit: limit,
|
||||
offset: offset,
|
||||
orderBy: orderBy?.call(PdfSource.t),
|
||||
orderByList: orderByList?.call(PdfSource.t),
|
||||
include: include,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return _is.SerializationManager.encode(this);
|
||||
}
|
||||
}
|
||||
|
||||
class _Undefined {}
|
||||
|
||||
class _PdfSourceImpl extends PdfSource {
|
||||
_PdfSourceImpl({
|
||||
_is.UuidValue? id,
|
||||
int? spaceId,
|
||||
required _is.UuidValue studysetId,
|
||||
_ie5k1jc2.Studyset? studyset,
|
||||
String? keyId,
|
||||
required _idt.ByteData payload,
|
||||
_idt.ByteData? nonce,
|
||||
}) : super._(
|
||||
id: id,
|
||||
spaceId: spaceId,
|
||||
studysetId: studysetId,
|
||||
studyset: studyset,
|
||||
keyId: keyId,
|
||||
payload: payload,
|
||||
nonce: nonce,
|
||||
);
|
||||
|
||||
/// Returns a shallow copy of this [PdfSource]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_is.useResult
|
||||
@override
|
||||
PdfSource copyWith({
|
||||
Object? id = _Undefined,
|
||||
Object? spaceId = _Undefined,
|
||||
_is.UuidValue? studysetId,
|
||||
Object? studyset = _Undefined,
|
||||
Object? keyId = _Undefined,
|
||||
_idt.ByteData? payload,
|
||||
Object? nonce = _Undefined,
|
||||
}) {
|
||||
return PdfSource(
|
||||
id: id is _is.UuidValue? ? id : this.id,
|
||||
spaceId: spaceId is int? ? spaceId : this.spaceId,
|
||||
studysetId: studysetId ?? this.studysetId,
|
||||
studyset: studyset is _ie5k1jc2.Studyset?
|
||||
? studyset
|
||||
: this.studyset?.copyWith(),
|
||||
keyId: keyId is String? ? keyId : this.keyId,
|
||||
payload: payload ?? this.payload.clone(),
|
||||
nonce: nonce is _idt.ByteData? ? nonce : this.nonce?.clone(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PdfSourceUpdateTable extends _is.UpdateTable<PdfSourceTable> {
|
||||
PdfSourceUpdateTable(super.table);
|
||||
|
||||
_is.ColumnValue<int, int> spaceId(int? value) => _is.ColumnValue(
|
||||
table.spaceId,
|
||||
value,
|
||||
);
|
||||
|
||||
_is.ColumnValue<_is.UuidValue, _is.UuidValue> studysetId(
|
||||
_is.UuidValue value,
|
||||
) => _is.ColumnValue(
|
||||
table.studysetId,
|
||||
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 PdfSourceTable extends _is.Table<_is.UuidValue?> {
|
||||
PdfSourceTable({super.tableRelation}) : super(tableName: 'pdf_source') {
|
||||
updateTable = PdfSourceUpdateTable(this);
|
||||
spaceId = _is.ColumnInt(
|
||||
'spaceId',
|
||||
this,
|
||||
);
|
||||
studysetId = _is.ColumnUuid(
|
||||
'studysetId',
|
||||
this,
|
||||
);
|
||||
keyId = _is.ColumnString(
|
||||
'keyId',
|
||||
this,
|
||||
);
|
||||
payload = _is.ColumnByteData(
|
||||
'payload',
|
||||
this,
|
||||
);
|
||||
nonce = _is.ColumnByteData(
|
||||
'nonce',
|
||||
this,
|
||||
);
|
||||
}
|
||||
|
||||
late final PdfSourceUpdateTable updateTable;
|
||||
|
||||
/// The space owning this row. Maintained by the sync engine.
|
||||
late final _is.ColumnInt spaceId;
|
||||
|
||||
late final _is.ColumnUuid studysetId;
|
||||
|
||||
/// The studyset the PDF belongs to; the PDF goes with it.
|
||||
_ie5k1jc2.StudysetTable? _studyset;
|
||||
|
||||
/// The encryption key's id; always null (ADR 0002, rule 6).
|
||||
late final _is.ColumnString keyId;
|
||||
|
||||
/// The content as UTF-8 JSON: {"schemaVersion", "title", "fileName", "asset",
|
||||
/// "pageCount", "deadline"}; the app's pdf_source_payload.dart describes each
|
||||
/// schema.
|
||||
late final _is.ColumnByteData payload;
|
||||
|
||||
/// The encryption nonce; always null, like keyId.
|
||||
late final _is.ColumnByteData nonce;
|
||||
|
||||
_ie5k1jc2.StudysetTable get studyset {
|
||||
if (_studyset != null) return _studyset!;
|
||||
_studyset = _is.createRelationTable(
|
||||
relationFieldName: 'studyset',
|
||||
field: PdfSource.t.studysetId,
|
||||
foreignField: _ie5k1jc2.Studyset.t.id,
|
||||
tableRelation: tableRelation,
|
||||
createTable: (foreignTableRelation) =>
|
||||
_ie5k1jc2.StudysetTable(tableRelation: foreignTableRelation),
|
||||
);
|
||||
return _studyset!;
|
||||
}
|
||||
|
||||
@override
|
||||
List<_is.Column> get columns => [
|
||||
id,
|
||||
spaceId,
|
||||
studysetId,
|
||||
keyId,
|
||||
payload,
|
||||
nonce,
|
||||
];
|
||||
|
||||
@override
|
||||
_is.Table? getRelationTable(String relationField) {
|
||||
if (relationField == 'studyset') {
|
||||
return studyset;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class PdfSourceInclude extends _is.IncludeObject {
|
||||
PdfSourceInclude._({_ie5k1jc2.StudysetInclude? studyset}) {
|
||||
_studyset = studyset;
|
||||
}
|
||||
|
||||
_ie5k1jc2.StudysetInclude? _studyset;
|
||||
|
||||
@override
|
||||
Map<String, _is.Include?> get includes => {'studyset': _studyset};
|
||||
|
||||
@override
|
||||
_is.Table<_is.UuidValue?> get table => PdfSource.t;
|
||||
}
|
||||
|
||||
class PdfSourceIncludeList extends _is.IncludeList {
|
||||
PdfSourceIncludeList._({
|
||||
_is.WhereExpressionBuilder<PdfSourceTable>? where,
|
||||
super.limit,
|
||||
super.offset,
|
||||
super.orderBy,
|
||||
super.orderByList,
|
||||
super.include,
|
||||
}) {
|
||||
super.where = where?.call(PdfSource.t);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, _is.Include?> get includes => include?.includes ?? {};
|
||||
|
||||
@override
|
||||
_is.Table<_is.UuidValue?> get table => PdfSource.t;
|
||||
}
|
||||
|
||||
class PdfSourceRepository {
|
||||
const PdfSourceRepository._();
|
||||
|
||||
final attachRow = const PdfSourceAttachRowRepository._();
|
||||
|
||||
/// Returns a list of [PdfSource]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<PdfSource>> find(
|
||||
_is.DatabaseSession session, {
|
||||
_is.WhereExpressionBuilder<PdfSourceTable>? where,
|
||||
int? limit,
|
||||
int? offset,
|
||||
_is.OrderByBuilder<PdfSourceTable>? orderBy,
|
||||
_is.OrderByListBuilder<PdfSourceTable>? orderByList,
|
||||
_is.Transaction? transaction,
|
||||
PdfSourceInclude? include,
|
||||
_is.LockMode? lockMode,
|
||||
_is.LockBehavior? lockBehavior,
|
||||
}) async {
|
||||
return session.db.find<PdfSource>(
|
||||
where: where?.call(PdfSource.t),
|
||||
orderBy: orderBy?.call(PdfSource.t),
|
||||
orderByList: orderByList?.call(PdfSource.t),
|
||||
limit: limit,
|
||||
offset: offset,
|
||||
transaction: transaction,
|
||||
include: include,
|
||||
lockMode: lockMode,
|
||||
lockBehavior: lockBehavior,
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns the first matching [PdfSource] 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<PdfSource?> findFirstRow(
|
||||
_is.DatabaseSession session, {
|
||||
_is.WhereExpressionBuilder<PdfSourceTable>? where,
|
||||
int? offset,
|
||||
_is.OrderByBuilder<PdfSourceTable>? orderBy,
|
||||
_is.OrderByListBuilder<PdfSourceTable>? orderByList,
|
||||
_is.Transaction? transaction,
|
||||
PdfSourceInclude? include,
|
||||
_is.LockMode? lockMode,
|
||||
_is.LockBehavior? lockBehavior,
|
||||
}) async {
|
||||
return session.db.findFirstRow<PdfSource>(
|
||||
where: where?.call(PdfSource.t),
|
||||
orderBy: orderBy?.call(PdfSource.t),
|
||||
orderByList: orderByList?.call(PdfSource.t),
|
||||
offset: offset,
|
||||
transaction: transaction,
|
||||
include: include,
|
||||
lockMode: lockMode,
|
||||
lockBehavior: lockBehavior,
|
||||
);
|
||||
}
|
||||
|
||||
/// Finds a single [PdfSource] by its [id] or null if no such row exists.
|
||||
Future<PdfSource?> findById(
|
||||
_is.DatabaseSession session,
|
||||
_is.UuidValue id, {
|
||||
_is.Transaction? transaction,
|
||||
PdfSourceInclude? include,
|
||||
_is.LockMode? lockMode,
|
||||
_is.LockBehavior? lockBehavior,
|
||||
}) async {
|
||||
return session.db.findById<PdfSource>(
|
||||
id,
|
||||
transaction: transaction,
|
||||
include: include,
|
||||
lockMode: lockMode,
|
||||
lockBehavior: lockBehavior,
|
||||
);
|
||||
}
|
||||
|
||||
/// Inserts all [PdfSource]s in the list and returns the inserted rows.
|
||||
///
|
||||
/// The returned [PdfSource]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<PdfSource>> insert(
|
||||
_is.DatabaseSession session,
|
||||
List<PdfSource> rows, {
|
||||
_is.Transaction? transaction,
|
||||
bool ignoreConflicts = false,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.insert<PdfSource>(
|
||||
rows,
|
||||
transaction: transaction,
|
||||
ignoreConflicts: ignoreConflicts,
|
||||
noReturn: noReturn,
|
||||
);
|
||||
}
|
||||
|
||||
/// Inserts a single [PdfSource] and returns the inserted row.
|
||||
///
|
||||
/// The returned [PdfSource] will have its `id` field set.
|
||||
Future<PdfSource> insertRow(
|
||||
_is.DatabaseSession session,
|
||||
PdfSource row, {
|
||||
_is.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.insertRow<PdfSource>(
|
||||
row,
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
|
||||
/// Upserts all [PdfSource]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 [PdfSource]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<PdfSource>> upsert(
|
||||
_is.DatabaseSession session,
|
||||
List<PdfSource> rows, {
|
||||
required _is.ColumnSelections<PdfSourceTable> conflictColumns,
|
||||
_is.ColumnSelections<PdfSourceTable>? updateColumns,
|
||||
_is.WhereExpressionBuilder<PdfSourceTable>? updateWhere,
|
||||
_is.Transaction? transaction,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.upsert<PdfSource>(
|
||||
rows,
|
||||
conflictColumns: conflictColumns(PdfSource.t),
|
||||
updateColumns: updateColumns?.call(PdfSource.t),
|
||||
updateWhere: updateWhere?.call(PdfSource.t),
|
||||
transaction: transaction,
|
||||
noReturn: noReturn,
|
||||
);
|
||||
}
|
||||
|
||||
/// Upserts a single [PdfSource] 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 [PdfSource] will have its `id` field set.
|
||||
Future<PdfSource?> upsertRow(
|
||||
_is.DatabaseSession session,
|
||||
PdfSource row, {
|
||||
required _is.ColumnSelections<PdfSourceTable> conflictColumns,
|
||||
_is.ColumnSelections<PdfSourceTable>? updateColumns,
|
||||
_is.WhereExpressionBuilder<PdfSourceTable>? updateWhere,
|
||||
_is.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.upsertRow<PdfSource>(
|
||||
row,
|
||||
conflictColumns: conflictColumns(PdfSource.t),
|
||||
updateColumns: updateColumns?.call(PdfSource.t),
|
||||
updateWhere: updateWhere?.call(PdfSource.t),
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
|
||||
/// Updates all [PdfSource]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<PdfSource>> update(
|
||||
_is.DatabaseSession session,
|
||||
List<PdfSource> rows, {
|
||||
_is.ColumnSelections<PdfSourceTable>? columns,
|
||||
_is.Transaction? transaction,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.update<PdfSource>(
|
||||
rows,
|
||||
columns: columns?.call(PdfSource.t),
|
||||
transaction: transaction,
|
||||
noReturn: noReturn,
|
||||
);
|
||||
}
|
||||
|
||||
/// Updates a single [PdfSource]. 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<PdfSource> updateRow(
|
||||
_is.DatabaseSession session,
|
||||
PdfSource row, {
|
||||
_is.ColumnSelections<PdfSourceTable>? columns,
|
||||
_is.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.updateRow<PdfSource>(
|
||||
row,
|
||||
columns: columns?.call(PdfSource.t),
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
|
||||
/// Updates a single [PdfSource] by its [id] with the specified [columnValues].
|
||||
/// Returns the updated row or null if no row with the given id exists.
|
||||
Future<PdfSource?> updateById(
|
||||
_is.DatabaseSession session,
|
||||
_is.UuidValue id, {
|
||||
required _is.ColumnValueListBuilder<PdfSourceUpdateTable> columnValues,
|
||||
_is.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.updateById<PdfSource>(
|
||||
id,
|
||||
columnValues: columnValues(PdfSource.t.updateTable),
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
|
||||
/// Updates all [PdfSource]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<PdfSource>> updateWhere(
|
||||
_is.DatabaseSession session, {
|
||||
required _is.ColumnValueListBuilder<PdfSourceUpdateTable> columnValues,
|
||||
required _is.WhereExpressionBuilder<PdfSourceTable> where,
|
||||
int? limit,
|
||||
int? offset,
|
||||
_is.OrderByBuilder<PdfSourceTable>? orderBy,
|
||||
_is.OrderByListBuilder<PdfSourceTable>? orderByList,
|
||||
_is.Transaction? transaction,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.updateWhere<PdfSource>(
|
||||
columnValues: columnValues(PdfSource.t.updateTable),
|
||||
where: where(PdfSource.t),
|
||||
limit: limit,
|
||||
offset: offset,
|
||||
orderBy: orderBy?.call(PdfSource.t),
|
||||
orderByList: orderByList?.call(PdfSource.t),
|
||||
transaction: transaction,
|
||||
noReturn: noReturn,
|
||||
);
|
||||
}
|
||||
|
||||
/// Deletes all [PdfSource]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<PdfSource>> delete(
|
||||
_is.DatabaseSession session,
|
||||
List<PdfSource> rows, {
|
||||
_is.OrderByBuilder<PdfSourceTable>? orderBy,
|
||||
_is.OrderByListBuilder<PdfSourceTable>? orderByList,
|
||||
_is.Transaction? transaction,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.delete<PdfSource>(
|
||||
rows,
|
||||
orderBy: orderBy?.call(PdfSource.t),
|
||||
orderByList: orderByList?.call(PdfSource.t),
|
||||
transaction: transaction,
|
||||
noReturn: noReturn,
|
||||
);
|
||||
}
|
||||
|
||||
/// Deletes a single [PdfSource].
|
||||
Future<PdfSource> deleteRow(
|
||||
_is.DatabaseSession session,
|
||||
PdfSource row, {
|
||||
_is.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.deleteRow<PdfSource>(
|
||||
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<PdfSource>> deleteWhere(
|
||||
_is.DatabaseSession session, {
|
||||
required _is.WhereExpressionBuilder<PdfSourceTable> where,
|
||||
_is.OrderByBuilder<PdfSourceTable>? orderBy,
|
||||
_is.OrderByListBuilder<PdfSourceTable>? orderByList,
|
||||
_is.Transaction? transaction,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.deleteWhere<PdfSource>(
|
||||
where: where(PdfSource.t),
|
||||
orderBy: orderBy?.call(PdfSource.t),
|
||||
orderByList: orderByList?.call(PdfSource.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<PdfSourceTable>? where,
|
||||
int? limit,
|
||||
_is.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.count<PdfSource>(
|
||||
where: where?.call(PdfSource.t),
|
||||
limit: limit,
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
|
||||
/// Acquires row-level locks on [PdfSource] rows matching the [where] expression.
|
||||
Future<void> lockRows(
|
||||
_is.DatabaseSession session, {
|
||||
required _is.WhereExpressionBuilder<PdfSourceTable> where,
|
||||
required _is.LockMode lockMode,
|
||||
required _is.Transaction transaction,
|
||||
_is.LockBehavior lockBehavior = _is.LockBehavior.wait,
|
||||
}) async {
|
||||
return session.db.lockRows<PdfSource>(
|
||||
where: where(PdfSource.t),
|
||||
lockMode: lockMode,
|
||||
lockBehavior: lockBehavior,
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PdfSourceAttachRowRepository {
|
||||
const PdfSourceAttachRowRepository._();
|
||||
|
||||
/// Creates a relation between the given [PdfSource] and [Studyset]
|
||||
/// by setting the [PdfSource]'s foreign key `studysetId` to refer to the [Studyset].
|
||||
Future<void> studyset(
|
||||
_is.DatabaseSession session,
|
||||
PdfSource pdfSource,
|
||||
_ie5k1jc2.Studyset studyset, {
|
||||
_is.Transaction? transaction,
|
||||
}) async {
|
||||
if (pdfSource.id == null) {
|
||||
throw ArgumentError.notNull('pdfSource.id');
|
||||
}
|
||||
if (studyset.id == null) {
|
||||
throw ArgumentError.notNull('studyset.id');
|
||||
}
|
||||
|
||||
var $pdfSource = pdfSource.copyWith(studysetId: studyset.id);
|
||||
await session.db.updateRow<PdfSource>(
|
||||
$pdfSource,
|
||||
columns: [PdfSource.t.studysetId],
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -38,6 +38,8 @@ import 'learning_path/learning_path_step.dart' as _irnpk9dc;
|
|||
import 'learning_path/learning_path_step_revision.dart' as _i59qfu1l;
|
||||
import 'note/note.dart' as _i9njjhyv;
|
||||
import 'note/note_revision.dart' as _i6j5dng4;
|
||||
import 'pdf_source/pdf_reading.dart' as _i4l20c7i;
|
||||
import 'pdf_source/pdf_source.dart' as _iz838ro9;
|
||||
import 'review/review_log.dart' as _irzjbdqz;
|
||||
import 'settings/person_settings.dart' as _irnhx5e5;
|
||||
import 'studyset/studyset.dart' as _ikbclmae;
|
||||
|
|
@ -62,6 +64,8 @@ export 'learning_path/learning_path_step.dart';
|
|||
export 'learning_path/learning_path_step_revision.dart';
|
||||
export 'note/note.dart';
|
||||
export 'note/note_revision.dart';
|
||||
export 'pdf_source/pdf_reading.dart';
|
||||
export 'pdf_source/pdf_source.dart';
|
||||
export 'review/review_log.dart';
|
||||
export 'settings/person_settings.dart';
|
||||
export 'studyset/studyset.dart';
|
||||
|
|
@ -1384,6 +1388,204 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
],
|
||||
managed: true,
|
||||
),
|
||||
_isp.TableDefinition(
|
||||
name: 'pdf_reading',
|
||||
dartName: 'PdfReading',
|
||||
schema: 'public',
|
||||
module: 'halkyon_learn',
|
||||
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: 'pdfSourceId',
|
||||
columnType: _isp.ColumnType.uuid,
|
||||
isNullable: false,
|
||||
dartType: 'UuidValue',
|
||||
),
|
||||
_isp.ColumnDefinition(
|
||||
name: 'readAt',
|
||||
columnType: _isp.ColumnType.timestampWithoutTimeZone,
|
||||
isNullable: false,
|
||||
dartType: 'DateTime',
|
||||
),
|
||||
_isp.ColumnDefinition(
|
||||
name: 'kind',
|
||||
columnType: _isp.ColumnType.text,
|
||||
isNullable: false,
|
||||
dartType: 'String',
|
||||
),
|
||||
_isp.ColumnDefinition(
|
||||
name: 'page',
|
||||
columnType: _isp.ColumnType.bigint,
|
||||
isNullable: false,
|
||||
dartType: 'int',
|
||||
),
|
||||
],
|
||||
foreignKeys: [
|
||||
_isp.ForeignKeyDefinition(
|
||||
constraintName: 'pdf_reading_fk_0',
|
||||
columns: ['spaceId'],
|
||||
referenceTable: 'offline_sync_spaces',
|
||||
referenceTableSchema: 'public',
|
||||
referenceColumns: ['id'],
|
||||
onUpdate: _isp.ForeignKeyAction.noAction,
|
||||
onDelete: _isp.ForeignKeyAction.cascade,
|
||||
matchType: null,
|
||||
),
|
||||
_isp.ForeignKeyDefinition(
|
||||
constraintName: 'pdf_reading_fk_1',
|
||||
columns: ['pdfSourceId'],
|
||||
referenceTable: 'pdf_source',
|
||||
referenceTableSchema: 'public',
|
||||
referenceColumns: ['id'],
|
||||
onUpdate: _isp.ForeignKeyAction.noAction,
|
||||
onDelete: _isp.ForeignKeyAction.cascade,
|
||||
matchType: null,
|
||||
deferrable: _isp.DeferrableConstraint.initiallyDeferred,
|
||||
),
|
||||
],
|
||||
indexes: [
|
||||
_isp.IndexDefinition(
|
||||
indexName: 'pdf_reading_space_idx',
|
||||
tableSpace: null,
|
||||
elements: [
|
||||
_isp.IndexElementDefinition(
|
||||
type: _isp.IndexElementDefinitionType.column,
|
||||
definition: 'spaceId',
|
||||
),
|
||||
],
|
||||
type: 'btree',
|
||||
isUnique: false,
|
||||
isPrimary: false,
|
||||
),
|
||||
_isp.IndexDefinition(
|
||||
indexName: 'pdf_reading_source_idx',
|
||||
tableSpace: null,
|
||||
elements: [
|
||||
_isp.IndexElementDefinition(
|
||||
type: _isp.IndexElementDefinitionType.column,
|
||||
definition: 'pdfSourceId',
|
||||
),
|
||||
_isp.IndexElementDefinition(
|
||||
type: _isp.IndexElementDefinitionType.column,
|
||||
definition: 'readAt',
|
||||
),
|
||||
],
|
||||
type: 'btree',
|
||||
isUnique: false,
|
||||
isPrimary: false,
|
||||
),
|
||||
],
|
||||
managed: true,
|
||||
),
|
||||
_isp.TableDefinition(
|
||||
name: 'pdf_source',
|
||||
dartName: 'PdfSource',
|
||||
schema: 'public',
|
||||
module: 'halkyon_learn',
|
||||
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: 'studysetId',
|
||||
columnType: _isp.ColumnType.uuid,
|
||||
isNullable: false,
|
||||
dartType: 'UuidValue',
|
||||
),
|
||||
_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: 'pdf_source_fk_0',
|
||||
columns: ['spaceId'],
|
||||
referenceTable: 'offline_sync_spaces',
|
||||
referenceTableSchema: 'public',
|
||||
referenceColumns: ['id'],
|
||||
onUpdate: _isp.ForeignKeyAction.noAction,
|
||||
onDelete: _isp.ForeignKeyAction.cascade,
|
||||
matchType: null,
|
||||
),
|
||||
_isp.ForeignKeyDefinition(
|
||||
constraintName: 'pdf_source_fk_1',
|
||||
columns: ['studysetId'],
|
||||
referenceTable: 'studyset',
|
||||
referenceTableSchema: 'public',
|
||||
referenceColumns: ['id'],
|
||||
onUpdate: _isp.ForeignKeyAction.noAction,
|
||||
onDelete: _isp.ForeignKeyAction.cascade,
|
||||
matchType: null,
|
||||
deferrable: _isp.DeferrableConstraint.initiallyDeferred,
|
||||
),
|
||||
],
|
||||
indexes: [
|
||||
_isp.IndexDefinition(
|
||||
indexName: 'pdf_source_space_idx',
|
||||
tableSpace: null,
|
||||
elements: [
|
||||
_isp.IndexElementDefinition(
|
||||
type: _isp.IndexElementDefinitionType.column,
|
||||
definition: 'spaceId',
|
||||
),
|
||||
],
|
||||
type: 'btree',
|
||||
isUnique: false,
|
||||
isPrimary: false,
|
||||
),
|
||||
_isp.IndexDefinition(
|
||||
indexName: 'pdf_source_studyset_idx',
|
||||
tableSpace: null,
|
||||
elements: [
|
||||
_isp.IndexElementDefinition(
|
||||
type: _isp.IndexElementDefinitionType.column,
|
||||
definition: 'studysetId',
|
||||
),
|
||||
],
|
||||
type: 'btree',
|
||||
isUnique: false,
|
||||
isPrimary: false,
|
||||
),
|
||||
],
|
||||
managed: true,
|
||||
),
|
||||
_isp.TableDefinition(
|
||||
name: 'person_settings',
|
||||
dartName: 'PersonSettings',
|
||||
|
|
@ -1836,6 +2038,12 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
if (t == _i6j5dng4.NoteRevision) {
|
||||
return _i6j5dng4.NoteRevision.fromJson(data) as T;
|
||||
}
|
||||
if (t == _i4l20c7i.PdfReading) {
|
||||
return _i4l20c7i.PdfReading.fromJson(data) as T;
|
||||
}
|
||||
if (t == _iz838ro9.PdfSource) {
|
||||
return _iz838ro9.PdfSource.fromJson(data) as T;
|
||||
}
|
||||
if (t == _irzjbdqz.ReviewLog) {
|
||||
return _irzjbdqz.ReviewLog.fromJson(data) as T;
|
||||
}
|
||||
|
|
@ -1924,6 +2132,12 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
if (t == _is.getType<_i6j5dng4.NoteRevision?>()) {
|
||||
return (data != null ? _i6j5dng4.NoteRevision.fromJson(data) : null) as T;
|
||||
}
|
||||
if (t == _is.getType<_i4l20c7i.PdfReading?>()) {
|
||||
return (data != null ? _i4l20c7i.PdfReading.fromJson(data) : null) as T;
|
||||
}
|
||||
if (t == _is.getType<_iz838ro9.PdfSource?>()) {
|
||||
return (data != null ? _iz838ro9.PdfSource.fromJson(data) : null) as T;
|
||||
}
|
||||
if (t == _is.getType<_irzjbdqz.ReviewLog?>()) {
|
||||
return (data != null ? _irzjbdqz.ReviewLog.fromJson(data) : null) as T;
|
||||
}
|
||||
|
|
@ -1991,6 +2205,8 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
_i59qfu1l.LearningPathStepRevision => 'LearningPathStepRevision',
|
||||
_i9njjhyv.Note => 'Note',
|
||||
_i6j5dng4.NoteRevision => 'NoteRevision',
|
||||
_i4l20c7i.PdfReading => 'PdfReading',
|
||||
_iz838ro9.PdfSource => 'PdfSource',
|
||||
_irzjbdqz.ReviewLog => 'ReviewLog',
|
||||
_irnhx5e5.PersonSettings => 'PersonSettings',
|
||||
_ikbclmae.Studyset => 'Studyset',
|
||||
|
|
@ -2049,6 +2265,10 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
return 'Note';
|
||||
case _i6j5dng4.NoteRevision():
|
||||
return 'NoteRevision';
|
||||
case _i4l20c7i.PdfReading():
|
||||
return 'PdfReading';
|
||||
case _iz838ro9.PdfSource():
|
||||
return 'PdfSource';
|
||||
case _irzjbdqz.ReviewLog():
|
||||
return 'ReviewLog';
|
||||
case _irnhx5e5.PersonSettings():
|
||||
|
|
@ -2146,6 +2366,12 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
if (dataClassName == 'NoteRevision') {
|
||||
return deserialize<_i6j5dng4.NoteRevision>(data['data']);
|
||||
}
|
||||
if (dataClassName == 'PdfReading') {
|
||||
return deserialize<_i4l20c7i.PdfReading>(data['data']);
|
||||
}
|
||||
if (dataClassName == 'PdfSource') {
|
||||
return deserialize<_iz838ro9.PdfSource>(data['data']);
|
||||
}
|
||||
if (dataClassName == 'ReviewLog') {
|
||||
return deserialize<_irzjbdqz.ReviewLog>(data['data']);
|
||||
}
|
||||
|
|
@ -2239,6 +2465,10 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
return _i9njjhyv.Note.t;
|
||||
case _i6j5dng4.NoteRevision:
|
||||
return _i6j5dng4.NoteRevision.t;
|
||||
case _i4l20c7i.PdfReading:
|
||||
return _i4l20c7i.PdfReading.t;
|
||||
case _iz838ro9.PdfSource:
|
||||
return _iz838ro9.PdfSource.t;
|
||||
case _irzjbdqz.ReviewLog:
|
||||
return _irzjbdqz.ReviewLog.t;
|
||||
case _irnhx5e5.PersonSettings:
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ import 'learning_path/learning_path_step.dart' as _irnpk9dc;
|
|||
import 'learning_path/learning_path_step_revision.dart' as _i59qfu1l;
|
||||
import 'note/note.dart' as _i9njjhyv;
|
||||
import 'note/note_revision.dart' as _i6j5dng4;
|
||||
import 'pdf_source/pdf_reading.dart' as _i4l20c7i;
|
||||
import 'pdf_source/pdf_source.dart' as _iz838ro9;
|
||||
import 'review/review_log.dart' as _irzjbdqz;
|
||||
import 'settings/person_settings.dart' as _irnhx5e5;
|
||||
import 'studyset/studyset.dart' as _ikbclmae;
|
||||
|
|
@ -44,6 +46,8 @@ final List<_is.Table> syncTables = [
|
|||
_i9njjhyv.Note.t,
|
||||
_i6j5dng4.NoteRevision.t,
|
||||
_itkfn57x.NoteTag.t,
|
||||
_i4l20c7i.PdfReading.t,
|
||||
_iz838ro9.PdfSource.t,
|
||||
_irnhx5e5.PersonSettings.t,
|
||||
_irzjbdqz.ReviewLog.t,
|
||||
_ikbclmae.Studyset.t,
|
||||
|
|
|
|||
26
halkyon_learn_server/lib/src/pdf_source/pdf_reading.spy.yaml
Normal file
26
halkyon_learn_server/lib/src/pdf_source/pdf_reading.spy.yaml
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
### How far a PDF has been read, as an entry that is never changed once written:
|
||||
### the furthest page isn't stored but derived from these, as a card's schedule
|
||||
### is from its reviews, so reading on two devices never conflicts. An entry is
|
||||
### written only when the furthest page grows, so a PDF has at most an entry per
|
||||
### page per fresh start. Synced; the entry goes with its PDF.
|
||||
class: PdfReading
|
||||
table: pdf_reading
|
||||
database: sync
|
||||
fields:
|
||||
### The PDF read.
|
||||
pdfSource: PdfSource?, relation(onDelete=Cascade, deferred)
|
||||
### When, in UTC.
|
||||
readAt: DateTime
|
||||
### 'reached' for a page reached (marking the PDF finished reaches its last
|
||||
### page); 'reset' for a fresh start: the furthest page is the furthest
|
||||
### reached since the last one.
|
||||
kind: String
|
||||
### The page reached, from 1; 0 for 'reset'.
|
||||
page: int
|
||||
indexes:
|
||||
# Deleting a space finds its rows through this index (see studyset.spy.yaml).
|
||||
pdf_reading_space_idx:
|
||||
fields: spaceId
|
||||
# A PDF's entries, in time order.
|
||||
pdf_reading_source_idx:
|
||||
fields: pdfSourceId, readAt
|
||||
28
halkyon_learn_server/lib/src/pdf_source/pdf_source.spy.yaml
Normal file
28
halkyon_learn_server/lib/src/pdf_source/pdf_source.spy.yaml
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
### A PDF in a studyset, to read and learn from: its cards and notes can be tied
|
||||
### to its pages. Synced; its space owns it.
|
||||
###
|
||||
### Its content (title, the name the file came with, the file, its page count
|
||||
### once known, a reading deadline) lives in the payload, last writer wins, as a
|
||||
### learning path's: rarely edited on two devices at once. The file itself is
|
||||
### an asset (ADR 0004), named in the payload by the asset row's id.
|
||||
class: PdfSource
|
||||
table: pdf_source
|
||||
database: sync
|
||||
fields:
|
||||
### The studyset the PDF belongs to; the PDF goes with it.
|
||||
studyset: Studyset?, relation(onDelete=Cascade, deferred)
|
||||
### The encryption key's id; always null (ADR 0002, rule 6).
|
||||
keyId: String?
|
||||
### The content as UTF-8 JSON: {"schemaVersion", "title", "fileName", "asset",
|
||||
### "pageCount", "deadline"}; the app's pdf_source_payload.dart describes each
|
||||
### schema.
|
||||
payload: ByteData
|
||||
### The encryption nonce; always null, like keyId.
|
||||
nonce: ByteData?
|
||||
indexes:
|
||||
# Deleting a space finds its rows through this index (see studyset.spy.yaml).
|
||||
pdf_source_space_idx:
|
||||
fields: spaceId
|
||||
# A studyset's PDFs.
|
||||
pdf_source_studyset_idx:
|
||||
fields: studysetId
|
||||
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,260 @@
|
|||
{
|
||||
"__className__": "serverpod.DatabaseMigration",
|
||||
"actions": [
|
||||
{
|
||||
"__className__": "serverpod.DatabaseMigrationAction",
|
||||
"type": "createTable",
|
||||
"createTable": {
|
||||
"__className__": "serverpod.TableDefinition",
|
||||
"name": "pdf_reading",
|
||||
"dartName": "PdfReading",
|
||||
"module": "halkyon_learn",
|
||||
"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": "pdfSourceId",
|
||||
"fieldName": "pdfSourceId",
|
||||
"columnType": 7,
|
||||
"isNullable": false,
|
||||
"dartType": "UuidValue"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "readAt",
|
||||
"fieldName": "readAt",
|
||||
"columnType": 4,
|
||||
"isNullable": false,
|
||||
"dartType": "DateTime"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "kind",
|
||||
"fieldName": "kind",
|
||||
"columnType": 0,
|
||||
"isNullable": false,
|
||||
"dartType": "String"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "page",
|
||||
"fieldName": "page",
|
||||
"columnType": 6,
|
||||
"isNullable": false,
|
||||
"dartType": "int"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"__className__": "serverpod.ForeignKeyDefinition",
|
||||
"constraintName": "pdf_reading_fk_0",
|
||||
"columns": [
|
||||
"spaceId"
|
||||
],
|
||||
"referenceTable": "offline_sync_spaces",
|
||||
"referenceTableSchema": "public",
|
||||
"referenceColumns": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": 3,
|
||||
"onDelete": 4
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ForeignKeyDefinition",
|
||||
"constraintName": "pdf_reading_fk_1",
|
||||
"columns": [
|
||||
"pdfSourceId"
|
||||
],
|
||||
"referenceTable": "pdf_source",
|
||||
"referenceTableSchema": "public",
|
||||
"referenceColumns": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": 3,
|
||||
"onDelete": 4,
|
||||
"deferrable": "initiallyDeferred"
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
{
|
||||
"__className__": "serverpod.IndexDefinition",
|
||||
"indexName": "pdf_reading_space_idx",
|
||||
"elements": [
|
||||
{
|
||||
"__className__": "serverpod.IndexElementDefinition",
|
||||
"type": 0,
|
||||
"definition": "spaceId"
|
||||
}
|
||||
],
|
||||
"type": "btree",
|
||||
"isUnique": false,
|
||||
"isPrimary": false
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.IndexDefinition",
|
||||
"indexName": "pdf_reading_source_idx",
|
||||
"elements": [
|
||||
{
|
||||
"__className__": "serverpod.IndexElementDefinition",
|
||||
"type": 0,
|
||||
"definition": "pdfSourceId"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.IndexElementDefinition",
|
||||
"type": 0,
|
||||
"definition": "readAt"
|
||||
}
|
||||
],
|
||||
"type": "btree",
|
||||
"isUnique": false,
|
||||
"isPrimary": false
|
||||
}
|
||||
],
|
||||
"managed": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.DatabaseMigrationAction",
|
||||
"type": "createTable",
|
||||
"createTable": {
|
||||
"__className__": "serverpod.TableDefinition",
|
||||
"name": "pdf_source",
|
||||
"dartName": "PdfSource",
|
||||
"module": "halkyon_learn",
|
||||
"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": "studysetId",
|
||||
"fieldName": "studysetId",
|
||||
"columnType": 7,
|
||||
"isNullable": false,
|
||||
"dartType": "UuidValue"
|
||||
},
|
||||
{
|
||||
"__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": "pdf_source_fk_0",
|
||||
"columns": [
|
||||
"spaceId"
|
||||
],
|
||||
"referenceTable": "offline_sync_spaces",
|
||||
"referenceTableSchema": "public",
|
||||
"referenceColumns": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": 3,
|
||||
"onDelete": 4
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ForeignKeyDefinition",
|
||||
"constraintName": "pdf_source_fk_1",
|
||||
"columns": [
|
||||
"studysetId"
|
||||
],
|
||||
"referenceTable": "studyset",
|
||||
"referenceTableSchema": "public",
|
||||
"referenceColumns": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": 3,
|
||||
"onDelete": 4,
|
||||
"deferrable": "initiallyDeferred"
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
{
|
||||
"__className__": "serverpod.IndexDefinition",
|
||||
"indexName": "pdf_source_space_idx",
|
||||
"elements": [
|
||||
{
|
||||
"__className__": "serverpod.IndexElementDefinition",
|
||||
"type": 0,
|
||||
"definition": "spaceId"
|
||||
}
|
||||
],
|
||||
"type": "btree",
|
||||
"isUnique": false,
|
||||
"isPrimary": false
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.IndexDefinition",
|
||||
"indexName": "pdf_source_studyset_idx",
|
||||
"elements": [
|
||||
{
|
||||
"__className__": "serverpod.IndexElementDefinition",
|
||||
"type": 0,
|
||||
"definition": "studysetId"
|
||||
}
|
||||
],
|
||||
"type": "btree",
|
||||
"isUnique": false,
|
||||
"isPrimary": false
|
||||
}
|
||||
],
|
||||
"managed": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"warnings": [],
|
||||
"migrationApiVersion": 1
|
||||
}
|
||||
|
|
@ -0,0 +1,140 @@
|
|||
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 "pdf_reading" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid_v7(),
|
||||
"spaceId" bigint,
|
||||
"pdfSourceId" uuid NOT NULL,
|
||||
"readAt" timestamp without time zone NOT NULL,
|
||||
"kind" text NOT NULL,
|
||||
"page" bigint NOT NULL
|
||||
);
|
||||
|
||||
-- Indexes
|
||||
CREATE INDEX "pdf_reading_space_idx" ON "pdf_reading" USING btree ("spaceId");
|
||||
CREATE INDEX "pdf_reading_source_idx" ON "pdf_reading" USING btree ("pdfSourceId", "readAt");
|
||||
|
||||
--
|
||||
-- ACTION CREATE TABLE
|
||||
--
|
||||
CREATE TABLE "pdf_source" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid_v7(),
|
||||
"spaceId" bigint,
|
||||
"studysetId" uuid NOT NULL,
|
||||
"keyId" text,
|
||||
"payload" bytea NOT NULL,
|
||||
"nonce" bytea
|
||||
);
|
||||
|
||||
-- Indexes
|
||||
CREATE INDEX "pdf_source_space_idx" ON "pdf_source" USING btree ("spaceId");
|
||||
CREATE INDEX "pdf_source_studyset_idx" ON "pdf_source" USING btree ("studysetId");
|
||||
|
||||
--
|
||||
-- ACTION CREATE FOREIGN KEY
|
||||
--
|
||||
ALTER TABLE ONLY "pdf_reading"
|
||||
ADD CONSTRAINT "pdf_reading_fk_0"
|
||||
FOREIGN KEY("spaceId")
|
||||
REFERENCES "offline_sync_spaces"("id")
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE NO ACTION;
|
||||
ALTER TABLE ONLY "pdf_reading"
|
||||
ADD CONSTRAINT "pdf_reading_fk_1"
|
||||
FOREIGN KEY("pdfSourceId")
|
||||
REFERENCES "pdf_source"("id")
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE NO ACTION
|
||||
DEFERRABLE INITIALLY DEFERRED;
|
||||
|
||||
--
|
||||
-- ACTION CREATE FOREIGN KEY
|
||||
--
|
||||
ALTER TABLE ONLY "pdf_source"
|
||||
ADD CONSTRAINT "pdf_source_fk_0"
|
||||
FOREIGN KEY("spaceId")
|
||||
REFERENCES "offline_sync_spaces"("id")
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE NO ACTION;
|
||||
ALTER TABLE ONLY "pdf_source"
|
||||
ADD CONSTRAINT "pdf_source_fk_1"
|
||||
FOREIGN KEY("studysetId")
|
||||
REFERENCES "studyset"("id")
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE NO ACTION
|
||||
DEFERRABLE INITIALLY DEFERRED;
|
||||
|
||||
|
||||
--
|
||||
-- MIGRATION VERSION FOR halkyon_learn
|
||||
--
|
||||
INSERT INTO "serverpod_migrations" ("module", "version", "timestamp")
|
||||
VALUES ('halkyon_learn', '20261003110855698-pdf-sources', now())
|
||||
ON CONFLICT ("module")
|
||||
DO UPDATE SET "version" = '20261003110855698-pdf-sources', "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 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;
|
||||
|
|
@ -12,3 +12,4 @@
|
|||
20261002035419149-file-allowance
|
||||
20261002191122122-notes
|
||||
20261002211346701-learning-paths
|
||||
20261003110855698-pdf-sources
|
||||
|
|
|
|||
|
|
@ -156,6 +156,33 @@ void main() {
|
|||
);
|
||||
});
|
||||
|
||||
test('keeps a file over 20 MB only when it is a PDF, which may take '
|
||||
'up to 50 MB', () async {
|
||||
const size = AssetEndpoint.maxOtherBytes + 1;
|
||||
final pdf = Uint8List(size)
|
||||
..setAll(0, utf8.encode('%PDF-1.7'))
|
||||
..fillRange(8, size, 32);
|
||||
final other = Uint8List(size)..fillRange(0, size, 32);
|
||||
final otherHash = sha256.convert(other).toString();
|
||||
|
||||
expect(await add(alice, pdf), isTrue);
|
||||
expect(await add(alice, other), isFalse);
|
||||
|
||||
// What isn't a PDF is gone: the server asks for it anew.
|
||||
expect(
|
||||
await endpoints.asset.startUpload(signedIn(alice), otherHash, size),
|
||||
isNotNull,
|
||||
);
|
||||
await expectLater(
|
||||
endpoints.asset.startUpload(
|
||||
signedIn(alice),
|
||||
otherHash,
|
||||
AssetEndpoint.maxBytes + 1,
|
||||
),
|
||||
throwsArgumentError,
|
||||
);
|
||||
});
|
||||
|
||||
test('finishes no upload that never came', () async {
|
||||
expect(
|
||||
await endpoints.asset.finishUpload(signedIn(alice), imageHash),
|
||||
|
|
|
|||
Loading…
Reference in a new issue