feat: mobile edits sync

This commit is contained in:
bwees
2026-01-21 14:32:43 -06:00
parent f90d38f93a
commit 0376a96de7
31 changed files with 10593 additions and 8 deletions

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
enum AssetEditAction { rotate, crop, mirror, other }

View File

@@ -118,6 +118,10 @@ class SyncStreamService {
return _syncStreamRepository.deleteAssetsV1(data.cast());
case SyncEntityType.assetExifV1:
return _syncStreamRepository.updateAssetsExifV1(data.cast());
case SyncEntityType.assetEditV1:
return _syncStreamRepository.updateAssetEditsV1(data.cast());
case SyncEntityType.assetEditDeleteV1:
return _syncStreamRepository.deleteAssetEditsV1(data.cast());
case SyncEntityType.assetMetadataV1:
return _syncStreamRepository.updateAssetsMetadataV1(data.cast());
case SyncEntityType.assetMetadataDeleteV1:

View File

@@ -0,0 +1,23 @@
import 'package:drift/drift.dart';
import 'package:immich_mobile/domain/models/asset_edit.model.dart';
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
class AssetEditEntity extends Table with DriftDefaultsMixin {
const AssetEditEntity();
TextColumn get id => text()();
TextColumn get assetId => text().references(RemoteAssetEntity, #id, onDelete: KeyAction.cascade)();
IntColumn get action => intEnum<AssetEditAction>()();
BlobColumn get parameters => blob().map(editParameterConverter)();
@override
Set<Column> get primaryKey => {id};
}
final JsonTypeConverter2<Map<String, Object?>, Uint8List, Object?> editParameterConverter = TypeConverter.jsonb(
fromJson: (json) => json as Map<String, Object?>,
);

View File

@@ -0,0 +1,678 @@
// dart format width=80
// ignore_for_file: type=lint
import 'package:drift/drift.dart' as i0;
import 'package:immich_mobile/infrastructure/entities/asset_edit.entity.drift.dart'
as i1;
import 'package:immich_mobile/domain/models/asset_edit.model.dart' as i2;
import 'dart:typed_data' as i3;
import 'package:immich_mobile/infrastructure/entities/asset_edit.entity.dart'
as i4;
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart'
as i5;
import 'package:drift/internal/modular.dart' as i6;
typedef $$AssetEditEntityTableCreateCompanionBuilder =
i1.AssetEditEntityCompanion Function({
required String id,
required String assetId,
required i2.AssetEditAction action,
required Map<String, Object?> parameters,
});
typedef $$AssetEditEntityTableUpdateCompanionBuilder =
i1.AssetEditEntityCompanion Function({
i0.Value<String> id,
i0.Value<String> assetId,
i0.Value<i2.AssetEditAction> action,
i0.Value<Map<String, Object?>> parameters,
});
final class $$AssetEditEntityTableReferences
extends
i0.BaseReferences<
i0.GeneratedDatabase,
i1.$AssetEditEntityTable,
i1.AssetEditEntityData
> {
$$AssetEditEntityTableReferences(
super.$_db,
super.$_table,
super.$_typedResult,
);
static i5.$RemoteAssetEntityTable _assetIdTable(i0.GeneratedDatabase db) =>
i6.ReadDatabaseContainer(db)
.resultSet<i5.$RemoteAssetEntityTable>('remote_asset_entity')
.createAlias(
i0.$_aliasNameGenerator(
i6.ReadDatabaseContainer(db)
.resultSet<i1.$AssetEditEntityTable>('asset_edit_entity')
.assetId,
i6.ReadDatabaseContainer(
db,
).resultSet<i5.$RemoteAssetEntityTable>('remote_asset_entity').id,
),
);
i5.$$RemoteAssetEntityTableProcessedTableManager get assetId {
final $_column = $_itemColumn<String>('asset_id')!;
final manager = i5
.$$RemoteAssetEntityTableTableManager(
$_db,
i6.ReadDatabaseContainer(
$_db,
).resultSet<i5.$RemoteAssetEntityTable>('remote_asset_entity'),
)
.filter((f) => f.id.sqlEquals($_column));
final item = $_typedResult.readTableOrNull(_assetIdTable($_db));
if (item == null) return manager;
return i0.ProcessedTableManager(
manager.$state.copyWith(prefetchedData: [item]),
);
}
}
class $$AssetEditEntityTableFilterComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$AssetEditEntityTable> {
$$AssetEditEntityTableFilterComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.ColumnFilters<String> get id => $composableBuilder(
column: $table.id,
builder: (column) => i0.ColumnFilters(column),
);
i0.ColumnWithTypeConverterFilters<i2.AssetEditAction, i2.AssetEditAction, int>
get action => $composableBuilder(
column: $table.action,
builder: (column) => i0.ColumnWithTypeConverterFilters(column),
);
i0.ColumnWithTypeConverterFilters<
Map<String, Object?>,
Map<String, Object>,
i3.Uint8List
>
get parameters => $composableBuilder(
column: $table.parameters,
builder: (column) => i0.ColumnWithTypeConverterFilters(column),
);
i5.$$RemoteAssetEntityTableFilterComposer get assetId {
final i5.$$RemoteAssetEntityTableFilterComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.assetId,
referencedTable: i6.ReadDatabaseContainer(
$db,
).resultSet<i5.$RemoteAssetEntityTable>('remote_asset_entity'),
getReferencedColumn: (t) => t.id,
builder:
(
joinBuilder, {
$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer,
}) => i5.$$RemoteAssetEntityTableFilterComposer(
$db: $db,
$table: i6.ReadDatabaseContainer(
$db,
).resultSet<i5.$RemoteAssetEntityTable>('remote_asset_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
),
);
return composer;
}
}
class $$AssetEditEntityTableOrderingComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$AssetEditEntityTable> {
$$AssetEditEntityTableOrderingComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.ColumnOrderings<String> get id => $composableBuilder(
column: $table.id,
builder: (column) => i0.ColumnOrderings(column),
);
i0.ColumnOrderings<int> get action => $composableBuilder(
column: $table.action,
builder: (column) => i0.ColumnOrderings(column),
);
i0.ColumnOrderings<i3.Uint8List> get parameters => $composableBuilder(
column: $table.parameters,
builder: (column) => i0.ColumnOrderings(column),
);
i5.$$RemoteAssetEntityTableOrderingComposer get assetId {
final i5.$$RemoteAssetEntityTableOrderingComposer composer =
$composerBuilder(
composer: this,
getCurrentColumn: (t) => t.assetId,
referencedTable: i6.ReadDatabaseContainer(
$db,
).resultSet<i5.$RemoteAssetEntityTable>('remote_asset_entity'),
getReferencedColumn: (t) => t.id,
builder:
(
joinBuilder, {
$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer,
}) => i5.$$RemoteAssetEntityTableOrderingComposer(
$db: $db,
$table: i6.ReadDatabaseContainer(
$db,
).resultSet<i5.$RemoteAssetEntityTable>('remote_asset_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
),
);
return composer;
}
}
class $$AssetEditEntityTableAnnotationComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$AssetEditEntityTable> {
$$AssetEditEntityTableAnnotationComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.GeneratedColumn<String> get id =>
$composableBuilder(column: $table.id, builder: (column) => column);
i0.GeneratedColumnWithTypeConverter<i2.AssetEditAction, int> get action =>
$composableBuilder(column: $table.action, builder: (column) => column);
i0.GeneratedColumnWithTypeConverter<Map<String, Object?>, i3.Uint8List>
get parameters => $composableBuilder(
column: $table.parameters,
builder: (column) => column,
);
i5.$$RemoteAssetEntityTableAnnotationComposer get assetId {
final i5.$$RemoteAssetEntityTableAnnotationComposer composer =
$composerBuilder(
composer: this,
getCurrentColumn: (t) => t.assetId,
referencedTable: i6.ReadDatabaseContainer(
$db,
).resultSet<i5.$RemoteAssetEntityTable>('remote_asset_entity'),
getReferencedColumn: (t) => t.id,
builder:
(
joinBuilder, {
$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer,
}) => i5.$$RemoteAssetEntityTableAnnotationComposer(
$db: $db,
$table: i6.ReadDatabaseContainer(
$db,
).resultSet<i5.$RemoteAssetEntityTable>('remote_asset_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
),
);
return composer;
}
}
class $$AssetEditEntityTableTableManager
extends
i0.RootTableManager<
i0.GeneratedDatabase,
i1.$AssetEditEntityTable,
i1.AssetEditEntityData,
i1.$$AssetEditEntityTableFilterComposer,
i1.$$AssetEditEntityTableOrderingComposer,
i1.$$AssetEditEntityTableAnnotationComposer,
$$AssetEditEntityTableCreateCompanionBuilder,
$$AssetEditEntityTableUpdateCompanionBuilder,
(i1.AssetEditEntityData, i1.$$AssetEditEntityTableReferences),
i1.AssetEditEntityData,
i0.PrefetchHooks Function({bool assetId})
> {
$$AssetEditEntityTableTableManager(
i0.GeneratedDatabase db,
i1.$AssetEditEntityTable table,
) : super(
i0.TableManagerState(
db: db,
table: table,
createFilteringComposer: () =>
i1.$$AssetEditEntityTableFilterComposer($db: db, $table: table),
createOrderingComposer: () =>
i1.$$AssetEditEntityTableOrderingComposer($db: db, $table: table),
createComputedFieldComposer: () => i1
.$$AssetEditEntityTableAnnotationComposer($db: db, $table: table),
updateCompanionCallback:
({
i0.Value<String> id = const i0.Value.absent(),
i0.Value<String> assetId = const i0.Value.absent(),
i0.Value<i2.AssetEditAction> action = const i0.Value.absent(),
i0.Value<Map<String, Object?>> parameters =
const i0.Value.absent(),
}) => i1.AssetEditEntityCompanion(
id: id,
assetId: assetId,
action: action,
parameters: parameters,
),
createCompanionCallback:
({
required String id,
required String assetId,
required i2.AssetEditAction action,
required Map<String, Object?> parameters,
}) => i1.AssetEditEntityCompanion.insert(
id: id,
assetId: assetId,
action: action,
parameters: parameters,
),
withReferenceMapper: (p0) => p0
.map(
(e) => (
e.readTable(table),
i1.$$AssetEditEntityTableReferences(db, table, e),
),
)
.toList(),
prefetchHooksCallback: ({assetId = false}) {
return i0.PrefetchHooks(
db: db,
explicitlyWatchedTables: [],
addJoins:
<
T extends i0.TableManagerState<
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic
>
>(state) {
if (assetId) {
state =
state.withJoin(
currentTable: table,
currentColumn: table.assetId,
referencedTable: i1
.$$AssetEditEntityTableReferences
._assetIdTable(db),
referencedColumn: i1
.$$AssetEditEntityTableReferences
._assetIdTable(db)
.id,
)
as T;
}
return state;
},
getPrefetchedDataCallback: (items) async {
return [];
},
);
},
),
);
}
typedef $$AssetEditEntityTableProcessedTableManager =
i0.ProcessedTableManager<
i0.GeneratedDatabase,
i1.$AssetEditEntityTable,
i1.AssetEditEntityData,
i1.$$AssetEditEntityTableFilterComposer,
i1.$$AssetEditEntityTableOrderingComposer,
i1.$$AssetEditEntityTableAnnotationComposer,
$$AssetEditEntityTableCreateCompanionBuilder,
$$AssetEditEntityTableUpdateCompanionBuilder,
(i1.AssetEditEntityData, i1.$$AssetEditEntityTableReferences),
i1.AssetEditEntityData,
i0.PrefetchHooks Function({bool assetId})
>;
class $AssetEditEntityTable extends i4.AssetEditEntity
with i0.TableInfo<$AssetEditEntityTable, i1.AssetEditEntityData> {
@override
final i0.GeneratedDatabase attachedDatabase;
final String? _alias;
$AssetEditEntityTable(this.attachedDatabase, [this._alias]);
static const i0.VerificationMeta _idMeta = const i0.VerificationMeta('id');
@override
late final i0.GeneratedColumn<String> id = i0.GeneratedColumn<String>(
'id',
aliasedName,
false,
type: i0.DriftSqlType.string,
requiredDuringInsert: true,
);
static const i0.VerificationMeta _assetIdMeta = const i0.VerificationMeta(
'assetId',
);
@override
late final i0.GeneratedColumn<String> assetId = i0.GeneratedColumn<String>(
'asset_id',
aliasedName,
false,
type: i0.DriftSqlType.string,
requiredDuringInsert: true,
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
'REFERENCES remote_asset_entity (id) ON DELETE CASCADE',
),
);
@override
late final i0.GeneratedColumnWithTypeConverter<i2.AssetEditAction, int>
action =
i0.GeneratedColumn<int>(
'action',
aliasedName,
false,
type: i0.DriftSqlType.int,
requiredDuringInsert: true,
).withConverter<i2.AssetEditAction>(
i1.$AssetEditEntityTable.$converteraction,
);
@override
late final i0.GeneratedColumnWithTypeConverter<
Map<String, Object?>,
i3.Uint8List
>
parameters =
i0.GeneratedColumn<i3.Uint8List>(
'parameters',
aliasedName,
false,
type: i0.DriftSqlType.blob,
requiredDuringInsert: true,
).withConverter<Map<String, Object?>>(
i1.$AssetEditEntityTable.$converterparameters,
);
@override
List<i0.GeneratedColumn> get $columns => [id, assetId, action, parameters];
@override
String get aliasedName => _alias ?? actualTableName;
@override
String get actualTableName => $name;
static const String $name = 'asset_edit_entity';
@override
i0.VerificationContext validateIntegrity(
i0.Insertable<i1.AssetEditEntityData> instance, {
bool isInserting = false,
}) {
final context = i0.VerificationContext();
final data = instance.toColumns(true);
if (data.containsKey('id')) {
context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta));
} else if (isInserting) {
context.missing(_idMeta);
}
if (data.containsKey('asset_id')) {
context.handle(
_assetIdMeta,
assetId.isAcceptableOrUnknown(data['asset_id']!, _assetIdMeta),
);
} else if (isInserting) {
context.missing(_assetIdMeta);
}
return context;
}
@override
Set<i0.GeneratedColumn> get $primaryKey => {id};
@override
i1.AssetEditEntityData map(Map<String, dynamic> data, {String? tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return i1.AssetEditEntityData(
id: attachedDatabase.typeMapping.read(
i0.DriftSqlType.string,
data['${effectivePrefix}id'],
)!,
assetId: attachedDatabase.typeMapping.read(
i0.DriftSqlType.string,
data['${effectivePrefix}asset_id'],
)!,
action: i1.$AssetEditEntityTable.$converteraction.fromSql(
attachedDatabase.typeMapping.read(
i0.DriftSqlType.int,
data['${effectivePrefix}action'],
)!,
),
parameters: i1.$AssetEditEntityTable.$converterparameters.fromSql(
attachedDatabase.typeMapping.read(
i0.DriftSqlType.blob,
data['${effectivePrefix}parameters'],
)!,
),
);
}
@override
$AssetEditEntityTable createAlias(String alias) {
return $AssetEditEntityTable(attachedDatabase, alias);
}
static i0.JsonTypeConverter2<i2.AssetEditAction, int, int> $converteraction =
const i0.EnumIndexConverter<i2.AssetEditAction>(
i2.AssetEditAction.values,
);
static i0.JsonTypeConverter2<Map<String, Object?>, i3.Uint8List, Object?>
$converterparameters = i4.editParameterConverter;
@override
bool get withoutRowId => true;
@override
bool get isStrict => true;
}
class AssetEditEntityData extends i0.DataClass
implements i0.Insertable<i1.AssetEditEntityData> {
final String id;
final String assetId;
final i2.AssetEditAction action;
final Map<String, Object?> parameters;
const AssetEditEntityData({
required this.id,
required this.assetId,
required this.action,
required this.parameters,
});
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
map['id'] = i0.Variable<String>(id);
map['asset_id'] = i0.Variable<String>(assetId);
{
map['action'] = i0.Variable<int>(
i1.$AssetEditEntityTable.$converteraction.toSql(action),
);
}
{
map['parameters'] = i0.Variable<i3.Uint8List>(
i1.$AssetEditEntityTable.$converterparameters.toSql(parameters),
);
}
return map;
}
factory AssetEditEntityData.fromJson(
Map<String, dynamic> json, {
i0.ValueSerializer? serializer,
}) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return AssetEditEntityData(
id: serializer.fromJson<String>(json['id']),
assetId: serializer.fromJson<String>(json['assetId']),
action: i1.$AssetEditEntityTable.$converteraction.fromJson(
serializer.fromJson<int>(json['action']),
),
parameters: i1.$AssetEditEntityTable.$converterparameters.fromJson(
serializer.fromJson<Object?>(json['parameters']),
),
);
}
@override
Map<String, dynamic> toJson({i0.ValueSerializer? serializer}) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'id': serializer.toJson<String>(id),
'assetId': serializer.toJson<String>(assetId),
'action': serializer.toJson<int>(
i1.$AssetEditEntityTable.$converteraction.toJson(action),
),
'parameters': serializer.toJson<Object?>(
i1.$AssetEditEntityTable.$converterparameters.toJson(parameters),
),
};
}
i1.AssetEditEntityData copyWith({
String? id,
String? assetId,
i2.AssetEditAction? action,
Map<String, Object?>? parameters,
}) => i1.AssetEditEntityData(
id: id ?? this.id,
assetId: assetId ?? this.assetId,
action: action ?? this.action,
parameters: parameters ?? this.parameters,
);
AssetEditEntityData copyWithCompanion(i1.AssetEditEntityCompanion data) {
return AssetEditEntityData(
id: data.id.present ? data.id.value : this.id,
assetId: data.assetId.present ? data.assetId.value : this.assetId,
action: data.action.present ? data.action.value : this.action,
parameters: data.parameters.present
? data.parameters.value
: this.parameters,
);
}
@override
String toString() {
return (StringBuffer('AssetEditEntityData(')
..write('id: $id, ')
..write('assetId: $assetId, ')
..write('action: $action, ')
..write('parameters: $parameters')
..write(')'))
.toString();
}
@override
int get hashCode => Object.hash(id, assetId, action, parameters);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is i1.AssetEditEntityData &&
other.id == this.id &&
other.assetId == this.assetId &&
other.action == this.action &&
other.parameters == this.parameters);
}
class AssetEditEntityCompanion
extends i0.UpdateCompanion<i1.AssetEditEntityData> {
final i0.Value<String> id;
final i0.Value<String> assetId;
final i0.Value<i2.AssetEditAction> action;
final i0.Value<Map<String, Object?>> parameters;
const AssetEditEntityCompanion({
this.id = const i0.Value.absent(),
this.assetId = const i0.Value.absent(),
this.action = const i0.Value.absent(),
this.parameters = const i0.Value.absent(),
});
AssetEditEntityCompanion.insert({
required String id,
required String assetId,
required i2.AssetEditAction action,
required Map<String, Object?> parameters,
}) : id = i0.Value(id),
assetId = i0.Value(assetId),
action = i0.Value(action),
parameters = i0.Value(parameters);
static i0.Insertable<i1.AssetEditEntityData> custom({
i0.Expression<String>? id,
i0.Expression<String>? assetId,
i0.Expression<int>? action,
i0.Expression<i3.Uint8List>? parameters,
}) {
return i0.RawValuesInsertable({
if (id != null) 'id': id,
if (assetId != null) 'asset_id': assetId,
if (action != null) 'action': action,
if (parameters != null) 'parameters': parameters,
});
}
i1.AssetEditEntityCompanion copyWith({
i0.Value<String>? id,
i0.Value<String>? assetId,
i0.Value<i2.AssetEditAction>? action,
i0.Value<Map<String, Object?>>? parameters,
}) {
return i1.AssetEditEntityCompanion(
id: id ?? this.id,
assetId: assetId ?? this.assetId,
action: action ?? this.action,
parameters: parameters ?? this.parameters,
);
}
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
if (id.present) {
map['id'] = i0.Variable<String>(id.value);
}
if (assetId.present) {
map['asset_id'] = i0.Variable<String>(assetId.value);
}
if (action.present) {
map['action'] = i0.Variable<int>(
i1.$AssetEditEntityTable.$converteraction.toSql(action.value),
);
}
if (parameters.present) {
map['parameters'] = i0.Variable<i3.Uint8List>(
i1.$AssetEditEntityTable.$converterparameters.toSql(parameters.value),
);
}
return map;
}
@override
String toString() {
return (StringBuffer('AssetEditEntityCompanion(')
..write('id: $id, ')
..write('assetId: $assetId, ')
..write('action: $action, ')
..write('parameters: $parameters')
..write(')'))
.toString();
}
}

View File

@@ -4,6 +4,7 @@ import 'package:drift/drift.dart';
import 'package:drift_flutter/drift_flutter.dart';
import 'package:flutter/foundation.dart';
import 'package:immich_mobile/domain/interfaces/db.interface.dart';
import 'package:immich_mobile/infrastructure/entities/asset_edit.entity.dart';
import 'package:immich_mobile/infrastructure/entities/asset_face.entity.dart';
import 'package:immich_mobile/infrastructure/entities/auth_user.entity.dart';
import 'package:immich_mobile/infrastructure/entities/exif.entity.dart';
@@ -66,6 +67,7 @@ class IsarDatabaseRepository implements IDatabaseRepository {
AssetFaceEntity,
StoreEntity,
TrashedLocalAssetEntity,
AssetEditEntity,
],
include: {'package:immich_mobile/infrastructure/entities/merged_asset.drift'},
)
@@ -97,7 +99,7 @@ class Drift extends $Drift implements IDatabaseRepository {
}
@override
int get schemaVersion => 17;
int get schemaVersion => 18;
@override
MigrationStrategy get migration => MigrationStrategy(
@@ -204,6 +206,9 @@ class Drift extends $Drift implements IDatabaseRepository {
from16To17: (m, v17) async {
await m.addColumn(v17.remoteAssetEntity, v17.remoteAssetEntity.isEdited);
},
from17To18: (m, v18) async {
await m.createTable(v18.assetEditEntity);
},
),
);

View File

@@ -41,9 +41,11 @@ import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart'
as i19;
import 'package:immich_mobile/infrastructure/entities/trashed_local_asset.entity.drift.dart'
as i20;
import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart'
import 'package:immich_mobile/infrastructure/entities/asset_edit.entity.drift.dart'
as i21;
import 'package:drift/internal/modular.dart' as i22;
import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart'
as i22;
import 'package:drift/internal/modular.dart' as i23;
abstract class $Drift extends i0.GeneratedDatabase {
$Drift(i0.QueryExecutor e) : super(e);
@@ -85,9 +87,11 @@ abstract class $Drift extends i0.GeneratedDatabase {
late final i19.$StoreEntityTable storeEntity = i19.$StoreEntityTable(this);
late final i20.$TrashedLocalAssetEntityTable trashedLocalAssetEntity = i20
.$TrashedLocalAssetEntityTable(this);
i21.MergedAssetDrift get mergedAssetDrift => i22.ReadDatabaseContainer(
late final i21.$AssetEditEntityTable assetEditEntity = i21
.$AssetEditEntityTable(this);
i22.MergedAssetDrift get mergedAssetDrift => i23.ReadDatabaseContainer(
this,
).accessor<i21.MergedAssetDrift>(i21.MergedAssetDrift.new);
).accessor<i22.MergedAssetDrift>(i22.MergedAssetDrift.new);
@override
Iterable<i0.TableInfo<i0.Table, Object?>> get allTables =>
allSchemaEntities.whereType<i0.TableInfo<i0.Table, Object?>>();
@@ -119,6 +123,7 @@ abstract class $Drift extends i0.GeneratedDatabase {
assetFaceEntity,
storeEntity,
trashedLocalAssetEntity,
assetEditEntity,
i11.idxLatLng,
i20.idxTrashedLocalAssetChecksum,
i20.idxTrashedLocalAssetAlbum,
@@ -313,6 +318,13 @@ abstract class $Drift extends i0.GeneratedDatabase {
),
result: [i0.TableUpdate('asset_face_entity', kind: i0.UpdateKind.update)],
),
i0.WritePropagation(
on: i0.TableUpdateQuery.onTableName(
'remote_asset_entity',
limitUpdateKind: i0.UpdateKind.delete,
),
result: [i0.TableUpdate('asset_edit_entity', kind: i0.UpdateKind.delete)],
),
]);
@override
i0.DriftDatabaseOptions get options =>
@@ -372,4 +384,6 @@ class $DriftManager {
_db,
_db.trashedLocalAssetEntity,
);
i21.$$AssetEditEntityTableTableManager get assetEditEntity =>
i21.$$AssetEditEntityTableTableManager(_db, _db.assetEditEntity);
}

View File

@@ -7408,6 +7408,488 @@ i1.GeneratedColumn<bool> _column_101(String aliasedName) =>
),
defaultValue: const CustomExpression('0'),
);
final class Schema18 extends i0.VersionedSchema {
Schema18({required super.database}) : super(version: 18);
@override
late final List<i1.DatabaseSchemaEntity> entities = [
userEntity,
remoteAssetEntity,
stackEntity,
localAssetEntity,
remoteAlbumEntity,
localAlbumEntity,
localAlbumAssetEntity,
idxLocalAssetChecksum,
idxLocalAssetCloudId,
idxRemoteAssetOwnerChecksum,
uQRemoteAssetsOwnerChecksum,
uQRemoteAssetsOwnerLibraryChecksum,
idxRemoteAssetChecksum,
authUserEntity,
userMetadataEntity,
partnerEntity,
remoteExifEntity,
remoteAlbumAssetEntity,
remoteAlbumUserEntity,
remoteAssetCloudIdEntity,
memoryEntity,
memoryAssetEntity,
personEntity,
assetFaceEntity,
storeEntity,
trashedLocalAssetEntity,
assetEditEntity,
idxLatLng,
idxTrashedLocalAssetChecksum,
idxTrashedLocalAssetAlbum,
];
late final Shape20 userEntity = Shape20(
source: i0.VersionedTable(
entityName: 'user_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_0,
_column_1,
_column_3,
_column_84,
_column_85,
_column_91,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape28 remoteAssetEntity = Shape28(
source: i0.VersionedTable(
entityName: 'remote_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_1,
_column_8,
_column_9,
_column_5,
_column_10,
_column_11,
_column_12,
_column_0,
_column_13,
_column_14,
_column_15,
_column_16,
_column_17,
_column_18,
_column_19,
_column_20,
_column_21,
_column_86,
_column_101,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape3 stackEntity = Shape3(
source: i0.VersionedTable(
entityName: 'stack_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [_column_0, _column_9, _column_5, _column_15, _column_75],
attachedDatabase: database,
),
alias: null,
);
late final Shape26 localAssetEntity = Shape26(
source: i0.VersionedTable(
entityName: 'local_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_1,
_column_8,
_column_9,
_column_5,
_column_10,
_column_11,
_column_12,
_column_0,
_column_22,
_column_14,
_column_23,
_column_98,
_column_96,
_column_46,
_column_47,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape9 remoteAlbumEntity = Shape9(
source: i0.VersionedTable(
entityName: 'remote_album_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_0,
_column_1,
_column_56,
_column_9,
_column_5,
_column_15,
_column_57,
_column_58,
_column_59,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape19 localAlbumEntity = Shape19(
source: i0.VersionedTable(
entityName: 'local_album_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_0,
_column_1,
_column_5,
_column_31,
_column_32,
_column_90,
_column_33,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape22 localAlbumAssetEntity = Shape22(
source: i0.VersionedTable(
entityName: 'local_album_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id, album_id)'],
columns: [_column_34, _column_35, _column_33],
attachedDatabase: database,
),
alias: null,
);
final i1.Index idxLocalAssetChecksum = i1.Index(
'idx_local_asset_checksum',
'CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)',
);
final i1.Index idxLocalAssetCloudId = i1.Index(
'idx_local_asset_cloud_id',
'CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)',
);
final i1.Index idxRemoteAssetOwnerChecksum = i1.Index(
'idx_remote_asset_owner_checksum',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)',
);
final i1.Index uQRemoteAssetsOwnerChecksum = i1.Index(
'UQ_remote_assets_owner_checksum',
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)',
);
final i1.Index uQRemoteAssetsOwnerLibraryChecksum = i1.Index(
'UQ_remote_assets_owner_library_checksum',
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum ON remote_asset_entity (owner_id, library_id, checksum) WHERE(library_id IS NOT NULL)',
);
final i1.Index idxRemoteAssetChecksum = i1.Index(
'idx_remote_asset_checksum',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)',
);
late final Shape21 authUserEntity = Shape21(
source: i0.VersionedTable(
entityName: 'auth_user_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_0,
_column_1,
_column_3,
_column_2,
_column_84,
_column_85,
_column_92,
_column_93,
_column_7,
_column_94,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape4 userMetadataEntity = Shape4(
source: i0.VersionedTable(
entityName: 'user_metadata_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(user_id, "key")'],
columns: [_column_25, _column_26, _column_27],
attachedDatabase: database,
),
alias: null,
);
late final Shape5 partnerEntity = Shape5(
source: i0.VersionedTable(
entityName: 'partner_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(shared_by_id, shared_with_id)'],
columns: [_column_28, _column_29, _column_30],
attachedDatabase: database,
),
alias: null,
);
late final Shape8 remoteExifEntity = Shape8(
source: i0.VersionedTable(
entityName: 'remote_exif_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id)'],
columns: [
_column_36,
_column_37,
_column_38,
_column_39,
_column_40,
_column_41,
_column_11,
_column_10,
_column_42,
_column_43,
_column_44,
_column_45,
_column_46,
_column_47,
_column_48,
_column_49,
_column_50,
_column_51,
_column_52,
_column_53,
_column_54,
_column_55,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape7 remoteAlbumAssetEntity = Shape7(
source: i0.VersionedTable(
entityName: 'remote_album_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id, album_id)'],
columns: [_column_36, _column_60],
attachedDatabase: database,
),
alias: null,
);
late final Shape10 remoteAlbumUserEntity = Shape10(
source: i0.VersionedTable(
entityName: 'remote_album_user_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(album_id, user_id)'],
columns: [_column_60, _column_25, _column_61],
attachedDatabase: database,
),
alias: null,
);
late final Shape27 remoteAssetCloudIdEntity = Shape27(
source: i0.VersionedTable(
entityName: 'remote_asset_cloud_id_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id)'],
columns: [
_column_36,
_column_99,
_column_100,
_column_96,
_column_46,
_column_47,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape11 memoryEntity = Shape11(
source: i0.VersionedTable(
entityName: 'memory_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_0,
_column_9,
_column_5,
_column_18,
_column_15,
_column_8,
_column_62,
_column_63,
_column_64,
_column_65,
_column_66,
_column_67,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape12 memoryAssetEntity = Shape12(
source: i0.VersionedTable(
entityName: 'memory_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id, memory_id)'],
columns: [_column_36, _column_68],
attachedDatabase: database,
),
alias: null,
);
late final Shape14 personEntity = Shape14(
source: i0.VersionedTable(
entityName: 'person_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_0,
_column_9,
_column_5,
_column_15,
_column_1,
_column_69,
_column_71,
_column_72,
_column_73,
_column_74,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape15 assetFaceEntity = Shape15(
source: i0.VersionedTable(
entityName: 'asset_face_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_0,
_column_36,
_column_76,
_column_77,
_column_78,
_column_79,
_column_80,
_column_81,
_column_82,
_column_83,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape18 storeEntity = Shape18(
source: i0.VersionedTable(
entityName: 'store_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [_column_87, _column_88, _column_89],
attachedDatabase: database,
),
alias: null,
);
late final Shape25 trashedLocalAssetEntity = Shape25(
source: i0.VersionedTable(
entityName: 'trashed_local_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id, album_id)'],
columns: [
_column_1,
_column_8,
_column_9,
_column_5,
_column_10,
_column_11,
_column_12,
_column_0,
_column_95,
_column_22,
_column_14,
_column_23,
_column_97,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape29 assetEditEntity = Shape29(
source: i0.VersionedTable(
entityName: 'asset_edit_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [_column_0, _column_36, _column_102, _column_103],
attachedDatabase: database,
),
alias: null,
);
final i1.Index idxLatLng = i1.Index(
'idx_lat_lng',
'CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)',
);
final i1.Index idxTrashedLocalAssetChecksum = i1.Index(
'idx_trashed_local_asset_checksum',
'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)',
);
final i1.Index idxTrashedLocalAssetAlbum = i1.Index(
'idx_trashed_local_asset_album',
'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)',
);
}
class Shape29 extends i0.VersionedTable {
Shape29({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<String> get id =>
columnsByName['id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get assetId =>
columnsByName['asset_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<int> get action =>
columnsByName['action']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<i2.Uint8List> get parameters =>
columnsByName['parameters']! as i1.GeneratedColumn<i2.Uint8List>;
}
i1.GeneratedColumn<int> _column_102(String aliasedName) =>
i1.GeneratedColumn<int>(
'action',
aliasedName,
false,
type: i1.DriftSqlType.int,
);
i1.GeneratedColumn<i2.Uint8List> _column_103(String aliasedName) =>
i1.GeneratedColumn<i2.Uint8List>(
'parameters',
aliasedName,
false,
type: i1.DriftSqlType.blob,
);
i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
@@ -7425,6 +7907,7 @@ i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema15 schema) from14To15,
required Future<void> Function(i1.Migrator m, Schema16 schema) from15To16,
required Future<void> Function(i1.Migrator m, Schema17 schema) from16To17,
required Future<void> Function(i1.Migrator m, Schema18 schema) from17To18,
}) {
return (currentVersion, database) async {
switch (currentVersion) {
@@ -7508,6 +7991,11 @@ i0.MigrationStepWithVersion migrationSteps({
final migrator = i1.Migrator(database, schema);
await from16To17(migrator, schema);
return 17;
case 17:
final schema = Schema18(database: database);
final migrator = i1.Migrator(database, schema);
await from17To18(migrator, schema);
return 18;
default:
throw ArgumentError.value('Unknown migration from $currentVersion');
}
@@ -7531,6 +8019,7 @@ i1.OnUpgrade stepByStep({
required Future<void> Function(i1.Migrator m, Schema15 schema) from14To15,
required Future<void> Function(i1.Migrator m, Schema16 schema) from15To16,
required Future<void> Function(i1.Migrator m, Schema17 schema) from16To17,
required Future<void> Function(i1.Migrator m, Schema18 schema) from17To18,
}) => i0.VersionedSchema.stepByStepHelper(
step: migrationSteps(
from1To2: from1To2,
@@ -7549,5 +8038,6 @@ i1.OnUpgrade stepByStep({
from14To15: from14To15,
from15To16: from15To16,
from16To17: from16To17,
from17To18: from17To18,
),
);

View File

@@ -45,6 +45,7 @@ class SyncApiRepository {
SyncRequestType.usersV1,
SyncRequestType.assetsV1,
SyncRequestType.assetExifsV1,
SyncRequestType.assetEditsV1,
SyncRequestType.assetMetadataV1,
SyncRequestType.partnersV1,
SyncRequestType.partnerAssetsV1,
@@ -149,6 +150,8 @@ const _kResponseMap = <SyncEntityType, Function(Object)>{
SyncEntityType.assetV1: SyncAssetV1.fromJson,
SyncEntityType.assetDeleteV1: SyncAssetDeleteV1.fromJson,
SyncEntityType.assetExifV1: SyncAssetExifV1.fromJson,
SyncEntityType.assetEditV1: SyncAssetEditV1.fromJson,
SyncEntityType.assetEditDeleteV1: SyncAssetEditDeleteV1.fromJson,
SyncEntityType.assetMetadataV1: SyncAssetMetadataV1.fromJson,
SyncEntityType.assetMetadataDeleteV1: SyncAssetMetadataDeleteV1.fromJson,
SyncEntityType.partnerAssetV1: SyncAssetV1.fromJson,

View File

@@ -5,9 +5,11 @@ import 'package:drift/drift.dart';
import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/models/album/album.model.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/asset_edit.model.dart';
import 'package:immich_mobile/domain/models/memory.model.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/domain/models/user_metadata.model.dart';
import 'package:immich_mobile/infrastructure/entities/asset_edit.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/asset_face.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/auth_user.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart';
@@ -26,8 +28,8 @@ import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/infrastructure/utils/exif.converter.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart' as api show AssetVisibility, AlbumUserRole, UserMetadataKey;
import 'package:openapi/api.dart' hide AssetVisibility, AlbumUserRole, UserMetadataKey;
import 'package:openapi/api.dart' as api show AssetVisibility, AlbumUserRole, UserMetadataKey, AssetEditAction;
import 'package:openapi/api.dart' hide AssetVisibility, AlbumUserRole, UserMetadataKey, AssetEditAction;
class SyncStreamRepository extends DriftDatabaseRepository {
final Logger _logger = Logger('DriftSyncStreamRepository');
@@ -58,6 +60,7 @@ class SyncStreamRepository extends DriftDatabaseRepository {
await _db.userEntity.deleteAll();
await _db.userMetadataEntity.deleteAll();
await _db.remoteAssetCloudIdEntity.deleteAll();
await _db.assetEditEntity.deleteAll();
});
await _db.customStatement('PRAGMA foreign_keys = ON');
});
@@ -276,6 +279,39 @@ class SyncStreamRepository extends DriftDatabaseRepository {
}
}
Future<void> updateAssetEditsV1(Iterable<SyncAssetEditV1> data) async {
try {
await _db.batch((batch) {
for (final edit in data) {
final companion = AssetEditEntityCompanion(
id: Value(edit.id),
assetId: Value(edit.assetId),
action: Value(edit.action.toAssetEditAction()),
parameters: Value(edit.parameters as Map<String, Object?>),
);
batch.insert(_db.assetEditEntity, companion, onConflict: DoUpdate((_) => companion));
}
});
} catch (error, stack) {
_logger.severe('Error: updateAssetEditsV1', error, stack);
rethrow;
}
}
Future<void> deleteAssetEditsV1(Iterable<SyncAssetEditDeleteV1> data) async {
try {
await _db.batch((batch) {
for (final edit in data) {
batch.deleteWhere(_db.assetEditEntity, (row) => row.assetId.equals(edit.assetId));
}
});
} catch (error, stack) {
_logger.severe('Error: deleteAssetEditsV1', error, stack);
rethrow;
}
}
Future<void> deleteAssetsMetadataV1(Iterable<SyncAssetMetadataDeleteV1> data) async {
try {
await _db.batch((batch) {
@@ -765,3 +801,12 @@ extension on String {
extension on UserAvatarColor {
AvatarColor? toAvatarColor() => AvatarColor.values.firstWhereOrNull((c) => c.name == value);
}
extension on api.AssetEditAction {
AssetEditAction toAssetEditAction() => switch (this) {
api.AssetEditAction.crop => AssetEditAction.crop,
api.AssetEditAction.rotate => AssetEditAction.rotate,
api.AssetEditAction.mirror => AssetEditAction.mirror,
_ => AssetEditAction.other,
};
}

View File

@@ -574,6 +574,8 @@ Class | Method | HTTP request | Description
- [SyncAlbumUserV1](doc//SyncAlbumUserV1.md)
- [SyncAlbumV1](doc//SyncAlbumV1.md)
- [SyncAssetDeleteV1](doc//SyncAssetDeleteV1.md)
- [SyncAssetEditDeleteV1](doc//SyncAssetEditDeleteV1.md)
- [SyncAssetEditV1](doc//SyncAssetEditV1.md)
- [SyncAssetExifV1](doc//SyncAssetExifV1.md)
- [SyncAssetFaceDeleteV1](doc//SyncAssetFaceDeleteV1.md)
- [SyncAssetFaceV1](doc//SyncAssetFaceV1.md)

View File

@@ -314,6 +314,8 @@ part 'model/sync_album_user_delete_v1.dart';
part 'model/sync_album_user_v1.dart';
part 'model/sync_album_v1.dart';
part 'model/sync_asset_delete_v1.dart';
part 'model/sync_asset_edit_delete_v1.dart';
part 'model/sync_asset_edit_v1.dart';
part 'model/sync_asset_exif_v1.dart';
part 'model/sync_asset_face_delete_v1.dart';
part 'model/sync_asset_face_v1.dart';

View File

@@ -674,6 +674,10 @@ class ApiClient {
return SyncAlbumV1.fromJson(value);
case 'SyncAssetDeleteV1':
return SyncAssetDeleteV1.fromJson(value);
case 'SyncAssetEditDeleteV1':
return SyncAssetEditDeleteV1.fromJson(value);
case 'SyncAssetEditV1':
return SyncAssetEditV1.fromJson(value);
case 'SyncAssetExifV1':
return SyncAssetExifV1.fromJson(value);
case 'SyncAssetFaceDeleteV1':

View File

@@ -0,0 +1,99 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class SyncAssetEditDeleteV1 {
/// Returns a new [SyncAssetEditDeleteV1] instance.
SyncAssetEditDeleteV1({
required this.assetId,
});
String assetId;
@override
bool operator ==(Object other) => identical(this, other) || other is SyncAssetEditDeleteV1 &&
other.assetId == assetId;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(assetId.hashCode);
@override
String toString() => 'SyncAssetEditDeleteV1[assetId=$assetId]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'assetId'] = this.assetId;
return json;
}
/// Returns a new [SyncAssetEditDeleteV1] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static SyncAssetEditDeleteV1? fromJson(dynamic value) {
upgradeDto(value, "SyncAssetEditDeleteV1");
if (value is Map) {
final json = value.cast<String, dynamic>();
return SyncAssetEditDeleteV1(
assetId: mapValueOfType<String>(json, r'assetId')!,
);
}
return null;
}
static List<SyncAssetEditDeleteV1> listFromJson(dynamic json, {bool growable = false,}) {
final result = <SyncAssetEditDeleteV1>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = SyncAssetEditDeleteV1.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, SyncAssetEditDeleteV1> mapFromJson(dynamic json) {
final map = <String, SyncAssetEditDeleteV1>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = SyncAssetEditDeleteV1.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of SyncAssetEditDeleteV1-objects as value to a dart map
static Map<String, List<SyncAssetEditDeleteV1>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<SyncAssetEditDeleteV1>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = SyncAssetEditDeleteV1.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'assetId',
};
}

View File

@@ -0,0 +1,123 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class SyncAssetEditV1 {
/// Returns a new [SyncAssetEditV1] instance.
SyncAssetEditV1({
required this.action,
required this.assetId,
required this.id,
required this.parameters,
});
AssetEditAction action;
String assetId;
String id;
Object parameters;
@override
bool operator ==(Object other) => identical(this, other) || other is SyncAssetEditV1 &&
other.action == action &&
other.assetId == assetId &&
other.id == id &&
other.parameters == parameters;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(action.hashCode) +
(assetId.hashCode) +
(id.hashCode) +
(parameters.hashCode);
@override
String toString() => 'SyncAssetEditV1[action=$action, assetId=$assetId, id=$id, parameters=$parameters]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'action'] = this.action;
json[r'assetId'] = this.assetId;
json[r'id'] = this.id;
json[r'parameters'] = this.parameters;
return json;
}
/// Returns a new [SyncAssetEditV1] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static SyncAssetEditV1? fromJson(dynamic value) {
upgradeDto(value, "SyncAssetEditV1");
if (value is Map) {
final json = value.cast<String, dynamic>();
return SyncAssetEditV1(
action: AssetEditAction.fromJson(json[r'action'])!,
assetId: mapValueOfType<String>(json, r'assetId')!,
id: mapValueOfType<String>(json, r'id')!,
parameters: mapValueOfType<Object>(json, r'parameters')!,
);
}
return null;
}
static List<SyncAssetEditV1> listFromJson(dynamic json, {bool growable = false,}) {
final result = <SyncAssetEditV1>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = SyncAssetEditV1.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, SyncAssetEditV1> mapFromJson(dynamic json) {
final map = <String, SyncAssetEditV1>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = SyncAssetEditV1.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of SyncAssetEditV1-objects as value to a dart map
static Map<String, List<SyncAssetEditV1>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<SyncAssetEditV1>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = SyncAssetEditV1.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'action',
'assetId',
'id',
'parameters',
};
}

View File

@@ -29,6 +29,8 @@ class SyncEntityType {
static const assetV1 = SyncEntityType._(r'AssetV1');
static const assetDeleteV1 = SyncEntityType._(r'AssetDeleteV1');
static const assetExifV1 = SyncEntityType._(r'AssetExifV1');
static const assetEditV1 = SyncEntityType._(r'AssetEditV1');
static const assetEditDeleteV1 = SyncEntityType._(r'AssetEditDeleteV1');
static const assetMetadataV1 = SyncEntityType._(r'AssetMetadataV1');
static const assetMetadataDeleteV1 = SyncEntityType._(r'AssetMetadataDeleteV1');
static const partnerV1 = SyncEntityType._(r'PartnerV1');
@@ -79,6 +81,8 @@ class SyncEntityType {
assetV1,
assetDeleteV1,
assetExifV1,
assetEditV1,
assetEditDeleteV1,
assetMetadataV1,
assetMetadataDeleteV1,
partnerV1,
@@ -164,6 +168,8 @@ class SyncEntityTypeTypeTransformer {
case r'AssetV1': return SyncEntityType.assetV1;
case r'AssetDeleteV1': return SyncEntityType.assetDeleteV1;
case r'AssetExifV1': return SyncEntityType.assetExifV1;
case r'AssetEditV1': return SyncEntityType.assetEditV1;
case r'AssetEditDeleteV1': return SyncEntityType.assetEditDeleteV1;
case r'AssetMetadataV1': return SyncEntityType.assetMetadataV1;
case r'AssetMetadataDeleteV1': return SyncEntityType.assetMetadataDeleteV1;
case r'PartnerV1': return SyncEntityType.partnerV1;

View File

@@ -30,6 +30,7 @@ class SyncRequestType {
static const albumAssetExifsV1 = SyncRequestType._(r'AlbumAssetExifsV1');
static const assetsV1 = SyncRequestType._(r'AssetsV1');
static const assetExifsV1 = SyncRequestType._(r'AssetExifsV1');
static const assetEditsV1 = SyncRequestType._(r'AssetEditsV1');
static const assetMetadataV1 = SyncRequestType._(r'AssetMetadataV1');
static const authUsersV1 = SyncRequestType._(r'AuthUsersV1');
static const memoriesV1 = SyncRequestType._(r'MemoriesV1');
@@ -53,6 +54,7 @@ class SyncRequestType {
albumAssetExifsV1,
assetsV1,
assetExifsV1,
assetEditsV1,
assetMetadataV1,
authUsersV1,
memoriesV1,
@@ -111,6 +113,7 @@ class SyncRequestTypeTypeTransformer {
case r'AlbumAssetExifsV1': return SyncRequestType.albumAssetExifsV1;
case r'AssetsV1': return SyncRequestType.assetsV1;
case r'AssetExifsV1': return SyncRequestType.assetExifsV1;
case r'AssetEditsV1': return SyncRequestType.assetEditsV1;
case r'AssetMetadataV1': return SyncRequestType.assetMetadataV1;
case r'AuthUsersV1': return SyncRequestType.authUsersV1;
case r'MemoriesV1': return SyncRequestType.memoriesV1;

View File

@@ -20,6 +20,7 @@ import 'schema_v14.dart' as v14;
import 'schema_v15.dart' as v15;
import 'schema_v16.dart' as v16;
import 'schema_v17.dart' as v17;
import 'schema_v18.dart' as v18;
class GeneratedHelper implements SchemaInstantiationHelper {
@override
@@ -59,6 +60,8 @@ class GeneratedHelper implements SchemaInstantiationHelper {
return v16.DatabaseAtV16(db);
case 17:
return v17.DatabaseAtV17(db);
case 18:
return v18.DatabaseAtV18(db);
default:
throw MissingSchemaException(version, versions);
}
@@ -82,5 +85,6 @@ class GeneratedHelper implements SchemaInstantiationHelper {
15,
16,
17,
18,
];
}

File diff suppressed because it is too large Load Diff

View File

@@ -21503,6 +21503,44 @@
],
"type": "object"
},
"SyncAssetEditDeleteV1": {
"properties": {
"assetId": {
"type": "string"
}
},
"required": [
"assetId"
],
"type": "object"
},
"SyncAssetEditV1": {
"properties": {
"action": {
"allOf": [
{
"$ref": "#/components/schemas/AssetEditAction"
}
]
},
"assetId": {
"type": "string"
},
"id": {
"type": "string"
},
"parameters": {
"type": "object"
}
},
"required": [
"action",
"assetId",
"id",
"parameters"
],
"type": "object"
},
"SyncAssetExifV1": {
"properties": {
"assetId": {
@@ -21922,6 +21960,8 @@
"AssetV1",
"AssetDeleteV1",
"AssetExifV1",
"AssetEditV1",
"AssetEditDeleteV1",
"AssetMetadataV1",
"AssetMetadataDeleteV1",
"PartnerV1",
@@ -22184,6 +22224,7 @@
"AlbumAssetExifsV1",
"AssetsV1",
"AssetExifsV1",
"AssetEditsV1",
"AssetMetadataV1",
"AuthUsersV1",
"MemoriesV1",

View File

@@ -5808,6 +5808,8 @@ export enum SyncEntityType {
AssetV1 = "AssetV1",
AssetDeleteV1 = "AssetDeleteV1",
AssetExifV1 = "AssetExifV1",
AssetEditV1 = "AssetEditV1",
AssetEditDeleteV1 = "AssetEditDeleteV1",
AssetMetadataV1 = "AssetMetadataV1",
AssetMetadataDeleteV1 = "AssetMetadataDeleteV1",
PartnerV1 = "PartnerV1",
@@ -5858,6 +5860,7 @@ export enum SyncRequestType {
AlbumAssetExifsV1 = "AlbumAssetExifsV1",
AssetsV1 = "AssetsV1",
AssetExifsV1 = "AssetExifsV1",
AssetEditsV1 = "AssetEditsV1",
AssetMetadataV1 = "AssetMetadataV1",
AuthUsersV1 = "AuthUsersV1",
MemoriesV1 = "MemoriesV1",

View File

@@ -2,6 +2,7 @@
import { ApiProperty } from '@nestjs/swagger';
import { ArrayMaxSize, IsInt, IsPositive, IsString } from 'class-validator';
import { AssetResponseDto } from 'src/dtos/asset-response.dto';
import { AssetEditAction } from 'src/dtos/editing.dto';
import {
AlbumUserRole,
AssetOrder,
@@ -169,6 +170,20 @@ export class SyncAssetExifV1 {
fps!: number | null;
}
@ExtraModel()
export class SyncAssetEditV1 {
id!: string;
assetId!: string;
@ValidateEnum({ enum: AssetEditAction, name: 'AssetEditAction' })
action!: AssetEditAction;
parameters!: object;
}
@ExtraModel()
export class SyncAssetEditDeleteV1 {
assetId!: string;
}
@ExtraModel()
export class SyncAssetMetadataV1 {
assetId!: string;
@@ -354,6 +369,8 @@ export type SyncItem = {
[SyncEntityType.AssetMetadataV1]: SyncAssetMetadataV1;
[SyncEntityType.AssetMetadataDeleteV1]: SyncAssetMetadataDeleteV1;
[SyncEntityType.AssetExifV1]: SyncAssetExifV1;
[SyncEntityType.AssetEditV1]: SyncAssetEditV1;
[SyncEntityType.AssetEditDeleteV1]: SyncAssetEditDeleteV1;
[SyncEntityType.PartnerAssetV1]: SyncAssetV1;
[SyncEntityType.PartnerAssetBackfillV1]: SyncAssetV1;
[SyncEntityType.PartnerAssetDeleteV1]: SyncAssetDeleteV1;

View File

@@ -725,6 +725,7 @@ export enum SyncRequestType {
AlbumAssetExifsV1 = 'AlbumAssetExifsV1',
AssetsV1 = 'AssetsV1',
AssetExifsV1 = 'AssetExifsV1',
AssetEditsV1 = 'AssetEditsV1',
AssetMetadataV1 = 'AssetMetadataV1',
AuthUsersV1 = 'AuthUsersV1',
MemoriesV1 = 'MemoriesV1',
@@ -749,6 +750,8 @@ export enum SyncEntityType {
AssetV1 = 'AssetV1',
AssetDeleteV1 = 'AssetDeleteV1',
AssetExifV1 = 'AssetExifV1',
AssetEditV1 = 'AssetEditV1',
AssetEditDeleteV1 = 'AssetEditDeleteV1',
AssetMetadataV1 = 'AssetMetadataV1',
AssetMetadataDeleteV1 = 'AssetMetadataDeleteV1',

View File

@@ -53,6 +53,7 @@ export class SyncRepository {
albumUser: AlbumUserSync;
asset: AssetSync;
assetExif: AssetExifSync;
assetEdit: AssetEditSync;
assetFace: AssetFaceSync;
assetMetadata: AssetMetadataSync;
authUser: AuthUserSync;
@@ -75,6 +76,7 @@ export class SyncRepository {
this.albumUser = new AlbumUserSync(this.db);
this.asset = new AssetSync(this.db);
this.assetExif = new AssetExifSync(this.db);
this.assetEdit = new AssetEditSync(this.db);
this.assetFace = new AssetFaceSync(this.db);
this.assetMetadata = new AssetMetadataSync(this.db);
this.authUser = new AuthUserSync(this.db);
@@ -499,6 +501,30 @@ class AssetExifSync extends BaseSync {
}
}
class AssetEditSync extends BaseSync {
@GenerateSql({ params: [dummyQueryOptions, DummyValue.UUID], stream: true })
getDeletes(options: SyncQueryOptions) {
return this.auditQuery('asset_edit_audit', options)
.select(['asset_edit_audit.id', 'assetId'])
.leftJoin('asset', 'asset.id', 'asset_edit_audit.assetId')
.where('asset.ownerId', '=', options.userId)
.stream();
}
cleanupAuditTable(daysAgo: number) {
return this.auditCleanup('asset_edit_audit', daysAgo);
}
@GenerateSql({ params: [dummyQueryOptions, DummyValue.UUID], stream: true })
getUpserts(options: SyncQueryOptions) {
return this.upsertQuery('asset_edit', options)
.select(['asset_edit.id', 'assetId', 'action', 'parameters', 'asset_edit.updateId'])
.innerJoin('asset', 'asset.id', 'asset_edit.assetId')
.where('asset.ownerId', '=', options.userId)
.stream();
}
}
class MemorySync extends BaseSync {
@GenerateSql({ params: [dummyQueryOptions], stream: true })
getDeletes(options: SyncQueryOptions) {

View File

@@ -286,3 +286,16 @@ export const asset_edit_delete = registerFunction({
END
`,
});
export const asset_edit_audit = registerFunction({
name: 'asset_edit_audit',
returnType: 'TRIGGER',
language: 'PLPGSQL',
body: `
BEGIN
INSERT INTO asset_edit_audit ("assetId")
SELECT "assetId"
FROM OLD;
RETURN NULL;
END`,
});

View File

@@ -28,6 +28,7 @@ import { AlbumUserTable } from 'src/schema/tables/album-user.table';
import { AlbumTable } from 'src/schema/tables/album.table';
import { ApiKeyTable } from 'src/schema/tables/api-key.table';
import { AssetAuditTable } from 'src/schema/tables/asset-audit.table';
import { AssetEditAuditTable } from 'src/schema/tables/asset-edit-audit.table';
import { AssetEditTable } from 'src/schema/tables/asset-edit.table';
import { AssetExifTable } from 'src/schema/tables/asset-exif.table';
import { AssetFaceAuditTable } from 'src/schema/tables/asset-face-audit.table';
@@ -88,6 +89,7 @@ export class ImmichDatabase {
ApiKeyTable,
AssetAuditTable,
AssetEditTable,
AssetEditAuditTable,
AssetFaceTable,
AssetFaceAuditTable,
AssetMetadataTable,
@@ -182,6 +184,7 @@ export interface DB {
asset: AssetTable;
asset_audit: AssetAuditTable;
asset_edit: AssetEditTable;
asset_edit_audit: AssetEditAuditTable;
asset_exif: AssetExifTable;
asset_face: AssetFaceTable;
asset_face_audit: AssetFaceAuditTable;

View File

@@ -0,0 +1,47 @@
import { Kysely, sql } from 'kysely';
export async function up(db: Kysely<any>): Promise<void> {
await sql`CREATE OR REPLACE FUNCTION asset_edit_audit()
RETURNS TRIGGER
LANGUAGE PLPGSQL
AS $$
BEGIN
INSERT INTO asset_edit_audit ("assetId")
SELECT "assetId"
FROM OLD;
RETURN NULL;
END
$$;`.execute(db);
await sql`CREATE TABLE "asset_edit_audit" (
"id" uuid NOT NULL DEFAULT immich_uuid_v7(),
"assetId" uuid NOT NULL,
"deletedAt" timestamp with time zone NOT NULL DEFAULT clock_timestamp(),
CONSTRAINT "asset_edit_audit_pkey" PRIMARY KEY ("id")
);`.execute(db);
await sql`CREATE INDEX "asset_edit_audit_assetId_idx" ON "asset_edit_audit" ("assetId");`.execute(db);
await sql`CREATE INDEX "asset_edit_audit_deletedAt_idx" ON "asset_edit_audit" ("deletedAt");`.execute(db);
await sql`ALTER TABLE "asset_edit" ADD "updateId" uuid NOT NULL DEFAULT immich_uuid_v7();`.execute(db);
await sql`CREATE INDEX "asset_edit_updateId_idx" ON "asset_edit" ("updateId");`.execute(db);
await sql`CREATE OR REPLACE TRIGGER "asset_edit_audit"
AFTER DELETE ON "asset_edit"
REFERENCING OLD TABLE AS "old"
FOR EACH STATEMENT
WHEN (pg_trigger_depth() = 0)
EXECUTE FUNCTION asset_edit_audit();`.execute(db);
await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('function_asset_edit_audit', '{"type":"function","name":"asset_edit_audit","sql":"CREATE OR REPLACE FUNCTION asset_edit_audit()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n INSERT INTO asset_edit_audit (\\"assetId\\")\\n SELECT \\"assetId\\"\\n FROM OLD;\\n RETURN NULL;\\n END\\n $$;"}'::jsonb);`.execute(
db,
);
await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_asset_edit_audit', '{"type":"trigger","name":"asset_edit_audit","sql":"CREATE OR REPLACE TRIGGER \\"asset_edit_audit\\"\\n AFTER DELETE ON \\"asset_edit\\"\\n REFERENCING OLD TABLE AS \\"old\\"\\n FOR EACH STATEMENT\\n WHEN (pg_trigger_depth() = 0)\\n EXECUTE FUNCTION asset_edit_audit();"}'::jsonb);`.execute(
db,
);
}
export async function down(db: Kysely<any>): Promise<void> {
await sql`DROP TRIGGER "asset_edit_audit" ON "asset_edit";`.execute(db);
await sql`DROP INDEX "asset_edit_updateId_idx";`.execute(db);
await sql`ALTER TABLE "asset_edit" DROP COLUMN "updateId";`.execute(db);
await sql`DROP TABLE "asset_edit_audit";`.execute(db);
await sql`DROP FUNCTION asset_edit_audit;`.execute(db);
await sql`DELETE FROM "migration_overrides" WHERE "name" = 'function_asset_edit_audit';`.execute(db);
await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_asset_edit_audit';`.execute(db);
}

View File

@@ -0,0 +1,14 @@
import { PrimaryGeneratedUuidV7Column } from 'src/decorators';
import { Column, CreateDateColumn, Generated, Table, Timestamp } from 'src/sql-tools';
@Table('asset_edit_audit')
export class AssetEditAuditTable {
@PrimaryGeneratedUuidV7Column()
id!: Generated<string>;
@Column({ type: 'uuid', index: true })
assetId!: string;
@CreateDateColumn({ default: () => 'clock_timestamp()', index: true })
deletedAt!: Generated<Timestamp>;
}

View File

@@ -1,5 +1,6 @@
import { UpdateIdColumn } from 'src/decorators';
import { AssetEditAction, AssetEditActionParameter } from 'src/dtos/editing.dto';
import { asset_edit_delete, asset_edit_insert } from 'src/schema/functions';
import { asset_edit_audit, asset_edit_delete, asset_edit_insert } from 'src/schema/functions';
import { AssetTable } from 'src/schema/tables/asset.table';
import {
AfterDeleteTrigger,
@@ -19,6 +20,12 @@ import {
referencingOldTableAs: 'deleted_edit',
when: 'pg_trigger_depth() = 0',
})
@AfterDeleteTrigger({
scope: 'statement',
function: asset_edit_audit,
referencingOldTableAs: 'old',
when: 'pg_trigger_depth() = 0',
})
export class AssetEditTable<T extends AssetEditAction = AssetEditAction> {
@PrimaryGeneratedColumn()
id!: Generated<string>;
@@ -31,4 +38,7 @@ export class AssetEditTable<T extends AssetEditAction = AssetEditAction> {
@Column({ type: 'jsonb' })
parameters!: AssetEditActionParameter[T];
@UpdateIdColumn({ index: true })
updateId!: Generated<string>;
}

View File

@@ -87,6 +87,7 @@ export const SYNC_TYPES_ORDER = [
SyncRequestType.AssetFacesV1,
SyncRequestType.UserMetadataV1,
SyncRequestType.AssetMetadataV1,
SyncRequestType.AssetEditsV1,
];
const throwSessionRequired = () => {
@@ -173,6 +174,7 @@ export class SyncService extends BaseService {
[SyncRequestType.PartnersV1]: () => this.syncPartnersV1(options, response, checkpointMap),
[SyncRequestType.AssetsV1]: () => this.syncAssetsV1(options, response, checkpointMap),
[SyncRequestType.AssetExifsV1]: () => this.syncAssetExifsV1(options, response, checkpointMap),
[SyncRequestType.AssetEditsV1]: () => this.syncAssetEditsV1(options, response, checkpointMap),
[SyncRequestType.PartnerAssetsV1]: () => this.syncPartnerAssetsV1(options, response, checkpointMap, session.id),
[SyncRequestType.AssetMetadataV1]: () => this.syncAssetMetadataV1(options, response, checkpointMap, auth),
[SyncRequestType.PartnerAssetExifsV1]: () =>
@@ -349,6 +351,23 @@ export class SyncService extends BaseService {
}
}
private async syncAssetEditsV1(options: SyncQueryOptions, response: Writable, checkpointMap: CheckpointMap) {
console.log('syncing asset edits');
const deleteType = SyncEntityType.AssetEditDeleteV1;
const deletes = this.syncRepository.assetEdit.getDeletes({ ...options, ack: checkpointMap[deleteType] });
for await (const { id, ...data } of deletes) {
send(response, { type: deleteType, ids: [id], data });
}
const upsertType = SyncEntityType.AssetEditV1;
const upserts = this.syncRepository.assetEdit.getUpserts({ ...options, ack: checkpointMap[upsertType] });
for await (const { updateId, ...data } of upserts) {
console.log('upsert asset edit', updateId);
send(response, { type: upsertType, ids: [updateId], data });
}
}
private async syncPartnerAssetExifsV1(
options: SyncQueryOptions,
response: Writable,

View File

@@ -0,0 +1,294 @@
import { Kysely } from 'kysely';
import { AssetEditAction, MirrorAxis } from 'src/dtos/editing.dto';
import { SyncEntityType, SyncRequestType } from 'src/enum';
import { AssetEditRepository } from 'src/repositories/asset-edit.repository';
import { DB } from 'src/schema';
import { SyncTestContext } from 'test/medium.factory';
import { factory } from 'test/small.factory';
import { getKyselyDB } from 'test/utils';
let defaultDatabase: Kysely<DB>;
const setup = async (db?: Kysely<DB>) => {
const ctx = new SyncTestContext(db || defaultDatabase);
const { auth, user, session } = await ctx.newSyncAuthUser();
return { auth, user, session, ctx };
};
beforeAll(async () => {
defaultDatabase = await getKyselyDB();
});
describe(SyncRequestType.AssetEditsV1, () => {
it('should detect and sync the first asset edit', async () => {
const { auth, ctx } = await setup();
const { asset } = await ctx.newAsset({ ownerId: auth.user.id });
const assetEditRepo = ctx.get(AssetEditRepository);
await assetEditRepo.replaceAll(asset.id, [
{
action: AssetEditAction.Crop,
parameters: { x: 10, y: 20, width: 100, height: 200 },
},
]);
const response = await ctx.syncStream(auth, [SyncRequestType.AssetEditsV1]);
expect(response).toEqual([
{
ack: expect.any(String),
data: {
id: expect.any(String),
assetId: asset.id,
action: AssetEditAction.Crop,
parameters: { x: 10, y: 20, width: 100, height: 200 },
},
type: SyncEntityType.AssetEditV1,
},
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]);
await ctx.syncAckAll(auth, response);
await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetEditsV1]);
});
it('should detect and sync multiple asset edits for the same asset', async () => {
const { auth, ctx } = await setup();
const { asset } = await ctx.newAsset({ ownerId: auth.user.id });
const assetEditRepo = ctx.get(AssetEditRepository);
await assetEditRepo.replaceAll(asset.id, [
{
action: AssetEditAction.Crop,
parameters: { x: 10, y: 20, width: 100, height: 200 },
},
{
action: AssetEditAction.Rotate,
parameters: { angle: 90 },
},
{
action: AssetEditAction.Mirror,
parameters: { axis: MirrorAxis.Horizontal },
},
]);
const response = await ctx.syncStream(auth, [SyncRequestType.AssetEditsV1]);
expect(response.length).toBe(4); // 3 edits + 1 complete
expect(response).toEqual(
expect.arrayContaining([
{
ack: expect.any(String),
data: {
id: expect.any(String),
assetId: asset.id,
action: AssetEditAction.Crop,
parameters: { x: 10, y: 20, width: 100, height: 200 },
},
type: SyncEntityType.AssetEditV1,
},
{
ack: expect.any(String),
data: {
id: expect.any(String),
assetId: asset.id,
action: AssetEditAction.Rotate,
parameters: { angle: 90 },
},
type: SyncEntityType.AssetEditV1,
},
{
ack: expect.any(String),
data: {
id: expect.any(String),
assetId: asset.id,
action: AssetEditAction.Mirror,
parameters: { axis: MirrorAxis.Horizontal },
},
type: SyncEntityType.AssetEditV1,
},
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]),
);
await ctx.syncAckAll(auth, response);
await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetEditsV1]);
});
it('should detect and sync updated edits', async () => {
const { auth, ctx } = await setup();
const { asset } = await ctx.newAsset({ ownerId: auth.user.id });
const assetEditRepo = ctx.get(AssetEditRepository);
// Create initial edit
await assetEditRepo.replaceAll(asset.id, [
{
action: AssetEditAction.Crop,
parameters: { x: 10, y: 20, width: 100, height: 200 },
},
]);
const response1 = await ctx.syncStream(auth, [SyncRequestType.AssetEditsV1]);
await ctx.syncAckAll(auth, response1);
await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetEditsV1]);
// Update the edit (replaceAll deletes old edits then inserts new ones)
await assetEditRepo.replaceAll(asset.id, [
{
action: AssetEditAction.Crop,
parameters: { x: 50, y: 60, width: 150, height: 250 },
},
]);
const response2 = await ctx.syncStream(auth, [SyncRequestType.AssetEditsV1]);
expect(response2).toEqual([
{
ack: expect.any(String),
data: {
assetId: asset.id,
},
type: SyncEntityType.AssetEditDeleteV1,
},
{
ack: expect.any(String),
data: {
id: expect.any(String),
assetId: asset.id,
action: AssetEditAction.Crop,
parameters: { x: 50, y: 60, width: 150, height: 250 },
},
type: SyncEntityType.AssetEditV1,
},
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]);
await ctx.syncAckAll(auth, response2);
await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetEditsV1]);
});
it('should detect and sync deleted asset edits', async () => {
const { auth, ctx } = await setup();
const { asset } = await ctx.newAsset({ ownerId: auth.user.id });
const assetEditRepo = ctx.get(AssetEditRepository);
// Create initial edit
await assetEditRepo.replaceAll(asset.id, [
{
action: AssetEditAction.Crop,
parameters: { x: 10, y: 20, width: 100, height: 200 },
},
]);
const response1 = await ctx.syncStream(auth, [SyncRequestType.AssetEditsV1]);
await ctx.syncAckAll(auth, response1);
await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetEditsV1]);
// Delete all edits
await assetEditRepo.replaceAll(asset.id, []);
const response2 = await ctx.syncStream(auth, [SyncRequestType.AssetEditsV1]);
expect(response2).toEqual([
{
ack: expect.any(String),
data: {
assetId: asset.id,
},
type: SyncEntityType.AssetEditDeleteV1,
},
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]);
await ctx.syncAckAll(auth, response2);
await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetEditsV1]);
});
it('should only sync asset edits for own user', async () => {
const { auth, ctx } = await setup();
const { user: user2 } = await ctx.newUser();
const { asset } = await ctx.newAsset({ ownerId: user2.id });
const assetEditRepo = ctx.get(AssetEditRepository);
const { session } = await ctx.newSession({ userId: user2.id });
const auth2 = factory.auth({ session, user: user2 });
await assetEditRepo.replaceAll(asset.id, [
{
action: AssetEditAction.Crop,
parameters: { x: 10, y: 20, width: 100, height: 200 },
},
]);
// User 2 should see their own edit
await expect(ctx.syncStream(auth2, [SyncRequestType.AssetEditsV1])).resolves.toEqual([
expect.objectContaining({ type: SyncEntityType.AssetEditV1 }),
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]);
// User 1 should not see user 2's edit
await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetEditsV1]);
});
it('should sync edits for multiple assets', async () => {
const { auth, ctx } = await setup();
const { asset: asset1 } = await ctx.newAsset({ ownerId: auth.user.id });
const { asset: asset2 } = await ctx.newAsset({ ownerId: auth.user.id });
const assetEditRepo = ctx.get(AssetEditRepository);
await assetEditRepo.replaceAll(asset1.id, [
{
action: AssetEditAction.Crop,
parameters: { x: 10, y: 20, width: 100, height: 200 },
},
]);
await assetEditRepo.replaceAll(asset2.id, [
{
action: AssetEditAction.Rotate,
parameters: { angle: 270 },
},
]);
const response = await ctx.syncStream(auth, [SyncRequestType.AssetEditsV1]);
expect(response).toEqual([
{
ack: expect.any(String),
data: {
id: expect.any(String),
assetId: asset1.id,
action: AssetEditAction.Crop,
parameters: { x: 10, y: 20, width: 100, height: 200 },
},
type: SyncEntityType.AssetEditV1,
},
{
ack: expect.any(String),
data: {
id: expect.any(String),
assetId: asset2.id,
action: AssetEditAction.Rotate,
parameters: { angle: 270 },
},
type: SyncEntityType.AssetEditV1,
},
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
]);
await ctx.syncAckAll(auth, response);
await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetEditsV1]);
});
it('should not sync edits for partner assets', async () => {
const { auth, ctx } = await setup();
const { user: partner } = await ctx.newUser();
await ctx.newPartner({ sharedById: partner.id, sharedWithId: auth.user.id });
const { asset } = await ctx.newAsset({ ownerId: partner.id });
const assetEditRepo = ctx.get(AssetEditRepository);
await assetEditRepo.replaceAll(asset.id, [
{
action: AssetEditAction.Crop,
parameters: { x: 10, y: 20, width: 100, height: 200 },
},
]);
// Should not see partner's asset edits in own sync
await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetEditsV1]);
});
});