feat(feedback): reports and suggestions on published studysets, gathered by the instance

Stage 4 of the public tier (ADR 0005, rule 6), its server and the app's
data. A learner writes a report or a suggestion on a published studyset,
a card, note or step of it, in their own synced space, so it works
offline; replies and the author's decisions (closed, hidden, accepted,
declined) likewise, in the writer's or the author's space. The instance
gathers them: public to everyone signed in, but for what the author
hides; for the author's inbox and the writer's. What counts is decided
as rows are read, since a modified app could write anything: threads
from people with an identity and a public profile the author hasn't
blocked, replies from the writer and the author alone, decisions from
the author alone, no suggestion under "no derivatives". The author gets
the files a suggestion names from its writer's, and the next release
credits the writers of the suggestions accepted since the last
(Co-authored-by). Deleting a person deletes their blocks and what they
read; what they wrote goes with their space.

In the app, the repository writes reports and suggestions (a card, note
or step as the learner has it, without what is theirs alone, with the
files it names) and takes an accepted suggestion into the author's own
card, note or step.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-10 01:04:17 +02:00
commit 9accc95df2
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
62 changed files with 35683 additions and 1 deletions

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,326 @@
{
"__className__": "serverpod.DatabaseMigration",
"actions": [
{
"__className__": "serverpod.DatabaseMigrationAction",
"type": "createTable",
"createTable": {
"__className__": "serverpod.TableDefinition",
"name": "feedback_decision",
"dartName": "FeedbackDecision",
"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": "threadId",
"fieldName": "threadId",
"columnType": 7,
"isNullable": false,
"dartType": "UuidValue"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "decision",
"fieldName": "decision",
"columnType": 0,
"isNullable": false,
"dartType": "String"
}
],
"foreignKeys": [
{
"__className__": "serverpod.ForeignKeyDefinition",
"constraintName": "feedback_decision_fk_0",
"columns": [
"spaceId"
],
"referenceTable": "offline_sync_spaces",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4
}
],
"indexes": [
{
"__className__": "serverpod.IndexDefinition",
"indexName": "feedback_decision_space_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "spaceId"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
},
{
"__className__": "serverpod.IndexDefinition",
"indexName": "feedback_decision_thread_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "threadId"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
}
],
"managed": true
}
},
{
"__className__": "serverpod.DatabaseMigrationAction",
"type": "createTable",
"createTable": {
"__className__": "serverpod.TableDefinition",
"name": "feedback_message",
"dartName": "FeedbackMessage",
"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": "threadId",
"fieldName": "threadId",
"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": "feedback_message_fk_0",
"columns": [
"spaceId"
],
"referenceTable": "offline_sync_spaces",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4
}
],
"indexes": [
{
"__className__": "serverpod.IndexDefinition",
"indexName": "feedback_message_space_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "spaceId"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
},
{
"__className__": "serverpod.IndexDefinition",
"indexName": "feedback_message_thread_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "threadId"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
}
],
"managed": true
}
},
{
"__className__": "serverpod.DatabaseMigrationAction",
"type": "createTable",
"createTable": {
"__className__": "serverpod.TableDefinition",
"name": "feedback_thread",
"dartName": "FeedbackThread",
"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": "publicationId",
"fieldName": "publicationId",
"columnType": 7,
"isNullable": false,
"dartType": "UuidValue"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "kind",
"fieldName": "kind",
"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": "feedback_thread_fk_0",
"columns": [
"spaceId"
],
"referenceTable": "offline_sync_spaces",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4
}
],
"indexes": [
{
"__className__": "serverpod.IndexDefinition",
"indexName": "feedback_thread_space_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "spaceId"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
},
{
"__className__": "serverpod.IndexDefinition",
"indexName": "feedback_thread_publication_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "publicationId"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
}
],
"managed": true
}
}
],
"warnings": [],
"migrationApiVersion": 1
}

View file

@ -13,6 +13,7 @@ part '20261002211346701-learning-paths/migration.dart';
part '20261003110855698-pdf-sources/migration.dart';
part '20261007205440868-card-buried-at/migration.dart';
part '20261008165051407-pdf-position/migration.dart';
part '20261009224606274-feedback/migration.dart';
/// Migration registry for the client-side database.
class MigrationRegistry {
@ -28,6 +29,7 @@ class MigrationRegistry {
_Migration20261003110855698_pdf_sources(),
_Migration20261007205440868_card_buried_at(),
_Migration20261008165051407_pdf_position(),
_Migration20261009224606274_feedback(),
];
/// List of all client-side database migration versions.

View file

@ -26,6 +26,8 @@ import 'package:halkyon_learn_client/src/protocol/explore/explore_query.dart'
as _i728kten;
import 'package:halkyon_learn_client/src/protocol/explore/release_content.dart'
as _icu7niot;
import 'package:halkyon_learn_client/src/protocol/feedback/feedback_view.dart'
as _idpwh21d;
import 'package:halkyon_learn_client/src/protocol/instance/instance_info.dart'
as _i26u8vl4;
import 'package:halkyon_learn_client/src/protocol/profile/profile.dart'
@ -400,6 +402,105 @@ class EndpointExplore extends _isc.EndpointRef {
);
}
/// The reports and suggestions on a published studyset (ADR 0005, rule 6),
/// for everyone signed in to read: they are public, but for those its author
/// hid. Writing them is the sync engine's: each person writes in their own
/// space.
/// {@category Endpoint}
class EndpointFeedback extends _isc.EndpointRef {
EndpointFeedback(_isc.EndpointCaller caller) : super(caller);
@override
String get name => 'feedback';
/// The threads on the published studyset [publicId] the caller may see,
/// the most recently active first.
_ida.Future<List<_idpwh21d.FeedbackView>> threadsOf(
_isc.UuidValue publicId,
) => caller.callServerEndpoint<List<_idpwh21d.FeedbackView>>(
'feedback',
'threadsOf',
{'publicId': publicId},
);
}
/// The person's inbox of reports and suggestions: those on their published
/// studysets and those they wrote, and what an author does about the people
/// who write. For people with an identity: anything involving other people
/// needs it.
/// {@category Endpoint}
class EndpointInbox extends _isc.EndpointRef {
EndpointInbox(_isc.EndpointCaller caller) : super(caller);
@override
String get name => 'inbox';
/// The threads on the caller's published studysets and those they wrote,
/// the most recently active first.
_ida.Future<List<_idpwh21d.FeedbackView>> inbox() =>
caller.callServerEndpoint<List<_idpwh21d.FeedbackView>>(
'inbox',
'inbox',
{},
);
/// How many threads of the inbox have something new.
_ida.Future<int> unseen() => caller.callServerEndpoint<int>(
'inbox',
'unseen',
{},
);
/// Records that the caller has read the thread [threadId] as it is now.
_ida.Future<void> markSeen(_isc.UuidValue threadId) =>
caller.callServerEndpoint<void>(
'inbox',
'markSeen',
{'threadId': threadId},
);
/// Keeps the person with [handle] from writing on the caller's published
/// studysets: what they wrote there, and write, shows to them alone.
/// Throws [NotFoundException] for a handle nobody has.
_ida.Future<void> block(String handle) => caller.callServerEndpoint<void>(
'inbox',
'block',
{'handle': handle},
);
/// Lets the person with [handle] write on the caller's published
/// studysets again.
_ida.Future<void> unblock(String handle) => caller.callServerEndpoint<void>(
'inbox',
'unblock',
{'handle': handle},
);
/// The handles of the people the caller blocked.
_ida.Future<List<String>> blockedPeople() =>
caller.callServerEndpoint<List<String>>(
'inbox',
'blockedPeople',
{},
);
/// The file [sha256] that the suggestion [threadId] names, for the author
/// of the studyset it is on, to take it into their own: from its writer's
/// files. Throws [NotFoundException] for anyone else, a thread that names
/// no such file, or a file not uploaded.
_ida.Future<_idt.ByteData> file(
_isc.UuidValue threadId,
String sha256,
) => caller.callServerEndpoint<_idt.ByteData>(
'inbox',
'file',
{
'threadId': threadId,
'sha256': sha256,
},
);
}
/// What this instance offers. Public: the app asks before anyone is signed in.
/// {@category Endpoint}
class EndpointInstance extends _isc.EndpointRef {
@ -615,6 +716,8 @@ class Client extends _isc.ServerpodClientShared {
gitHubUpgrade = EndpointGitHubUpgrade(this);
dictionary = EndpointDictionary(this);
explore = EndpointExplore(this);
feedback = EndpointFeedback(this);
inbox = EndpointInbox(this);
instance = EndpointInstance(this);
profile = EndpointProfile(this);
profiles = EndpointProfiles(this);
@ -638,6 +741,10 @@ class Client extends _isc.ServerpodClientShared {
late final EndpointExplore explore;
late final EndpointFeedback feedback;
late final EndpointInbox inbox;
late final EndpointInstance instance;
late final EndpointProfile profile;
@ -658,6 +765,8 @@ class Client extends _isc.ServerpodClientShared {
'gitHubUpgrade': gitHubUpgrade,
'dictionary': dictionary,
'explore': explore,
'feedback': feedback,
'inbox': inbox,
'instance': instance,
'profile': profile,
'profiles': profiles,

View file

@ -0,0 +1,646 @@
/* 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 'package:serverpod_client/serverpod_client.dart' as _isc;
import 'package:serverpod_database/serverpod_database.dart' as _isd;
/// What a published studyset's author decided about a thread of reports and
/// suggestions (FeedbackThread): 'closed', 'reopened', 'hidden', 'shown',
/// 'accepted' or 'declined' (a suggestion). Written in the author's own
/// space, and only the author's count; the latest of each pair (by its id,
/// a UUID v7) stands. Never changed. Synced.
abstract class FeedbackDecision
implements _isd.TableRow<_isc.UuidValue?>, _isc.ProtocolSerialization {
FeedbackDecision._({
this.id,
this.spaceId,
required this.threadId,
required this.decision,
});
factory FeedbackDecision({
_isc.UuidValue? id,
int? spaceId,
required _isc.UuidValue threadId,
required String decision,
}) = _FeedbackDecisionImpl;
factory FeedbackDecision.fromJson(Map<String, dynamic> jsonSerialization) {
return FeedbackDecision(
id: jsonSerialization['id'] == null
? null
: _isc.UuidValueJsonExtension.fromJson(jsonSerialization['id']),
spaceId: jsonSerialization['spaceId'] as int?,
threadId: _isc.UuidValueJsonExtension.fromJson(
jsonSerialization['threadId'],
),
decision: jsonSerialization['decision'] as String,
);
}
static final t = FeedbackDecisionTable();
static const db = FeedbackDecisionRepository._();
@override
_isc.UuidValue? id;
/// The space owning this row. Maintained by the sync engine.
int? spaceId;
/// The thread decided on.
_isc.UuidValue threadId;
/// What was decided.
String decision;
@override
_isd.Table<_isc.UuidValue?> get table => t;
/// Returns a shallow copy of this [FeedbackDecision]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
FeedbackDecision copyWith({
_isc.UuidValue? id,
int? spaceId,
_isc.UuidValue? threadId,
String? decision,
});
@override
Map<String, dynamic> toJson() {
return {
'__className__': 'FeedbackDecision',
if (id != null) 'id': id?.toJson(),
if (spaceId != null) 'spaceId': spaceId,
'threadId': threadId.toJson(),
'decision': decision,
};
}
@override
Map<String, dynamic> toJsonForProtocol() {
return {
'__className__': 'FeedbackDecision',
if (id != null) 'id': id?.toJson(),
if (spaceId != null) 'spaceId': spaceId,
'threadId': threadId.toJson(),
'decision': decision,
};
}
static FeedbackDecisionInclude include() {
return FeedbackDecisionInclude._();
}
static FeedbackDecisionIncludeList includeList({
_isd.WhereExpressionBuilder<FeedbackDecisionTable>? where,
int? limit,
int? offset,
_isd.OrderByBuilder<FeedbackDecisionTable>? orderBy,
_isd.OrderByListBuilder<FeedbackDecisionTable>? orderByList,
FeedbackDecisionInclude? include,
}) {
return FeedbackDecisionIncludeList._(
where: where,
limit: limit,
offset: offset,
orderBy: orderBy?.call(FeedbackDecision.t),
orderByList: orderByList?.call(FeedbackDecision.t),
include: include,
);
}
@override
String toString() {
return _isc.SerializationManager.encode(this);
}
}
class _Undefined {}
class _FeedbackDecisionImpl extends FeedbackDecision {
_FeedbackDecisionImpl({
_isc.UuidValue? id,
int? spaceId,
required _isc.UuidValue threadId,
required String decision,
}) : super._(
id: id,
spaceId: spaceId,
threadId: threadId,
decision: decision,
);
/// Returns a shallow copy of this [FeedbackDecision]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
@override
FeedbackDecision copyWith({
Object? id = _Undefined,
Object? spaceId = _Undefined,
_isc.UuidValue? threadId,
String? decision,
}) {
return FeedbackDecision(
id: id is _isc.UuidValue? ? id : this.id,
spaceId: spaceId is int? ? spaceId : this.spaceId,
threadId: threadId ?? this.threadId,
decision: decision ?? this.decision,
);
}
}
class FeedbackDecisionUpdateTable
extends _isd.UpdateTable<FeedbackDecisionTable> {
FeedbackDecisionUpdateTable(super.table);
_isd.ColumnValue<int, int> spaceId(int? value) => _isd.ColumnValue(
table.spaceId,
value,
);
_isd.ColumnValue<_isc.UuidValue, _isc.UuidValue> threadId(
_isc.UuidValue value,
) => _isd.ColumnValue(
table.threadId,
value,
);
_isd.ColumnValue<String, String> decision(String value) => _isd.ColumnValue(
table.decision,
value,
);
}
class FeedbackDecisionTable extends _isd.Table<_isc.UuidValue?> {
FeedbackDecisionTable({super.tableRelation})
: super(tableName: 'feedback_decision') {
updateTable = FeedbackDecisionUpdateTable(this);
spaceId = _isd.ColumnInt(
'spaceId',
this,
);
threadId = _isd.ColumnUuid(
'threadId',
this,
);
decision = _isd.ColumnString(
'decision',
this,
);
}
late final FeedbackDecisionUpdateTable updateTable;
/// The space owning this row. Maintained by the sync engine.
late final _isd.ColumnInt spaceId;
/// The thread decided on.
late final _isd.ColumnUuid threadId;
/// What was decided.
late final _isd.ColumnString decision;
@override
List<_isd.Column> get columns => [
id,
spaceId,
threadId,
decision,
];
}
class FeedbackDecisionInclude extends _isd.IncludeObject {
FeedbackDecisionInclude._();
@override
Map<String, _isd.Include?> get includes => {};
@override
_isd.Table<_isc.UuidValue?> get table => FeedbackDecision.t;
}
class FeedbackDecisionIncludeList extends _isd.IncludeList {
FeedbackDecisionIncludeList._({
_isd.WhereExpressionBuilder<FeedbackDecisionTable>? where,
super.limit,
super.offset,
super.orderBy,
super.orderByList,
super.include,
}) {
super.where = where?.call(FeedbackDecision.t);
}
@override
Map<String, _isd.Include?> get includes => include?.includes ?? {};
@override
_isd.Table<_isc.UuidValue?> get table => FeedbackDecision.t;
}
class FeedbackDecisionRepository {
const FeedbackDecisionRepository._();
/// Returns a list of [FeedbackDecision]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<FeedbackDecision>> find(
_isd.DatabaseSession session, {
_isd.WhereExpressionBuilder<FeedbackDecisionTable>? where,
int? limit,
int? offset,
_isd.OrderByBuilder<FeedbackDecisionTable>? orderBy,
_isd.OrderByListBuilder<FeedbackDecisionTable>? orderByList,
_isd.Transaction? transaction,
_isd.LockMode? lockMode,
_isd.LockBehavior? lockBehavior,
}) async {
return session.db.find<FeedbackDecision>(
where: where?.call(FeedbackDecision.t),
orderBy: orderBy?.call(FeedbackDecision.t),
orderByList: orderByList?.call(FeedbackDecision.t),
limit: limit,
offset: offset,
transaction: transaction,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Returns the first matching [FeedbackDecision] 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<FeedbackDecision?> findFirstRow(
_isd.DatabaseSession session, {
_isd.WhereExpressionBuilder<FeedbackDecisionTable>? where,
int? offset,
_isd.OrderByBuilder<FeedbackDecisionTable>? orderBy,
_isd.OrderByListBuilder<FeedbackDecisionTable>? orderByList,
_isd.Transaction? transaction,
_isd.LockMode? lockMode,
_isd.LockBehavior? lockBehavior,
}) async {
return session.db.findFirstRow<FeedbackDecision>(
where: where?.call(FeedbackDecision.t),
orderBy: orderBy?.call(FeedbackDecision.t),
orderByList: orderByList?.call(FeedbackDecision.t),
offset: offset,
transaction: transaction,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Finds a single [FeedbackDecision] by its [id] or null if no such row exists.
Future<FeedbackDecision?> findById(
_isd.DatabaseSession session,
_isc.UuidValue id, {
_isd.Transaction? transaction,
_isd.LockMode? lockMode,
_isd.LockBehavior? lockBehavior,
}) async {
return session.db.findById<FeedbackDecision>(
id,
transaction: transaction,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Inserts all [FeedbackDecision]s in the list and returns the inserted rows.
///
/// The returned [FeedbackDecision]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<FeedbackDecision>> insert(
_isd.DatabaseSession session,
List<FeedbackDecision> rows, {
_isd.Transaction? transaction,
bool ignoreConflicts = false,
bool noReturn = false,
}) async {
return session.db.insert<FeedbackDecision>(
rows,
transaction: transaction,
ignoreConflicts: ignoreConflicts,
noReturn: noReturn,
);
}
/// Inserts a single [FeedbackDecision] and returns the inserted row.
///
/// The returned [FeedbackDecision] will have its `id` field set.
Future<FeedbackDecision> insertRow(
_isd.DatabaseSession session,
FeedbackDecision row, {
_isd.Transaction? transaction,
}) async {
return session.db.insertRow<FeedbackDecision>(
row,
transaction: transaction,
);
}
/// Upserts all [FeedbackDecision]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 [FeedbackDecision]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<FeedbackDecision>> upsert(
_isd.DatabaseSession session,
List<FeedbackDecision> rows, {
required _isd.ColumnSelections<FeedbackDecisionTable> conflictColumns,
_isd.ColumnSelections<FeedbackDecisionTable>? updateColumns,
_isd.WhereExpressionBuilder<FeedbackDecisionTable>? updateWhere,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.upsert<FeedbackDecision>(
rows,
conflictColumns: conflictColumns(FeedbackDecision.t),
updateColumns: updateColumns?.call(FeedbackDecision.t),
updateWhere: updateWhere?.call(FeedbackDecision.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Upserts a single [FeedbackDecision] 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 [FeedbackDecision] will have its `id` field set.
Future<FeedbackDecision?> upsertRow(
_isd.DatabaseSession session,
FeedbackDecision row, {
required _isd.ColumnSelections<FeedbackDecisionTable> conflictColumns,
_isd.ColumnSelections<FeedbackDecisionTable>? updateColumns,
_isd.WhereExpressionBuilder<FeedbackDecisionTable>? updateWhere,
_isd.Transaction? transaction,
}) async {
return session.db.upsertRow<FeedbackDecision>(
row,
conflictColumns: conflictColumns(FeedbackDecision.t),
updateColumns: updateColumns?.call(FeedbackDecision.t),
updateWhere: updateWhere?.call(FeedbackDecision.t),
transaction: transaction,
);
}
/// Updates all [FeedbackDecision]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<FeedbackDecision>> update(
_isd.DatabaseSession session,
List<FeedbackDecision> rows, {
_isd.ColumnSelections<FeedbackDecisionTable>? columns,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.update<FeedbackDecision>(
rows,
columns: columns?.call(FeedbackDecision.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Updates a single [FeedbackDecision]. 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<FeedbackDecision> updateRow(
_isd.DatabaseSession session,
FeedbackDecision row, {
_isd.ColumnSelections<FeedbackDecisionTable>? columns,
_isd.Transaction? transaction,
}) async {
return session.db.updateRow<FeedbackDecision>(
row,
columns: columns?.call(FeedbackDecision.t),
transaction: transaction,
);
}
/// Updates a single [FeedbackDecision] by its [id] with the specified [columnValues].
/// Returns the updated row or null if no row with the given id exists.
Future<FeedbackDecision?> updateById(
_isd.DatabaseSession session,
_isc.UuidValue id, {
required _isd.ColumnValueListBuilder<FeedbackDecisionUpdateTable>
columnValues,
_isd.Transaction? transaction,
}) async {
return session.db.updateById<FeedbackDecision>(
id,
columnValues: columnValues(FeedbackDecision.t.updateTable),
transaction: transaction,
);
}
/// Updates all [FeedbackDecision]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<FeedbackDecision>> updateWhere(
_isd.DatabaseSession session, {
required _isd.ColumnValueListBuilder<FeedbackDecisionUpdateTable>
columnValues,
required _isd.WhereExpressionBuilder<FeedbackDecisionTable> where,
int? limit,
int? offset,
_isd.OrderByBuilder<FeedbackDecisionTable>? orderBy,
_isd.OrderByListBuilder<FeedbackDecisionTable>? orderByList,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.updateWhere<FeedbackDecision>(
columnValues: columnValues(FeedbackDecision.t.updateTable),
where: where(FeedbackDecision.t),
limit: limit,
offset: offset,
orderBy: orderBy?.call(FeedbackDecision.t),
orderByList: orderByList?.call(FeedbackDecision.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Deletes all [FeedbackDecision]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<FeedbackDecision>> delete(
_isd.DatabaseSession session,
List<FeedbackDecision> rows, {
_isd.OrderByBuilder<FeedbackDecisionTable>? orderBy,
_isd.OrderByListBuilder<FeedbackDecisionTable>? orderByList,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.delete<FeedbackDecision>(
rows,
orderBy: orderBy?.call(FeedbackDecision.t),
orderByList: orderByList?.call(FeedbackDecision.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Deletes a single [FeedbackDecision].
Future<FeedbackDecision> deleteRow(
_isd.DatabaseSession session,
FeedbackDecision row, {
_isd.Transaction? transaction,
}) async {
return session.db.deleteRow<FeedbackDecision>(
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<FeedbackDecision>> deleteWhere(
_isd.DatabaseSession session, {
required _isd.WhereExpressionBuilder<FeedbackDecisionTable> where,
_isd.OrderByBuilder<FeedbackDecisionTable>? orderBy,
_isd.OrderByListBuilder<FeedbackDecisionTable>? orderByList,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.deleteWhere<FeedbackDecision>(
where: where(FeedbackDecision.t),
orderBy: orderBy?.call(FeedbackDecision.t),
orderByList: orderByList?.call(FeedbackDecision.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(
_isd.DatabaseSession session, {
_isd.WhereExpressionBuilder<FeedbackDecisionTable>? where,
int? limit,
_isd.Transaction? transaction,
}) async {
return session.db.count<FeedbackDecision>(
where: where?.call(FeedbackDecision.t),
limit: limit,
transaction: transaction,
);
}
/// Acquires row-level locks on [FeedbackDecision] rows matching the [where] expression.
Future<void> lockRows(
_isd.DatabaseSession session, {
required _isd.WhereExpressionBuilder<FeedbackDecisionTable> where,
required _isd.LockMode lockMode,
required _isd.Transaction transaction,
_isd.LockBehavior lockBehavior = _isd.LockBehavior.wait,
}) async {
return session.db.lockRows<FeedbackDecision>(
where: where(FeedbackDecision.t),
lockMode: lockMode,
lockBehavior: lockBehavior,
transaction: transaction,
);
}
}

View file

@ -0,0 +1,706 @@
/* 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_client/serverpod_client.dart' as _isc;
import 'package:serverpod_database/serverpod_database.dart' as _isd;
/// A reply in a thread of reports and suggestions (FeedbackThread): by the
/// thread's writer or by the published studyset's author, in their own
/// space, so it works offline; the server gathers the replies of both. The
/// thread is named by its id, as it may be another person's (so no foreign
/// key: a device holds only its person's rows). Never changed but to blank
/// it. Synced.
abstract class FeedbackMessage
implements _isd.TableRow<_isc.UuidValue?>, _isc.ProtocolSerialization {
FeedbackMessage._({
this.id,
this.spaceId,
required this.threadId,
this.keyId,
required this.payload,
this.nonce,
});
factory FeedbackMessage({
_isc.UuidValue? id,
int? spaceId,
required _isc.UuidValue threadId,
String? keyId,
required _idt.ByteData payload,
_idt.ByteData? nonce,
}) = _FeedbackMessageImpl;
factory FeedbackMessage.fromJson(Map<String, dynamic> jsonSerialization) {
return FeedbackMessage(
id: jsonSerialization['id'] == null
? null
: _isc.UuidValueJsonExtension.fromJson(jsonSerialization['id']),
spaceId: jsonSerialization['spaceId'] as int?,
threadId: _isc.UuidValueJsonExtension.fromJson(
jsonSerialization['threadId'],
),
keyId: jsonSerialization['keyId'] as String?,
payload: _isc.ByteDataJsonExtension.fromJson(
jsonSerialization['payload'],
),
nonce: jsonSerialization['nonce'] == null
? null
: _isc.ByteDataJsonExtension.fromJson(jsonSerialization['nonce']),
);
}
static final t = FeedbackMessageTable();
static const db = FeedbackMessageRepository._();
@override
_isc.UuidValue? id;
/// The space owning this row. Maintained by the sync engine.
int? spaceId;
/// The thread replied in.
_isc.UuidValue threadId;
/// The encryption key's id; null, plaintext, the only value (ADR 0002).
String? keyId;
/// Its content: UTF-8 JSON with a `schemaVersion` and its text.
_idt.ByteData payload;
/// The encryption nonce; null, as for keyId.
_idt.ByteData? nonce;
@override
_isd.Table<_isc.UuidValue?> get table => t;
/// Returns a shallow copy of this [FeedbackMessage]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
FeedbackMessage copyWith({
_isc.UuidValue? id,
int? spaceId,
_isc.UuidValue? threadId,
String? keyId,
_idt.ByteData? payload,
_idt.ByteData? nonce,
});
@override
Map<String, dynamic> toJson() {
return {
'__className__': 'FeedbackMessage',
if (id != null) 'id': id?.toJson(),
if (spaceId != null) 'spaceId': spaceId,
'threadId': threadId.toJson(),
if (keyId != null) 'keyId': keyId,
'payload': payload.toJson(),
if (nonce != null) 'nonce': nonce?.toJson(),
};
}
@override
Map<String, dynamic> toJsonForProtocol() {
return {
'__className__': 'FeedbackMessage',
if (id != null) 'id': id?.toJson(),
if (spaceId != null) 'spaceId': spaceId,
'threadId': threadId.toJson(),
if (keyId != null) 'keyId': keyId,
'payload': payload.toJson(),
if (nonce != null) 'nonce': nonce?.toJson(),
};
}
static FeedbackMessageInclude include() {
return FeedbackMessageInclude._();
}
static FeedbackMessageIncludeList includeList({
_isd.WhereExpressionBuilder<FeedbackMessageTable>? where,
int? limit,
int? offset,
_isd.OrderByBuilder<FeedbackMessageTable>? orderBy,
_isd.OrderByListBuilder<FeedbackMessageTable>? orderByList,
FeedbackMessageInclude? include,
}) {
return FeedbackMessageIncludeList._(
where: where,
limit: limit,
offset: offset,
orderBy: orderBy?.call(FeedbackMessage.t),
orderByList: orderByList?.call(FeedbackMessage.t),
include: include,
);
}
@override
String toString() {
return _isc.SerializationManager.encode(this);
}
}
class _Undefined {}
class _FeedbackMessageImpl extends FeedbackMessage {
_FeedbackMessageImpl({
_isc.UuidValue? id,
int? spaceId,
required _isc.UuidValue threadId,
String? keyId,
required _idt.ByteData payload,
_idt.ByteData? nonce,
}) : super._(
id: id,
spaceId: spaceId,
threadId: threadId,
keyId: keyId,
payload: payload,
nonce: nonce,
);
/// Returns a shallow copy of this [FeedbackMessage]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
@override
FeedbackMessage copyWith({
Object? id = _Undefined,
Object? spaceId = _Undefined,
_isc.UuidValue? threadId,
Object? keyId = _Undefined,
_idt.ByteData? payload,
Object? nonce = _Undefined,
}) {
return FeedbackMessage(
id: id is _isc.UuidValue? ? id : this.id,
spaceId: spaceId is int? ? spaceId : this.spaceId,
threadId: threadId ?? this.threadId,
keyId: keyId is String? ? keyId : this.keyId,
payload: payload ?? this.payload.clone(),
nonce: nonce is _idt.ByteData? ? nonce : this.nonce?.clone(),
);
}
}
class FeedbackMessageUpdateTable
extends _isd.UpdateTable<FeedbackMessageTable> {
FeedbackMessageUpdateTable(super.table);
_isd.ColumnValue<int, int> spaceId(int? value) => _isd.ColumnValue(
table.spaceId,
value,
);
_isd.ColumnValue<_isc.UuidValue, _isc.UuidValue> threadId(
_isc.UuidValue value,
) => _isd.ColumnValue(
table.threadId,
value,
);
_isd.ColumnValue<String, String> keyId(String? value) => _isd.ColumnValue(
table.keyId,
value,
);
_isd.ColumnValue<_idt.ByteData, _idt.ByteData> payload(_idt.ByteData value) =>
_isd.ColumnValue(
table.payload,
value,
);
_isd.ColumnValue<_idt.ByteData, _idt.ByteData> nonce(_idt.ByteData? value) =>
_isd.ColumnValue(
table.nonce,
value,
);
}
class FeedbackMessageTable extends _isd.Table<_isc.UuidValue?> {
FeedbackMessageTable({super.tableRelation})
: super(tableName: 'feedback_message') {
updateTable = FeedbackMessageUpdateTable(this);
spaceId = _isd.ColumnInt(
'spaceId',
this,
);
threadId = _isd.ColumnUuid(
'threadId',
this,
);
keyId = _isd.ColumnString(
'keyId',
this,
);
payload = _isd.ColumnByteData(
'payload',
this,
);
nonce = _isd.ColumnByteData(
'nonce',
this,
);
}
late final FeedbackMessageUpdateTable updateTable;
/// The space owning this row. Maintained by the sync engine.
late final _isd.ColumnInt spaceId;
/// The thread replied in.
late final _isd.ColumnUuid threadId;
/// The encryption key's id; null, plaintext, the only value (ADR 0002).
late final _isd.ColumnString keyId;
/// Its content: UTF-8 JSON with a `schemaVersion` and its text.
late final _isd.ColumnByteData payload;
/// The encryption nonce; null, as for keyId.
late final _isd.ColumnByteData nonce;
@override
List<_isd.Column> get columns => [
id,
spaceId,
threadId,
keyId,
payload,
nonce,
];
}
class FeedbackMessageInclude extends _isd.IncludeObject {
FeedbackMessageInclude._();
@override
Map<String, _isd.Include?> get includes => {};
@override
_isd.Table<_isc.UuidValue?> get table => FeedbackMessage.t;
}
class FeedbackMessageIncludeList extends _isd.IncludeList {
FeedbackMessageIncludeList._({
_isd.WhereExpressionBuilder<FeedbackMessageTable>? where,
super.limit,
super.offset,
super.orderBy,
super.orderByList,
super.include,
}) {
super.where = where?.call(FeedbackMessage.t);
}
@override
Map<String, _isd.Include?> get includes => include?.includes ?? {};
@override
_isd.Table<_isc.UuidValue?> get table => FeedbackMessage.t;
}
class FeedbackMessageRepository {
const FeedbackMessageRepository._();
/// Returns a list of [FeedbackMessage]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<FeedbackMessage>> find(
_isd.DatabaseSession session, {
_isd.WhereExpressionBuilder<FeedbackMessageTable>? where,
int? limit,
int? offset,
_isd.OrderByBuilder<FeedbackMessageTable>? orderBy,
_isd.OrderByListBuilder<FeedbackMessageTable>? orderByList,
_isd.Transaction? transaction,
_isd.LockMode? lockMode,
_isd.LockBehavior? lockBehavior,
}) async {
return session.db.find<FeedbackMessage>(
where: where?.call(FeedbackMessage.t),
orderBy: orderBy?.call(FeedbackMessage.t),
orderByList: orderByList?.call(FeedbackMessage.t),
limit: limit,
offset: offset,
transaction: transaction,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Returns the first matching [FeedbackMessage] 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<FeedbackMessage?> findFirstRow(
_isd.DatabaseSession session, {
_isd.WhereExpressionBuilder<FeedbackMessageTable>? where,
int? offset,
_isd.OrderByBuilder<FeedbackMessageTable>? orderBy,
_isd.OrderByListBuilder<FeedbackMessageTable>? orderByList,
_isd.Transaction? transaction,
_isd.LockMode? lockMode,
_isd.LockBehavior? lockBehavior,
}) async {
return session.db.findFirstRow<FeedbackMessage>(
where: where?.call(FeedbackMessage.t),
orderBy: orderBy?.call(FeedbackMessage.t),
orderByList: orderByList?.call(FeedbackMessage.t),
offset: offset,
transaction: transaction,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Finds a single [FeedbackMessage] by its [id] or null if no such row exists.
Future<FeedbackMessage?> findById(
_isd.DatabaseSession session,
_isc.UuidValue id, {
_isd.Transaction? transaction,
_isd.LockMode? lockMode,
_isd.LockBehavior? lockBehavior,
}) async {
return session.db.findById<FeedbackMessage>(
id,
transaction: transaction,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Inserts all [FeedbackMessage]s in the list and returns the inserted rows.
///
/// The returned [FeedbackMessage]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<FeedbackMessage>> insert(
_isd.DatabaseSession session,
List<FeedbackMessage> rows, {
_isd.Transaction? transaction,
bool ignoreConflicts = false,
bool noReturn = false,
}) async {
return session.db.insert<FeedbackMessage>(
rows,
transaction: transaction,
ignoreConflicts: ignoreConflicts,
noReturn: noReturn,
);
}
/// Inserts a single [FeedbackMessage] and returns the inserted row.
///
/// The returned [FeedbackMessage] will have its `id` field set.
Future<FeedbackMessage> insertRow(
_isd.DatabaseSession session,
FeedbackMessage row, {
_isd.Transaction? transaction,
}) async {
return session.db.insertRow<FeedbackMessage>(
row,
transaction: transaction,
);
}
/// Upserts all [FeedbackMessage]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 [FeedbackMessage]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<FeedbackMessage>> upsert(
_isd.DatabaseSession session,
List<FeedbackMessage> rows, {
required _isd.ColumnSelections<FeedbackMessageTable> conflictColumns,
_isd.ColumnSelections<FeedbackMessageTable>? updateColumns,
_isd.WhereExpressionBuilder<FeedbackMessageTable>? updateWhere,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.upsert<FeedbackMessage>(
rows,
conflictColumns: conflictColumns(FeedbackMessage.t),
updateColumns: updateColumns?.call(FeedbackMessage.t),
updateWhere: updateWhere?.call(FeedbackMessage.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Upserts a single [FeedbackMessage] 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 [FeedbackMessage] will have its `id` field set.
Future<FeedbackMessage?> upsertRow(
_isd.DatabaseSession session,
FeedbackMessage row, {
required _isd.ColumnSelections<FeedbackMessageTable> conflictColumns,
_isd.ColumnSelections<FeedbackMessageTable>? updateColumns,
_isd.WhereExpressionBuilder<FeedbackMessageTable>? updateWhere,
_isd.Transaction? transaction,
}) async {
return session.db.upsertRow<FeedbackMessage>(
row,
conflictColumns: conflictColumns(FeedbackMessage.t),
updateColumns: updateColumns?.call(FeedbackMessage.t),
updateWhere: updateWhere?.call(FeedbackMessage.t),
transaction: transaction,
);
}
/// Updates all [FeedbackMessage]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<FeedbackMessage>> update(
_isd.DatabaseSession session,
List<FeedbackMessage> rows, {
_isd.ColumnSelections<FeedbackMessageTable>? columns,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.update<FeedbackMessage>(
rows,
columns: columns?.call(FeedbackMessage.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Updates a single [FeedbackMessage]. 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<FeedbackMessage> updateRow(
_isd.DatabaseSession session,
FeedbackMessage row, {
_isd.ColumnSelections<FeedbackMessageTable>? columns,
_isd.Transaction? transaction,
}) async {
return session.db.updateRow<FeedbackMessage>(
row,
columns: columns?.call(FeedbackMessage.t),
transaction: transaction,
);
}
/// Updates a single [FeedbackMessage] by its [id] with the specified [columnValues].
/// Returns the updated row or null if no row with the given id exists.
Future<FeedbackMessage?> updateById(
_isd.DatabaseSession session,
_isc.UuidValue id, {
required _isd.ColumnValueListBuilder<FeedbackMessageUpdateTable>
columnValues,
_isd.Transaction? transaction,
}) async {
return session.db.updateById<FeedbackMessage>(
id,
columnValues: columnValues(FeedbackMessage.t.updateTable),
transaction: transaction,
);
}
/// Updates all [FeedbackMessage]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<FeedbackMessage>> updateWhere(
_isd.DatabaseSession session, {
required _isd.ColumnValueListBuilder<FeedbackMessageUpdateTable>
columnValues,
required _isd.WhereExpressionBuilder<FeedbackMessageTable> where,
int? limit,
int? offset,
_isd.OrderByBuilder<FeedbackMessageTable>? orderBy,
_isd.OrderByListBuilder<FeedbackMessageTable>? orderByList,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.updateWhere<FeedbackMessage>(
columnValues: columnValues(FeedbackMessage.t.updateTable),
where: where(FeedbackMessage.t),
limit: limit,
offset: offset,
orderBy: orderBy?.call(FeedbackMessage.t),
orderByList: orderByList?.call(FeedbackMessage.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Deletes all [FeedbackMessage]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<FeedbackMessage>> delete(
_isd.DatabaseSession session,
List<FeedbackMessage> rows, {
_isd.OrderByBuilder<FeedbackMessageTable>? orderBy,
_isd.OrderByListBuilder<FeedbackMessageTable>? orderByList,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.delete<FeedbackMessage>(
rows,
orderBy: orderBy?.call(FeedbackMessage.t),
orderByList: orderByList?.call(FeedbackMessage.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Deletes a single [FeedbackMessage].
Future<FeedbackMessage> deleteRow(
_isd.DatabaseSession session,
FeedbackMessage row, {
_isd.Transaction? transaction,
}) async {
return session.db.deleteRow<FeedbackMessage>(
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<FeedbackMessage>> deleteWhere(
_isd.DatabaseSession session, {
required _isd.WhereExpressionBuilder<FeedbackMessageTable> where,
_isd.OrderByBuilder<FeedbackMessageTable>? orderBy,
_isd.OrderByListBuilder<FeedbackMessageTable>? orderByList,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.deleteWhere<FeedbackMessage>(
where: where(FeedbackMessage.t),
orderBy: orderBy?.call(FeedbackMessage.t),
orderByList: orderByList?.call(FeedbackMessage.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(
_isd.DatabaseSession session, {
_isd.WhereExpressionBuilder<FeedbackMessageTable>? where,
int? limit,
_isd.Transaction? transaction,
}) async {
return session.db.count<FeedbackMessage>(
where: where?.call(FeedbackMessage.t),
limit: limit,
transaction: transaction,
);
}
/// Acquires row-level locks on [FeedbackMessage] rows matching the [where] expression.
Future<void> lockRows(
_isd.DatabaseSession session, {
required _isd.WhereExpressionBuilder<FeedbackMessageTable> where,
required _isd.LockMode lockMode,
required _isd.Transaction transaction,
_isd.LockBehavior lockBehavior = _isd.LockBehavior.wait,
}) async {
return session.db.lockRows<FeedbackMessage>(
where: where(FeedbackMessage.t),
lockMode: lockMode,
lockBehavior: lockBehavior,
transaction: transaction,
);
}
}

View file

@ -0,0 +1,139 @@
/* 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 'package:halkyon_learn_client/src/protocol/protocol.dart' as _itx5j3f5;
import 'package:serverpod_client/serverpod_client.dart' as _isc;
import '../feedback/feedback_person.dart' as _i4q3oto5;
/// A reply in a thread, as the app shows it.
abstract class FeedbackMessageView
implements _isc.SerializableModel, _isc.ProtocolSerialization {
FeedbackMessageView._({
required this.id,
required this.by,
required this.fromAuthor,
required this.payload,
required this.createdAt,
});
factory FeedbackMessageView({
required _isc.UuidValue id,
required _i4q3oto5.FeedbackPerson by,
required bool fromAuthor,
required String payload,
required DateTime createdAt,
}) = _FeedbackMessageViewImpl;
factory FeedbackMessageView.fromJson(Map<String, dynamic> jsonSerialization) {
return FeedbackMessageView(
id: _isc.UuidValueJsonExtension.fromJson(jsonSerialization['id']),
by: _itx5j3f5.Protocol().deserialize<_i4q3oto5.FeedbackPerson>(
jsonSerialization['by'],
),
fromAuthor: _isc.BoolJsonExtension.fromJson(
jsonSerialization['fromAuthor'],
),
payload: jsonSerialization['payload'] as String,
createdAt: _isc.DateTimeJsonExtension.fromJson(
jsonSerialization['createdAt'],
),
);
}
_isc.UuidValue id;
/// Who wrote it, and whether that is the published studyset's author.
_i4q3oto5.FeedbackPerson by;
bool fromAuthor;
/// What it says: its payload as written.
String payload;
/// When, in UTC.
DateTime createdAt;
/// Returns a shallow copy of this [FeedbackMessageView]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
FeedbackMessageView copyWith({
_isc.UuidValue? id,
_i4q3oto5.FeedbackPerson? by,
bool? fromAuthor,
String? payload,
DateTime? createdAt,
});
@override
Map<String, dynamic> toJson() {
return {
'__className__': 'FeedbackMessageView',
'id': id.toJson(),
'by': by.toJson(),
'fromAuthor': fromAuthor,
'payload': payload,
'createdAt': createdAt.toJson(),
};
}
@override
Map<String, dynamic> toJsonForProtocol() {
return {
'__className__': 'FeedbackMessageView',
'id': id.toJson(),
'by': by.toJsonForProtocol(),
'fromAuthor': fromAuthor,
'payload': payload,
'createdAt': createdAt.toJson(),
};
}
@override
String toString() {
return _isc.SerializationManager.encode(this);
}
}
class _FeedbackMessageViewImpl extends FeedbackMessageView {
_FeedbackMessageViewImpl({
required _isc.UuidValue id,
required _i4q3oto5.FeedbackPerson by,
required bool fromAuthor,
required String payload,
required DateTime createdAt,
}) : super._(
id: id,
by: by,
fromAuthor: fromAuthor,
payload: payload,
createdAt: createdAt,
);
/// Returns a shallow copy of this [FeedbackMessageView]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
@override
FeedbackMessageView copyWith({
_isc.UuidValue? id,
_i4q3oto5.FeedbackPerson? by,
bool? fromAuthor,
String? payload,
DateTime? createdAt,
}) {
return FeedbackMessageView(
id: id ?? this.id,
by: by ?? this.by.copyWith(),
fromAuthor: fromAuthor ?? this.fromAuthor,
payload: payload ?? this.payload,
createdAt: createdAt ?? this.createdAt,
);
}
}

View file

@ -0,0 +1,106 @@
/* 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 'package:serverpod_client/serverpod_client.dart' as _isc;
/// Someone in a thread, as their public profile shows them.
abstract class FeedbackPerson
implements _isc.SerializableModel, _isc.ProtocolSerialization {
FeedbackPerson._({
required this.handle,
required this.displayName,
this.avatarUrl,
});
factory FeedbackPerson({
required String handle,
required String displayName,
String? avatarUrl,
}) = _FeedbackPersonImpl;
factory FeedbackPerson.fromJson(Map<String, dynamic> jsonSerialization) {
return FeedbackPerson(
handle: jsonSerialization['handle'] as String,
displayName: jsonSerialization['displayName'] as String,
avatarUrl: jsonSerialization['avatarUrl'] as String?,
);
}
String handle;
String displayName;
String? avatarUrl;
/// Returns a shallow copy of this [FeedbackPerson]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
FeedbackPerson copyWith({
String? handle,
String? displayName,
String? avatarUrl,
});
@override
Map<String, dynamic> toJson() {
return {
'__className__': 'FeedbackPerson',
'handle': handle,
'displayName': displayName,
if (avatarUrl != null) 'avatarUrl': avatarUrl,
};
}
@override
Map<String, dynamic> toJsonForProtocol() {
return {
'__className__': 'FeedbackPerson',
'handle': handle,
'displayName': displayName,
if (avatarUrl != null) 'avatarUrl': avatarUrl,
};
}
@override
String toString() {
return _isc.SerializationManager.encode(this);
}
}
class _Undefined {}
class _FeedbackPersonImpl extends FeedbackPerson {
_FeedbackPersonImpl({
required String handle,
required String displayName,
String? avatarUrl,
}) : super._(
handle: handle,
displayName: displayName,
avatarUrl: avatarUrl,
);
/// Returns a shallow copy of this [FeedbackPerson]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
@override
FeedbackPerson copyWith({
String? handle,
String? displayName,
Object? avatarUrl = _Undefined,
}) {
return FeedbackPerson(
handle: handle ?? this.handle,
displayName: displayName ?? this.displayName,
avatarUrl: avatarUrl is String? ? avatarUrl : this.avatarUrl,
);
}
}

View file

@ -0,0 +1,735 @@
/* 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_client/serverpod_client.dart' as _isc;
import 'package:serverpod_database/serverpod_database.dart' as _isd;
/// A report or a suggestion on a published studyset (ADR 0005, rule 6):
/// written in its writer's own space, so it works offline; the server gathers
/// each publication's for its author's inbox and for everyone learning it to
/// read (public unless the author hides it). What it says is in its payload:
/// what it is about (the studyset, or one of its cards, notes or steps, by
/// its id in the release, and the release), a report's category and text, a
/// suggestion's message and the content it proposes, and the files that
/// content names. Never changed but to blank it. Synced.
abstract class FeedbackThread
implements _isd.TableRow<_isc.UuidValue?>, _isc.ProtocolSerialization {
FeedbackThread._({
this.id,
this.spaceId,
required this.publicationId,
required this.kind,
this.keyId,
required this.payload,
this.nonce,
});
factory FeedbackThread({
_isc.UuidValue? id,
int? spaceId,
required _isc.UuidValue publicationId,
required String kind,
String? keyId,
required _idt.ByteData payload,
_idt.ByteData? nonce,
}) = _FeedbackThreadImpl;
factory FeedbackThread.fromJson(Map<String, dynamic> jsonSerialization) {
return FeedbackThread(
id: jsonSerialization['id'] == null
? null
: _isc.UuidValueJsonExtension.fromJson(jsonSerialization['id']),
spaceId: jsonSerialization['spaceId'] as int?,
publicationId: _isc.UuidValueJsonExtension.fromJson(
jsonSerialization['publicationId'],
),
kind: jsonSerialization['kind'] as String,
keyId: jsonSerialization['keyId'] as String?,
payload: _isc.ByteDataJsonExtension.fromJson(
jsonSerialization['payload'],
),
nonce: jsonSerialization['nonce'] == null
? null
: _isc.ByteDataJsonExtension.fromJson(jsonSerialization['nonce']),
);
}
static final t = FeedbackThreadTable();
static const db = FeedbackThreadRepository._();
@override
_isc.UuidValue? id;
/// The space owning this row. Maintained by the sync engine.
int? spaceId;
/// The published studyset it is on (its public id): a column, for the
/// server to gather by.
_isc.UuidValue publicationId;
/// 'report' or 'suggestion'; a kind this app doesn't know is skipped.
String kind;
/// The encryption key's id; null, plaintext, the only value (ADR 0002).
String? keyId;
/// Its content: UTF-8 JSON with a `schemaVersion`.
_idt.ByteData payload;
/// The encryption nonce; null, as for keyId.
_idt.ByteData? nonce;
@override
_isd.Table<_isc.UuidValue?> get table => t;
/// Returns a shallow copy of this [FeedbackThread]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
FeedbackThread copyWith({
_isc.UuidValue? id,
int? spaceId,
_isc.UuidValue? publicationId,
String? kind,
String? keyId,
_idt.ByteData? payload,
_idt.ByteData? nonce,
});
@override
Map<String, dynamic> toJson() {
return {
'__className__': 'FeedbackThread',
if (id != null) 'id': id?.toJson(),
if (spaceId != null) 'spaceId': spaceId,
'publicationId': publicationId.toJson(),
'kind': kind,
if (keyId != null) 'keyId': keyId,
'payload': payload.toJson(),
if (nonce != null) 'nonce': nonce?.toJson(),
};
}
@override
Map<String, dynamic> toJsonForProtocol() {
return {
'__className__': 'FeedbackThread',
if (id != null) 'id': id?.toJson(),
if (spaceId != null) 'spaceId': spaceId,
'publicationId': publicationId.toJson(),
'kind': kind,
if (keyId != null) 'keyId': keyId,
'payload': payload.toJson(),
if (nonce != null) 'nonce': nonce?.toJson(),
};
}
static FeedbackThreadInclude include() {
return FeedbackThreadInclude._();
}
static FeedbackThreadIncludeList includeList({
_isd.WhereExpressionBuilder<FeedbackThreadTable>? where,
int? limit,
int? offset,
_isd.OrderByBuilder<FeedbackThreadTable>? orderBy,
_isd.OrderByListBuilder<FeedbackThreadTable>? orderByList,
FeedbackThreadInclude? include,
}) {
return FeedbackThreadIncludeList._(
where: where,
limit: limit,
offset: offset,
orderBy: orderBy?.call(FeedbackThread.t),
orderByList: orderByList?.call(FeedbackThread.t),
include: include,
);
}
@override
String toString() {
return _isc.SerializationManager.encode(this);
}
}
class _Undefined {}
class _FeedbackThreadImpl extends FeedbackThread {
_FeedbackThreadImpl({
_isc.UuidValue? id,
int? spaceId,
required _isc.UuidValue publicationId,
required String kind,
String? keyId,
required _idt.ByteData payload,
_idt.ByteData? nonce,
}) : super._(
id: id,
spaceId: spaceId,
publicationId: publicationId,
kind: kind,
keyId: keyId,
payload: payload,
nonce: nonce,
);
/// Returns a shallow copy of this [FeedbackThread]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
@override
FeedbackThread copyWith({
Object? id = _Undefined,
Object? spaceId = _Undefined,
_isc.UuidValue? publicationId,
String? kind,
Object? keyId = _Undefined,
_idt.ByteData? payload,
Object? nonce = _Undefined,
}) {
return FeedbackThread(
id: id is _isc.UuidValue? ? id : this.id,
spaceId: spaceId is int? ? spaceId : this.spaceId,
publicationId: publicationId ?? this.publicationId,
kind: kind ?? this.kind,
keyId: keyId is String? ? keyId : this.keyId,
payload: payload ?? this.payload.clone(),
nonce: nonce is _idt.ByteData? ? nonce : this.nonce?.clone(),
);
}
}
class FeedbackThreadUpdateTable extends _isd.UpdateTable<FeedbackThreadTable> {
FeedbackThreadUpdateTable(super.table);
_isd.ColumnValue<int, int> spaceId(int? value) => _isd.ColumnValue(
table.spaceId,
value,
);
_isd.ColumnValue<_isc.UuidValue, _isc.UuidValue> publicationId(
_isc.UuidValue value,
) => _isd.ColumnValue(
table.publicationId,
value,
);
_isd.ColumnValue<String, String> kind(String value) => _isd.ColumnValue(
table.kind,
value,
);
_isd.ColumnValue<String, String> keyId(String? value) => _isd.ColumnValue(
table.keyId,
value,
);
_isd.ColumnValue<_idt.ByteData, _idt.ByteData> payload(_idt.ByteData value) =>
_isd.ColumnValue(
table.payload,
value,
);
_isd.ColumnValue<_idt.ByteData, _idt.ByteData> nonce(_idt.ByteData? value) =>
_isd.ColumnValue(
table.nonce,
value,
);
}
class FeedbackThreadTable extends _isd.Table<_isc.UuidValue?> {
FeedbackThreadTable({super.tableRelation})
: super(tableName: 'feedback_thread') {
updateTable = FeedbackThreadUpdateTable(this);
spaceId = _isd.ColumnInt(
'spaceId',
this,
);
publicationId = _isd.ColumnUuid(
'publicationId',
this,
);
kind = _isd.ColumnString(
'kind',
this,
);
keyId = _isd.ColumnString(
'keyId',
this,
);
payload = _isd.ColumnByteData(
'payload',
this,
);
nonce = _isd.ColumnByteData(
'nonce',
this,
);
}
late final FeedbackThreadUpdateTable updateTable;
/// The space owning this row. Maintained by the sync engine.
late final _isd.ColumnInt spaceId;
/// The published studyset it is on (its public id): a column, for the
/// server to gather by.
late final _isd.ColumnUuid publicationId;
/// 'report' or 'suggestion'; a kind this app doesn't know is skipped.
late final _isd.ColumnString kind;
/// The encryption key's id; null, plaintext, the only value (ADR 0002).
late final _isd.ColumnString keyId;
/// Its content: UTF-8 JSON with a `schemaVersion`.
late final _isd.ColumnByteData payload;
/// The encryption nonce; null, as for keyId.
late final _isd.ColumnByteData nonce;
@override
List<_isd.Column> get columns => [
id,
spaceId,
publicationId,
kind,
keyId,
payload,
nonce,
];
}
class FeedbackThreadInclude extends _isd.IncludeObject {
FeedbackThreadInclude._();
@override
Map<String, _isd.Include?> get includes => {};
@override
_isd.Table<_isc.UuidValue?> get table => FeedbackThread.t;
}
class FeedbackThreadIncludeList extends _isd.IncludeList {
FeedbackThreadIncludeList._({
_isd.WhereExpressionBuilder<FeedbackThreadTable>? where,
super.limit,
super.offset,
super.orderBy,
super.orderByList,
super.include,
}) {
super.where = where?.call(FeedbackThread.t);
}
@override
Map<String, _isd.Include?> get includes => include?.includes ?? {};
@override
_isd.Table<_isc.UuidValue?> get table => FeedbackThread.t;
}
class FeedbackThreadRepository {
const FeedbackThreadRepository._();
/// Returns a list of [FeedbackThread]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<FeedbackThread>> find(
_isd.DatabaseSession session, {
_isd.WhereExpressionBuilder<FeedbackThreadTable>? where,
int? limit,
int? offset,
_isd.OrderByBuilder<FeedbackThreadTable>? orderBy,
_isd.OrderByListBuilder<FeedbackThreadTable>? orderByList,
_isd.Transaction? transaction,
_isd.LockMode? lockMode,
_isd.LockBehavior? lockBehavior,
}) async {
return session.db.find<FeedbackThread>(
where: where?.call(FeedbackThread.t),
orderBy: orderBy?.call(FeedbackThread.t),
orderByList: orderByList?.call(FeedbackThread.t),
limit: limit,
offset: offset,
transaction: transaction,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Returns the first matching [FeedbackThread] 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<FeedbackThread?> findFirstRow(
_isd.DatabaseSession session, {
_isd.WhereExpressionBuilder<FeedbackThreadTable>? where,
int? offset,
_isd.OrderByBuilder<FeedbackThreadTable>? orderBy,
_isd.OrderByListBuilder<FeedbackThreadTable>? orderByList,
_isd.Transaction? transaction,
_isd.LockMode? lockMode,
_isd.LockBehavior? lockBehavior,
}) async {
return session.db.findFirstRow<FeedbackThread>(
where: where?.call(FeedbackThread.t),
orderBy: orderBy?.call(FeedbackThread.t),
orderByList: orderByList?.call(FeedbackThread.t),
offset: offset,
transaction: transaction,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Finds a single [FeedbackThread] by its [id] or null if no such row exists.
Future<FeedbackThread?> findById(
_isd.DatabaseSession session,
_isc.UuidValue id, {
_isd.Transaction? transaction,
_isd.LockMode? lockMode,
_isd.LockBehavior? lockBehavior,
}) async {
return session.db.findById<FeedbackThread>(
id,
transaction: transaction,
lockMode: lockMode,
lockBehavior: lockBehavior,
);
}
/// Inserts all [FeedbackThread]s in the list and returns the inserted rows.
///
/// The returned [FeedbackThread]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<FeedbackThread>> insert(
_isd.DatabaseSession session,
List<FeedbackThread> rows, {
_isd.Transaction? transaction,
bool ignoreConflicts = false,
bool noReturn = false,
}) async {
return session.db.insert<FeedbackThread>(
rows,
transaction: transaction,
ignoreConflicts: ignoreConflicts,
noReturn: noReturn,
);
}
/// Inserts a single [FeedbackThread] and returns the inserted row.
///
/// The returned [FeedbackThread] will have its `id` field set.
Future<FeedbackThread> insertRow(
_isd.DatabaseSession session,
FeedbackThread row, {
_isd.Transaction? transaction,
}) async {
return session.db.insertRow<FeedbackThread>(
row,
transaction: transaction,
);
}
/// Upserts all [FeedbackThread]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 [FeedbackThread]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<FeedbackThread>> upsert(
_isd.DatabaseSession session,
List<FeedbackThread> rows, {
required _isd.ColumnSelections<FeedbackThreadTable> conflictColumns,
_isd.ColumnSelections<FeedbackThreadTable>? updateColumns,
_isd.WhereExpressionBuilder<FeedbackThreadTable>? updateWhere,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.upsert<FeedbackThread>(
rows,
conflictColumns: conflictColumns(FeedbackThread.t),
updateColumns: updateColumns?.call(FeedbackThread.t),
updateWhere: updateWhere?.call(FeedbackThread.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Upserts a single [FeedbackThread] 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 [FeedbackThread] will have its `id` field set.
Future<FeedbackThread?> upsertRow(
_isd.DatabaseSession session,
FeedbackThread row, {
required _isd.ColumnSelections<FeedbackThreadTable> conflictColumns,
_isd.ColumnSelections<FeedbackThreadTable>? updateColumns,
_isd.WhereExpressionBuilder<FeedbackThreadTable>? updateWhere,
_isd.Transaction? transaction,
}) async {
return session.db.upsertRow<FeedbackThread>(
row,
conflictColumns: conflictColumns(FeedbackThread.t),
updateColumns: updateColumns?.call(FeedbackThread.t),
updateWhere: updateWhere?.call(FeedbackThread.t),
transaction: transaction,
);
}
/// Updates all [FeedbackThread]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<FeedbackThread>> update(
_isd.DatabaseSession session,
List<FeedbackThread> rows, {
_isd.ColumnSelections<FeedbackThreadTable>? columns,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.update<FeedbackThread>(
rows,
columns: columns?.call(FeedbackThread.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Updates a single [FeedbackThread]. 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<FeedbackThread> updateRow(
_isd.DatabaseSession session,
FeedbackThread row, {
_isd.ColumnSelections<FeedbackThreadTable>? columns,
_isd.Transaction? transaction,
}) async {
return session.db.updateRow<FeedbackThread>(
row,
columns: columns?.call(FeedbackThread.t),
transaction: transaction,
);
}
/// Updates a single [FeedbackThread] by its [id] with the specified [columnValues].
/// Returns the updated row or null if no row with the given id exists.
Future<FeedbackThread?> updateById(
_isd.DatabaseSession session,
_isc.UuidValue id, {
required _isd.ColumnValueListBuilder<FeedbackThreadUpdateTable>
columnValues,
_isd.Transaction? transaction,
}) async {
return session.db.updateById<FeedbackThread>(
id,
columnValues: columnValues(FeedbackThread.t.updateTable),
transaction: transaction,
);
}
/// Updates all [FeedbackThread]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<FeedbackThread>> updateWhere(
_isd.DatabaseSession session, {
required _isd.ColumnValueListBuilder<FeedbackThreadUpdateTable>
columnValues,
required _isd.WhereExpressionBuilder<FeedbackThreadTable> where,
int? limit,
int? offset,
_isd.OrderByBuilder<FeedbackThreadTable>? orderBy,
_isd.OrderByListBuilder<FeedbackThreadTable>? orderByList,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.updateWhere<FeedbackThread>(
columnValues: columnValues(FeedbackThread.t.updateTable),
where: where(FeedbackThread.t),
limit: limit,
offset: offset,
orderBy: orderBy?.call(FeedbackThread.t),
orderByList: orderByList?.call(FeedbackThread.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Deletes all [FeedbackThread]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<FeedbackThread>> delete(
_isd.DatabaseSession session,
List<FeedbackThread> rows, {
_isd.OrderByBuilder<FeedbackThreadTable>? orderBy,
_isd.OrderByListBuilder<FeedbackThreadTable>? orderByList,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.delete<FeedbackThread>(
rows,
orderBy: orderBy?.call(FeedbackThread.t),
orderByList: orderByList?.call(FeedbackThread.t),
transaction: transaction,
noReturn: noReturn,
);
}
/// Deletes a single [FeedbackThread].
Future<FeedbackThread> deleteRow(
_isd.DatabaseSession session,
FeedbackThread row, {
_isd.Transaction? transaction,
}) async {
return session.db.deleteRow<FeedbackThread>(
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<FeedbackThread>> deleteWhere(
_isd.DatabaseSession session, {
required _isd.WhereExpressionBuilder<FeedbackThreadTable> where,
_isd.OrderByBuilder<FeedbackThreadTable>? orderBy,
_isd.OrderByListBuilder<FeedbackThreadTable>? orderByList,
_isd.Transaction? transaction,
bool noReturn = false,
}) async {
return session.db.deleteWhere<FeedbackThread>(
where: where(FeedbackThread.t),
orderBy: orderBy?.call(FeedbackThread.t),
orderByList: orderByList?.call(FeedbackThread.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(
_isd.DatabaseSession session, {
_isd.WhereExpressionBuilder<FeedbackThreadTable>? where,
int? limit,
_isd.Transaction? transaction,
}) async {
return session.db.count<FeedbackThread>(
where: where?.call(FeedbackThread.t),
limit: limit,
transaction: transaction,
);
}
/// Acquires row-level locks on [FeedbackThread] rows matching the [where] expression.
Future<void> lockRows(
_isd.DatabaseSession session, {
required _isd.WhereExpressionBuilder<FeedbackThreadTable> where,
required _isd.LockMode lockMode,
required _isd.Transaction transaction,
_isd.LockBehavior lockBehavior = _isd.LockBehavior.wait,
}) async {
return session.db.lockRows<FeedbackThread>(
where: where(FeedbackThread.t),
lockMode: lockMode,
lockBehavior: lockBehavior,
transaction: transaction,
);
}
}

View file

@ -0,0 +1,262 @@
/* 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 'package:halkyon_learn_client/src/protocol/protocol.dart' as _itx5j3f5;
import 'package:serverpod_client/serverpod_client.dart' as _isc;
import '../feedback/feedback_message_view.dart' as _irmjyw1o;
import '../feedback/feedback_person.dart' as _i4q3oto5;
/// A thread of reports and suggestions as the app shows it: on which
/// published studyset, by whom, what it says (its payload's JSON), its
/// replies, and where it stands.
abstract class FeedbackView
implements _isc.SerializableModel, _isc.ProtocolSerialization {
FeedbackView._({
required this.threadId,
required this.publicId,
required this.studysetTitle,
required this.location,
required this.kind,
required this.writer,
required this.payload,
required this.createdAt,
required this.messages,
required this.status,
required this.hidden,
required this.mine,
required this.forMe,
required this.unseen,
});
factory FeedbackView({
required _isc.UuidValue threadId,
required _isc.UuidValue publicId,
required String studysetTitle,
required String location,
required String kind,
required _i4q3oto5.FeedbackPerson writer,
required String payload,
required DateTime createdAt,
required List<_irmjyw1o.FeedbackMessageView> messages,
required String status,
required bool hidden,
required bool mine,
required bool forMe,
required bool unseen,
}) = _FeedbackViewImpl;
factory FeedbackView.fromJson(Map<String, dynamic> jsonSerialization) {
return FeedbackView(
threadId: _isc.UuidValueJsonExtension.fromJson(
jsonSerialization['threadId'],
),
publicId: _isc.UuidValueJsonExtension.fromJson(
jsonSerialization['publicId'],
),
studysetTitle: jsonSerialization['studysetTitle'] as String,
location: jsonSerialization['location'] as String,
kind: jsonSerialization['kind'] as String,
writer: _itx5j3f5.Protocol().deserialize<_i4q3oto5.FeedbackPerson>(
jsonSerialization['writer'],
),
payload: jsonSerialization['payload'] as String,
createdAt: _isc.DateTimeJsonExtension.fromJson(
jsonSerialization['createdAt'],
),
messages: _itx5j3f5.Protocol()
.deserialize<List<_irmjyw1o.FeedbackMessageView>>(
jsonSerialization['messages'],
),
status: jsonSerialization['status'] as String,
hidden: _isc.BoolJsonExtension.fromJson(jsonSerialization['hidden']),
mine: _isc.BoolJsonExtension.fromJson(jsonSerialization['mine']),
forMe: _isc.BoolJsonExtension.fromJson(jsonSerialization['forMe']),
unseen: _isc.BoolJsonExtension.fromJson(jsonSerialization['unseen']),
);
}
_isc.UuidValue threadId;
/// The published studyset: its public id, title and location.
_isc.UuidValue publicId;
String studysetTitle;
String location;
/// 'report' or 'suggestion'.
String kind;
/// Who wrote it.
_i4q3oto5.FeedbackPerson writer;
/// What it says: its payload as written.
String payload;
/// When it was written, in UTC.
DateTime createdAt;
/// Its replies, oldest first.
List<_irmjyw1o.FeedbackMessageView> messages;
/// 'open', 'closed', 'accepted' or 'declined'.
String status;
/// Whether the author hid it: then only its writer and the author see it.
bool hidden;
/// Whether the caller wrote it; whether the caller is the author.
bool mine;
bool forMe;
/// Whether something in it is new to the caller.
bool unseen;
/// Returns a shallow copy of this [FeedbackView]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
FeedbackView copyWith({
_isc.UuidValue? threadId,
_isc.UuidValue? publicId,
String? studysetTitle,
String? location,
String? kind,
_i4q3oto5.FeedbackPerson? writer,
String? payload,
DateTime? createdAt,
List<_irmjyw1o.FeedbackMessageView>? messages,
String? status,
bool? hidden,
bool? mine,
bool? forMe,
bool? unseen,
});
@override
Map<String, dynamic> toJson() {
return {
'__className__': 'FeedbackView',
'threadId': threadId.toJson(),
'publicId': publicId.toJson(),
'studysetTitle': studysetTitle,
'location': location,
'kind': kind,
'writer': writer.toJson(),
'payload': payload,
'createdAt': createdAt.toJson(),
'messages': messages.toJson(valueToJson: (v) => v.toJson()),
'status': status,
'hidden': hidden,
'mine': mine,
'forMe': forMe,
'unseen': unseen,
};
}
@override
Map<String, dynamic> toJsonForProtocol() {
return {
'__className__': 'FeedbackView',
'threadId': threadId.toJson(),
'publicId': publicId.toJson(),
'studysetTitle': studysetTitle,
'location': location,
'kind': kind,
'writer': writer.toJsonForProtocol(),
'payload': payload,
'createdAt': createdAt.toJson(),
'messages': messages.toJson(valueToJson: (v) => v.toJsonForProtocol()),
'status': status,
'hidden': hidden,
'mine': mine,
'forMe': forMe,
'unseen': unseen,
};
}
@override
String toString() {
return _isc.SerializationManager.encode(this);
}
}
class _FeedbackViewImpl extends FeedbackView {
_FeedbackViewImpl({
required _isc.UuidValue threadId,
required _isc.UuidValue publicId,
required String studysetTitle,
required String location,
required String kind,
required _i4q3oto5.FeedbackPerson writer,
required String payload,
required DateTime createdAt,
required List<_irmjyw1o.FeedbackMessageView> messages,
required String status,
required bool hidden,
required bool mine,
required bool forMe,
required bool unseen,
}) : super._(
threadId: threadId,
publicId: publicId,
studysetTitle: studysetTitle,
location: location,
kind: kind,
writer: writer,
payload: payload,
createdAt: createdAt,
messages: messages,
status: status,
hidden: hidden,
mine: mine,
forMe: forMe,
unseen: unseen,
);
/// Returns a shallow copy of this [FeedbackView]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
@override
FeedbackView copyWith({
_isc.UuidValue? threadId,
_isc.UuidValue? publicId,
String? studysetTitle,
String? location,
String? kind,
_i4q3oto5.FeedbackPerson? writer,
String? payload,
DateTime? createdAt,
List<_irmjyw1o.FeedbackMessageView>? messages,
String? status,
bool? hidden,
bool? mine,
bool? forMe,
bool? unseen,
}) {
return FeedbackView(
threadId: threadId ?? this.threadId,
publicId: publicId ?? this.publicId,
studysetTitle: studysetTitle ?? this.studysetTitle,
location: location ?? this.location,
kind: kind ?? this.kind,
writer: writer ?? this.writer.copyWith(),
payload: payload ?? this.payload,
createdAt: createdAt ?? this.createdAt,
messages: messages ?? this.messages.map((e0) => e0.copyWith()).toList(),
status: status ?? this.status,
hidden: hidden ?? this.hidden,
mine: mine ?? this.mine,
forMe: forMe ?? this.forMe,
unseen: unseen ?? this.unseen,
);
}
}

View file

@ -19,6 +19,8 @@ import 'package:halkyon_learn_client/src/protocol/dictionary/dictionary_source.d
as _i8vj64sp;
import 'package:halkyon_learn_client/src/protocol/explore/explore_entry.dart'
as _iyigb6vc;
import 'package:halkyon_learn_client/src/protocol/feedback/feedback_view.dart'
as _idpwh21d;
import 'package:serverpod_auth_core_client/serverpod_auth_core_client.dart'
as _iacc;
import 'package:serverpod_auth_idp_client/serverpod_auth_idp_client.dart'
@ -45,6 +47,12 @@ import 'explore/explore_entry.dart' as _iaxdli27;
import 'explore/explore_query.dart' as _ibl9h9o9;
import 'explore/release_content.dart' as _ievpeb4f;
import 'explore/release_item.dart' as _iaswq7ap;
import 'feedback/feedback_decision.dart' as _idsmz0xa;
import 'feedback/feedback_message.dart' as _iqqrr7at;
import 'feedback/feedback_message_view.dart' as _inlv2ksv;
import 'feedback/feedback_person.dart' as _i8yyqgue;
import 'feedback/feedback_thread.dart' as _ihfcaz5v;
import 'feedback/feedback_view.dart' as _in90wtl8;
import 'instance/instance_info.dart' as _iskrg2g3;
import 'learning_path/learning_path.dart' as _ivt5m868;
import 'learning_path/learning_path_answer.dart' as _iy9sv3bs;
@ -86,6 +94,12 @@ export 'explore/explore_entry.dart';
export 'explore/explore_query.dart';
export 'explore/release_content.dart';
export 'explore/release_item.dart';
export 'feedback/feedback_decision.dart';
export 'feedback/feedback_message.dart';
export 'feedback/feedback_message_view.dart';
export 'feedback/feedback_person.dart';
export 'feedback/feedback_thread.dart';
export 'feedback/feedback_view.dart';
export 'instance/instance_info.dart';
export 'learning_path/learning_path.dart';
export 'learning_path/learning_path_answer.dart';
@ -541,6 +555,258 @@ class Protocol extends _isd.DatabaseSerializationManager {
],
managed: true,
),
_isd.TableDefinition(
name: 'feedback_decision',
dartName: 'FeedbackDecision',
schema: 'public',
module: 'halkyon_learn',
columns: [
_isd.ColumnDefinition(
name: 'id',
columnType: _isd.ColumnType.uuid,
isNullable: false,
dartType: 'UuidValue?',
columnDefault: 'random_v7',
),
_isd.ColumnDefinition(
name: 'spaceId',
columnType: _isd.ColumnType.bigint,
isNullable: true,
dartType: 'int?',
),
_isd.ColumnDefinition(
name: 'threadId',
columnType: _isd.ColumnType.uuid,
isNullable: false,
dartType: 'UuidValue',
),
_isd.ColumnDefinition(
name: 'decision',
columnType: _isd.ColumnType.text,
isNullable: false,
dartType: 'String',
),
],
foreignKeys: [
_isd.ForeignKeyDefinition(
constraintName: 'feedback_decision_fk_0',
columns: ['spaceId'],
referenceTable: 'offline_sync_spaces',
referenceTableSchema: 'public',
referenceColumns: ['id'],
onUpdate: _isd.ForeignKeyAction.noAction,
onDelete: _isd.ForeignKeyAction.cascade,
matchType: null,
),
],
indexes: [
_isd.IndexDefinition(
indexName: 'feedback_decision_space_idx',
tableSpace: null,
elements: [
_isd.IndexElementDefinition(
type: _isd.IndexElementDefinitionType.column,
definition: 'spaceId',
),
],
type: 'btree',
isUnique: false,
isPrimary: false,
),
_isd.IndexDefinition(
indexName: 'feedback_decision_thread_idx',
tableSpace: null,
elements: [
_isd.IndexElementDefinition(
type: _isd.IndexElementDefinitionType.column,
definition: 'threadId',
),
],
type: 'btree',
isUnique: false,
isPrimary: false,
),
],
managed: true,
),
_isd.TableDefinition(
name: 'feedback_message',
dartName: 'FeedbackMessage',
schema: 'public',
module: 'halkyon_learn',
columns: [
_isd.ColumnDefinition(
name: 'id',
columnType: _isd.ColumnType.uuid,
isNullable: false,
dartType: 'UuidValue?',
columnDefault: 'random_v7',
),
_isd.ColumnDefinition(
name: 'spaceId',
columnType: _isd.ColumnType.bigint,
isNullable: true,
dartType: 'int?',
),
_isd.ColumnDefinition(
name: 'threadId',
columnType: _isd.ColumnType.uuid,
isNullable: false,
dartType: 'UuidValue',
),
_isd.ColumnDefinition(
name: 'keyId',
columnType: _isd.ColumnType.text,
isNullable: true,
dartType: 'String?',
),
_isd.ColumnDefinition(
name: 'payload',
columnType: _isd.ColumnType.bytea,
isNullable: false,
dartType: 'dart:typed_data:ByteData',
),
_isd.ColumnDefinition(
name: 'nonce',
columnType: _isd.ColumnType.bytea,
isNullable: true,
dartType: 'dart:typed_data:ByteData?',
),
],
foreignKeys: [
_isd.ForeignKeyDefinition(
constraintName: 'feedback_message_fk_0',
columns: ['spaceId'],
referenceTable: 'offline_sync_spaces',
referenceTableSchema: 'public',
referenceColumns: ['id'],
onUpdate: _isd.ForeignKeyAction.noAction,
onDelete: _isd.ForeignKeyAction.cascade,
matchType: null,
),
],
indexes: [
_isd.IndexDefinition(
indexName: 'feedback_message_space_idx',
tableSpace: null,
elements: [
_isd.IndexElementDefinition(
type: _isd.IndexElementDefinitionType.column,
definition: 'spaceId',
),
],
type: 'btree',
isUnique: false,
isPrimary: false,
),
_isd.IndexDefinition(
indexName: 'feedback_message_thread_idx',
tableSpace: null,
elements: [
_isd.IndexElementDefinition(
type: _isd.IndexElementDefinitionType.column,
definition: 'threadId',
),
],
type: 'btree',
isUnique: false,
isPrimary: false,
),
],
managed: true,
),
_isd.TableDefinition(
name: 'feedback_thread',
dartName: 'FeedbackThread',
schema: 'public',
module: 'halkyon_learn',
columns: [
_isd.ColumnDefinition(
name: 'id',
columnType: _isd.ColumnType.uuid,
isNullable: false,
dartType: 'UuidValue?',
columnDefault: 'random_v7',
),
_isd.ColumnDefinition(
name: 'spaceId',
columnType: _isd.ColumnType.bigint,
isNullable: true,
dartType: 'int?',
),
_isd.ColumnDefinition(
name: 'publicationId',
columnType: _isd.ColumnType.uuid,
isNullable: false,
dartType: 'UuidValue',
),
_isd.ColumnDefinition(
name: 'kind',
columnType: _isd.ColumnType.text,
isNullable: false,
dartType: 'String',
),
_isd.ColumnDefinition(
name: 'keyId',
columnType: _isd.ColumnType.text,
isNullable: true,
dartType: 'String?',
),
_isd.ColumnDefinition(
name: 'payload',
columnType: _isd.ColumnType.bytea,
isNullable: false,
dartType: 'dart:typed_data:ByteData',
),
_isd.ColumnDefinition(
name: 'nonce',
columnType: _isd.ColumnType.bytea,
isNullable: true,
dartType: 'dart:typed_data:ByteData?',
),
],
foreignKeys: [
_isd.ForeignKeyDefinition(
constraintName: 'feedback_thread_fk_0',
columns: ['spaceId'],
referenceTable: 'offline_sync_spaces',
referenceTableSchema: 'public',
referenceColumns: ['id'],
onUpdate: _isd.ForeignKeyAction.noAction,
onDelete: _isd.ForeignKeyAction.cascade,
matchType: null,
),
],
indexes: [
_isd.IndexDefinition(
indexName: 'feedback_thread_space_idx',
tableSpace: null,
elements: [
_isd.IndexElementDefinition(
type: _isd.IndexElementDefinitionType.column,
definition: 'spaceId',
),
],
type: 'btree',
isUnique: false,
isPrimary: false,
),
_isd.IndexDefinition(
indexName: 'feedback_thread_publication_idx',
tableSpace: null,
elements: [
_isd.IndexElementDefinition(
type: _isd.IndexElementDefinitionType.column,
definition: 'publicationId',
),
],
type: 'btree',
isUnique: false,
isPrimary: false,
),
],
managed: true,
),
_isd.TableDefinition(
name: 'learning_path',
dartName: 'LearningPath',
@ -2053,6 +2319,24 @@ class Protocol extends _isd.DatabaseSerializationManager {
if (t == _iaswq7ap.ReleaseItem) {
return _iaswq7ap.ReleaseItem.fromJson(data) as T;
}
if (t == _idsmz0xa.FeedbackDecision) {
return _idsmz0xa.FeedbackDecision.fromJson(data) as T;
}
if (t == _iqqrr7at.FeedbackMessage) {
return _iqqrr7at.FeedbackMessage.fromJson(data) as T;
}
if (t == _inlv2ksv.FeedbackMessageView) {
return _inlv2ksv.FeedbackMessageView.fromJson(data) as T;
}
if (t == _i8yyqgue.FeedbackPerson) {
return _i8yyqgue.FeedbackPerson.fromJson(data) as T;
}
if (t == _ihfcaz5v.FeedbackThread) {
return _ihfcaz5v.FeedbackThread.fromJson(data) as T;
}
if (t == _in90wtl8.FeedbackView) {
return _in90wtl8.FeedbackView.fromJson(data) as T;
}
if (t == _iskrg2g3.InstanceInfo) {
return _iskrg2g3.InstanceInfo.fromJson(data) as T;
}
@ -2192,6 +2476,31 @@ class Protocol extends _isd.DatabaseSerializationManager {
if (t == _isc.getType<_iaswq7ap.ReleaseItem?>()) {
return (data != null ? _iaswq7ap.ReleaseItem.fromJson(data) : null) as T;
}
if (t == _isc.getType<_idsmz0xa.FeedbackDecision?>()) {
return (data != null ? _idsmz0xa.FeedbackDecision.fromJson(data) : null)
as T;
}
if (t == _isc.getType<_iqqrr7at.FeedbackMessage?>()) {
return (data != null ? _iqqrr7at.FeedbackMessage.fromJson(data) : null)
as T;
}
if (t == _isc.getType<_inlv2ksv.FeedbackMessageView?>()) {
return (data != null
? _inlv2ksv.FeedbackMessageView.fromJson(data)
: null)
as T;
}
if (t == _isc.getType<_i8yyqgue.FeedbackPerson?>()) {
return (data != null ? _i8yyqgue.FeedbackPerson.fromJson(data) : null)
as T;
}
if (t == _isc.getType<_ihfcaz5v.FeedbackThread?>()) {
return (data != null ? _ihfcaz5v.FeedbackThread.fromJson(data) : null)
as T;
}
if (t == _isc.getType<_in90wtl8.FeedbackView?>()) {
return (data != null ? _in90wtl8.FeedbackView.fromJson(data) : null) as T;
}
if (t == _isc.getType<_iskrg2g3.InstanceInfo?>()) {
return (data != null ? _iskrg2g3.InstanceInfo.fromJson(data) : null) as T;
}
@ -2295,6 +2604,12 @@ class Protocol extends _isd.DatabaseSerializationManager {
.toList()
as T;
}
if (t == List<_inlv2ksv.FeedbackMessageView>) {
return (data as List)
.map((e) => deserialize<_inlv2ksv.FeedbackMessageView>(e))
.toList()
as T;
}
if (t == List<_i7kd0wkp.ReleaseInfo>) {
return (data as List)
.map((e) => deserialize<_i7kd0wkp.ReleaseInfo>(e))
@ -2335,6 +2650,12 @@ class Protocol extends _isd.DatabaseSerializationManager {
return (data as List).map((e) => deserialize<_isc.UuidValue>(e)).toList()
as T;
}
if (t == List<_idpwh21d.FeedbackView>) {
return (data as List)
.map((e) => deserialize<_idpwh21d.FeedbackView>(e))
.toList()
as T;
}
if (t == List<String>) {
return (data as List).map((e) => deserialize<String>(e)).toList() as T;
}
@ -2370,6 +2691,12 @@ class Protocol extends _isd.DatabaseSerializationManager {
_ibl9h9o9.ExploreQuery => 'ExploreQuery',
_ievpeb4f.ReleaseContent => 'ReleaseContent',
_iaswq7ap.ReleaseItem => 'ReleaseItem',
_idsmz0xa.FeedbackDecision => 'FeedbackDecision',
_iqqrr7at.FeedbackMessage => 'FeedbackMessage',
_inlv2ksv.FeedbackMessageView => 'FeedbackMessageView',
_i8yyqgue.FeedbackPerson => 'FeedbackPerson',
_ihfcaz5v.FeedbackThread => 'FeedbackThread',
_in90wtl8.FeedbackView => 'FeedbackView',
_iskrg2g3.InstanceInfo => 'InstanceInfo',
_ivt5m868.LearningPath => 'LearningPath',
_iy9sv3bs.LearningPathAnswer => 'LearningPathAnswer',
@ -2446,6 +2773,18 @@ class Protocol extends _isd.DatabaseSerializationManager {
return 'ReleaseContent';
case _iaswq7ap.ReleaseItem():
return 'ReleaseItem';
case _idsmz0xa.FeedbackDecision():
return 'FeedbackDecision';
case _iqqrr7at.FeedbackMessage():
return 'FeedbackMessage';
case _inlv2ksv.FeedbackMessageView():
return 'FeedbackMessageView';
case _i8yyqgue.FeedbackPerson():
return 'FeedbackPerson';
case _ihfcaz5v.FeedbackThread():
return 'FeedbackThread';
case _in90wtl8.FeedbackView():
return 'FeedbackView';
case _iskrg2g3.InstanceInfo():
return 'InstanceInfo';
case _ivt5m868.LearningPath():
@ -2574,6 +2913,24 @@ class Protocol extends _isd.DatabaseSerializationManager {
if (dataClassName == 'ReleaseItem') {
return deserialize<_iaswq7ap.ReleaseItem>(data['data']);
}
if (dataClassName == 'FeedbackDecision') {
return deserialize<_idsmz0xa.FeedbackDecision>(data['data']);
}
if (dataClassName == 'FeedbackMessage') {
return deserialize<_iqqrr7at.FeedbackMessage>(data['data']);
}
if (dataClassName == 'FeedbackMessageView') {
return deserialize<_inlv2ksv.FeedbackMessageView>(data['data']);
}
if (dataClassName == 'FeedbackPerson') {
return deserialize<_i8yyqgue.FeedbackPerson>(data['data']);
}
if (dataClassName == 'FeedbackThread') {
return deserialize<_ihfcaz5v.FeedbackThread>(data['data']);
}
if (dataClassName == 'FeedbackView') {
return deserialize<_in90wtl8.FeedbackView>(data['data']);
}
if (dataClassName == 'InstanceInfo') {
return deserialize<_iskrg2g3.InstanceInfo>(data['data']);
}
@ -2700,6 +3057,12 @@ class Protocol extends _isd.DatabaseSerializationManager {
return _ipiw1cmx.Card.t;
case _ipbyuef1.CardRevision:
return _ipbyuef1.CardRevision.t;
case _idsmz0xa.FeedbackDecision:
return _idsmz0xa.FeedbackDecision.t;
case _iqqrr7at.FeedbackMessage:
return _iqqrr7at.FeedbackMessage.t;
case _ihfcaz5v.FeedbackThread:
return _ihfcaz5v.FeedbackThread.t;
case _ivt5m868.LearningPath:
return _ivt5m868.LearningPath.t;
case _iy9sv3bs.LearningPathAnswer:

View file

@ -14,6 +14,9 @@ import 'package:serverpod_database/serverpod_database.dart' as _isd;
import 'asset/asset.dart' as _ipb7vkxr;
import 'card/card.dart' as _ipiw1cmx;
import 'card/card_revision.dart' as _ipbyuef1;
import 'feedback/feedback_decision.dart' as _idsmz0xa;
import 'feedback/feedback_message.dart' as _iqqrr7at;
import 'feedback/feedback_thread.dart' as _ihfcaz5v;
import 'learning_path/learning_path.dart' as _ivt5m868;
import 'learning_path/learning_path_answer.dart' as _iy9sv3bs;
import 'learning_path/learning_path_step.dart' as _irnpk9dc;
@ -38,6 +41,9 @@ final List<_isd.Table> syncTables = [
_ipiw1cmx.Card.t,
_ipbyuef1.CardRevision.t,
_i6jz6nyq.CardTag.t,
_idsmz0xa.FeedbackDecision.t,
_iqqrr7at.FeedbackMessage.t,
_ihfcaz5v.FeedbackThread.t,
_ivt5m868.LearningPath.t,
_iy9sv3bs.LearningPathAnswer.t,
_irnpk9dc.LearningPathStep.t,

View file

@ -33,6 +33,9 @@ import 'package:halkyon_learn_flutter/src/feature/explore/data/copy_updater.dart
import 'package:halkyon_learn_flutter/src/feature/explore/data/explore_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/explore/data/release_copier.dart';
import 'package:halkyon_learn_flutter/src/feature/explore/data/serverpod_explore_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/feedback/data/feedback_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/feedback/data/serverpod_feedback_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/feedback/data/sync_feedback_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/geo_map/data/geo_map_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/kanji/data/stroke_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/learning_path/data/learning_path_deletions.dart';
@ -314,6 +317,14 @@ Future<PersonDependencies> _openPerson(
CopyUpdater(studysets, cards, notes, paths, pdfSources, tags, assets),
studysets,
),
feedback: FeedbackRepository(
SyncFeedbackGateway(db, sync),
ServerpodFeedbackGateway(client),
cards,
notes,
paths,
assets,
),
onDispose: () async {
try {
// Deletions first: those still waiting are dropped, those under way

View file

@ -3,6 +3,7 @@ import 'package:halkyon_learn_flutter/src/core/sync/sync_service.dart';
import 'package:halkyon_learn_flutter/src/feature/card/data/card_deletions.dart';
import 'package:halkyon_learn_flutter/src/feature/card/data/card_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/explore/data/explore_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/feedback/data/feedback_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/learning_path/data/learning_path_deletions.dart';
import 'package:halkyon_learn_flutter/src/feature/learning_path/data/learning_path_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/learning_path/data/path_progress_repository.dart';
@ -87,6 +88,9 @@ class PersonDependencies({
/// What others published, and the person's copies of it.
required final ExploreRepository explore,
/// Reports and suggestions on published studysets.
required final FeedbackRepository feedback,
final Future<void> Function()? _onDispose,
}) {
/// Closes what was opened for the person.

View file

@ -125,6 +125,16 @@ class CardRepository(final CardGateway _gateway) {
/// Every card of the person on this device: its id, its studyset, when it
/// was made, and whether it was imported (its first revision says where
/// from).
/// The card [id] on this device now; null when there is none.
Future<Card?> byId(String id) async {
final stamp = (await allCards()).where((card) => card.id == id).firstOrNull;
return stamp == null
? null
: (await cardsOf(
stamp.studysetId,
)).where((card) => card.id == id).firstOrNull;
}
Future<List<CardStamp>> allCards() async {
final imported = <String>{};
for (final revision in await _gateway.allRevisions()) {

View file

@ -0,0 +1,48 @@
import 'dart:typed_data';
import 'package:halkyon_learn_flutter/src/feature/feedback/model/feedback.dart';
/// The person's own feedback rows, in their synced database: what they
/// write works offline and reaches the instance with the next sync. Tests
/// replace it with a fake.
abstract interface class FeedbackWriteGateway() {
/// Stores a thread of [kind] on the published studyset [publicationId].
Future<String> insertThread({
required String publicationId,
required FeedbackKind kind,
required ByteData payload,
});
/// Stores a reply in the thread [threadId].
Future<void> insertMessage(String threadId, ByteData payload);
/// Stores an author's [decision] on the thread [threadId].
Future<void> insertDecision(String threadId, FeedbackDecision decision);
}
/// The feedback the instance gathers from everyone (ADR 0005, rule 6).
/// Tests replace it with a fake.
abstract interface class FeedbackServerGateway() {
/// The threads on the published studyset [publicId] the person may see.
Future<List<FeedbackThread>> threadsOf(String publicId);
/// The threads on the person's published studysets and those they wrote.
Future<List<FeedbackThread>> inbox();
/// How many threads of the inbox have something new.
Future<int> unseen();
/// Records that the person has read the thread [threadId].
Future<void> markSeen(String threadId);
/// Keeps the person with [handle] from writing on the person's published
/// studysets, or lets them again.
Future<void> block(String handle);
Future<void> unblock(String handle);
/// The handles of the people the person blocked.
Future<List<String>> blockedPeople();
/// The file [sha256] the suggestion [threadId] names, from its writer.
Future<Uint8List> file(String threadId, String sha256);
}

View file

@ -0,0 +1,122 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:halkyon_learn_flutter/src/core/content/payload.dart';
import 'package:halkyon_learn_flutter/src/feature/card/model/card_content.dart';
import 'package:halkyon_learn_flutter/src/feature/feedback/model/feedback.dart';
// A thread's and a reply's content as their synced rows store them (see
// core/content/payload). A thread's, schema 1: what it is about (`target`:
// {kind, item, version}), a report's `category` (as cards' reports name
// them), what its writer says (`body`), and a suggestion's `proposed`
// content (the item's payload JSON as the writer has it) with the `files`
// it names ([{assetId, sha256, mimeType, fileName}]). A reply's, schema 1:
// its `body`.
/// The schema this app writes.
const feedbackSchemaVersion = 1;
/// A thread's payload.
ByteData encodeThreadPayload({
required FeedbackTarget target,
required String body,
ReportCategory? category,
Map<String, Object?>? proposed,
List<FeedbackFile> files = const [],
}) => encodePayload(feedbackSchemaVersion, {
'target': {
'kind': target.kind.name,
'item': target.item,
'version': target.version,
},
'body': body,
'category': ?category?.name,
'proposed': ?proposed,
if (files.isNotEmpty)
'files': [
for (final file in files)
{
'assetId': file.assetId,
'sha256': file.sha256,
'mimeType': file.mimeType,
'fileName': file.fileName,
},
],
});
/// A reply's payload.
ByteData encodeMessagePayload(String body) =>
encodePayload(feedbackSchemaVersion, {'body': body});
/// What a thread's [payload] (its JSON as written) says; throws a
/// [FormatException] for one this app can't read.
({
FeedbackTarget target,
String body,
ReportCategory? category,
Map<String, Object?>? proposed,
List<FeedbackFile> files,
})
decodeThreadPayload(String payload) {
final json = decodePayload(
ByteData.sublistView(utf8.encode(payload)),
newestSchema: feedbackSchemaVersion,
);
if (json case {
'target': {
'kind': final String kind,
'item': final String item,
'version': final int version,
},
}) {
return (
target: FeedbackTarget(
kind:
FeedbackTargetKind.values.asNameMap()[kind] ??
(throw const FormatException('A target of a kind not known.')),
item: item,
version: version,
),
body: switch (json['body']) {
final String body => body,
_ => '',
},
category: switch (json['category']) {
final String name =>
ReportCategory.values.asNameMap()[name] ?? ReportCategory.other,
_ => null,
},
proposed: switch (json['proposed']) {
final Map<String, Object?> proposed => proposed,
_ => null,
},
files: [
for (final file in json['files'] as List<Object?>? ?? const [])
if (file case {
'assetId': final String assetId,
'sha256': final String sha256,
'mimeType': final String mimeType,
})
FeedbackFile(
assetId: assetId,
sha256: sha256,
mimeType: mimeType,
fileName: switch (file['fileName']) {
final String name => name,
_ => '',
},
),
],
);
}
throw const FormatException('Not a feedback thread.');
}
/// What a reply's [payload] (its JSON as written) says.
String decodeMessagePayload(String payload) => switch (decodePayload(
ByteData.sublistView(utf8.encode(payload)),
newestSchema: feedbackSchemaVersion,
)) {
{'body': final String body} => body,
_ => throw const FormatException('Not a feedback reply.'),
};

View file

@ -0,0 +1,248 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:crypto/crypto.dart';
import 'package:halkyon_learn_flutter/src/core/asset/data/asset_repository.dart';
import 'package:halkyon_learn_flutter/src/core/content/published_ref.dart';
import 'package:halkyon_learn_flutter/src/core/content/rich_content.dart';
import 'package:halkyon_learn_flutter/src/feature/card/data/card_payload.dart';
import 'package:halkyon_learn_flutter/src/feature/card/data/card_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/card/model/card_content.dart';
import 'package:halkyon_learn_flutter/src/feature/feedback/data/feedback_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/feedback/data/feedback_payload.dart';
import 'package:halkyon_learn_flutter/src/feature/feedback/model/feedback.dart';
import 'package:halkyon_learn_flutter/src/feature/learning_path/data/learning_path_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/learning_path/data/learning_path_step_payload.dart';
import 'package:halkyon_learn_flutter/src/feature/learning_path/model/step_content.dart';
import 'package:halkyon_learn_flutter/src/feature/note/data/note_payload.dart';
import 'package:halkyon_learn_flutter/src/feature/note/data/note_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/note/model/note_content.dart';
/// A suggestion the author can't take: what it is about isn't in their
/// studyset any more, or its content can't be read by this app.
final class SuggestionNotTaken() implements Exception {
@override
String toString() => 'SuggestionNotTaken';
}
/// Reports and suggestions on published studysets (ADR 0005, rule 6): the
/// person's own are written in their synced space, so writing them works
/// offline; what everyone wrote is read from the instance, which gathers
/// it. Reports and suggestions are public to everyone learning the
/// studyset, unless its author hides them.
class FeedbackRepository(
final FeedbackWriteGateway _write,
final FeedbackServerGateway _server,
final CardRepository _cards,
final NoteRepository _notes,
final LearningPathRepository _paths,
final AssetRepository _assets,
) {
/// Reports what is wrong with the item [origin] names (a copy's card,
/// note or step, or with [kind] studyset the studyset itself) to the
/// author of the published studyset it came from.
Future<void> report(
PublishedRef origin, {
required FeedbackTargetKind kind,
required ReportCategory category,
required String body,
}) async {
await _write.insertThread(
publicationId: origin.publication,
kind: FeedbackKind.report,
payload: encodeThreadPayload(
target: FeedbackTarget(
kind: kind,
item: origin.item,
version: origin.version,
),
category: category,
body: body.trim(),
),
);
}
/// Suggests a copy's card, as the person has it ([content]), to the
/// author of the published studyset it came from, with [body] saying
/// why. Throws an [ArgumentError] for a card of the person's own.
Future<void> suggestCard(CardContent content, {required String body}) =>
_suggest(
content.published,
FeedbackTargetKind.card,
_json(
encodeCardPayload(
content
.withField(CardField.published, null)
.withField(CardField.report, null),
),
),
body,
);
/// Suggests a copy's note, as with [suggestCard].
Future<void> suggestNote(NoteContent content, {required String body}) =>
_suggest(
content.published,
FeedbackTargetKind.note,
_json(
encodeNotePayload(content.withField(NoteField.published, null)),
),
body,
);
/// Suggests a copy's step, as with [suggestCard].
Future<void> suggestStep(StepContent content, {required String body}) =>
_suggest(
content.published,
FeedbackTargetKind.step,
_json(
encodeStepPayload(content.withField(StepField.published, null)),
),
body,
);
Future<void> _suggest(
PublishedRef? origin,
FeedbackTargetKind kind,
Map<String, Object?> proposed,
String body,
) async {
if (origin == null) {
throw ArgumentError.value(origin, 'origin', 'is not a copy');
}
// The files its content names, for the author to take into theirs.
final files = <FeedbackFile>[];
for (final assetId in _assetIdsIn(proposed)) {
if (await _assets.fileOf(assetId) case final file?) {
files.add(
FeedbackFile(
assetId: assetId,
sha256: sha256.convert(file.bytes).toString(),
mimeType: file.mimeType,
fileName: file.fileName,
),
);
}
}
await _write.insertThread(
publicationId: origin.publication,
kind: FeedbackKind.suggestion,
payload: encodeThreadPayload(
target: FeedbackTarget(
kind: kind,
item: origin.item,
version: origin.version,
),
body: body.trim(),
proposed: proposed,
files: files,
),
);
}
/// Replies [body] in [thread].
Future<void> reply(FeedbackThread thread, String body) =>
_write.insertMessage(thread.threadId, encodeMessagePayload(body.trim()));
/// Records the author's [decision] on [thread].
Future<void> decide(FeedbackThread thread, FeedbackDecision decision) =>
_write.insertDecision(thread.threadId, decision);
/// Takes the suggestion [thread] into the author's own studyset: its
/// content as a revision of their card, note or step (which they edit
/// freely afterwards), the files it names among their files; and records
/// it accepted, so that the next release credits its writer. Throws
/// [SuggestionNotTaken] when what it is about is gone or can't be read.
Future<void> accept(FeedbackThread thread) async {
final proposed = thread.proposed;
if (proposed == null) {
throw SuggestionNotTaken();
}
// The writer's files as the author's own.
var text = jsonEncode(proposed);
for (final file in thread.files) {
final own =
await _assets.idOf(file.sha256) ??
await _assets.add(
await _server.file(thread.threadId, file.sha256),
fileName: file.fileName.isEmpty ? file.sha256 : file.fileName,
mimeType: file.mimeType,
);
text = text.replaceAll(file.assetId, own);
}
final payload = ByteData.sublistView(utf8.encode(text));
final item = thread.target.item;
try {
switch (thread.target.kind) {
case FeedbackTargetKind.card:
final card = await _cards.byId(item) ?? (throw SuggestionNotTaken());
await _cards.saveEdit(
card.id,
decodeCardPayload(
payload,
).withField(CardField.report, card.content.report),
basedOn: card.heads,
);
case FeedbackTargetKind.note:
final note = await _notes.byId(item) ?? (throw SuggestionNotTaken());
await _notes.saveEdit(
note.id,
decodeNotePayload(payload),
basedOn: note.heads,
);
case FeedbackTargetKind.step:
final (:step, pathId: _) =
await _paths.stepById(item) ?? (throw SuggestionNotTaken());
await _paths.saveStep(
step.id,
decodeStepPayload(payload, step.kind),
basedOn: step.heads,
);
case FeedbackTargetKind.studyset:
throw SuggestionNotTaken();
}
} on FormatException {
throw SuggestionNotTaken();
}
await decide(thread, FeedbackDecision.accepted);
}
/// The threads on the published studyset [publicId] the person may see.
Future<List<FeedbackThread>> threadsOf(String publicId) =>
_server.threadsOf(publicId);
/// The person's inbox: threads on their published studysets and those
/// they wrote, the most recently active first.
Future<List<FeedbackThread>> inbox() => _server.inbox();
/// How many threads of the inbox have something new.
Future<int> unseen() => _server.unseen();
/// Records that the person has read [thread].
Future<void> markSeen(FeedbackThread thread) =>
_server.markSeen(thread.threadId);
/// Keeps [person] from writing on the person's published studysets.
Future<void> block(FeedbackPerson person) => _server.block(person.handle);
/// Lets the person with [handle] write on them again.
Future<void> unblock(String handle) => _server.unblock(handle);
/// The handles of the people the person blocked.
Future<List<String>> blockedPeople() => _server.blockedPeople();
static Map<String, Object?> _json(ByteData payload) =>
jsonDecode(utf8.decode(Uint8List.sublistView(payload)))
as Map<String, Object?>;
}
/// The person's files [json] (content's JSON) names: the `url` of every
/// block that shows or plays one, as [RichContent.assetIds] finds them.
Set<String> _assetIdsIn(Object? json) => switch (json) {
final Map<String, Object?> map => {
?assetIdOf(map['url']),
for (final value in map.values) ..._assetIdsIn(value),
},
final List<Object?> list => {for (final value in list) ..._assetIdsIn(value)},
_ => const {},
};

Some files were not shown because too many files have changed in this diff Show more