feat(publish): releases in git (public tier, stage 2)
A person with an identity and a public profile publishes a studyset with
everything inside it as its next release (ADR 0005, rule 3): the server
reads their synced rows and writes them through the git command as a
commit on main and the annotated tag vN, in a bare repository of the
studyset's own, named by a random public id made at the first publish.
A release is a directory per studyset with a file per card, note, path,
step and PDF (its id, position, tag names and payload as synced, keys
sorted and indented) and the files the content names, without what is
only the author's (a card's report, a PDF's deadline). Publishing is
refused without a licence allowing passing it on in every studyset ("no
derivatives" allowed), with revisions to settle, with files not uploaded
yet, when nothing changed, or for a name that won't do; the name is
fixed from the first release, as is the person's handle.
In the app, Share in a studyset's action row opens the Publish sheet;
publishing syncs first, and settles by itself the revisions that merge
cleanly, listing only real conflicts. The server's image gets git, the
repositories a volume of their own, and the hourly backup a git bundle of
each.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
7f6147d689
commit
f635b0816a
31 changed files with 20287 additions and 77 deletions
2
halkyon_learn_server/.gitignore
vendored
2
halkyon_learn_server/.gitignore
vendored
|
|
@ -24,3 +24,5 @@ config/firebase_service_account_key.json
|
|||
# Dictionary files: made by halkyon_learn_curator, not kept in git.
|
||||
/dictionaries/*.jsonl
|
||||
/dictionaries/*.jsonl.gz
|
||||
# The published studysets' repositories of a development server
|
||||
/repositories/
|
||||
|
|
|
|||
|
|
@ -34,10 +34,11 @@ RUN mkdir -p config migrations
|
|||
# Ubuntu 24.04: glibc 2.39 (the bundled binaries need at most 2.34, measured) and
|
||||
# libgcc_s (needed by libsqlite3_connection_pool, a Rust library; a server running
|
||||
# against Postgres never loads it, but a future code path might). CA certificates
|
||||
# for outgoing TLS, e.g. GitHub sign-in.
|
||||
# for outgoing TLS, e.g. GitHub sign-in. git for the published studysets' releases
|
||||
# (ADR 0005, rule 3), which the server writes through the git command.
|
||||
FROM ubuntu:24.04
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends ca-certificates \
|
||||
&& apt-get install -y --no-install-recommends ca-certificates git \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
WORKDIR /app
|
||||
|
||||
|
|
@ -51,9 +52,11 @@ COPY --from=build /app/halkyon_learn_server/config/ config/
|
|||
COPY --from=build /app/halkyon_learn_server/migrations/ migrations/
|
||||
COPY --from=build /app/halkyon_learn_server/lib/src/generated/protocol.yaml lib/src/generated/protocol.yaml
|
||||
|
||||
# The server needs no root rights (it listens on 8080 and writes nothing to the
|
||||
# image), so it runs as an unprivileged system user.
|
||||
RUN useradd --system --no-create-home --shell /usr/sbin/nologin app
|
||||
# The server needs no root rights (it listens on 8080 and writes only into the
|
||||
# repositories folder), so it runs as an unprivileged system user. The folder is the
|
||||
# app user's, and a new volume mounted on it takes that over.
|
||||
RUN useradd --system --no-create-home --shell /usr/sbin/nologin app \
|
||||
&& mkdir repositories && chown app:app repositories
|
||||
USER app
|
||||
|
||||
# Only the API server runs in production (no Insights, no web server).
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:halkyon_learn_server/src/asset/asset_endpoint.dart';
|
||||
import 'package:halkyon_learn_server/src/generated/protocol.dart';
|
||||
import 'package:halkyon_learn_server/src/profile/profile_rules.dart';
|
||||
import 'package:halkyon_learn_server/src/publish/release_repository.dart';
|
||||
import 'package:serverpod/protocol.dart' show CloudStorageEntry;
|
||||
import 'package:serverpod/serverpod.dart';
|
||||
import 'package:serverpod_auth_idp_server/core.dart';
|
||||
|
|
@ -11,65 +12,79 @@ import 'package:serverpod_offline_sync_server/serverpod_offline_sync_server.dart
|
|||
/// device with a valid session would re-upload everything; then their
|
||||
/// personal space goes with all its content (the foreign keys cascade into
|
||||
/// every synced table and the sync engine's bookkeeping) and its stored
|
||||
/// files; then the person with their identities.
|
||||
Future<void> deletePerson(Session session, UuidValue authUserId) =>
|
||||
session.db.transaction((transaction) async {
|
||||
// DELETE FROM "serverpod_auth_core_session" WHERE "authUserId" = $1
|
||||
await ServerSideSession.db.deleteWhere(
|
||||
session,
|
||||
where: (t) => t.authUserId.equals(authUserId),
|
||||
transaction: transaction,
|
||||
);
|
||||
// DELETE FROM "offline_sync_space_members" WHERE "userUuid" = $1
|
||||
await OfflineSyncSpaceMember.db.deleteWhere(
|
||||
session,
|
||||
where: (t) => t.userUuid.equals(authUserId),
|
||||
transaction: transaction,
|
||||
);
|
||||
// The personal space is keyed on the person's id.
|
||||
// DELETE FROM "offline_sync_spaces" WHERE "uuidSpaceId" = $1
|
||||
await OfflineSyncSpace.db.deleteWhere(
|
||||
session,
|
||||
where: (t) => t.uuidSpaceId.equals(authUserId),
|
||||
transaction: transaction,
|
||||
);
|
||||
// Their space's files are stored outside the space's rows (ADR 0004,
|
||||
// rule 5), in the database as Serverpod keeps private files; files
|
||||
// moved to other storage (S3) need deleting there instead.
|
||||
// DELETE FROM "serverpod_cloud_storage"
|
||||
// WHERE "storageId" = 'private' AND "path" LIKE 'spaces/<id>/%'
|
||||
await CloudStorageEntry.db.deleteWhere(
|
||||
session,
|
||||
where: (t) =>
|
||||
t.storageId.equals(assetStorage) &
|
||||
t.path.like('${assetPath(authUserId, '')}%'),
|
||||
transaction: transaction,
|
||||
);
|
||||
// DELETE FROM "public_profile" WHERE "authUserId" = $1, and its avatar
|
||||
// unless another profile shows the same picture.
|
||||
final profiles = await PublicProfile.db.deleteWhere(
|
||||
session,
|
||||
where: (t) => t.authUserId.equals(authUserId),
|
||||
transaction: transaction,
|
||||
);
|
||||
for (final profile in profiles) {
|
||||
if (profile.avatarHash case final hash?) {
|
||||
await forgetAvatar(session, hash, transaction: transaction);
|
||||
}
|
||||
/// files; then the person with their identities. Their published studysets
|
||||
/// go too, and once the transaction holds, their repositories: what
|
||||
/// learners copied stays theirs (ADR 0005, rule 4).
|
||||
Future<void> deletePerson(Session session, UuidValue authUserId) async {
|
||||
final published = await session.db.transaction((transaction) async {
|
||||
// DELETE FROM "serverpod_auth_core_session" WHERE "authUserId" = $1
|
||||
await ServerSideSession.db.deleteWhere(
|
||||
session,
|
||||
where: (t) => t.authUserId.equals(authUserId),
|
||||
transaction: transaction,
|
||||
);
|
||||
// DELETE FROM "offline_sync_space_members" WHERE "userUuid" = $1
|
||||
await OfflineSyncSpaceMember.db.deleteWhere(
|
||||
session,
|
||||
where: (t) => t.userUuid.equals(authUserId),
|
||||
transaction: transaction,
|
||||
);
|
||||
// The personal space is keyed on the person's id.
|
||||
// DELETE FROM "offline_sync_spaces" WHERE "uuidSpaceId" = $1
|
||||
await OfflineSyncSpace.db.deleteWhere(
|
||||
session,
|
||||
where: (t) => t.uuidSpaceId.equals(authUserId),
|
||||
transaction: transaction,
|
||||
);
|
||||
// Their space's files are stored outside the space's rows (ADR 0004,
|
||||
// rule 5), in the database as Serverpod keeps private files; files
|
||||
// moved to other storage (S3) need deleting there instead.
|
||||
// DELETE FROM "serverpod_cloud_storage"
|
||||
// WHERE "storageId" = 'private' AND "path" LIKE 'spaces/<id>/%'
|
||||
await CloudStorageEntry.db.deleteWhere(
|
||||
session,
|
||||
where: (t) =>
|
||||
t.storageId.equals(assetStorage) &
|
||||
t.path.like('${assetPath(authUserId, '')}%'),
|
||||
transaction: transaction,
|
||||
);
|
||||
// DELETE FROM "public_profile" WHERE "authUserId" = $1, and its avatar
|
||||
// unless another profile shows the same picture.
|
||||
final profiles = await PublicProfile.db.deleteWhere(
|
||||
session,
|
||||
where: (t) => t.authUserId.equals(authUserId),
|
||||
transaction: transaction,
|
||||
);
|
||||
for (final profile in profiles) {
|
||||
if (profile.avatarHash case final hash?) {
|
||||
await forgetAvatar(session, hash, transaction: transaction);
|
||||
}
|
||||
// DELETE FROM "file_allowance" WHERE "authUserId" = $1
|
||||
await FileAllowance.db.deleteWhere(
|
||||
session,
|
||||
where: (t) => t.authUserId.equals(authUserId),
|
||||
transaction: transaction,
|
||||
);
|
||||
// DELETE FROM "serverpod_auth_core_user" WHERE "id" = $1
|
||||
await AuthServices.instance.authUsers.delete(
|
||||
session,
|
||||
authUserId: authUserId,
|
||||
transaction: transaction,
|
||||
);
|
||||
});
|
||||
}
|
||||
// Their releases go with them.
|
||||
// DELETE FROM "published_studyset" WHERE "authUserId" = $1
|
||||
final published = await PublishedStudyset.db.deleteWhere(
|
||||
session,
|
||||
where: (t) => t.authUserId.equals(authUserId),
|
||||
transaction: transaction,
|
||||
);
|
||||
// DELETE FROM "file_allowance" WHERE "authUserId" = $1
|
||||
await FileAllowance.db.deleteWhere(
|
||||
session,
|
||||
where: (t) => t.authUserId.equals(authUserId),
|
||||
transaction: transaction,
|
||||
);
|
||||
// DELETE FROM "serverpod_auth_core_user" WHERE "id" = $1
|
||||
await AuthServices.instance.authUsers.delete(
|
||||
session,
|
||||
authUserId: authUserId,
|
||||
transaction: transaction,
|
||||
);
|
||||
return published;
|
||||
});
|
||||
for (final studyset in published) {
|
||||
await repositoryOf(studyset.publicId).delete();
|
||||
}
|
||||
}
|
||||
|
||||
/// Deletes the anonymous people nobody can return to, at most [limit] per
|
||||
/// call, and returns how many (decided 2026-09-30): created before and unused
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import '../auth/github_upgrade_endpoint.dart' as _ivnugbf2;
|
|||
import '../dictionary/dictionary_endpoint.dart' as _ilgtwlto;
|
||||
import '../instance/instance_endpoint.dart' as _i3g8bfc2;
|
||||
import '../profile/profile_endpoint.dart' as _i6ky944g;
|
||||
import '../publish/publish_endpoint.dart' as _iyiav298;
|
||||
export 'future_calls.dart' show ServerpodFutureCallsGetter;
|
||||
|
||||
class Endpoints extends _is.EndpointDispatch {
|
||||
|
|
@ -95,6 +96,12 @@ class Endpoints extends _is.EndpointDispatch {
|
|||
'profiles',
|
||||
null,
|
||||
),
|
||||
'publish': _iyiav298.PublishEndpoint()
|
||||
..initialize(
|
||||
server,
|
||||
'publish',
|
||||
null,
|
||||
),
|
||||
};
|
||||
connectors['asset'] = _is.EndpointConnector(
|
||||
name: 'asset',
|
||||
|
|
@ -626,6 +633,62 @@ class Endpoints extends _is.EndpointDispatch {
|
|||
),
|
||||
},
|
||||
);
|
||||
connectors['publish'] = _is.EndpointConnector(
|
||||
name: 'publish',
|
||||
endpoint: endpoints['publish']!,
|
||||
methodConnectors: {
|
||||
'publication': _is.MethodConnector(
|
||||
name: 'publication',
|
||||
params: {
|
||||
'studysetId': _is.ParameterDescription(
|
||||
name: 'studysetId',
|
||||
type: _is.getType<_is.UuidValue>(),
|
||||
nullable: false,
|
||||
),
|
||||
},
|
||||
call:
|
||||
(
|
||||
_is.Session session,
|
||||
Map<String, dynamic> params,
|
||||
) async => (endpoints['publish'] as _iyiav298.PublishEndpoint)
|
||||
.publication(
|
||||
session,
|
||||
params['studysetId'],
|
||||
),
|
||||
),
|
||||
'publish': _is.MethodConnector(
|
||||
name: 'publish',
|
||||
params: {
|
||||
'studysetId': _is.ParameterDescription(
|
||||
name: 'studysetId',
|
||||
type: _is.getType<_is.UuidValue>(),
|
||||
nullable: false,
|
||||
),
|
||||
'slug': _is.ParameterDescription(
|
||||
name: 'slug',
|
||||
type: _is.getType<String>(),
|
||||
nullable: false,
|
||||
),
|
||||
'notes': _is.ParameterDescription(
|
||||
name: 'notes',
|
||||
type: _is.getType<String>(),
|
||||
nullable: false,
|
||||
),
|
||||
},
|
||||
call:
|
||||
(
|
||||
_is.Session session,
|
||||
Map<String, dynamic> params,
|
||||
) async =>
|
||||
(endpoints['publish'] as _iyiav298.PublishEndpoint).publish(
|
||||
session,
|
||||
params['studysetId'],
|
||||
params['slug'],
|
||||
params['notes'],
|
||||
),
|
||||
),
|
||||
},
|
||||
);
|
||||
modules['serverpod_auth_idp'] = _iais.Endpoints()
|
||||
..initializeEndpoints(server);
|
||||
modules['serverpod_offline_sync'] = _izehhkf5.Endpoints()
|
||||
|
|
|
|||
|
|
@ -52,6 +52,12 @@ import 'profile/profile.dart' as _ia5j8fch;
|
|||
import 'profile/profile_refusal.dart' as _i6j6efd1;
|
||||
import 'profile/profile_refused_exception.dart' as _idvc5e7p;
|
||||
import 'profile/public_profile.dart' as _ioppe2ep;
|
||||
import 'publish/publication.dart' as _i9o1kyhh;
|
||||
import 'publish/publish_refusal.dart' as _iofgvwaa;
|
||||
import 'publish/publish_refused_exception.dart' as _itdn8966;
|
||||
import 'publish/published_studyset.dart' as _irp51t0o;
|
||||
import 'publish/release_info.dart' as _i7kd0wkp;
|
||||
import 'publish/studyset_release.dart' as _i6qpha3x;
|
||||
import 'review/review_log.dart' as _irzjbdqz;
|
||||
import 'settings/person_settings.dart' as _irnhx5e5;
|
||||
import 'studyset/studyset.dart' as _ikbclmae;
|
||||
|
|
@ -86,6 +92,12 @@ export 'profile/profile.dart';
|
|||
export 'profile/profile_refusal.dart';
|
||||
export 'profile/profile_refused_exception.dart';
|
||||
export 'profile/public_profile.dart';
|
||||
export 'publish/publication.dart';
|
||||
export 'publish/publish_refusal.dart';
|
||||
export 'publish/publish_refused_exception.dart';
|
||||
export 'publish/published_studyset.dart';
|
||||
export 'publish/release_info.dart';
|
||||
export 'publish/studyset_release.dart';
|
||||
export 'review/review_log.dart';
|
||||
export 'settings/person_settings.dart';
|
||||
export 'studyset/studyset.dart';
|
||||
|
|
@ -2026,6 +2038,102 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
],
|
||||
managed: true,
|
||||
),
|
||||
_isp.TableDefinition(
|
||||
name: 'published_studyset',
|
||||
dartName: 'PublishedStudyset',
|
||||
schema: 'public',
|
||||
module: 'halkyon_learn',
|
||||
columns: [
|
||||
_isp.ColumnDefinition(
|
||||
name: 'id',
|
||||
columnType: _isp.ColumnType.bigint,
|
||||
isNullable: false,
|
||||
dartType: 'int?',
|
||||
columnDefault: 'serial',
|
||||
),
|
||||
_isp.ColumnDefinition(
|
||||
name: 'publicId',
|
||||
columnType: _isp.ColumnType.uuid,
|
||||
isNullable: false,
|
||||
dartType: 'UuidValue',
|
||||
),
|
||||
_isp.ColumnDefinition(
|
||||
name: 'studysetId',
|
||||
columnType: _isp.ColumnType.uuid,
|
||||
isNullable: false,
|
||||
dartType: 'UuidValue',
|
||||
),
|
||||
_isp.ColumnDefinition(
|
||||
name: 'authUserId',
|
||||
columnType: _isp.ColumnType.uuid,
|
||||
isNullable: false,
|
||||
dartType: 'UuidValue',
|
||||
),
|
||||
_isp.ColumnDefinition(
|
||||
name: 'slug',
|
||||
columnType: _isp.ColumnType.text,
|
||||
isNullable: false,
|
||||
dartType: 'String',
|
||||
),
|
||||
_isp.ColumnDefinition(
|
||||
name: 'createdAt',
|
||||
columnType: _isp.ColumnType.timestampWithoutTimeZone,
|
||||
isNullable: false,
|
||||
dartType: 'DateTime',
|
||||
),
|
||||
],
|
||||
foreignKeys: [],
|
||||
indexes: [
|
||||
_isp.IndexDefinition(
|
||||
indexName: 'published_studyset_public_idx',
|
||||
tableSpace: null,
|
||||
elements: [
|
||||
_isp.IndexElementDefinition(
|
||||
type: _isp.IndexElementDefinitionType.column,
|
||||
definition: 'publicId',
|
||||
),
|
||||
],
|
||||
type: 'btree',
|
||||
isUnique: true,
|
||||
isPrimary: false,
|
||||
),
|
||||
_isp.IndexDefinition(
|
||||
indexName: 'published_studyset_studyset_idx',
|
||||
tableSpace: null,
|
||||
elements: [
|
||||
_isp.IndexElementDefinition(
|
||||
type: _isp.IndexElementDefinitionType.column,
|
||||
definition: 'authUserId',
|
||||
),
|
||||
_isp.IndexElementDefinition(
|
||||
type: _isp.IndexElementDefinitionType.column,
|
||||
definition: 'studysetId',
|
||||
),
|
||||
],
|
||||
type: 'btree',
|
||||
isUnique: true,
|
||||
isPrimary: false,
|
||||
),
|
||||
_isp.IndexDefinition(
|
||||
indexName: 'published_studyset_slug_idx',
|
||||
tableSpace: null,
|
||||
elements: [
|
||||
_isp.IndexElementDefinition(
|
||||
type: _isp.IndexElementDefinitionType.column,
|
||||
definition: 'authUserId',
|
||||
),
|
||||
_isp.IndexElementDefinition(
|
||||
type: _isp.IndexElementDefinitionType.column,
|
||||
definition: 'slug',
|
||||
),
|
||||
],
|
||||
type: 'btree',
|
||||
isUnique: true,
|
||||
isPrimary: false,
|
||||
),
|
||||
],
|
||||
managed: true,
|
||||
),
|
||||
_isp.TableDefinition(
|
||||
name: 'review_log',
|
||||
dartName: 'ReviewLog',
|
||||
|
|
@ -2260,6 +2368,125 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
],
|
||||
managed: true,
|
||||
),
|
||||
_isp.TableDefinition(
|
||||
name: 'studyset_release',
|
||||
dartName: 'StudysetRelease',
|
||||
schema: 'public',
|
||||
module: 'halkyon_learn',
|
||||
columns: [
|
||||
_isp.ColumnDefinition(
|
||||
name: 'id',
|
||||
columnType: _isp.ColumnType.bigint,
|
||||
isNullable: false,
|
||||
dartType: 'int?',
|
||||
columnDefault: 'serial',
|
||||
),
|
||||
_isp.ColumnDefinition(
|
||||
name: 'publishedStudysetId',
|
||||
columnType: _isp.ColumnType.bigint,
|
||||
isNullable: false,
|
||||
dartType: 'int',
|
||||
),
|
||||
_isp.ColumnDefinition(
|
||||
name: 'version',
|
||||
columnType: _isp.ColumnType.bigint,
|
||||
isNullable: false,
|
||||
dartType: 'int',
|
||||
),
|
||||
_isp.ColumnDefinition(
|
||||
name: 'commit',
|
||||
columnType: _isp.ColumnType.text,
|
||||
isNullable: false,
|
||||
dartType: 'String',
|
||||
),
|
||||
_isp.ColumnDefinition(
|
||||
name: 'tree',
|
||||
columnType: _isp.ColumnType.text,
|
||||
isNullable: false,
|
||||
dartType: 'String',
|
||||
),
|
||||
_isp.ColumnDefinition(
|
||||
name: 'notes',
|
||||
columnType: _isp.ColumnType.text,
|
||||
isNullable: false,
|
||||
dartType: 'String',
|
||||
),
|
||||
_isp.ColumnDefinition(
|
||||
name: 'license',
|
||||
columnType: _isp.ColumnType.text,
|
||||
isNullable: false,
|
||||
dartType: 'String',
|
||||
),
|
||||
_isp.ColumnDefinition(
|
||||
name: 'studysetCount',
|
||||
columnType: _isp.ColumnType.bigint,
|
||||
isNullable: false,
|
||||
dartType: 'int',
|
||||
),
|
||||
_isp.ColumnDefinition(
|
||||
name: 'cardCount',
|
||||
columnType: _isp.ColumnType.bigint,
|
||||
isNullable: false,
|
||||
dartType: 'int',
|
||||
),
|
||||
_isp.ColumnDefinition(
|
||||
name: 'noteCount',
|
||||
columnType: _isp.ColumnType.bigint,
|
||||
isNullable: false,
|
||||
dartType: 'int',
|
||||
),
|
||||
_isp.ColumnDefinition(
|
||||
name: 'pathCount',
|
||||
columnType: _isp.ColumnType.bigint,
|
||||
isNullable: false,
|
||||
dartType: 'int',
|
||||
),
|
||||
_isp.ColumnDefinition(
|
||||
name: 'pdfCount',
|
||||
columnType: _isp.ColumnType.bigint,
|
||||
isNullable: false,
|
||||
dartType: 'int',
|
||||
),
|
||||
_isp.ColumnDefinition(
|
||||
name: 'releasedAt',
|
||||
columnType: _isp.ColumnType.timestampWithoutTimeZone,
|
||||
isNullable: false,
|
||||
dartType: 'DateTime',
|
||||
),
|
||||
],
|
||||
foreignKeys: [
|
||||
_isp.ForeignKeyDefinition(
|
||||
constraintName: 'studyset_release_fk_0',
|
||||
columns: ['publishedStudysetId'],
|
||||
referenceTable: 'published_studyset',
|
||||
referenceTableSchema: 'public',
|
||||
referenceColumns: ['id'],
|
||||
onUpdate: _isp.ForeignKeyAction.noAction,
|
||||
onDelete: _isp.ForeignKeyAction.cascade,
|
||||
matchType: null,
|
||||
),
|
||||
],
|
||||
indexes: [
|
||||
_isp.IndexDefinition(
|
||||
indexName: 'studyset_release_version_idx',
|
||||
tableSpace: null,
|
||||
elements: [
|
||||
_isp.IndexElementDefinition(
|
||||
type: _isp.IndexElementDefinitionType.column,
|
||||
definition: 'publishedStudysetId',
|
||||
),
|
||||
_isp.IndexElementDefinition(
|
||||
type: _isp.IndexElementDefinitionType.column,
|
||||
definition: 'version',
|
||||
),
|
||||
],
|
||||
type: 'btree',
|
||||
isUnique: true,
|
||||
isPrimary: false,
|
||||
),
|
||||
],
|
||||
managed: true,
|
||||
),
|
||||
_isp.TableDefinition(
|
||||
name: 'tag',
|
||||
dartName: 'Tag',
|
||||
|
|
@ -2441,6 +2668,24 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
if (t == _ioppe2ep.PublicProfile) {
|
||||
return _ioppe2ep.PublicProfile.fromJson(data) as T;
|
||||
}
|
||||
if (t == _i9o1kyhh.Publication) {
|
||||
return _i9o1kyhh.Publication.fromJson(data) as T;
|
||||
}
|
||||
if (t == _iofgvwaa.PublishRefusal) {
|
||||
return _iofgvwaa.PublishRefusal.fromJson(data) as T;
|
||||
}
|
||||
if (t == _itdn8966.PublishRefusedException) {
|
||||
return _itdn8966.PublishRefusedException.fromJson(data) as T;
|
||||
}
|
||||
if (t == _irp51t0o.PublishedStudyset) {
|
||||
return _irp51t0o.PublishedStudyset.fromJson(data) as T;
|
||||
}
|
||||
if (t == _i7kd0wkp.ReleaseInfo) {
|
||||
return _i7kd0wkp.ReleaseInfo.fromJson(data) as T;
|
||||
}
|
||||
if (t == _i6qpha3x.StudysetRelease) {
|
||||
return _i6qpha3x.StudysetRelease.fromJson(data) as T;
|
||||
}
|
||||
if (t == _irzjbdqz.ReviewLog) {
|
||||
return _irzjbdqz.ReviewLog.fromJson(data) as T;
|
||||
}
|
||||
|
|
@ -2568,6 +2813,30 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
return (data != null ? _ioppe2ep.PublicProfile.fromJson(data) : null)
|
||||
as T;
|
||||
}
|
||||
if (t == _is.getType<_i9o1kyhh.Publication?>()) {
|
||||
return (data != null ? _i9o1kyhh.Publication.fromJson(data) : null) as T;
|
||||
}
|
||||
if (t == _is.getType<_iofgvwaa.PublishRefusal?>()) {
|
||||
return (data != null ? _iofgvwaa.PublishRefusal.fromJson(data) : null)
|
||||
as T;
|
||||
}
|
||||
if (t == _is.getType<_itdn8966.PublishRefusedException?>()) {
|
||||
return (data != null
|
||||
? _itdn8966.PublishRefusedException.fromJson(data)
|
||||
: null)
|
||||
as T;
|
||||
}
|
||||
if (t == _is.getType<_irp51t0o.PublishedStudyset?>()) {
|
||||
return (data != null ? _irp51t0o.PublishedStudyset.fromJson(data) : null)
|
||||
as T;
|
||||
}
|
||||
if (t == _is.getType<_i7kd0wkp.ReleaseInfo?>()) {
|
||||
return (data != null ? _i7kd0wkp.ReleaseInfo.fromJson(data) : null) as T;
|
||||
}
|
||||
if (t == _is.getType<_i6qpha3x.StudysetRelease?>()) {
|
||||
return (data != null ? _i6qpha3x.StudysetRelease.fromJson(data) : null)
|
||||
as T;
|
||||
}
|
||||
if (t == _is.getType<_irzjbdqz.ReviewLog?>()) {
|
||||
return (data != null ? _irzjbdqz.ReviewLog.fromJson(data) : null) as T;
|
||||
}
|
||||
|
|
@ -2604,6 +2873,12 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
.toList()
|
||||
as T;
|
||||
}
|
||||
if (t == List<_i7kd0wkp.ReleaseInfo>) {
|
||||
return (data as List)
|
||||
.map((e) => deserialize<_i7kd0wkp.ReleaseInfo>(e))
|
||||
.toList()
|
||||
as T;
|
||||
}
|
||||
if (t == List<_ipdmo8cq.Device>) {
|
||||
return (data as List)
|
||||
.map((e) => deserialize<_ipdmo8cq.Device>(e))
|
||||
|
|
@ -2675,6 +2950,12 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
_i6j6efd1.ProfileRefusal => 'ProfileRefusal',
|
||||
_idvc5e7p.ProfileRefusedException => 'ProfileRefusedException',
|
||||
_ioppe2ep.PublicProfile => 'PublicProfile',
|
||||
_i9o1kyhh.Publication => 'Publication',
|
||||
_iofgvwaa.PublishRefusal => 'PublishRefusal',
|
||||
_itdn8966.PublishRefusedException => 'PublishRefusedException',
|
||||
_irp51t0o.PublishedStudyset => 'PublishedStudyset',
|
||||
_i7kd0wkp.ReleaseInfo => 'ReleaseInfo',
|
||||
_i6qpha3x.StudysetRelease => 'StudysetRelease',
|
||||
_irzjbdqz.ReviewLog => 'ReviewLog',
|
||||
_irnhx5e5.PersonSettings => 'PersonSettings',
|
||||
_ikbclmae.Studyset => 'Studyset',
|
||||
|
|
@ -2753,6 +3034,18 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
return 'ProfileRefusedException';
|
||||
case _ioppe2ep.PublicProfile():
|
||||
return 'PublicProfile';
|
||||
case _i9o1kyhh.Publication():
|
||||
return 'Publication';
|
||||
case _iofgvwaa.PublishRefusal():
|
||||
return 'PublishRefusal';
|
||||
case _itdn8966.PublishRefusedException():
|
||||
return 'PublishRefusedException';
|
||||
case _irp51t0o.PublishedStudyset():
|
||||
return 'PublishedStudyset';
|
||||
case _i7kd0wkp.ReleaseInfo():
|
||||
return 'ReleaseInfo';
|
||||
case _i6qpha3x.StudysetRelease():
|
||||
return 'StudysetRelease';
|
||||
case _irzjbdqz.ReviewLog():
|
||||
return 'ReviewLog';
|
||||
case _irnhx5e5.PersonSettings():
|
||||
|
|
@ -2880,6 +3173,24 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
if (dataClassName == 'PublicProfile') {
|
||||
return deserialize<_ioppe2ep.PublicProfile>(data['data']);
|
||||
}
|
||||
if (dataClassName == 'Publication') {
|
||||
return deserialize<_i9o1kyhh.Publication>(data['data']);
|
||||
}
|
||||
if (dataClassName == 'PublishRefusal') {
|
||||
return deserialize<_iofgvwaa.PublishRefusal>(data['data']);
|
||||
}
|
||||
if (dataClassName == 'PublishRefusedException') {
|
||||
return deserialize<_itdn8966.PublishRefusedException>(data['data']);
|
||||
}
|
||||
if (dataClassName == 'PublishedStudyset') {
|
||||
return deserialize<_irp51t0o.PublishedStudyset>(data['data']);
|
||||
}
|
||||
if (dataClassName == 'ReleaseInfo') {
|
||||
return deserialize<_i7kd0wkp.ReleaseInfo>(data['data']);
|
||||
}
|
||||
if (dataClassName == 'StudysetRelease') {
|
||||
return deserialize<_i6qpha3x.StudysetRelease>(data['data']);
|
||||
}
|
||||
if (dataClassName == 'ReviewLog') {
|
||||
return deserialize<_irzjbdqz.ReviewLog>(data['data']);
|
||||
}
|
||||
|
|
@ -2983,6 +3294,10 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
return _iz838ro9.PdfSource.t;
|
||||
case _ioppe2ep.PublicProfile:
|
||||
return _ioppe2ep.PublicProfile.t;
|
||||
case _irp51t0o.PublishedStudyset:
|
||||
return _irp51t0o.PublishedStudyset.t;
|
||||
case _i6qpha3x.StudysetRelease:
|
||||
return _i6qpha3x.StudysetRelease.t;
|
||||
case _irzjbdqz.ReviewLog:
|
||||
return _irzjbdqz.ReviewLog.t;
|
||||
case _irnhx5e5.PersonSettings:
|
||||
|
|
|
|||
|
|
@ -30,3 +30,6 @@ profile:
|
|||
- remove:
|
||||
profiles:
|
||||
- byHandle:
|
||||
publish:
|
||||
- publication:
|
||||
- publish:
|
||||
|
|
|
|||
141
halkyon_learn_server/lib/src/generated/publish/publication.dart
Normal file
141
halkyon_learn_server/lib/src/generated/publish/publication.dart
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
/* 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_server/src/generated/protocol.dart' as _i5lsenq5;
|
||||
import 'package:serverpod/serverpod.dart' as _is;
|
||||
import '../publish/release_info.dart' as _ia42v78b;
|
||||
|
||||
/// A published studyset as the app shows it to its person: where it is, and
|
||||
/// its releases, the newest first.
|
||||
abstract class Publication
|
||||
implements _is.SerializableModel, _is.ProtocolSerialization {
|
||||
Publication._({
|
||||
required this.publicId,
|
||||
required this.studysetId,
|
||||
required this.slug,
|
||||
required this.location,
|
||||
required this.releases,
|
||||
});
|
||||
|
||||
factory Publication({
|
||||
required _is.UuidValue publicId,
|
||||
required _is.UuidValue studysetId,
|
||||
required String slug,
|
||||
required String location,
|
||||
required List<_ia42v78b.ReleaseInfo> releases,
|
||||
}) = _PublicationImpl;
|
||||
|
||||
factory Publication.fromJson(Map<String, dynamic> jsonSerialization) {
|
||||
return Publication(
|
||||
publicId: _is.UuidValueJsonExtension.fromJson(
|
||||
jsonSerialization['publicId'],
|
||||
),
|
||||
studysetId: _is.UuidValueJsonExtension.fromJson(
|
||||
jsonSerialization['studysetId'],
|
||||
),
|
||||
slug: jsonSerialization['slug'] as String,
|
||||
location: jsonSerialization['location'] as String,
|
||||
releases: _i5lsenq5.Protocol().deserialize<List<_ia42v78b.ReleaseInfo>>(
|
||||
jsonSerialization['releases'],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Its identity for good.
|
||||
_is.UuidValue publicId;
|
||||
|
||||
/// The studyset published.
|
||||
_is.UuidValue studysetId;
|
||||
|
||||
/// Its name in its location.
|
||||
String slug;
|
||||
|
||||
/// Where it is (`halkyon.eu/@raisondetredev/japanese`).
|
||||
String location;
|
||||
|
||||
List<_ia42v78b.ReleaseInfo> releases;
|
||||
|
||||
/// Returns a shallow copy of this [Publication]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_is.useResult
|
||||
Publication copyWith({
|
||||
_is.UuidValue? publicId,
|
||||
_is.UuidValue? studysetId,
|
||||
String? slug,
|
||||
String? location,
|
||||
List<_ia42v78b.ReleaseInfo>? releases,
|
||||
});
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'__className__': 'Publication',
|
||||
'publicId': publicId.toJson(),
|
||||
'studysetId': studysetId.toJson(),
|
||||
'slug': slug,
|
||||
'location': location,
|
||||
'releases': releases.toJson(valueToJson: (v) => v.toJson()),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJsonForProtocol() {
|
||||
return {
|
||||
'__className__': 'Publication',
|
||||
'publicId': publicId.toJson(),
|
||||
'studysetId': studysetId.toJson(),
|
||||
'slug': slug,
|
||||
'location': location,
|
||||
'releases': releases.toJson(valueToJson: (v) => v.toJsonForProtocol()),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return _is.SerializationManager.encode(this);
|
||||
}
|
||||
}
|
||||
|
||||
class _PublicationImpl extends Publication {
|
||||
_PublicationImpl({
|
||||
required _is.UuidValue publicId,
|
||||
required _is.UuidValue studysetId,
|
||||
required String slug,
|
||||
required String location,
|
||||
required List<_ia42v78b.ReleaseInfo> releases,
|
||||
}) : super._(
|
||||
publicId: publicId,
|
||||
studysetId: studysetId,
|
||||
slug: slug,
|
||||
location: location,
|
||||
releases: releases,
|
||||
);
|
||||
|
||||
/// Returns a shallow copy of this [Publication]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_is.useResult
|
||||
@override
|
||||
Publication copyWith({
|
||||
_is.UuidValue? publicId,
|
||||
_is.UuidValue? studysetId,
|
||||
String? slug,
|
||||
String? location,
|
||||
List<_ia42v78b.ReleaseInfo>? releases,
|
||||
}) {
|
||||
return Publication(
|
||||
publicId: publicId ?? this.publicId,
|
||||
studysetId: studysetId ?? this.studysetId,
|
||||
slug: slug ?? this.slug,
|
||||
location: location ?? this.location,
|
||||
releases: releases ?? this.releases.map((e0) => e0.copyWith()).toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
/* 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/serverpod.dart' as _is;
|
||||
|
||||
/// Why publishing was refused; nothing was published.
|
||||
enum PublishRefusal implements _is.SerializableModel {
|
||||
/// The person has no public profile yet: a release names its author.
|
||||
noProfile,
|
||||
|
||||
/// A studyset's licence doesn't allow passing it on (none, or one not
|
||||
/// known to be open): mirroring and copying are passing it on. `items`
|
||||
/// says which studysets.
|
||||
licenseNotOpen,
|
||||
|
||||
/// Cards, notes or steps have a conflict to settle first (`items` says
|
||||
/// which).
|
||||
conflicts,
|
||||
|
||||
/// Files of the content haven't reached the instance yet: a device still
|
||||
/// uploads them.
|
||||
filesMissing,
|
||||
|
||||
/// Nothing changed since the last release.
|
||||
nothingChanged,
|
||||
|
||||
/// The release notes are longer than 5,000 characters.
|
||||
notesTooLong,
|
||||
|
||||
/// The name isn't 1 to 50 lower-case letters, digits or hyphens, starting
|
||||
/// with a letter or digit.
|
||||
slugInvalid,
|
||||
|
||||
/// Another studyset of the person has the name.
|
||||
slugTaken,
|
||||
|
||||
/// The studyset has its name since its first publish: others refer to it.
|
||||
slugFixed,
|
||||
|
||||
/// Publishing that studyset is already under way.
|
||||
busy;
|
||||
|
||||
static PublishRefusal fromJson(String name) {
|
||||
switch (name) {
|
||||
case 'noProfile':
|
||||
return PublishRefusal.noProfile;
|
||||
case 'licenseNotOpen':
|
||||
return PublishRefusal.licenseNotOpen;
|
||||
case 'conflicts':
|
||||
return PublishRefusal.conflicts;
|
||||
case 'filesMissing':
|
||||
return PublishRefusal.filesMissing;
|
||||
case 'nothingChanged':
|
||||
return PublishRefusal.nothingChanged;
|
||||
case 'notesTooLong':
|
||||
return PublishRefusal.notesTooLong;
|
||||
case 'slugInvalid':
|
||||
return PublishRefusal.slugInvalid;
|
||||
case 'slugTaken':
|
||||
return PublishRefusal.slugTaken;
|
||||
case 'slugFixed':
|
||||
return PublishRefusal.slugFixed;
|
||||
case 'busy':
|
||||
return PublishRefusal.busy;
|
||||
default:
|
||||
throw ArgumentError(
|
||||
'Value "$name" cannot be converted to "PublishRefusal"',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String toJson() => name;
|
||||
|
||||
@override
|
||||
String toString() => name;
|
||||
}
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
/* 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_server/src/generated/protocol.dart' as _i5lsenq5;
|
||||
import 'package:serverpod/serverpod.dart' as _is;
|
||||
import '../publish/publish_refusal.dart' as _i5rbkflk;
|
||||
|
||||
/// Publishing was refused; nothing was published.
|
||||
abstract class PublishRefusedException
|
||||
implements
|
||||
_is.SerializableException,
|
||||
_is.SerializableModel,
|
||||
_is.ProtocolSerialization {
|
||||
PublishRefusedException._({
|
||||
required this.reason,
|
||||
this.items,
|
||||
});
|
||||
|
||||
factory PublishRefusedException({
|
||||
required _i5rbkflk.PublishRefusal reason,
|
||||
List<_is.UuidValue>? items,
|
||||
}) = _PublishRefusedExceptionImpl;
|
||||
|
||||
factory PublishRefusedException.fromJson(
|
||||
Map<String, dynamic> jsonSerialization,
|
||||
) {
|
||||
return PublishRefusedException(
|
||||
reason: _i5rbkflk.PublishRefusal.fromJson(
|
||||
(jsonSerialization['reason'] as String),
|
||||
),
|
||||
items: jsonSerialization['items'] == null
|
||||
? null
|
||||
: _i5lsenq5.Protocol().deserialize<List<_is.UuidValue>>(
|
||||
jsonSerialization['items'],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Why.
|
||||
_i5rbkflk.PublishRefusal reason;
|
||||
|
||||
/// With `conflicts`, the cards, notes and steps to settle; with
|
||||
/// `licenseNotOpen`, the studysets whose licence doesn't allow it.
|
||||
List<_is.UuidValue>? items;
|
||||
|
||||
/// Returns a shallow copy of this [PublishRefusedException]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_is.useResult
|
||||
PublishRefusedException copyWith({
|
||||
_i5rbkflk.PublishRefusal? reason,
|
||||
List<_is.UuidValue>? items,
|
||||
});
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'__className__': 'PublishRefusedException',
|
||||
'reason': reason.toJson(),
|
||||
if (items != null) 'items': items?.toJson(valueToJson: (v) => v.toJson()),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJsonForProtocol() {
|
||||
return {
|
||||
'__className__': 'PublishRefusedException',
|
||||
'reason': reason.toJson(),
|
||||
if (items != null) 'items': items?.toJson(valueToJson: (v) => v.toJson()),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'PublishRefusedException(reason: $reason, items: $items)';
|
||||
}
|
||||
}
|
||||
|
||||
class _Undefined {}
|
||||
|
||||
class _PublishRefusedExceptionImpl extends PublishRefusedException {
|
||||
_PublishRefusedExceptionImpl({
|
||||
required _i5rbkflk.PublishRefusal reason,
|
||||
List<_is.UuidValue>? items,
|
||||
}) : super._(
|
||||
reason: reason,
|
||||
items: items,
|
||||
);
|
||||
|
||||
/// Returns a shallow copy of this [PublishRefusedException]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_is.useResult
|
||||
@override
|
||||
PublishRefusedException copyWith({
|
||||
_i5rbkflk.PublishRefusal? reason,
|
||||
Object? items = _Undefined,
|
||||
}) {
|
||||
return PublishRefusedException(
|
||||
reason: reason ?? this.reason,
|
||||
items: items is List<_is.UuidValue>?
|
||||
? items
|
||||
: this.items?.map((e0) => e0).toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,703 @@
|
|||
/* 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/serverpod.dart' as _is;
|
||||
|
||||
/// A studyset its person published (ADR 0005, rule 3): found at
|
||||
/// `<account domain>/@<handle>/<slug>`, its releases kept in a git repository
|
||||
/// of its own, named by its public id. Server only.
|
||||
abstract class PublishedStudyset
|
||||
implements _is.TableRow<int?>, _is.ProtocolSerialization {
|
||||
PublishedStudyset._({
|
||||
this.id,
|
||||
required this.publicId,
|
||||
required this.studysetId,
|
||||
required this.authUserId,
|
||||
required this.slug,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
factory PublishedStudyset({
|
||||
int? id,
|
||||
required _is.UuidValue publicId,
|
||||
required _is.UuidValue studysetId,
|
||||
required _is.UuidValue authUserId,
|
||||
required String slug,
|
||||
required DateTime createdAt,
|
||||
}) = _PublishedStudysetImpl;
|
||||
|
||||
factory PublishedStudyset.fromJson(Map<String, dynamic> jsonSerialization) {
|
||||
return PublishedStudyset(
|
||||
id: jsonSerialization['id'] as int?,
|
||||
publicId: _is.UuidValueJsonExtension.fromJson(
|
||||
jsonSerialization['publicId'],
|
||||
),
|
||||
studysetId: _is.UuidValueJsonExtension.fromJson(
|
||||
jsonSerialization['studysetId'],
|
||||
),
|
||||
authUserId: _is.UuidValueJsonExtension.fromJson(
|
||||
jsonSerialization['authUserId'],
|
||||
),
|
||||
slug: jsonSerialization['slug'] as String,
|
||||
createdAt: _is.DateTimeJsonExtension.fromJson(
|
||||
jsonSerialization['createdAt'],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static final t = PublishedStudysetTable();
|
||||
|
||||
static const db = PublishedStudysetRepository._();
|
||||
|
||||
@override
|
||||
int? id;
|
||||
|
||||
/// Its identity for good, made at its first publish: never a synced row's
|
||||
/// id, which devices make (anyone could make one with another's id in
|
||||
/// their own space).
|
||||
_is.UuidValue publicId;
|
||||
|
||||
/// The studyset published, in its person's space: the root of what each
|
||||
/// release holds.
|
||||
_is.UuidValue studysetId;
|
||||
|
||||
/// Who published it: the AuthUser's id.
|
||||
_is.UuidValue authUserId;
|
||||
|
||||
/// Its name in its location, chosen at the first publish and fixed from
|
||||
/// then on (`japanese`).
|
||||
String slug;
|
||||
|
||||
/// When it was first published, in UTC.
|
||||
DateTime createdAt;
|
||||
|
||||
@override
|
||||
_is.Table<int?> get table => t;
|
||||
|
||||
/// Returns a shallow copy of this [PublishedStudyset]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_is.useResult
|
||||
PublishedStudyset copyWith({
|
||||
int? id,
|
||||
_is.UuidValue? publicId,
|
||||
_is.UuidValue? studysetId,
|
||||
_is.UuidValue? authUserId,
|
||||
String? slug,
|
||||
DateTime? createdAt,
|
||||
});
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'__className__': 'PublishedStudyset',
|
||||
if (id != null) 'id': id,
|
||||
'publicId': publicId.toJson(),
|
||||
'studysetId': studysetId.toJson(),
|
||||
'authUserId': authUserId.toJson(),
|
||||
'slug': slug,
|
||||
'createdAt': createdAt.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJsonForProtocol() {
|
||||
return {};
|
||||
}
|
||||
|
||||
static PublishedStudysetInclude include() {
|
||||
return PublishedStudysetInclude._();
|
||||
}
|
||||
|
||||
static PublishedStudysetIncludeList includeList({
|
||||
_is.WhereExpressionBuilder<PublishedStudysetTable>? where,
|
||||
int? limit,
|
||||
int? offset,
|
||||
_is.OrderByBuilder<PublishedStudysetTable>? orderBy,
|
||||
_is.OrderByListBuilder<PublishedStudysetTable>? orderByList,
|
||||
PublishedStudysetInclude? include,
|
||||
}) {
|
||||
return PublishedStudysetIncludeList._(
|
||||
where: where,
|
||||
limit: limit,
|
||||
offset: offset,
|
||||
orderBy: orderBy?.call(PublishedStudyset.t),
|
||||
orderByList: orderByList?.call(PublishedStudyset.t),
|
||||
include: include,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return _is.SerializationManager.encode(this);
|
||||
}
|
||||
}
|
||||
|
||||
class _Undefined {}
|
||||
|
||||
class _PublishedStudysetImpl extends PublishedStudyset {
|
||||
_PublishedStudysetImpl({
|
||||
int? id,
|
||||
required _is.UuidValue publicId,
|
||||
required _is.UuidValue studysetId,
|
||||
required _is.UuidValue authUserId,
|
||||
required String slug,
|
||||
required DateTime createdAt,
|
||||
}) : super._(
|
||||
id: id,
|
||||
publicId: publicId,
|
||||
studysetId: studysetId,
|
||||
authUserId: authUserId,
|
||||
slug: slug,
|
||||
createdAt: createdAt,
|
||||
);
|
||||
|
||||
/// Returns a shallow copy of this [PublishedStudyset]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_is.useResult
|
||||
@override
|
||||
PublishedStudyset copyWith({
|
||||
Object? id = _Undefined,
|
||||
_is.UuidValue? publicId,
|
||||
_is.UuidValue? studysetId,
|
||||
_is.UuidValue? authUserId,
|
||||
String? slug,
|
||||
DateTime? createdAt,
|
||||
}) {
|
||||
return PublishedStudyset(
|
||||
id: id is int? ? id : this.id,
|
||||
publicId: publicId ?? this.publicId,
|
||||
studysetId: studysetId ?? this.studysetId,
|
||||
authUserId: authUserId ?? this.authUserId,
|
||||
slug: slug ?? this.slug,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PublishedStudysetUpdateTable
|
||||
extends _is.UpdateTable<PublishedStudysetTable> {
|
||||
PublishedStudysetUpdateTable(super.table);
|
||||
|
||||
_is.ColumnValue<_is.UuidValue, _is.UuidValue> publicId(_is.UuidValue value) =>
|
||||
_is.ColumnValue(
|
||||
table.publicId,
|
||||
value,
|
||||
);
|
||||
|
||||
_is.ColumnValue<_is.UuidValue, _is.UuidValue> studysetId(
|
||||
_is.UuidValue value,
|
||||
) => _is.ColumnValue(
|
||||
table.studysetId,
|
||||
value,
|
||||
);
|
||||
|
||||
_is.ColumnValue<_is.UuidValue, _is.UuidValue> authUserId(
|
||||
_is.UuidValue value,
|
||||
) => _is.ColumnValue(
|
||||
table.authUserId,
|
||||
value,
|
||||
);
|
||||
|
||||
_is.ColumnValue<String, String> slug(String value) => _is.ColumnValue(
|
||||
table.slug,
|
||||
value,
|
||||
);
|
||||
|
||||
_is.ColumnValue<DateTime, DateTime> createdAt(DateTime value) =>
|
||||
_is.ColumnValue(
|
||||
table.createdAt,
|
||||
value,
|
||||
);
|
||||
}
|
||||
|
||||
class PublishedStudysetTable extends _is.Table<int?> {
|
||||
PublishedStudysetTable({super.tableRelation})
|
||||
: super(tableName: 'published_studyset') {
|
||||
updateTable = PublishedStudysetUpdateTable(this);
|
||||
publicId = _is.ColumnUuid(
|
||||
'publicId',
|
||||
this,
|
||||
);
|
||||
studysetId = _is.ColumnUuid(
|
||||
'studysetId',
|
||||
this,
|
||||
);
|
||||
authUserId = _is.ColumnUuid(
|
||||
'authUserId',
|
||||
this,
|
||||
);
|
||||
slug = _is.ColumnString(
|
||||
'slug',
|
||||
this,
|
||||
);
|
||||
createdAt = _is.ColumnDateTime(
|
||||
'createdAt',
|
||||
this,
|
||||
);
|
||||
}
|
||||
|
||||
late final PublishedStudysetUpdateTable updateTable;
|
||||
|
||||
/// Its identity for good, made at its first publish: never a synced row's
|
||||
/// id, which devices make (anyone could make one with another's id in
|
||||
/// their own space).
|
||||
late final _is.ColumnUuid publicId;
|
||||
|
||||
/// The studyset published, in its person's space: the root of what each
|
||||
/// release holds.
|
||||
late final _is.ColumnUuid studysetId;
|
||||
|
||||
/// Who published it: the AuthUser's id.
|
||||
late final _is.ColumnUuid authUserId;
|
||||
|
||||
/// Its name in its location, chosen at the first publish and fixed from
|
||||
/// then on (`japanese`).
|
||||
late final _is.ColumnString slug;
|
||||
|
||||
/// When it was first published, in UTC.
|
||||
late final _is.ColumnDateTime createdAt;
|
||||
|
||||
@override
|
||||
List<_is.Column> get columns => [
|
||||
id,
|
||||
publicId,
|
||||
studysetId,
|
||||
authUserId,
|
||||
slug,
|
||||
createdAt,
|
||||
];
|
||||
}
|
||||
|
||||
class PublishedStudysetInclude extends _is.IncludeObject {
|
||||
PublishedStudysetInclude._();
|
||||
|
||||
@override
|
||||
Map<String, _is.Include?> get includes => {};
|
||||
|
||||
@override
|
||||
_is.Table<int?> get table => PublishedStudyset.t;
|
||||
}
|
||||
|
||||
class PublishedStudysetIncludeList extends _is.IncludeList {
|
||||
PublishedStudysetIncludeList._({
|
||||
_is.WhereExpressionBuilder<PublishedStudysetTable>? where,
|
||||
super.limit,
|
||||
super.offset,
|
||||
super.orderBy,
|
||||
super.orderByList,
|
||||
super.include,
|
||||
}) {
|
||||
super.where = where?.call(PublishedStudyset.t);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, _is.Include?> get includes => include?.includes ?? {};
|
||||
|
||||
@override
|
||||
_is.Table<int?> get table => PublishedStudyset.t;
|
||||
}
|
||||
|
||||
class PublishedStudysetRepository {
|
||||
const PublishedStudysetRepository._();
|
||||
|
||||
/// Returns a list of [PublishedStudyset]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<PublishedStudyset>> find(
|
||||
_is.DatabaseSession session, {
|
||||
_is.WhereExpressionBuilder<PublishedStudysetTable>? where,
|
||||
int? limit,
|
||||
int? offset,
|
||||
_is.OrderByBuilder<PublishedStudysetTable>? orderBy,
|
||||
_is.OrderByListBuilder<PublishedStudysetTable>? orderByList,
|
||||
_is.Transaction? transaction,
|
||||
_is.LockMode? lockMode,
|
||||
_is.LockBehavior? lockBehavior,
|
||||
}) async {
|
||||
return session.db.find<PublishedStudyset>(
|
||||
where: where?.call(PublishedStudyset.t),
|
||||
orderBy: orderBy?.call(PublishedStudyset.t),
|
||||
orderByList: orderByList?.call(PublishedStudyset.t),
|
||||
limit: limit,
|
||||
offset: offset,
|
||||
transaction: transaction,
|
||||
lockMode: lockMode,
|
||||
lockBehavior: lockBehavior,
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns the first matching [PublishedStudyset] 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<PublishedStudyset?> findFirstRow(
|
||||
_is.DatabaseSession session, {
|
||||
_is.WhereExpressionBuilder<PublishedStudysetTable>? where,
|
||||
int? offset,
|
||||
_is.OrderByBuilder<PublishedStudysetTable>? orderBy,
|
||||
_is.OrderByListBuilder<PublishedStudysetTable>? orderByList,
|
||||
_is.Transaction? transaction,
|
||||
_is.LockMode? lockMode,
|
||||
_is.LockBehavior? lockBehavior,
|
||||
}) async {
|
||||
return session.db.findFirstRow<PublishedStudyset>(
|
||||
where: where?.call(PublishedStudyset.t),
|
||||
orderBy: orderBy?.call(PublishedStudyset.t),
|
||||
orderByList: orderByList?.call(PublishedStudyset.t),
|
||||
offset: offset,
|
||||
transaction: transaction,
|
||||
lockMode: lockMode,
|
||||
lockBehavior: lockBehavior,
|
||||
);
|
||||
}
|
||||
|
||||
/// Finds a single [PublishedStudyset] by its [id] or null if no such row exists.
|
||||
Future<PublishedStudyset?> findById(
|
||||
_is.DatabaseSession session,
|
||||
int id, {
|
||||
_is.Transaction? transaction,
|
||||
_is.LockMode? lockMode,
|
||||
_is.LockBehavior? lockBehavior,
|
||||
}) async {
|
||||
return session.db.findById<PublishedStudyset>(
|
||||
id,
|
||||
transaction: transaction,
|
||||
lockMode: lockMode,
|
||||
lockBehavior: lockBehavior,
|
||||
);
|
||||
}
|
||||
|
||||
/// Inserts all [PublishedStudyset]s in the list and returns the inserted rows.
|
||||
///
|
||||
/// The returned [PublishedStudyset]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<PublishedStudyset>> insert(
|
||||
_is.DatabaseSession session,
|
||||
List<PublishedStudyset> rows, {
|
||||
_is.Transaction? transaction,
|
||||
bool ignoreConflicts = false,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.insert<PublishedStudyset>(
|
||||
rows,
|
||||
transaction: transaction,
|
||||
ignoreConflicts: ignoreConflicts,
|
||||
noReturn: noReturn,
|
||||
);
|
||||
}
|
||||
|
||||
/// Inserts a single [PublishedStudyset] and returns the inserted row.
|
||||
///
|
||||
/// The returned [PublishedStudyset] will have its `id` field set.
|
||||
Future<PublishedStudyset> insertRow(
|
||||
_is.DatabaseSession session,
|
||||
PublishedStudyset row, {
|
||||
_is.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.insertRow<PublishedStudyset>(
|
||||
row,
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
|
||||
/// Upserts all [PublishedStudyset]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 [PublishedStudyset]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<PublishedStudyset>> upsert(
|
||||
_is.DatabaseSession session,
|
||||
List<PublishedStudyset> rows, {
|
||||
required _is.ColumnSelections<PublishedStudysetTable> conflictColumns,
|
||||
_is.ColumnSelections<PublishedStudysetTable>? updateColumns,
|
||||
_is.WhereExpressionBuilder<PublishedStudysetTable>? updateWhere,
|
||||
_is.Transaction? transaction,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.upsert<PublishedStudyset>(
|
||||
rows,
|
||||
conflictColumns: conflictColumns(PublishedStudyset.t),
|
||||
updateColumns: updateColumns?.call(PublishedStudyset.t),
|
||||
updateWhere: updateWhere?.call(PublishedStudyset.t),
|
||||
transaction: transaction,
|
||||
noReturn: noReturn,
|
||||
);
|
||||
}
|
||||
|
||||
/// Upserts a single [PublishedStudyset] 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 [PublishedStudyset] will have its `id` field set.
|
||||
Future<PublishedStudyset?> upsertRow(
|
||||
_is.DatabaseSession session,
|
||||
PublishedStudyset row, {
|
||||
required _is.ColumnSelections<PublishedStudysetTable> conflictColumns,
|
||||
_is.ColumnSelections<PublishedStudysetTable>? updateColumns,
|
||||
_is.WhereExpressionBuilder<PublishedStudysetTable>? updateWhere,
|
||||
_is.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.upsertRow<PublishedStudyset>(
|
||||
row,
|
||||
conflictColumns: conflictColumns(PublishedStudyset.t),
|
||||
updateColumns: updateColumns?.call(PublishedStudyset.t),
|
||||
updateWhere: updateWhere?.call(PublishedStudyset.t),
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
|
||||
/// Updates all [PublishedStudyset]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<PublishedStudyset>> update(
|
||||
_is.DatabaseSession session,
|
||||
List<PublishedStudyset> rows, {
|
||||
_is.ColumnSelections<PublishedStudysetTable>? columns,
|
||||
_is.Transaction? transaction,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.update<PublishedStudyset>(
|
||||
rows,
|
||||
columns: columns?.call(PublishedStudyset.t),
|
||||
transaction: transaction,
|
||||
noReturn: noReturn,
|
||||
);
|
||||
}
|
||||
|
||||
/// Updates a single [PublishedStudyset]. 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<PublishedStudyset> updateRow(
|
||||
_is.DatabaseSession session,
|
||||
PublishedStudyset row, {
|
||||
_is.ColumnSelections<PublishedStudysetTable>? columns,
|
||||
_is.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.updateRow<PublishedStudyset>(
|
||||
row,
|
||||
columns: columns?.call(PublishedStudyset.t),
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
|
||||
/// Updates a single [PublishedStudyset] by its [id] with the specified [columnValues].
|
||||
/// Returns the updated row or null if no row with the given id exists.
|
||||
Future<PublishedStudyset?> updateById(
|
||||
_is.DatabaseSession session,
|
||||
int id, {
|
||||
required _is.ColumnValueListBuilder<PublishedStudysetUpdateTable>
|
||||
columnValues,
|
||||
_is.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.updateById<PublishedStudyset>(
|
||||
id,
|
||||
columnValues: columnValues(PublishedStudyset.t.updateTable),
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
|
||||
/// Updates all [PublishedStudyset]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<PublishedStudyset>> updateWhere(
|
||||
_is.DatabaseSession session, {
|
||||
required _is.ColumnValueListBuilder<PublishedStudysetUpdateTable>
|
||||
columnValues,
|
||||
required _is.WhereExpressionBuilder<PublishedStudysetTable> where,
|
||||
int? limit,
|
||||
int? offset,
|
||||
_is.OrderByBuilder<PublishedStudysetTable>? orderBy,
|
||||
_is.OrderByListBuilder<PublishedStudysetTable>? orderByList,
|
||||
_is.Transaction? transaction,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.updateWhere<PublishedStudyset>(
|
||||
columnValues: columnValues(PublishedStudyset.t.updateTable),
|
||||
where: where(PublishedStudyset.t),
|
||||
limit: limit,
|
||||
offset: offset,
|
||||
orderBy: orderBy?.call(PublishedStudyset.t),
|
||||
orderByList: orderByList?.call(PublishedStudyset.t),
|
||||
transaction: transaction,
|
||||
noReturn: noReturn,
|
||||
);
|
||||
}
|
||||
|
||||
/// Deletes all [PublishedStudyset]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<PublishedStudyset>> delete(
|
||||
_is.DatabaseSession session,
|
||||
List<PublishedStudyset> rows, {
|
||||
_is.OrderByBuilder<PublishedStudysetTable>? orderBy,
|
||||
_is.OrderByListBuilder<PublishedStudysetTable>? orderByList,
|
||||
_is.Transaction? transaction,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.delete<PublishedStudyset>(
|
||||
rows,
|
||||
orderBy: orderBy?.call(PublishedStudyset.t),
|
||||
orderByList: orderByList?.call(PublishedStudyset.t),
|
||||
transaction: transaction,
|
||||
noReturn: noReturn,
|
||||
);
|
||||
}
|
||||
|
||||
/// Deletes a single [PublishedStudyset].
|
||||
Future<PublishedStudyset> deleteRow(
|
||||
_is.DatabaseSession session,
|
||||
PublishedStudyset row, {
|
||||
_is.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.deleteRow<PublishedStudyset>(
|
||||
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<PublishedStudyset>> deleteWhere(
|
||||
_is.DatabaseSession session, {
|
||||
required _is.WhereExpressionBuilder<PublishedStudysetTable> where,
|
||||
_is.OrderByBuilder<PublishedStudysetTable>? orderBy,
|
||||
_is.OrderByListBuilder<PublishedStudysetTable>? orderByList,
|
||||
_is.Transaction? transaction,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.deleteWhere<PublishedStudyset>(
|
||||
where: where(PublishedStudyset.t),
|
||||
orderBy: orderBy?.call(PublishedStudyset.t),
|
||||
orderByList: orderByList?.call(PublishedStudyset.t),
|
||||
transaction: transaction,
|
||||
noReturn: noReturn,
|
||||
);
|
||||
}
|
||||
|
||||
/// Counts the number of rows matching the [where] expression. If omitted,
|
||||
/// will return the count of all rows in the table.
|
||||
Future<int> count(
|
||||
_is.DatabaseSession session, {
|
||||
_is.WhereExpressionBuilder<PublishedStudysetTable>? where,
|
||||
int? limit,
|
||||
_is.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.count<PublishedStudyset>(
|
||||
where: where?.call(PublishedStudyset.t),
|
||||
limit: limit,
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
|
||||
/// Acquires row-level locks on [PublishedStudyset] rows matching the [where] expression.
|
||||
Future<void> lockRows(
|
||||
_is.DatabaseSession session, {
|
||||
required _is.WhereExpressionBuilder<PublishedStudysetTable> where,
|
||||
required _is.LockMode lockMode,
|
||||
required _is.Transaction transaction,
|
||||
_is.LockBehavior lockBehavior = _is.LockBehavior.wait,
|
||||
}) async {
|
||||
return session.db.lockRows<PublishedStudyset>(
|
||||
where: where(PublishedStudyset.t),
|
||||
lockMode: lockMode,
|
||||
lockBehavior: lockBehavior,
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
}
|
||||
182
halkyon_learn_server/lib/src/generated/publish/release_info.dart
Normal file
182
halkyon_learn_server/lib/src/generated/publish/release_info.dart
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
/* 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/serverpod.dart' as _is;
|
||||
|
||||
/// A release as the app shows it.
|
||||
abstract class ReleaseInfo
|
||||
implements _is.SerializableModel, _is.ProtocolSerialization {
|
||||
ReleaseInfo._({
|
||||
required this.version,
|
||||
required this.notes,
|
||||
required this.license,
|
||||
required this.studysetCount,
|
||||
required this.cardCount,
|
||||
required this.noteCount,
|
||||
required this.pathCount,
|
||||
required this.pdfCount,
|
||||
required this.releasedAt,
|
||||
});
|
||||
|
||||
factory ReleaseInfo({
|
||||
required int version,
|
||||
required String notes,
|
||||
required String license,
|
||||
required int studysetCount,
|
||||
required int cardCount,
|
||||
required int noteCount,
|
||||
required int pathCount,
|
||||
required int pdfCount,
|
||||
required DateTime releasedAt,
|
||||
}) = _ReleaseInfoImpl;
|
||||
|
||||
factory ReleaseInfo.fromJson(Map<String, dynamic> jsonSerialization) {
|
||||
return ReleaseInfo(
|
||||
version: jsonSerialization['version'] as int,
|
||||
notes: jsonSerialization['notes'] as String,
|
||||
license: jsonSerialization['license'] as String,
|
||||
studysetCount: jsonSerialization['studysetCount'] as int,
|
||||
cardCount: jsonSerialization['cardCount'] as int,
|
||||
noteCount: jsonSerialization['noteCount'] as int,
|
||||
pathCount: jsonSerialization['pathCount'] as int,
|
||||
pdfCount: jsonSerialization['pdfCount'] as int,
|
||||
releasedAt: _is.DateTimeJsonExtension.fromJson(
|
||||
jsonSerialization['releasedAt'],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 1, 2, 3, …
|
||||
int version;
|
||||
|
||||
/// What changed; empty for nothing said.
|
||||
String notes;
|
||||
|
||||
/// The licence it is under.
|
||||
String license;
|
||||
|
||||
int studysetCount;
|
||||
|
||||
int cardCount;
|
||||
|
||||
int noteCount;
|
||||
|
||||
int pathCount;
|
||||
|
||||
int pdfCount;
|
||||
|
||||
/// When it was released, in UTC.
|
||||
DateTime releasedAt;
|
||||
|
||||
/// Returns a shallow copy of this [ReleaseInfo]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_is.useResult
|
||||
ReleaseInfo copyWith({
|
||||
int? version,
|
||||
String? notes,
|
||||
String? license,
|
||||
int? studysetCount,
|
||||
int? cardCount,
|
||||
int? noteCount,
|
||||
int? pathCount,
|
||||
int? pdfCount,
|
||||
DateTime? releasedAt,
|
||||
});
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'__className__': 'ReleaseInfo',
|
||||
'version': version,
|
||||
'notes': notes,
|
||||
'license': license,
|
||||
'studysetCount': studysetCount,
|
||||
'cardCount': cardCount,
|
||||
'noteCount': noteCount,
|
||||
'pathCount': pathCount,
|
||||
'pdfCount': pdfCount,
|
||||
'releasedAt': releasedAt.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJsonForProtocol() {
|
||||
return {
|
||||
'__className__': 'ReleaseInfo',
|
||||
'version': version,
|
||||
'notes': notes,
|
||||
'license': license,
|
||||
'studysetCount': studysetCount,
|
||||
'cardCount': cardCount,
|
||||
'noteCount': noteCount,
|
||||
'pathCount': pathCount,
|
||||
'pdfCount': pdfCount,
|
||||
'releasedAt': releasedAt.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return _is.SerializationManager.encode(this);
|
||||
}
|
||||
}
|
||||
|
||||
class _ReleaseInfoImpl extends ReleaseInfo {
|
||||
_ReleaseInfoImpl({
|
||||
required int version,
|
||||
required String notes,
|
||||
required String license,
|
||||
required int studysetCount,
|
||||
required int cardCount,
|
||||
required int noteCount,
|
||||
required int pathCount,
|
||||
required int pdfCount,
|
||||
required DateTime releasedAt,
|
||||
}) : super._(
|
||||
version: version,
|
||||
notes: notes,
|
||||
license: license,
|
||||
studysetCount: studysetCount,
|
||||
cardCount: cardCount,
|
||||
noteCount: noteCount,
|
||||
pathCount: pathCount,
|
||||
pdfCount: pdfCount,
|
||||
releasedAt: releasedAt,
|
||||
);
|
||||
|
||||
/// Returns a shallow copy of this [ReleaseInfo]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_is.useResult
|
||||
@override
|
||||
ReleaseInfo copyWith({
|
||||
int? version,
|
||||
String? notes,
|
||||
String? license,
|
||||
int? studysetCount,
|
||||
int? cardCount,
|
||||
int? noteCount,
|
||||
int? pathCount,
|
||||
int? pdfCount,
|
||||
DateTime? releasedAt,
|
||||
}) {
|
||||
return ReleaseInfo(
|
||||
version: version ?? this.version,
|
||||
notes: notes ?? this.notes,
|
||||
license: license ?? this.license,
|
||||
studysetCount: studysetCount ?? this.studysetCount,
|
||||
cardCount: cardCount ?? this.cardCount,
|
||||
noteCount: noteCount ?? this.noteCount,
|
||||
pathCount: pathCount ?? this.pathCount,
|
||||
pdfCount: pdfCount ?? this.pdfCount,
|
||||
releasedAt: releasedAt ?? this.releasedAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,847 @@
|
|||
/* 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/serverpod.dart' as _is;
|
||||
|
||||
/// One release of a published studyset (ADR 0005, rule 3): a commit and the tag
|
||||
/// `v<version>` in its repository, never changed. Server only.
|
||||
abstract class StudysetRelease
|
||||
implements _is.TableRow<int?>, _is.ProtocolSerialization {
|
||||
StudysetRelease._({
|
||||
this.id,
|
||||
required this.publishedStudysetId,
|
||||
required this.version,
|
||||
required this.commit,
|
||||
required this.tree,
|
||||
required this.notes,
|
||||
required this.license,
|
||||
required this.studysetCount,
|
||||
required this.cardCount,
|
||||
required this.noteCount,
|
||||
required this.pathCount,
|
||||
required this.pdfCount,
|
||||
required this.releasedAt,
|
||||
});
|
||||
|
||||
factory StudysetRelease({
|
||||
int? id,
|
||||
required int publishedStudysetId,
|
||||
required int version,
|
||||
required String commit,
|
||||
required String tree,
|
||||
required String notes,
|
||||
required String license,
|
||||
required int studysetCount,
|
||||
required int cardCount,
|
||||
required int noteCount,
|
||||
required int pathCount,
|
||||
required int pdfCount,
|
||||
required DateTime releasedAt,
|
||||
}) = _StudysetReleaseImpl;
|
||||
|
||||
factory StudysetRelease.fromJson(Map<String, dynamic> jsonSerialization) {
|
||||
return StudysetRelease(
|
||||
id: jsonSerialization['id'] as int?,
|
||||
publishedStudysetId: jsonSerialization['publishedStudysetId'] as int,
|
||||
version: jsonSerialization['version'] as int,
|
||||
commit: jsonSerialization['commit'] as String,
|
||||
tree: jsonSerialization['tree'] as String,
|
||||
notes: jsonSerialization['notes'] as String,
|
||||
license: jsonSerialization['license'] as String,
|
||||
studysetCount: jsonSerialization['studysetCount'] as int,
|
||||
cardCount: jsonSerialization['cardCount'] as int,
|
||||
noteCount: jsonSerialization['noteCount'] as int,
|
||||
pathCount: jsonSerialization['pathCount'] as int,
|
||||
pdfCount: jsonSerialization['pdfCount'] as int,
|
||||
releasedAt: _is.DateTimeJsonExtension.fromJson(
|
||||
jsonSerialization['releasedAt'],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static final t = StudysetReleaseTable();
|
||||
|
||||
static const db = StudysetReleaseRepository._();
|
||||
|
||||
@override
|
||||
int? id;
|
||||
|
||||
/// The published studyset; its releases go with it.
|
||||
int publishedStudysetId;
|
||||
|
||||
/// 1, 2, 3, … in the order released.
|
||||
int version;
|
||||
|
||||
/// The commit and its tree in the repository (SHA-1, as git names them).
|
||||
String commit;
|
||||
|
||||
String tree;
|
||||
|
||||
/// What changed, in its person's words; empty for nothing said.
|
||||
String notes;
|
||||
|
||||
/// The licence it is under, as its studyset gave it then.
|
||||
String license;
|
||||
|
||||
/// What it holds.
|
||||
int studysetCount;
|
||||
|
||||
int cardCount;
|
||||
|
||||
int noteCount;
|
||||
|
||||
int pathCount;
|
||||
|
||||
int pdfCount;
|
||||
|
||||
/// When it was released, in UTC.
|
||||
DateTime releasedAt;
|
||||
|
||||
@override
|
||||
_is.Table<int?> get table => t;
|
||||
|
||||
/// Returns a shallow copy of this [StudysetRelease]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_is.useResult
|
||||
StudysetRelease copyWith({
|
||||
int? id,
|
||||
int? publishedStudysetId,
|
||||
int? version,
|
||||
String? commit,
|
||||
String? tree,
|
||||
String? notes,
|
||||
String? license,
|
||||
int? studysetCount,
|
||||
int? cardCount,
|
||||
int? noteCount,
|
||||
int? pathCount,
|
||||
int? pdfCount,
|
||||
DateTime? releasedAt,
|
||||
});
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'__className__': 'StudysetRelease',
|
||||
if (id != null) 'id': id,
|
||||
'publishedStudysetId': publishedStudysetId,
|
||||
'version': version,
|
||||
'commit': commit,
|
||||
'tree': tree,
|
||||
'notes': notes,
|
||||
'license': license,
|
||||
'studysetCount': studysetCount,
|
||||
'cardCount': cardCount,
|
||||
'noteCount': noteCount,
|
||||
'pathCount': pathCount,
|
||||
'pdfCount': pdfCount,
|
||||
'releasedAt': releasedAt.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJsonForProtocol() {
|
||||
return {};
|
||||
}
|
||||
|
||||
static StudysetReleaseInclude include() {
|
||||
return StudysetReleaseInclude._();
|
||||
}
|
||||
|
||||
static StudysetReleaseIncludeList includeList({
|
||||
_is.WhereExpressionBuilder<StudysetReleaseTable>? where,
|
||||
int? limit,
|
||||
int? offset,
|
||||
_is.OrderByBuilder<StudysetReleaseTable>? orderBy,
|
||||
_is.OrderByListBuilder<StudysetReleaseTable>? orderByList,
|
||||
StudysetReleaseInclude? include,
|
||||
}) {
|
||||
return StudysetReleaseIncludeList._(
|
||||
where: where,
|
||||
limit: limit,
|
||||
offset: offset,
|
||||
orderBy: orderBy?.call(StudysetRelease.t),
|
||||
orderByList: orderByList?.call(StudysetRelease.t),
|
||||
include: include,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return _is.SerializationManager.encode(this);
|
||||
}
|
||||
}
|
||||
|
||||
class _Undefined {}
|
||||
|
||||
class _StudysetReleaseImpl extends StudysetRelease {
|
||||
_StudysetReleaseImpl({
|
||||
int? id,
|
||||
required int publishedStudysetId,
|
||||
required int version,
|
||||
required String commit,
|
||||
required String tree,
|
||||
required String notes,
|
||||
required String license,
|
||||
required int studysetCount,
|
||||
required int cardCount,
|
||||
required int noteCount,
|
||||
required int pathCount,
|
||||
required int pdfCount,
|
||||
required DateTime releasedAt,
|
||||
}) : super._(
|
||||
id: id,
|
||||
publishedStudysetId: publishedStudysetId,
|
||||
version: version,
|
||||
commit: commit,
|
||||
tree: tree,
|
||||
notes: notes,
|
||||
license: license,
|
||||
studysetCount: studysetCount,
|
||||
cardCount: cardCount,
|
||||
noteCount: noteCount,
|
||||
pathCount: pathCount,
|
||||
pdfCount: pdfCount,
|
||||
releasedAt: releasedAt,
|
||||
);
|
||||
|
||||
/// Returns a shallow copy of this [StudysetRelease]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_is.useResult
|
||||
@override
|
||||
StudysetRelease copyWith({
|
||||
Object? id = _Undefined,
|
||||
int? publishedStudysetId,
|
||||
int? version,
|
||||
String? commit,
|
||||
String? tree,
|
||||
String? notes,
|
||||
String? license,
|
||||
int? studysetCount,
|
||||
int? cardCount,
|
||||
int? noteCount,
|
||||
int? pathCount,
|
||||
int? pdfCount,
|
||||
DateTime? releasedAt,
|
||||
}) {
|
||||
return StudysetRelease(
|
||||
id: id is int? ? id : this.id,
|
||||
publishedStudysetId: publishedStudysetId ?? this.publishedStudysetId,
|
||||
version: version ?? this.version,
|
||||
commit: commit ?? this.commit,
|
||||
tree: tree ?? this.tree,
|
||||
notes: notes ?? this.notes,
|
||||
license: license ?? this.license,
|
||||
studysetCount: studysetCount ?? this.studysetCount,
|
||||
cardCount: cardCount ?? this.cardCount,
|
||||
noteCount: noteCount ?? this.noteCount,
|
||||
pathCount: pathCount ?? this.pathCount,
|
||||
pdfCount: pdfCount ?? this.pdfCount,
|
||||
releasedAt: releasedAt ?? this.releasedAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class StudysetReleaseUpdateTable extends _is.UpdateTable<StudysetReleaseTable> {
|
||||
StudysetReleaseUpdateTable(super.table);
|
||||
|
||||
_is.ColumnValue<int, int> publishedStudysetId(int value) => _is.ColumnValue(
|
||||
table.publishedStudysetId,
|
||||
value,
|
||||
);
|
||||
|
||||
_is.ColumnValue<int, int> version(int value) => _is.ColumnValue(
|
||||
table.version,
|
||||
value,
|
||||
);
|
||||
|
||||
_is.ColumnValue<String, String> commit(String value) => _is.ColumnValue(
|
||||
table.commit,
|
||||
value,
|
||||
);
|
||||
|
||||
_is.ColumnValue<String, String> tree(String value) => _is.ColumnValue(
|
||||
table.tree,
|
||||
value,
|
||||
);
|
||||
|
||||
_is.ColumnValue<String, String> notes(String value) => _is.ColumnValue(
|
||||
table.notes,
|
||||
value,
|
||||
);
|
||||
|
||||
_is.ColumnValue<String, String> license(String value) => _is.ColumnValue(
|
||||
table.license,
|
||||
value,
|
||||
);
|
||||
|
||||
_is.ColumnValue<int, int> studysetCount(int value) => _is.ColumnValue(
|
||||
table.studysetCount,
|
||||
value,
|
||||
);
|
||||
|
||||
_is.ColumnValue<int, int> cardCount(int value) => _is.ColumnValue(
|
||||
table.cardCount,
|
||||
value,
|
||||
);
|
||||
|
||||
_is.ColumnValue<int, int> noteCount(int value) => _is.ColumnValue(
|
||||
table.noteCount,
|
||||
value,
|
||||
);
|
||||
|
||||
_is.ColumnValue<int, int> pathCount(int value) => _is.ColumnValue(
|
||||
table.pathCount,
|
||||
value,
|
||||
);
|
||||
|
||||
_is.ColumnValue<int, int> pdfCount(int value) => _is.ColumnValue(
|
||||
table.pdfCount,
|
||||
value,
|
||||
);
|
||||
|
||||
_is.ColumnValue<DateTime, DateTime> releasedAt(DateTime value) =>
|
||||
_is.ColumnValue(
|
||||
table.releasedAt,
|
||||
value,
|
||||
);
|
||||
}
|
||||
|
||||
class StudysetReleaseTable extends _is.Table<int?> {
|
||||
StudysetReleaseTable({super.tableRelation})
|
||||
: super(tableName: 'studyset_release') {
|
||||
updateTable = StudysetReleaseUpdateTable(this);
|
||||
publishedStudysetId = _is.ColumnInt(
|
||||
'publishedStudysetId',
|
||||
this,
|
||||
);
|
||||
version = _is.ColumnInt(
|
||||
'version',
|
||||
this,
|
||||
);
|
||||
commit = _is.ColumnString(
|
||||
'commit',
|
||||
this,
|
||||
);
|
||||
tree = _is.ColumnString(
|
||||
'tree',
|
||||
this,
|
||||
);
|
||||
notes = _is.ColumnString(
|
||||
'notes',
|
||||
this,
|
||||
);
|
||||
license = _is.ColumnString(
|
||||
'license',
|
||||
this,
|
||||
);
|
||||
studysetCount = _is.ColumnInt(
|
||||
'studysetCount',
|
||||
this,
|
||||
);
|
||||
cardCount = _is.ColumnInt(
|
||||
'cardCount',
|
||||
this,
|
||||
);
|
||||
noteCount = _is.ColumnInt(
|
||||
'noteCount',
|
||||
this,
|
||||
);
|
||||
pathCount = _is.ColumnInt(
|
||||
'pathCount',
|
||||
this,
|
||||
);
|
||||
pdfCount = _is.ColumnInt(
|
||||
'pdfCount',
|
||||
this,
|
||||
);
|
||||
releasedAt = _is.ColumnDateTime(
|
||||
'releasedAt',
|
||||
this,
|
||||
);
|
||||
}
|
||||
|
||||
late final StudysetReleaseUpdateTable updateTable;
|
||||
|
||||
/// The published studyset; its releases go with it.
|
||||
late final _is.ColumnInt publishedStudysetId;
|
||||
|
||||
/// 1, 2, 3, … in the order released.
|
||||
late final _is.ColumnInt version;
|
||||
|
||||
/// The commit and its tree in the repository (SHA-1, as git names them).
|
||||
late final _is.ColumnString commit;
|
||||
|
||||
late final _is.ColumnString tree;
|
||||
|
||||
/// What changed, in its person's words; empty for nothing said.
|
||||
late final _is.ColumnString notes;
|
||||
|
||||
/// The licence it is under, as its studyset gave it then.
|
||||
late final _is.ColumnString license;
|
||||
|
||||
/// What it holds.
|
||||
late final _is.ColumnInt studysetCount;
|
||||
|
||||
late final _is.ColumnInt cardCount;
|
||||
|
||||
late final _is.ColumnInt noteCount;
|
||||
|
||||
late final _is.ColumnInt pathCount;
|
||||
|
||||
late final _is.ColumnInt pdfCount;
|
||||
|
||||
/// When it was released, in UTC.
|
||||
late final _is.ColumnDateTime releasedAt;
|
||||
|
||||
@override
|
||||
List<_is.Column> get columns => [
|
||||
id,
|
||||
publishedStudysetId,
|
||||
version,
|
||||
commit,
|
||||
tree,
|
||||
notes,
|
||||
license,
|
||||
studysetCount,
|
||||
cardCount,
|
||||
noteCount,
|
||||
pathCount,
|
||||
pdfCount,
|
||||
releasedAt,
|
||||
];
|
||||
}
|
||||
|
||||
class StudysetReleaseInclude extends _is.IncludeObject {
|
||||
StudysetReleaseInclude._();
|
||||
|
||||
@override
|
||||
Map<String, _is.Include?> get includes => {};
|
||||
|
||||
@override
|
||||
_is.Table<int?> get table => StudysetRelease.t;
|
||||
}
|
||||
|
||||
class StudysetReleaseIncludeList extends _is.IncludeList {
|
||||
StudysetReleaseIncludeList._({
|
||||
_is.WhereExpressionBuilder<StudysetReleaseTable>? where,
|
||||
super.limit,
|
||||
super.offset,
|
||||
super.orderBy,
|
||||
super.orderByList,
|
||||
super.include,
|
||||
}) {
|
||||
super.where = where?.call(StudysetRelease.t);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, _is.Include?> get includes => include?.includes ?? {};
|
||||
|
||||
@override
|
||||
_is.Table<int?> get table => StudysetRelease.t;
|
||||
}
|
||||
|
||||
class StudysetReleaseRepository {
|
||||
const StudysetReleaseRepository._();
|
||||
|
||||
/// Returns a list of [StudysetRelease]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<StudysetRelease>> find(
|
||||
_is.DatabaseSession session, {
|
||||
_is.WhereExpressionBuilder<StudysetReleaseTable>? where,
|
||||
int? limit,
|
||||
int? offset,
|
||||
_is.OrderByBuilder<StudysetReleaseTable>? orderBy,
|
||||
_is.OrderByListBuilder<StudysetReleaseTable>? orderByList,
|
||||
_is.Transaction? transaction,
|
||||
_is.LockMode? lockMode,
|
||||
_is.LockBehavior? lockBehavior,
|
||||
}) async {
|
||||
return session.db.find<StudysetRelease>(
|
||||
where: where?.call(StudysetRelease.t),
|
||||
orderBy: orderBy?.call(StudysetRelease.t),
|
||||
orderByList: orderByList?.call(StudysetRelease.t),
|
||||
limit: limit,
|
||||
offset: offset,
|
||||
transaction: transaction,
|
||||
lockMode: lockMode,
|
||||
lockBehavior: lockBehavior,
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns the first matching [StudysetRelease] 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<StudysetRelease?> findFirstRow(
|
||||
_is.DatabaseSession session, {
|
||||
_is.WhereExpressionBuilder<StudysetReleaseTable>? where,
|
||||
int? offset,
|
||||
_is.OrderByBuilder<StudysetReleaseTable>? orderBy,
|
||||
_is.OrderByListBuilder<StudysetReleaseTable>? orderByList,
|
||||
_is.Transaction? transaction,
|
||||
_is.LockMode? lockMode,
|
||||
_is.LockBehavior? lockBehavior,
|
||||
}) async {
|
||||
return session.db.findFirstRow<StudysetRelease>(
|
||||
where: where?.call(StudysetRelease.t),
|
||||
orderBy: orderBy?.call(StudysetRelease.t),
|
||||
orderByList: orderByList?.call(StudysetRelease.t),
|
||||
offset: offset,
|
||||
transaction: transaction,
|
||||
lockMode: lockMode,
|
||||
lockBehavior: lockBehavior,
|
||||
);
|
||||
}
|
||||
|
||||
/// Finds a single [StudysetRelease] by its [id] or null if no such row exists.
|
||||
Future<StudysetRelease?> findById(
|
||||
_is.DatabaseSession session,
|
||||
int id, {
|
||||
_is.Transaction? transaction,
|
||||
_is.LockMode? lockMode,
|
||||
_is.LockBehavior? lockBehavior,
|
||||
}) async {
|
||||
return session.db.findById<StudysetRelease>(
|
||||
id,
|
||||
transaction: transaction,
|
||||
lockMode: lockMode,
|
||||
lockBehavior: lockBehavior,
|
||||
);
|
||||
}
|
||||
|
||||
/// Inserts all [StudysetRelease]s in the list and returns the inserted rows.
|
||||
///
|
||||
/// The returned [StudysetRelease]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<StudysetRelease>> insert(
|
||||
_is.DatabaseSession session,
|
||||
List<StudysetRelease> rows, {
|
||||
_is.Transaction? transaction,
|
||||
bool ignoreConflicts = false,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.insert<StudysetRelease>(
|
||||
rows,
|
||||
transaction: transaction,
|
||||
ignoreConflicts: ignoreConflicts,
|
||||
noReturn: noReturn,
|
||||
);
|
||||
}
|
||||
|
||||
/// Inserts a single [StudysetRelease] and returns the inserted row.
|
||||
///
|
||||
/// The returned [StudysetRelease] will have its `id` field set.
|
||||
Future<StudysetRelease> insertRow(
|
||||
_is.DatabaseSession session,
|
||||
StudysetRelease row, {
|
||||
_is.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.insertRow<StudysetRelease>(
|
||||
row,
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
|
||||
/// Upserts all [StudysetRelease]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 [StudysetRelease]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<StudysetRelease>> upsert(
|
||||
_is.DatabaseSession session,
|
||||
List<StudysetRelease> rows, {
|
||||
required _is.ColumnSelections<StudysetReleaseTable> conflictColumns,
|
||||
_is.ColumnSelections<StudysetReleaseTable>? updateColumns,
|
||||
_is.WhereExpressionBuilder<StudysetReleaseTable>? updateWhere,
|
||||
_is.Transaction? transaction,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.upsert<StudysetRelease>(
|
||||
rows,
|
||||
conflictColumns: conflictColumns(StudysetRelease.t),
|
||||
updateColumns: updateColumns?.call(StudysetRelease.t),
|
||||
updateWhere: updateWhere?.call(StudysetRelease.t),
|
||||
transaction: transaction,
|
||||
noReturn: noReturn,
|
||||
);
|
||||
}
|
||||
|
||||
/// Upserts a single [StudysetRelease] 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 [StudysetRelease] will have its `id` field set.
|
||||
Future<StudysetRelease?> upsertRow(
|
||||
_is.DatabaseSession session,
|
||||
StudysetRelease row, {
|
||||
required _is.ColumnSelections<StudysetReleaseTable> conflictColumns,
|
||||
_is.ColumnSelections<StudysetReleaseTable>? updateColumns,
|
||||
_is.WhereExpressionBuilder<StudysetReleaseTable>? updateWhere,
|
||||
_is.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.upsertRow<StudysetRelease>(
|
||||
row,
|
||||
conflictColumns: conflictColumns(StudysetRelease.t),
|
||||
updateColumns: updateColumns?.call(StudysetRelease.t),
|
||||
updateWhere: updateWhere?.call(StudysetRelease.t),
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
|
||||
/// Updates all [StudysetRelease]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<StudysetRelease>> update(
|
||||
_is.DatabaseSession session,
|
||||
List<StudysetRelease> rows, {
|
||||
_is.ColumnSelections<StudysetReleaseTable>? columns,
|
||||
_is.Transaction? transaction,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.update<StudysetRelease>(
|
||||
rows,
|
||||
columns: columns?.call(StudysetRelease.t),
|
||||
transaction: transaction,
|
||||
noReturn: noReturn,
|
||||
);
|
||||
}
|
||||
|
||||
/// Updates a single [StudysetRelease]. 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<StudysetRelease> updateRow(
|
||||
_is.DatabaseSession session,
|
||||
StudysetRelease row, {
|
||||
_is.ColumnSelections<StudysetReleaseTable>? columns,
|
||||
_is.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.updateRow<StudysetRelease>(
|
||||
row,
|
||||
columns: columns?.call(StudysetRelease.t),
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
|
||||
/// Updates a single [StudysetRelease] by its [id] with the specified [columnValues].
|
||||
/// Returns the updated row or null if no row with the given id exists.
|
||||
Future<StudysetRelease?> updateById(
|
||||
_is.DatabaseSession session,
|
||||
int id, {
|
||||
required _is.ColumnValueListBuilder<StudysetReleaseUpdateTable>
|
||||
columnValues,
|
||||
_is.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.updateById<StudysetRelease>(
|
||||
id,
|
||||
columnValues: columnValues(StudysetRelease.t.updateTable),
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
|
||||
/// Updates all [StudysetRelease]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<StudysetRelease>> updateWhere(
|
||||
_is.DatabaseSession session, {
|
||||
required _is.ColumnValueListBuilder<StudysetReleaseUpdateTable>
|
||||
columnValues,
|
||||
required _is.WhereExpressionBuilder<StudysetReleaseTable> where,
|
||||
int? limit,
|
||||
int? offset,
|
||||
_is.OrderByBuilder<StudysetReleaseTable>? orderBy,
|
||||
_is.OrderByListBuilder<StudysetReleaseTable>? orderByList,
|
||||
_is.Transaction? transaction,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.updateWhere<StudysetRelease>(
|
||||
columnValues: columnValues(StudysetRelease.t.updateTable),
|
||||
where: where(StudysetRelease.t),
|
||||
limit: limit,
|
||||
offset: offset,
|
||||
orderBy: orderBy?.call(StudysetRelease.t),
|
||||
orderByList: orderByList?.call(StudysetRelease.t),
|
||||
transaction: transaction,
|
||||
noReturn: noReturn,
|
||||
);
|
||||
}
|
||||
|
||||
/// Deletes all [StudysetRelease]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<StudysetRelease>> delete(
|
||||
_is.DatabaseSession session,
|
||||
List<StudysetRelease> rows, {
|
||||
_is.OrderByBuilder<StudysetReleaseTable>? orderBy,
|
||||
_is.OrderByListBuilder<StudysetReleaseTable>? orderByList,
|
||||
_is.Transaction? transaction,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.delete<StudysetRelease>(
|
||||
rows,
|
||||
orderBy: orderBy?.call(StudysetRelease.t),
|
||||
orderByList: orderByList?.call(StudysetRelease.t),
|
||||
transaction: transaction,
|
||||
noReturn: noReturn,
|
||||
);
|
||||
}
|
||||
|
||||
/// Deletes a single [StudysetRelease].
|
||||
Future<StudysetRelease> deleteRow(
|
||||
_is.DatabaseSession session,
|
||||
StudysetRelease row, {
|
||||
_is.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.deleteRow<StudysetRelease>(
|
||||
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<StudysetRelease>> deleteWhere(
|
||||
_is.DatabaseSession session, {
|
||||
required _is.WhereExpressionBuilder<StudysetReleaseTable> where,
|
||||
_is.OrderByBuilder<StudysetReleaseTable>? orderBy,
|
||||
_is.OrderByListBuilder<StudysetReleaseTable>? orderByList,
|
||||
_is.Transaction? transaction,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.deleteWhere<StudysetRelease>(
|
||||
where: where(StudysetRelease.t),
|
||||
orderBy: orderBy?.call(StudysetRelease.t),
|
||||
orderByList: orderByList?.call(StudysetRelease.t),
|
||||
transaction: transaction,
|
||||
noReturn: noReturn,
|
||||
);
|
||||
}
|
||||
|
||||
/// Counts the number of rows matching the [where] expression. If omitted,
|
||||
/// will return the count of all rows in the table.
|
||||
Future<int> count(
|
||||
_is.DatabaseSession session, {
|
||||
_is.WhereExpressionBuilder<StudysetReleaseTable>? where,
|
||||
int? limit,
|
||||
_is.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.count<StudysetRelease>(
|
||||
where: where?.call(StudysetRelease.t),
|
||||
limit: limit,
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
|
||||
/// Acquires row-level locks on [StudysetRelease] rows matching the [where] expression.
|
||||
Future<void> lockRows(
|
||||
_is.DatabaseSession session, {
|
||||
required _is.WhereExpressionBuilder<StudysetReleaseTable> where,
|
||||
required _is.LockMode lockMode,
|
||||
required _is.Transaction transaction,
|
||||
_is.LockBehavior lockBehavior = _is.LockBehavior.wait,
|
||||
}) async {
|
||||
return session.db.lockRows<StudysetRelease>(
|
||||
where: where(StudysetRelease.t),
|
||||
lockMode: lockMode,
|
||||
lockBehavior: lockBehavior,
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
}
|
||||
13
halkyon_learn_server/lib/src/publish/publication.spy.yaml
Normal file
13
halkyon_learn_server/lib/src/publish/publication.spy.yaml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
### A published studyset as the app shows it to its person: where it is, and
|
||||
### its releases, the newest first.
|
||||
class: Publication
|
||||
fields:
|
||||
### Its identity for good.
|
||||
publicId: UuidValue
|
||||
### The studyset published.
|
||||
studysetId: UuidValue
|
||||
### Its name in its location.
|
||||
slug: String
|
||||
### Where it is (`halkyon.eu/@raisondetredev/japanese`).
|
||||
location: String
|
||||
releases: List<ReleaseInfo>
|
||||
274
halkyon_learn_server/lib/src/publish/publish_endpoint.dart
Normal file
274
halkyon_learn_server/lib/src/publish/publish_endpoint.dart
Normal file
|
|
@ -0,0 +1,274 @@
|
|||
import 'package:clock/clock.dart';
|
||||
import 'package:halkyon_learn_server/src/auth/scopes.dart';
|
||||
import 'package:halkyon_learn_server/src/generated/protocol.dart';
|
||||
import 'package:halkyon_learn_server/src/profile/profile_rules.dart';
|
||||
import 'package:halkyon_learn_server/src/publish/release_files.dart';
|
||||
import 'package:halkyon_learn_server/src/publish/release_license.dart';
|
||||
import 'package:halkyon_learn_server/src/publish/release_repository.dart';
|
||||
import 'package:serverpod/serverpod.dart';
|
||||
import 'package:serverpod_auth_idp_server/core.dart';
|
||||
|
||||
/// The name of a published studyset in its location: 1 to 50 lower-case
|
||||
/// letters, digits or hyphens, starting with a letter or digit.
|
||||
final _slug = RegExp(r'^[a-z0-9][a-z0-9-]{0,49}$');
|
||||
|
||||
/// The longest release notes, in characters.
|
||||
const maxReleaseNotesLength = 5000;
|
||||
|
||||
/// The studysets being published right now, by person and studyset: one
|
||||
/// release at a time each.
|
||||
final _publishing = <String>{};
|
||||
|
||||
/// Publishing your studysets (ADR 0005, rule 3): each release a commit and
|
||||
/// a tag in a git repository of the studyset's own, which the server builds
|
||||
/// from your synced rows. Only for people with an identity and a public
|
||||
/// profile: a release names its author.
|
||||
///
|
||||
/// The person always comes from the session, never from the client: a
|
||||
/// studyset that isn't the caller's can't be told from one that doesn't
|
||||
/// exist.
|
||||
class PublishEndpoint extends Endpoint {
|
||||
@override
|
||||
Set<Scope> get requiredScopes => {identified};
|
||||
|
||||
/// Your studyset [studysetId] as published, with its releases, the newest
|
||||
/// first; null while it never was.
|
||||
Future<Publication?> publication(
|
||||
Session session,
|
||||
UuidValue studysetId,
|
||||
) async {
|
||||
final person = session.authenticated!.authUserId;
|
||||
// SELECT … FROM "published_studyset"
|
||||
// WHERE "authUserId" = $1 AND "studysetId" = $2
|
||||
final published = await PublishedStudyset.db.findFirstRow(
|
||||
session,
|
||||
where: (t) =>
|
||||
t.authUserId.equals(person) & t.studysetId.equals(studysetId),
|
||||
);
|
||||
return published == null ? null : await _publicationOf(session, published);
|
||||
}
|
||||
|
||||
/// Publishes your studyset [studysetId] with everything inside it as its
|
||||
/// next release, [notes] saying what changed; at the first, under the
|
||||
/// name [slug] in its location, which stays its name from then on.
|
||||
///
|
||||
/// Throws [PublishRefusedException] when it can't be published (see
|
||||
/// [PublishRefusal]), and [NotFoundException] for a studyset that isn't
|
||||
/// yours.
|
||||
Future<Publication> publish(
|
||||
Session session,
|
||||
UuidValue studysetId,
|
||||
String slug,
|
||||
String notes,
|
||||
) async {
|
||||
final person = session.authenticated!.authUserId;
|
||||
final key = '${person.uuid}/${studysetId.uuid}';
|
||||
if (!_publishing.add(key)) {
|
||||
throw PublishRefusedException(reason: PublishRefusal.busy);
|
||||
}
|
||||
try {
|
||||
return await _publish(
|
||||
session,
|
||||
person: person,
|
||||
studysetId: studysetId,
|
||||
slug: slug.trim(),
|
||||
notes: notes.trim(),
|
||||
);
|
||||
} finally {
|
||||
_publishing.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
Future<Publication> _publish(
|
||||
Session session, {
|
||||
required UuidValue person,
|
||||
required UuidValue studysetId,
|
||||
required String slug,
|
||||
required String notes,
|
||||
}) async {
|
||||
// SELECT … FROM "public_profile" WHERE "authUserId" = $1
|
||||
final profile = await PublicProfile.db.findFirstRow(
|
||||
session,
|
||||
where: (t) => t.authUserId.equals(person),
|
||||
);
|
||||
if (profile == null) {
|
||||
throw PublishRefusedException(reason: PublishRefusal.noProfile);
|
||||
}
|
||||
if (notes.length > maxReleaseNotesLength) {
|
||||
throw PublishRefusedException(reason: PublishRefusal.notesTooLong);
|
||||
}
|
||||
// SELECT … FROM "published_studyset"
|
||||
// WHERE "authUserId" = $1 AND "studysetId" = $2
|
||||
final existing = await PublishedStudyset.db.findFirstRow(
|
||||
session,
|
||||
where: (t) =>
|
||||
t.authUserId.equals(person) & t.studysetId.equals(studysetId),
|
||||
);
|
||||
if (existing != null && existing.slug != slug) {
|
||||
throw PublishRefusedException(reason: PublishRefusal.slugFixed);
|
||||
}
|
||||
if (existing == null) {
|
||||
if (!_slug.hasMatch(slug)) {
|
||||
throw PublishRefusedException(reason: PublishRefusal.slugInvalid);
|
||||
}
|
||||
// SELECT … FROM "published_studyset"
|
||||
// WHERE "authUserId" = $1 AND "slug" = $2
|
||||
if (await PublishedStudyset.db.count(
|
||||
session,
|
||||
where: (t) => t.authUserId.equals(person) & t.slug.equals(slug),
|
||||
) >
|
||||
0) {
|
||||
throw PublishRefusedException(reason: PublishRefusal.slugTaken);
|
||||
}
|
||||
}
|
||||
|
||||
final content = await readReleaseFiles(
|
||||
session,
|
||||
person: person,
|
||||
studysetId: studysetId,
|
||||
);
|
||||
final closed = [
|
||||
for (final MapEntry(key: id, value: license) in content.licenses.entries)
|
||||
if (!allowsPublishing(license)) id,
|
||||
];
|
||||
if (closed.isNotEmpty) {
|
||||
throw PublishRefusedException(
|
||||
reason: PublishRefusal.licenseNotOpen,
|
||||
items: closed,
|
||||
);
|
||||
}
|
||||
|
||||
final publicId = existing?.publicId ?? const Uuid().v4obj();
|
||||
final repository = repositoryOf(publicId);
|
||||
// A version git has but the database lacks (it failed to record one
|
||||
// and the release couldn't be taken back) is never given again.
|
||||
// SELECT max("version") … FROM "studyset_release"
|
||||
// WHERE "publishedStudysetId" = $1
|
||||
final recorded = existing == null
|
||||
? 0
|
||||
: (await StudysetRelease.db.findFirstRow(
|
||||
session,
|
||||
where: (t) => t.publishedStudysetId.equals(existing.id),
|
||||
orderBy: (t) => t.version.desc(),
|
||||
))?.version ??
|
||||
0;
|
||||
final tagged = await repository.latestVersion();
|
||||
final version = (recorded > tagged ? recorded : tagged) + 1;
|
||||
final domain = accountDomain(Serverpod.instance);
|
||||
final now = clock.now().toUtc();
|
||||
|
||||
final released = await repository.release(
|
||||
files: content.files,
|
||||
version: version,
|
||||
message: notes.isEmpty ? 'v$version' : 'v$version\n\n$notes',
|
||||
author: (
|
||||
name: profile.displayName.isEmpty
|
||||
? profile.handle
|
||||
: profile.displayName,
|
||||
email: '${profile.handle}@$domain',
|
||||
),
|
||||
committer: (name: domain, email: 'noreply@$domain'),
|
||||
at: now,
|
||||
);
|
||||
if (released == null) {
|
||||
throw PublishRefusedException(reason: PublishRefusal.nothingChanged);
|
||||
}
|
||||
|
||||
try {
|
||||
final published = await session.db.transaction((transaction) async {
|
||||
final published =
|
||||
existing ??
|
||||
// INSERT INTO "published_studyset" (…) VALUES (…)
|
||||
await PublishedStudyset.db.insertRow(
|
||||
session,
|
||||
PublishedStudyset(
|
||||
publicId: publicId,
|
||||
studysetId: studysetId,
|
||||
authUserId: person,
|
||||
slug: slug,
|
||||
createdAt: now,
|
||||
),
|
||||
transaction: transaction,
|
||||
);
|
||||
// INSERT INTO "studyset_release" (…) VALUES (…)
|
||||
await StudysetRelease.db.insertRow(
|
||||
session,
|
||||
StudysetRelease(
|
||||
publishedStudysetId: published.id!,
|
||||
version: version,
|
||||
commit: released.commit,
|
||||
tree: released.tree,
|
||||
notes: notes,
|
||||
license: content.licenses[studysetId]!,
|
||||
studysetCount: content.studysets,
|
||||
cardCount: content.cards,
|
||||
noteCount: content.notes,
|
||||
pathCount: content.paths,
|
||||
pdfCount: content.pdfs,
|
||||
releasedAt: now,
|
||||
),
|
||||
transaction: transaction,
|
||||
);
|
||||
// The handle is fixed from the first publish on (ADR 0005, rule 2).
|
||||
// UPDATE "public_profile" SET "firstPublishedAt" = $1
|
||||
// WHERE "authUserId" = $2 AND "firstPublishedAt" IS NULL
|
||||
await PublicProfile.db.updateWhere(
|
||||
session,
|
||||
columnValues: (t) => [t.firstPublishedAt(now)],
|
||||
where: (t) =>
|
||||
t.authUserId.equals(person) & t.firstPublishedAt.equals(null),
|
||||
transaction: transaction,
|
||||
);
|
||||
return published;
|
||||
});
|
||||
return await _publicationOf(session, published);
|
||||
} on Object catch (error) {
|
||||
await repository.undo(released, version);
|
||||
// Another publish of the person took the name in between.
|
||||
if (error is DatabaseQueryException && error.code == '23505') {
|
||||
throw PublishRefusedException(reason: PublishRefusal.slugTaken);
|
||||
}
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<Publication> _publicationOf(
|
||||
Session session,
|
||||
PublishedStudyset published,
|
||||
) async {
|
||||
// SELECT "handle" FROM "public_profile" WHERE "authUserId" = $1
|
||||
final profile = await PublicProfile.db.findFirstRow(
|
||||
session,
|
||||
where: (t) => t.authUserId.equals(published.authUserId),
|
||||
);
|
||||
// SELECT … FROM "studyset_release" WHERE "publishedStudysetId" = $1
|
||||
// ORDER BY "version" DESC
|
||||
final releases = await StudysetRelease.db.find(
|
||||
session,
|
||||
where: (t) => t.publishedStudysetId.equals(published.id),
|
||||
orderBy: (t) => t.version.desc(),
|
||||
);
|
||||
return Publication(
|
||||
publicId: published.publicId,
|
||||
studysetId: published.studysetId,
|
||||
slug: published.slug,
|
||||
location:
|
||||
'${accountDomain(Serverpod.instance)}/@${profile?.handle}/'
|
||||
'${published.slug}',
|
||||
releases: [
|
||||
for (final release in releases)
|
||||
ReleaseInfo(
|
||||
version: release.version,
|
||||
notes: release.notes,
|
||||
license: release.license,
|
||||
studysetCount: release.studysetCount,
|
||||
cardCount: release.cardCount,
|
||||
noteCount: release.noteCount,
|
||||
pathCount: release.pathCount,
|
||||
pdfCount: release.pdfCount,
|
||||
releasedAt: release.releasedAt,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
### Why publishing was refused; nothing was published.
|
||||
enum: PublishRefusal
|
||||
serialized: byName
|
||||
values:
|
||||
### The person has no public profile yet: a release names its author.
|
||||
- noProfile
|
||||
### A studyset's licence doesn't allow passing it on (none, or one not
|
||||
### known to be open): mirroring and copying are passing it on. `items`
|
||||
### says which studysets.
|
||||
- licenseNotOpen
|
||||
### Cards, notes or steps have a conflict to settle first (`items` says
|
||||
### which).
|
||||
- conflicts
|
||||
### Files of the content haven't reached the instance yet: a device still
|
||||
### uploads them.
|
||||
- filesMissing
|
||||
### Nothing changed since the last release.
|
||||
- nothingChanged
|
||||
### The release notes are longer than 5,000 characters.
|
||||
- notesTooLong
|
||||
### The name isn't 1 to 50 lower-case letters, digits or hyphens, starting
|
||||
### with a letter or digit.
|
||||
- slugInvalid
|
||||
### Another studyset of the person has the name.
|
||||
- slugTaken
|
||||
### The studyset has its name since its first publish: others refer to it.
|
||||
- slugFixed
|
||||
### Publishing that studyset is already under way.
|
||||
- busy
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
### Publishing was refused; nothing was published.
|
||||
exception: PublishRefusedException
|
||||
fields:
|
||||
### Why.
|
||||
reason: PublishRefusal
|
||||
### With `conflicts`, the cards, notes and steps to settle; with
|
||||
### `licenseNotOpen`, the studysets whose licence doesn't allow it.
|
||||
items: List<UuidValue>?
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
### A studyset its person published (ADR 0005, rule 3): found at
|
||||
### `<account domain>/@<handle>/<slug>`, its releases kept in a git repository
|
||||
### of its own, named by its public id. Server only.
|
||||
class: PublishedStudyset
|
||||
serverOnly: true
|
||||
table: published_studyset
|
||||
fields:
|
||||
### Its identity for good, made at its first publish: never a synced row's
|
||||
### id, which devices make (anyone could make one with another's id in
|
||||
### their own space).
|
||||
publicId: UuidValue
|
||||
### The studyset published, in its person's space: the root of what each
|
||||
### release holds.
|
||||
studysetId: UuidValue
|
||||
### Who published it: the AuthUser's id.
|
||||
authUserId: UuidValue
|
||||
### Its name in its location, chosen at the first publish and fixed from
|
||||
### then on (`japanese`).
|
||||
slug: String
|
||||
### When it was first published, in UTC.
|
||||
createdAt: DateTime
|
||||
indexes:
|
||||
published_studyset_public_idx:
|
||||
fields: publicId
|
||||
unique: true
|
||||
published_studyset_studyset_idx:
|
||||
fields: authUserId, studysetId
|
||||
unique: true
|
||||
published_studyset_slug_idx:
|
||||
fields: authUserId, slug
|
||||
unique: true
|
||||
474
halkyon_learn_server/lib/src/publish/release_files.dart
Normal file
474
halkyon_learn_server/lib/src/publish/release_files.dart
Normal file
|
|
@ -0,0 +1,474 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:halkyon_learn_server/src/asset/asset_endpoint.dart';
|
||||
import 'package:halkyon_learn_server/src/generated/protocol.dart';
|
||||
import 'package:halkyon_learn_server/src/publish/release_repository.dart';
|
||||
import 'package:serverpod/serverpod.dart';
|
||||
import 'package:serverpod_offline_sync_server/serverpod_offline_sync_server.dart';
|
||||
|
||||
/// What a release holds (ADR 0005, rule 3), as read from its person's
|
||||
/// synced rows: each file of its repository by path, the licence each of its
|
||||
/// studysets is under (its own, else the one above it), and how much of
|
||||
/// each kind it holds.
|
||||
final class ReleaseFiles {
|
||||
ReleaseFiles({
|
||||
required this.files,
|
||||
required this.licenses,
|
||||
required this.studysets,
|
||||
required this.cards,
|
||||
required this.notes,
|
||||
required this.paths,
|
||||
required this.pdfs,
|
||||
});
|
||||
|
||||
/// The repository's files, by path.
|
||||
final Map<String, ReleaseFile> files;
|
||||
|
||||
/// Each studyset's licence, its own or the nearest above; null for none.
|
||||
final Map<UuidValue, String?> licenses;
|
||||
|
||||
final int studysets;
|
||||
final int cards;
|
||||
final int notes;
|
||||
final int paths;
|
||||
final int pdfs;
|
||||
}
|
||||
|
||||
/// The studyset [studysetId] of [person], with everything inside it, as a
|
||||
/// release's files: a directory per studyset of the tree (the published one
|
||||
/// at the top, each inside it named by its id), and in each `studyset.json`,
|
||||
/// `cards/<id>.json`, `notes/<id>.json`, `paths/<id>/path.json` with
|
||||
/// `paths/<id>/steps/<id>.json`, `pdfs/<id>.json`; the files their content
|
||||
/// names in `files/<sha256>`, described in `files.json`. Each item file
|
||||
/// holds its id, its position, its tags by name and its payload, the JSON
|
||||
/// as synced, keys sorted and indented, so a diff of two releases reads,
|
||||
/// without what is only its author's ([_personal]).
|
||||
///
|
||||
/// Throws [NotFoundException] for a studyset that isn't the person's, and
|
||||
/// [PublishRefusedException] for a card, note or step with a conflict to
|
||||
/// settle (more than one current revision), or for a file the content names
|
||||
/// that hasn't reached the instance yet.
|
||||
Future<ReleaseFiles> readReleaseFiles(
|
||||
Session session, {
|
||||
required UuidValue person,
|
||||
required UuidValue studysetId,
|
||||
}) async {
|
||||
// SELECT "id" FROM "offline_sync_spaces" WHERE "uuidSpaceId" = $1
|
||||
final space = (await OfflineSyncSpace.db.findFirstRow(
|
||||
session,
|
||||
where: (t) => t.uuidSpaceId.equals(person),
|
||||
))?.id;
|
||||
if (space == null) {
|
||||
throw NotFoundException(what: 'studyset');
|
||||
}
|
||||
// Every studyset of the person's space; the sync engine hides deleted
|
||||
// ones.
|
||||
final all = await Studyset.db.find(
|
||||
session,
|
||||
where: (t) => t.spaceId.equals(space),
|
||||
);
|
||||
final byId = {for (final studyset in all) studyset.id!: studyset};
|
||||
// A studyset whose payload is blank is being deleted, with everything
|
||||
// inside it (ADR 0002, rule 2): it isn't published.
|
||||
final payloads = <UuidValue, Map<String, Object?>>{
|
||||
for (final studyset in all) studyset.id!: ?_decoded(studyset.payload),
|
||||
};
|
||||
if (!payloads.containsKey(studysetId)) {
|
||||
throw NotFoundException(what: 'studyset');
|
||||
}
|
||||
|
||||
// The tree: each studyset's directory, the published one at the top.
|
||||
final childrenOf = <UuidValue, List<Studyset>>{};
|
||||
for (final studyset in all) {
|
||||
if (studyset.parentId case final parent?) {
|
||||
(childrenOf[parent] ??= []).add(studyset);
|
||||
}
|
||||
}
|
||||
final directoryOf = <UuidValue, String>{studysetId: ''};
|
||||
final queue = [studysetId];
|
||||
while (queue.isNotEmpty) {
|
||||
final parent = queue.removeAt(0);
|
||||
for (final child in childrenOf[parent] ?? const <Studyset>[]) {
|
||||
// A studyset caught in a cycle comes once.
|
||||
if (directoryOf.containsKey(child.id) ||
|
||||
!payloads.containsKey(child.id)) {
|
||||
continue;
|
||||
}
|
||||
directoryOf[child.id!] = '${directoryOf[parent]}${child.id!.uuid}/';
|
||||
queue.add(child.id!);
|
||||
}
|
||||
}
|
||||
final ids = directoryOf.keys.toSet();
|
||||
|
||||
final files = <String, ReleaseFile>{};
|
||||
for (final id in ids) {
|
||||
files['${directoryOf[id]}studyset.json'] = _item({
|
||||
'id': id.uuid,
|
||||
if (id != studysetId) 'position': byId[id]!.position,
|
||||
'payload': _published(payloads[id]!, 'studyset'),
|
||||
});
|
||||
}
|
||||
|
||||
// Each studyset's licence, its own or the nearest above.
|
||||
final licenses = <UuidValue, String?>{};
|
||||
String? licenseOf(UuidValue id) =>
|
||||
licenses[id] ??= switch (payloads[id]?['license']) {
|
||||
final String own when own.trim().isNotEmpty => own.trim(),
|
||||
_ when id == studysetId => null,
|
||||
_ => switch (byId[id]?.parentId) {
|
||||
final parent? when ids.contains(parent) => licenseOf(parent),
|
||||
_ => null,
|
||||
},
|
||||
};
|
||||
ids.forEach(licenseOf);
|
||||
|
||||
final names = await _tagNames(session, space);
|
||||
final unsettled = <UuidValue>[];
|
||||
final contentText = StringBuffer();
|
||||
|
||||
// Cards: their current revision; refused while there are several.
|
||||
final cards = await Card.db.find(
|
||||
session,
|
||||
where: (t) => t.spaceId.equals(space) & t.studysetId.inSet(ids),
|
||||
);
|
||||
final cardTags = await CardTag.db.find(
|
||||
session,
|
||||
where: (t) =>
|
||||
t.spaceId.equals(space) &
|
||||
t.cardId.inSet({for (final c in cards) c.id!}),
|
||||
);
|
||||
final cardHeads = _heads(
|
||||
await CardRevision.db.find(
|
||||
session,
|
||||
where: (t) =>
|
||||
t.spaceId.equals(space) &
|
||||
t.cardId.inSet({for (final card in cards) card.id!}),
|
||||
),
|
||||
owner: (revision) => revision.cardId,
|
||||
id: (revision) => revision.id!,
|
||||
parents: (revision) => revision.parents,
|
||||
);
|
||||
var cardCount = 0;
|
||||
for (final card in cards) {
|
||||
final heads = cardHeads[card.id!] ?? const [];
|
||||
if (heads.length != 1) {
|
||||
if (heads.length > 1) {
|
||||
unsettled.add(card.id!);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// A card being deleted has its revisions blanked first.
|
||||
final decoded = _decoded(heads.single.payload);
|
||||
if (decoded == null) {
|
||||
continue;
|
||||
}
|
||||
final payload = _published(decoded, 'card');
|
||||
contentText.write(jsonEncode(payload));
|
||||
cardCount++;
|
||||
files['${directoryOf[card.studysetId]}cards/${card.id!.uuid}.json'] = _item(
|
||||
{
|
||||
'id': card.id!.uuid,
|
||||
'position': card.position,
|
||||
'tags': _namesOf(
|
||||
card.id!,
|
||||
cardTags,
|
||||
(tag) => tag.cardId,
|
||||
(tag) => tag.tagId,
|
||||
names,
|
||||
),
|
||||
'payload': payload,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Notes, as cards.
|
||||
final notes = await Note.db.find(
|
||||
session,
|
||||
where: (t) => t.spaceId.equals(space) & t.studysetId.inSet(ids),
|
||||
);
|
||||
final noteTags = await NoteTag.db.find(
|
||||
session,
|
||||
where: (t) =>
|
||||
t.spaceId.equals(space) &
|
||||
t.noteId.inSet({for (final n in notes) n.id!}),
|
||||
);
|
||||
final noteHeads = _heads(
|
||||
await NoteRevision.db.find(
|
||||
session,
|
||||
where: (t) =>
|
||||
t.spaceId.equals(space) &
|
||||
t.noteId.inSet({for (final note in notes) note.id!}),
|
||||
),
|
||||
owner: (revision) => revision.noteId,
|
||||
id: (revision) => revision.id!,
|
||||
parents: (revision) => revision.parents,
|
||||
);
|
||||
var noteCount = 0;
|
||||
for (final note in notes) {
|
||||
final heads = noteHeads[note.id!] ?? const [];
|
||||
if (heads.length != 1) {
|
||||
if (heads.length > 1) {
|
||||
unsettled.add(note.id!);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
final payload = _decoded(heads.single.payload);
|
||||
if (payload == null) {
|
||||
continue;
|
||||
}
|
||||
contentText.write(jsonEncode(payload));
|
||||
noteCount++;
|
||||
files['${directoryOf[note.studysetId]}notes/${note.id!.uuid}.json'] = _item(
|
||||
{
|
||||
'id': note.id!.uuid,
|
||||
'position': note.position,
|
||||
'tags': _namesOf(
|
||||
note.id!,
|
||||
noteTags,
|
||||
(tag) => tag.noteId,
|
||||
(tag) => tag.tagId,
|
||||
names,
|
||||
),
|
||||
'payload': payload,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Learning paths, and their steps' current revisions.
|
||||
final paths = await LearningPath.db.find(
|
||||
session,
|
||||
where: (t) => t.spaceId.equals(space) & t.studysetId.inSet(ids),
|
||||
);
|
||||
final pathIds = {for (final path in paths) path.id!};
|
||||
final pathTags = await LearningPathTag.db.find(
|
||||
session,
|
||||
where: (t) => t.spaceId.equals(space) & t.pathId.inSet(pathIds),
|
||||
);
|
||||
final steps = await LearningPathStep.db.find(
|
||||
session,
|
||||
where: (t) => t.spaceId.equals(space) & t.pathId.inSet(pathIds),
|
||||
);
|
||||
final stepHeads = _heads(
|
||||
await LearningPathStepRevision.db.find(
|
||||
session,
|
||||
where: (t) =>
|
||||
t.spaceId.equals(space) &
|
||||
t.stepId.inSet({for (final step in steps) step.id!}),
|
||||
),
|
||||
owner: (revision) => revision.stepId,
|
||||
id: (revision) => revision.id!,
|
||||
parents: (revision) => revision.parents,
|
||||
);
|
||||
final pathDirectory = <UuidValue, String>{};
|
||||
var pathCount = 0;
|
||||
for (final path in paths) {
|
||||
final payload = _decoded(path.payload);
|
||||
if (payload == null) {
|
||||
continue;
|
||||
}
|
||||
contentText.write(jsonEncode(payload));
|
||||
pathCount++;
|
||||
final directory = pathDirectory[path.id!] =
|
||||
'${directoryOf[path.studysetId]}paths/${path.id!.uuid}/';
|
||||
files['${directory}path.json'] = _item({
|
||||
'id': path.id!.uuid,
|
||||
'position': path.position,
|
||||
'tags': _namesOf(
|
||||
path.id!,
|
||||
pathTags,
|
||||
(tag) => tag.pathId,
|
||||
(tag) => tag.tagId,
|
||||
names,
|
||||
),
|
||||
'payload': payload,
|
||||
});
|
||||
}
|
||||
for (final step in steps) {
|
||||
final directory = pathDirectory[step.pathId];
|
||||
if (directory == null) {
|
||||
continue;
|
||||
}
|
||||
final heads = stepHeads[step.id!] ?? const [];
|
||||
if (heads.length != 1) {
|
||||
if (heads.length > 1) {
|
||||
unsettled.add(step.id!);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
final payload = _decoded(heads.single.payload);
|
||||
if (payload == null) {
|
||||
continue;
|
||||
}
|
||||
contentText.write(jsonEncode(payload));
|
||||
files['${directory}steps/${step.id!.uuid}.json'] = _item({
|
||||
'id': step.id!.uuid,
|
||||
'position': step.position,
|
||||
'kind': step.kind,
|
||||
'payload': payload,
|
||||
});
|
||||
}
|
||||
|
||||
// PDFs, and the file each names.
|
||||
final pdfs = await PdfSource.db.find(
|
||||
session,
|
||||
where: (t) => t.spaceId.equals(space) & t.studysetId.inSet(ids),
|
||||
);
|
||||
final named = <UuidValue>{};
|
||||
var pdfCount = 0;
|
||||
for (final pdf in pdfs) {
|
||||
final decoded = _decoded(pdf.payload);
|
||||
if (decoded == null) {
|
||||
continue;
|
||||
}
|
||||
pdfCount++;
|
||||
final payload = _published(decoded, 'pdf');
|
||||
if (payload['asset'] case final String asset when _uuid.hasMatch(asset)) {
|
||||
named.add(UuidValue.fromString(asset));
|
||||
}
|
||||
files['${directoryOf[pdf.studysetId]}pdfs/${pdf.id!.uuid}.json'] = _item({
|
||||
'id': pdf.id!.uuid,
|
||||
'position': pdf.position,
|
||||
'payload': payload,
|
||||
});
|
||||
}
|
||||
|
||||
if (unsettled.isNotEmpty) {
|
||||
throw PublishRefusedException(
|
||||
reason: PublishRefusal.conflicts,
|
||||
items: unsettled,
|
||||
);
|
||||
}
|
||||
|
||||
// The files the content names (`asset:<id>`), and the PDFs'.
|
||||
for (final match in _assetUrl.allMatches(contentText.toString())) {
|
||||
named.add(UuidValue.fromString(match[1]!));
|
||||
}
|
||||
final assets = named.isEmpty
|
||||
? const <Asset>[]
|
||||
: await Asset.db.find(
|
||||
session,
|
||||
where: (t) => t.spaceId.equals(space) & t.id.inSet(named),
|
||||
);
|
||||
final index = <String, Object?>{};
|
||||
for (final asset in assets) {
|
||||
final path = assetPath(person, asset.sha256);
|
||||
if (!await session.storage.fileExists(
|
||||
storageId: assetStorage,
|
||||
path: path,
|
||||
)) {
|
||||
throw PublishRefusedException(reason: PublishRefusal.filesMissing);
|
||||
}
|
||||
files['files/${asset.sha256}'] = () async => Uint8List.sublistView(
|
||||
await session.storage.retrieveFile(storageId: assetStorage, path: path),
|
||||
);
|
||||
index[asset.id!.uuid] = {
|
||||
'sha256': asset.sha256,
|
||||
'mimeType': asset.mimeType,
|
||||
'size': asset.byteSize,
|
||||
'payload': ?_decoded(asset.payload),
|
||||
};
|
||||
}
|
||||
if (index.isNotEmpty) {
|
||||
files['files.json'] = _item(index);
|
||||
}
|
||||
|
||||
return ReleaseFiles(
|
||||
files: files,
|
||||
licenses: licenses,
|
||||
studysets: ids.length,
|
||||
cards: cardCount,
|
||||
notes: noteCount,
|
||||
paths: pathCount,
|
||||
pdfs: pdfCount,
|
||||
);
|
||||
}
|
||||
|
||||
const _uuidPattern =
|
||||
'[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}';
|
||||
final _uuid = RegExp('^$_uuidPattern\$');
|
||||
|
||||
/// A file a content names: `asset:<id>`.
|
||||
final _assetUrl = RegExp('asset:($_uuidPattern)');
|
||||
|
||||
/// What a payload of each kind holds that is only its author's, never
|
||||
/// published: a card's report that something on it is wrong (their own
|
||||
/// reminder; reports to an author are another thing, ADR 0005 rule 6) and
|
||||
/// a PDF's reading deadline.
|
||||
const _personal = {
|
||||
'card': {'report'},
|
||||
'studyset': <String>{},
|
||||
'pdf': {'deadline'},
|
||||
};
|
||||
|
||||
/// [payload] without what is only its author's.
|
||||
Map<String, Object?> _published(Map<String, Object?> payload, String kind) => {
|
||||
for (final MapEntry(:key, :value) in payload.entries)
|
||||
if (!_personal[kind]!.contains(key)) key: value,
|
||||
};
|
||||
|
||||
/// The current revisions of each owner (a card, a note, a step): those no
|
||||
/// other revision of it replaces.
|
||||
Map<UuidValue, List<R>> _heads<R>(
|
||||
List<R> revisions, {
|
||||
required UuidValue Function(R) owner,
|
||||
required UuidValue Function(R) id,
|
||||
required List<UuidValue> Function(R) parents,
|
||||
}) {
|
||||
final replaced = {for (final revision in revisions) ...parents(revision)};
|
||||
final heads = <UuidValue, List<R>>{};
|
||||
for (final revision in revisions) {
|
||||
if (!replaced.contains(id(revision))) {
|
||||
(heads[owner(revision)] ??= []).add(revision);
|
||||
}
|
||||
}
|
||||
return heads;
|
||||
}
|
||||
|
||||
/// Each tag's name in [space], by its id; a tag without one is left out.
|
||||
Future<Map<UuidValue, String>> _tagNames(Session session, int space) async => {
|
||||
for (final tag in await Tag.db.find(
|
||||
session,
|
||||
where: (t) => t.spaceId.equals(space),
|
||||
))
|
||||
if (_decoded(tag.payload)?['name'] case final String name
|
||||
when name.trim().isNotEmpty)
|
||||
tag.id!: name.trim(),
|
||||
};
|
||||
|
||||
/// The names of the tags [rows] give [item], sorted.
|
||||
List<String> _namesOf<T>(
|
||||
UuidValue item,
|
||||
List<T> rows,
|
||||
UuidValue Function(T) itemOf,
|
||||
UuidValue Function(T) tagOf,
|
||||
Map<UuidValue, String> names,
|
||||
) => {
|
||||
for (final row in rows)
|
||||
if (itemOf(row) == item) ?names[tagOf(row)],
|
||||
}.toList()..sort();
|
||||
|
||||
/// A synced payload's JSON; null for a blank one, of a row being deleted.
|
||||
Map<String, Object?>? _decoded(ByteData payload) => payload.lengthInBytes == 0
|
||||
? null
|
||||
: switch (jsonDecode(utf8.decode(Uint8List.sublistView(payload)))) {
|
||||
final Map<String, Object?> json => json,
|
||||
_ => throw const FormatException('A payload that is no JSON object.'),
|
||||
};
|
||||
|
||||
const _encoder = JsonEncoder.withIndent(' ');
|
||||
|
||||
/// [fields] as an item's file: JSON with its keys sorted at every level,
|
||||
/// indented, ending in a new line.
|
||||
ReleaseFile _item(Map<String, Object?> fields) {
|
||||
final bytes = utf8.encode('${_encoder.convert(_sorted(fields))}\n');
|
||||
return () async => bytes;
|
||||
}
|
||||
|
||||
Object? _sorted(Object? json) => switch (json) {
|
||||
final Map<String, Object?> map => {
|
||||
for (final key in map.keys.toList()..sort()) key: _sorted(map[key]),
|
||||
},
|
||||
final List<Object?> list => [for (final value in list) _sorted(value)],
|
||||
_ => json,
|
||||
};
|
||||
16
halkyon_learn_server/lib/src/publish/release_info.spy.yaml
Normal file
16
halkyon_learn_server/lib/src/publish/release_info.spy.yaml
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
### A release as the app shows it.
|
||||
class: ReleaseInfo
|
||||
fields:
|
||||
### 1, 2, 3, …
|
||||
version: int
|
||||
### What changed; empty for nothing said.
|
||||
notes: String
|
||||
### The licence it is under.
|
||||
license: String
|
||||
studysetCount: int
|
||||
cardCount: int
|
||||
noteCount: int
|
||||
pathCount: int
|
||||
pdfCount: int
|
||||
### When it was released, in UTC.
|
||||
releasedAt: DateTime
|
||||
56
halkyon_learn_server/lib/src/publish/release_license.dart
Normal file
56
halkyon_learn_server/lib/src/publish/release_license.dart
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
/// Licences known to allow passing content on, as a published studyset's
|
||||
/// releases are (mirroring and copying, ADR 0005 rule 3), by their SPDX
|
||||
/// identifiers as the app keeps licences, in capitals: the public domain
|
||||
/// dedications and marks, open-data and documentation licences, and the
|
||||
/// common software ones.
|
||||
const _open = {
|
||||
'CC0-1.0',
|
||||
'CC-PDDC',
|
||||
'CC-PDM-1.0',
|
||||
'PDDL-1.0',
|
||||
'UNLICENSE',
|
||||
// Not a licence (the app keeps it as typed), but what a studyset says
|
||||
// when its content is in the public domain.
|
||||
'PUBLIC DOMAIN',
|
||||
'ODC-BY-1.0',
|
||||
'ODBL-1.0',
|
||||
'MIT',
|
||||
'MIT-0',
|
||||
'ISC',
|
||||
'0BSD',
|
||||
'BSD-2-CLAUSE',
|
||||
'BSD-3-CLAUSE',
|
||||
'APACHE-2.0',
|
||||
'MPL-2.0',
|
||||
'EUPL-1.2',
|
||||
'ZLIB',
|
||||
};
|
||||
|
||||
/// Every Creative Commons licence with attribution, any version or port:
|
||||
/// non-commercial ones too (a release is passed on free of charge), and
|
||||
/// those forbidding adaptations ([forbidsAdaptations]).
|
||||
final _creativeCommons = RegExp(
|
||||
r'^CC-BY(-NC)?(-SA|-ND)?-\d\.\d(-[A-Z]{2,3})?$',
|
||||
caseSensitive: false,
|
||||
);
|
||||
|
||||
/// The GNU licences, any version, only or or later.
|
||||
final _gnu = RegExp(
|
||||
r'^((A|L)?GPL-[23]\.[01]|GFDL-1\.[123](-NO)?(-INVARIANTS)?)-(ONLY|OR-LATER)$',
|
||||
caseSensitive: false,
|
||||
);
|
||||
|
||||
/// Whether [license] allows passing a studyset on.
|
||||
bool allowsPublishing(String? license) => switch (license?.trim()) {
|
||||
null || '' => false,
|
||||
final license =>
|
||||
_open.contains(license.toUpperCase()) ||
|
||||
_creativeCommons.hasMatch(license) ||
|
||||
_gnu.hasMatch(license),
|
||||
};
|
||||
|
||||
/// Whether [license] forbids passing on adaptations (Creative Commons'
|
||||
/// "no derivatives"): learners may change their own copies, but no
|
||||
/// suggestion can be taken into the original.
|
||||
bool forbidsAdaptations(String license) =>
|
||||
_creativeCommons.firstMatch(license.trim())?[2]?.toUpperCase() == '-ND';
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue