feat(meditate): dreams on the server, sealed and synced like the sittings

The dream journal's table (ADR 0038, D14): `dream`, in the sync engine's
database, a row a dream, its content sealed on the device (key ID,
payload, nonce), so the instance sees only that a dream exists, its size
and when it changed. The same rules as the sittings: the sync engine's
connector refuses a call without a session, each row lives in its
person's space, and deleting the person deletes their dreams with it
(the deletion test now writes and counts a dream too, and leaves
another person's).

The migration `20261103130213716-dreams`:
- `CREATE OR REPLACE FUNCTION gen_random_uuid_v7()`: the generator
  repeats the UUID v7 function every migration; replacing it with the
  same body changes nothing.
- `CREATE TABLE "dream"`: `id` a UUID v7 by default, `spaceId` the
  person's sync space, `keyId` the key it is sealed with, `payload` the
  sealed bytes (not null), `nonce` the sealing's nonce.
- `CREATE INDEX "dream_space_idx"` on `spaceId`, so deleting a space
  finds its dreams without scanning the table.
- The foreign key from `spaceId` to `offline_sync_spaces("id")` with
  `ON DELETE CASCADE`: deleting the space deletes its dreams.
- The version rows of every module in `serverpod_migrations`.
A new, empty table: nothing existing changes and no data moves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-11-03 14:04:57 +01:00
commit 769ae3b20a
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -21,9 +21,11 @@ import 'package:serverpod_client/serverpod_client.dart' as _isc;
import 'package:serverpod_database/serverpod_database.dart' as _isd;
import 'package:serverpod_offline_sync_client/serverpod_offline_sync_client.dart'
as _ipulbpi2;
import 'journal/dream.dart' as _is2va3v7;
import 'privacy/content_key.dart' as _imed3x5d;
import 'settings/meditate_settings.dart' as _iutacwgf;
import 'sitting/sitting.dart' as _ic8w2fbu;
export 'journal/dream.dart';
export 'privacy/content_key.dart';
export 'settings/meditate_settings.dart';
export 'sitting/sitting.dart';
@ -99,6 +101,73 @@ class Protocol extends _isd.DatabaseSerializationManager {
],
managed: true,
),
_isd.TableDefinition(
name: 'dream',
dartName: 'Dream',
schema: 'public',
module: 'halkyon_meditate',
columns: [
_isd.ColumnDefinition(
name: 'id',
columnType: _isd.ColumnType.uuid,
isNullable: false,
dartType: 'UuidValue?',
columnDefault: 'random_v7',
),
_isd.ColumnDefinition(
name: 'spaceId',
columnType: _isd.ColumnType.bigint,
isNullable: true,
dartType: 'int?',
),
_isd.ColumnDefinition(
name: 'keyId',
columnType: _isd.ColumnType.text,
isNullable: true,
dartType: 'String?',
),
_isd.ColumnDefinition(
name: 'payload',
columnType: _isd.ColumnType.bytea,
isNullable: false,
dartType: 'dart:typed_data:ByteData',
),
_isd.ColumnDefinition(
name: 'nonce',
columnType: _isd.ColumnType.bytea,
isNullable: true,
dartType: 'dart:typed_data:ByteData?',
),
],
foreignKeys: [
_isd.ForeignKeyDefinition(
constraintName: 'dream_fk_0',
columns: ['spaceId'],
referenceTable: 'offline_sync_spaces',
referenceTableSchema: 'public',
referenceColumns: ['id'],
onUpdate: _isd.ForeignKeyAction.noAction,
onDelete: _isd.ForeignKeyAction.cascade,
matchType: null,
),
],
indexes: [
_isd.IndexDefinition(
indexName: 'dream_space_idx',
tableSpace: null,
elements: [
_isd.IndexElementDefinition(
type: _isd.IndexElementDefinitionType.column,
definition: 'spaceId',
),
],
type: 'btree',
isUnique: false,
isPrimary: false,
),
],
managed: true,
),
_isd.TableDefinition(
name: 'meditate_settings',
dartName: 'MeditateSettings',
@ -282,6 +351,9 @@ class Protocol extends _isd.DatabaseSerializationManager {
}
}
if (t == _is2va3v7.Dream) {
return _is2va3v7.Dream.fromJson(data) as T;
}
if (t == _imed3x5d.ContentKey) {
return _imed3x5d.ContentKey.fromJson(data) as T;
}
@ -291,6 +363,9 @@ class Protocol extends _isd.DatabaseSerializationManager {
if (t == _ic8w2fbu.Sitting) {
return _ic8w2fbu.Sitting.fromJson(data) as T;
}
if (t == _isc.getType<_is2va3v7.Dream?>()) {
return (data != null ? _is2va3v7.Dream.fromJson(data) : null) as T;
}
if (t == _isc.getType<_imed3x5d.ContentKey?>()) {
return (data != null ? _imed3x5d.ContentKey.fromJson(data) : null) as T;
}
@ -321,6 +396,7 @@ class Protocol extends _isd.DatabaseSerializationManager {
static String? getClassNameForType(Type type) {
return switch (type) {
_is2va3v7.Dream => 'Dream',
_imed3x5d.ContentKey => 'ContentKey',
_iutacwgf.MeditateSettings => 'MeditateSettings',
_ic8w2fbu.Sitting => 'Sitting',
@ -341,6 +417,8 @@ class Protocol extends _isd.DatabaseSerializationManager {
}
switch (data) {
case _is2va3v7.Dream():
return 'Dream';
case _imed3x5d.ContentKey():
return 'ContentKey';
case _iutacwgf.MeditateSettings():
@ -383,6 +461,9 @@ class Protocol extends _isd.DatabaseSerializationManager {
if (dataClassName is! String) {
return super.deserializeByClassName(data);
}
if (dataClassName == 'Dream') {
return deserialize<_is2va3v7.Dream>(data['data']);
}
if (dataClassName == 'ContentKey') {
return deserialize<_imed3x5d.ContentKey>(data['data']);
}
@ -471,6 +552,8 @@ class Protocol extends _isd.DatabaseSerializationManager {
}
}
switch (t) {
case _is2va3v7.Dream:
return _is2va3v7.Dream.t;
case _imed3x5d.ContentKey:
return _imed3x5d.ContentKey.t;
case _iutacwgf.MeditateSettings: