mirror of
https://github.com/immich-app/immich.git
synced 2025-05-24 01:12:58 -04:00
feat: sync albums and album users (#18377)
This commit is contained in:
parent
58af574241
commit
cd288533a1
4
mobile/openapi/README.md
generated
4
mobile/openapi/README.md
generated
@ -443,6 +443,10 @@ Class | Method | HTTP request | Description
|
|||||||
- [SyncAckDeleteDto](doc//SyncAckDeleteDto.md)
|
- [SyncAckDeleteDto](doc//SyncAckDeleteDto.md)
|
||||||
- [SyncAckDto](doc//SyncAckDto.md)
|
- [SyncAckDto](doc//SyncAckDto.md)
|
||||||
- [SyncAckSetDto](doc//SyncAckSetDto.md)
|
- [SyncAckSetDto](doc//SyncAckSetDto.md)
|
||||||
|
- [SyncAlbumDeleteV1](doc//SyncAlbumDeleteV1.md)
|
||||||
|
- [SyncAlbumUserDeleteV1](doc//SyncAlbumUserDeleteV1.md)
|
||||||
|
- [SyncAlbumUserV1](doc//SyncAlbumUserV1.md)
|
||||||
|
- [SyncAlbumV1](doc//SyncAlbumV1.md)
|
||||||
- [SyncAssetDeleteV1](doc//SyncAssetDeleteV1.md)
|
- [SyncAssetDeleteV1](doc//SyncAssetDeleteV1.md)
|
||||||
- [SyncAssetExifV1](doc//SyncAssetExifV1.md)
|
- [SyncAssetExifV1](doc//SyncAssetExifV1.md)
|
||||||
- [SyncAssetV1](doc//SyncAssetV1.md)
|
- [SyncAssetV1](doc//SyncAssetV1.md)
|
||||||
|
4
mobile/openapi/lib/api.dart
generated
4
mobile/openapi/lib/api.dart
generated
@ -238,6 +238,10 @@ part 'model/stack_update_dto.dart';
|
|||||||
part 'model/sync_ack_delete_dto.dart';
|
part 'model/sync_ack_delete_dto.dart';
|
||||||
part 'model/sync_ack_dto.dart';
|
part 'model/sync_ack_dto.dart';
|
||||||
part 'model/sync_ack_set_dto.dart';
|
part 'model/sync_ack_set_dto.dart';
|
||||||
|
part 'model/sync_album_delete_v1.dart';
|
||||||
|
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_delete_v1.dart';
|
||||||
part 'model/sync_asset_exif_v1.dart';
|
part 'model/sync_asset_exif_v1.dart';
|
||||||
part 'model/sync_asset_v1.dart';
|
part 'model/sync_asset_v1.dart';
|
||||||
|
8
mobile/openapi/lib/api_client.dart
generated
8
mobile/openapi/lib/api_client.dart
generated
@ -532,6 +532,14 @@ class ApiClient {
|
|||||||
return SyncAckDto.fromJson(value);
|
return SyncAckDto.fromJson(value);
|
||||||
case 'SyncAckSetDto':
|
case 'SyncAckSetDto':
|
||||||
return SyncAckSetDto.fromJson(value);
|
return SyncAckSetDto.fromJson(value);
|
||||||
|
case 'SyncAlbumDeleteV1':
|
||||||
|
return SyncAlbumDeleteV1.fromJson(value);
|
||||||
|
case 'SyncAlbumUserDeleteV1':
|
||||||
|
return SyncAlbumUserDeleteV1.fromJson(value);
|
||||||
|
case 'SyncAlbumUserV1':
|
||||||
|
return SyncAlbumUserV1.fromJson(value);
|
||||||
|
case 'SyncAlbumV1':
|
||||||
|
return SyncAlbumV1.fromJson(value);
|
||||||
case 'SyncAssetDeleteV1':
|
case 'SyncAssetDeleteV1':
|
||||||
return SyncAssetDeleteV1.fromJson(value);
|
return SyncAssetDeleteV1.fromJson(value);
|
||||||
case 'SyncAssetExifV1':
|
case 'SyncAssetExifV1':
|
||||||
|
99
mobile/openapi/lib/model/sync_album_delete_v1.dart
generated
Normal file
99
mobile/openapi/lib/model/sync_album_delete_v1.dart
generated
Normal 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 SyncAlbumDeleteV1 {
|
||||||
|
/// Returns a new [SyncAlbumDeleteV1] instance.
|
||||||
|
SyncAlbumDeleteV1({
|
||||||
|
required this.albumId,
|
||||||
|
});
|
||||||
|
|
||||||
|
String albumId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => identical(this, other) || other is SyncAlbumDeleteV1 &&
|
||||||
|
other.albumId == albumId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
// ignore: unnecessary_parenthesis
|
||||||
|
(albumId.hashCode);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'SyncAlbumDeleteV1[albumId=$albumId]';
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final json = <String, dynamic>{};
|
||||||
|
json[r'albumId'] = this.albumId;
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new [SyncAlbumDeleteV1] instance and imports its values from
|
||||||
|
/// [value] if it's a [Map], null otherwise.
|
||||||
|
// ignore: prefer_constructors_over_static_methods
|
||||||
|
static SyncAlbumDeleteV1? fromJson(dynamic value) {
|
||||||
|
upgradeDto(value, "SyncAlbumDeleteV1");
|
||||||
|
if (value is Map) {
|
||||||
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
|
return SyncAlbumDeleteV1(
|
||||||
|
albumId: mapValueOfType<String>(json, r'albumId')!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<SyncAlbumDeleteV1> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <SyncAlbumDeleteV1>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = SyncAlbumDeleteV1.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, SyncAlbumDeleteV1> mapFromJson(dynamic json) {
|
||||||
|
final map = <String, SyncAlbumDeleteV1>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
final value = SyncAlbumDeleteV1.fromJson(entry.value);
|
||||||
|
if (value != null) {
|
||||||
|
map[entry.key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// maps a json object with a list of SyncAlbumDeleteV1-objects as value to a dart map
|
||||||
|
static Map<String, List<SyncAlbumDeleteV1>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final map = <String, List<SyncAlbumDeleteV1>>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
json = json.cast<String, dynamic>();
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
map[entry.key] = SyncAlbumDeleteV1.listFromJson(entry.value, growable: growable,);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The list of required keys that must be present in a JSON.
|
||||||
|
static const requiredKeys = <String>{
|
||||||
|
'albumId',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
107
mobile/openapi/lib/model/sync_album_user_delete_v1.dart
generated
Normal file
107
mobile/openapi/lib/model/sync_album_user_delete_v1.dart
generated
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
//
|
||||||
|
// 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 SyncAlbumUserDeleteV1 {
|
||||||
|
/// Returns a new [SyncAlbumUserDeleteV1] instance.
|
||||||
|
SyncAlbumUserDeleteV1({
|
||||||
|
required this.albumId,
|
||||||
|
required this.userId,
|
||||||
|
});
|
||||||
|
|
||||||
|
String albumId;
|
||||||
|
|
||||||
|
String userId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => identical(this, other) || other is SyncAlbumUserDeleteV1 &&
|
||||||
|
other.albumId == albumId &&
|
||||||
|
other.userId == userId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
// ignore: unnecessary_parenthesis
|
||||||
|
(albumId.hashCode) +
|
||||||
|
(userId.hashCode);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'SyncAlbumUserDeleteV1[albumId=$albumId, userId=$userId]';
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final json = <String, dynamic>{};
|
||||||
|
json[r'albumId'] = this.albumId;
|
||||||
|
json[r'userId'] = this.userId;
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new [SyncAlbumUserDeleteV1] instance and imports its values from
|
||||||
|
/// [value] if it's a [Map], null otherwise.
|
||||||
|
// ignore: prefer_constructors_over_static_methods
|
||||||
|
static SyncAlbumUserDeleteV1? fromJson(dynamic value) {
|
||||||
|
upgradeDto(value, "SyncAlbumUserDeleteV1");
|
||||||
|
if (value is Map) {
|
||||||
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
|
return SyncAlbumUserDeleteV1(
|
||||||
|
albumId: mapValueOfType<String>(json, r'albumId')!,
|
||||||
|
userId: mapValueOfType<String>(json, r'userId')!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<SyncAlbumUserDeleteV1> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <SyncAlbumUserDeleteV1>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = SyncAlbumUserDeleteV1.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, SyncAlbumUserDeleteV1> mapFromJson(dynamic json) {
|
||||||
|
final map = <String, SyncAlbumUserDeleteV1>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
final value = SyncAlbumUserDeleteV1.fromJson(entry.value);
|
||||||
|
if (value != null) {
|
||||||
|
map[entry.key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// maps a json object with a list of SyncAlbumUserDeleteV1-objects as value to a dart map
|
||||||
|
static Map<String, List<SyncAlbumUserDeleteV1>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final map = <String, List<SyncAlbumUserDeleteV1>>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
json = json.cast<String, dynamic>();
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
map[entry.key] = SyncAlbumUserDeleteV1.listFromJson(entry.value, growable: growable,);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The list of required keys that must be present in a JSON.
|
||||||
|
static const requiredKeys = <String>{
|
||||||
|
'albumId',
|
||||||
|
'userId',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
189
mobile/openapi/lib/model/sync_album_user_v1.dart
generated
Normal file
189
mobile/openapi/lib/model/sync_album_user_v1.dart
generated
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
//
|
||||||
|
// 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 SyncAlbumUserV1 {
|
||||||
|
/// Returns a new [SyncAlbumUserV1] instance.
|
||||||
|
SyncAlbumUserV1({
|
||||||
|
required this.albumId,
|
||||||
|
required this.role,
|
||||||
|
required this.userId,
|
||||||
|
});
|
||||||
|
|
||||||
|
String albumId;
|
||||||
|
|
||||||
|
SyncAlbumUserV1RoleEnum role;
|
||||||
|
|
||||||
|
String userId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => identical(this, other) || other is SyncAlbumUserV1 &&
|
||||||
|
other.albumId == albumId &&
|
||||||
|
other.role == role &&
|
||||||
|
other.userId == userId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
// ignore: unnecessary_parenthesis
|
||||||
|
(albumId.hashCode) +
|
||||||
|
(role.hashCode) +
|
||||||
|
(userId.hashCode);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'SyncAlbumUserV1[albumId=$albumId, role=$role, userId=$userId]';
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final json = <String, dynamic>{};
|
||||||
|
json[r'albumId'] = this.albumId;
|
||||||
|
json[r'role'] = this.role;
|
||||||
|
json[r'userId'] = this.userId;
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new [SyncAlbumUserV1] instance and imports its values from
|
||||||
|
/// [value] if it's a [Map], null otherwise.
|
||||||
|
// ignore: prefer_constructors_over_static_methods
|
||||||
|
static SyncAlbumUserV1? fromJson(dynamic value) {
|
||||||
|
upgradeDto(value, "SyncAlbumUserV1");
|
||||||
|
if (value is Map) {
|
||||||
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
|
return SyncAlbumUserV1(
|
||||||
|
albumId: mapValueOfType<String>(json, r'albumId')!,
|
||||||
|
role: SyncAlbumUserV1RoleEnum.fromJson(json[r'role'])!,
|
||||||
|
userId: mapValueOfType<String>(json, r'userId')!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<SyncAlbumUserV1> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <SyncAlbumUserV1>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = SyncAlbumUserV1.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, SyncAlbumUserV1> mapFromJson(dynamic json) {
|
||||||
|
final map = <String, SyncAlbumUserV1>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
final value = SyncAlbumUserV1.fromJson(entry.value);
|
||||||
|
if (value != null) {
|
||||||
|
map[entry.key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// maps a json object with a list of SyncAlbumUserV1-objects as value to a dart map
|
||||||
|
static Map<String, List<SyncAlbumUserV1>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final map = <String, List<SyncAlbumUserV1>>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
json = json.cast<String, dynamic>();
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
map[entry.key] = SyncAlbumUserV1.listFromJson(entry.value, growable: growable,);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The list of required keys that must be present in a JSON.
|
||||||
|
static const requiredKeys = <String>{
|
||||||
|
'albumId',
|
||||||
|
'role',
|
||||||
|
'userId',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class SyncAlbumUserV1RoleEnum {
|
||||||
|
/// Instantiate a new enum with the provided [value].
|
||||||
|
const SyncAlbumUserV1RoleEnum._(this.value);
|
||||||
|
|
||||||
|
/// The underlying value of this enum member.
|
||||||
|
final String value;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => value;
|
||||||
|
|
||||||
|
String toJson() => value;
|
||||||
|
|
||||||
|
static const editor = SyncAlbumUserV1RoleEnum._(r'editor');
|
||||||
|
static const viewer = SyncAlbumUserV1RoleEnum._(r'viewer');
|
||||||
|
|
||||||
|
/// List of all possible values in this [enum][SyncAlbumUserV1RoleEnum].
|
||||||
|
static const values = <SyncAlbumUserV1RoleEnum>[
|
||||||
|
editor,
|
||||||
|
viewer,
|
||||||
|
];
|
||||||
|
|
||||||
|
static SyncAlbumUserV1RoleEnum? fromJson(dynamic value) => SyncAlbumUserV1RoleEnumTypeTransformer().decode(value);
|
||||||
|
|
||||||
|
static List<SyncAlbumUserV1RoleEnum> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <SyncAlbumUserV1RoleEnum>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = SyncAlbumUserV1RoleEnum.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transformation class that can [encode] an instance of [SyncAlbumUserV1RoleEnum] to String,
|
||||||
|
/// and [decode] dynamic data back to [SyncAlbumUserV1RoleEnum].
|
||||||
|
class SyncAlbumUserV1RoleEnumTypeTransformer {
|
||||||
|
factory SyncAlbumUserV1RoleEnumTypeTransformer() => _instance ??= const SyncAlbumUserV1RoleEnumTypeTransformer._();
|
||||||
|
|
||||||
|
const SyncAlbumUserV1RoleEnumTypeTransformer._();
|
||||||
|
|
||||||
|
String encode(SyncAlbumUserV1RoleEnum data) => data.value;
|
||||||
|
|
||||||
|
/// Decodes a [dynamic value][data] to a SyncAlbumUserV1RoleEnum.
|
||||||
|
///
|
||||||
|
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
|
||||||
|
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
|
||||||
|
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
|
||||||
|
///
|
||||||
|
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
|
||||||
|
/// and users are still using an old app with the old code.
|
||||||
|
SyncAlbumUserV1RoleEnum? decode(dynamic data, {bool allowNull = true}) {
|
||||||
|
if (data != null) {
|
||||||
|
switch (data) {
|
||||||
|
case r'editor': return SyncAlbumUserV1RoleEnum.editor;
|
||||||
|
case r'viewer': return SyncAlbumUserV1RoleEnum.viewer;
|
||||||
|
default:
|
||||||
|
if (!allowNull) {
|
||||||
|
throw ArgumentError('Unknown enum value to decode: $data');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Singleton [SyncAlbumUserV1RoleEnumTypeTransformer] instance.
|
||||||
|
static SyncAlbumUserV1RoleEnumTypeTransformer? _instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
167
mobile/openapi/lib/model/sync_album_v1.dart
generated
Normal file
167
mobile/openapi/lib/model/sync_album_v1.dart
generated
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
//
|
||||||
|
// 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 SyncAlbumV1 {
|
||||||
|
/// Returns a new [SyncAlbumV1] instance.
|
||||||
|
SyncAlbumV1({
|
||||||
|
required this.createdAt,
|
||||||
|
required this.description,
|
||||||
|
required this.id,
|
||||||
|
required this.isActivityEnabled,
|
||||||
|
required this.name,
|
||||||
|
required this.order,
|
||||||
|
required this.ownerId,
|
||||||
|
required this.thumbnailAssetId,
|
||||||
|
required this.updatedAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
DateTime createdAt;
|
||||||
|
|
||||||
|
String description;
|
||||||
|
|
||||||
|
String id;
|
||||||
|
|
||||||
|
bool isActivityEnabled;
|
||||||
|
|
||||||
|
String name;
|
||||||
|
|
||||||
|
AssetOrder order;
|
||||||
|
|
||||||
|
String ownerId;
|
||||||
|
|
||||||
|
String? thumbnailAssetId;
|
||||||
|
|
||||||
|
DateTime updatedAt;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => identical(this, other) || other is SyncAlbumV1 &&
|
||||||
|
other.createdAt == createdAt &&
|
||||||
|
other.description == description &&
|
||||||
|
other.id == id &&
|
||||||
|
other.isActivityEnabled == isActivityEnabled &&
|
||||||
|
other.name == name &&
|
||||||
|
other.order == order &&
|
||||||
|
other.ownerId == ownerId &&
|
||||||
|
other.thumbnailAssetId == thumbnailAssetId &&
|
||||||
|
other.updatedAt == updatedAt;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
// ignore: unnecessary_parenthesis
|
||||||
|
(createdAt.hashCode) +
|
||||||
|
(description.hashCode) +
|
||||||
|
(id.hashCode) +
|
||||||
|
(isActivityEnabled.hashCode) +
|
||||||
|
(name.hashCode) +
|
||||||
|
(order.hashCode) +
|
||||||
|
(ownerId.hashCode) +
|
||||||
|
(thumbnailAssetId == null ? 0 : thumbnailAssetId!.hashCode) +
|
||||||
|
(updatedAt.hashCode);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'SyncAlbumV1[createdAt=$createdAt, description=$description, id=$id, isActivityEnabled=$isActivityEnabled, name=$name, order=$order, ownerId=$ownerId, thumbnailAssetId=$thumbnailAssetId, updatedAt=$updatedAt]';
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final json = <String, dynamic>{};
|
||||||
|
json[r'createdAt'] = this.createdAt.toUtc().toIso8601String();
|
||||||
|
json[r'description'] = this.description;
|
||||||
|
json[r'id'] = this.id;
|
||||||
|
json[r'isActivityEnabled'] = this.isActivityEnabled;
|
||||||
|
json[r'name'] = this.name;
|
||||||
|
json[r'order'] = this.order;
|
||||||
|
json[r'ownerId'] = this.ownerId;
|
||||||
|
if (this.thumbnailAssetId != null) {
|
||||||
|
json[r'thumbnailAssetId'] = this.thumbnailAssetId;
|
||||||
|
} else {
|
||||||
|
// json[r'thumbnailAssetId'] = null;
|
||||||
|
}
|
||||||
|
json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String();
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new [SyncAlbumV1] instance and imports its values from
|
||||||
|
/// [value] if it's a [Map], null otherwise.
|
||||||
|
// ignore: prefer_constructors_over_static_methods
|
||||||
|
static SyncAlbumV1? fromJson(dynamic value) {
|
||||||
|
upgradeDto(value, "SyncAlbumV1");
|
||||||
|
if (value is Map) {
|
||||||
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
|
return SyncAlbumV1(
|
||||||
|
createdAt: mapDateTime(json, r'createdAt', r'')!,
|
||||||
|
description: mapValueOfType<String>(json, r'description')!,
|
||||||
|
id: mapValueOfType<String>(json, r'id')!,
|
||||||
|
isActivityEnabled: mapValueOfType<bool>(json, r'isActivityEnabled')!,
|
||||||
|
name: mapValueOfType<String>(json, r'name')!,
|
||||||
|
order: AssetOrder.fromJson(json[r'order'])!,
|
||||||
|
ownerId: mapValueOfType<String>(json, r'ownerId')!,
|
||||||
|
thumbnailAssetId: mapValueOfType<String>(json, r'thumbnailAssetId'),
|
||||||
|
updatedAt: mapDateTime(json, r'updatedAt', r'')!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<SyncAlbumV1> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <SyncAlbumV1>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = SyncAlbumV1.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, SyncAlbumV1> mapFromJson(dynamic json) {
|
||||||
|
final map = <String, SyncAlbumV1>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
final value = SyncAlbumV1.fromJson(entry.value);
|
||||||
|
if (value != null) {
|
||||||
|
map[entry.key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// maps a json object with a list of SyncAlbumV1-objects as value to a dart map
|
||||||
|
static Map<String, List<SyncAlbumV1>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final map = <String, List<SyncAlbumV1>>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
json = json.cast<String, dynamic>();
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
map[entry.key] = SyncAlbumV1.listFromJson(entry.value, growable: growable,);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The list of required keys that must be present in a JSON.
|
||||||
|
static const requiredKeys = <String>{
|
||||||
|
'createdAt',
|
||||||
|
'description',
|
||||||
|
'id',
|
||||||
|
'isActivityEnabled',
|
||||||
|
'name',
|
||||||
|
'order',
|
||||||
|
'ownerId',
|
||||||
|
'thumbnailAssetId',
|
||||||
|
'updatedAt',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
12
mobile/openapi/lib/model/sync_entity_type.dart
generated
12
mobile/openapi/lib/model/sync_entity_type.dart
generated
@ -33,6 +33,10 @@ class SyncEntityType {
|
|||||||
static const partnerAssetV1 = SyncEntityType._(r'PartnerAssetV1');
|
static const partnerAssetV1 = SyncEntityType._(r'PartnerAssetV1');
|
||||||
static const partnerAssetDeleteV1 = SyncEntityType._(r'PartnerAssetDeleteV1');
|
static const partnerAssetDeleteV1 = SyncEntityType._(r'PartnerAssetDeleteV1');
|
||||||
static const partnerAssetExifV1 = SyncEntityType._(r'PartnerAssetExifV1');
|
static const partnerAssetExifV1 = SyncEntityType._(r'PartnerAssetExifV1');
|
||||||
|
static const albumV1 = SyncEntityType._(r'AlbumV1');
|
||||||
|
static const albumDeleteV1 = SyncEntityType._(r'AlbumDeleteV1');
|
||||||
|
static const albumUserV1 = SyncEntityType._(r'AlbumUserV1');
|
||||||
|
static const albumUserDeleteV1 = SyncEntityType._(r'AlbumUserDeleteV1');
|
||||||
|
|
||||||
/// List of all possible values in this [enum][SyncEntityType].
|
/// List of all possible values in this [enum][SyncEntityType].
|
||||||
static const values = <SyncEntityType>[
|
static const values = <SyncEntityType>[
|
||||||
@ -46,6 +50,10 @@ class SyncEntityType {
|
|||||||
partnerAssetV1,
|
partnerAssetV1,
|
||||||
partnerAssetDeleteV1,
|
partnerAssetDeleteV1,
|
||||||
partnerAssetExifV1,
|
partnerAssetExifV1,
|
||||||
|
albumV1,
|
||||||
|
albumDeleteV1,
|
||||||
|
albumUserV1,
|
||||||
|
albumUserDeleteV1,
|
||||||
];
|
];
|
||||||
|
|
||||||
static SyncEntityType? fromJson(dynamic value) => SyncEntityTypeTypeTransformer().decode(value);
|
static SyncEntityType? fromJson(dynamic value) => SyncEntityTypeTypeTransformer().decode(value);
|
||||||
@ -94,6 +102,10 @@ class SyncEntityTypeTypeTransformer {
|
|||||||
case r'PartnerAssetV1': return SyncEntityType.partnerAssetV1;
|
case r'PartnerAssetV1': return SyncEntityType.partnerAssetV1;
|
||||||
case r'PartnerAssetDeleteV1': return SyncEntityType.partnerAssetDeleteV1;
|
case r'PartnerAssetDeleteV1': return SyncEntityType.partnerAssetDeleteV1;
|
||||||
case r'PartnerAssetExifV1': return SyncEntityType.partnerAssetExifV1;
|
case r'PartnerAssetExifV1': return SyncEntityType.partnerAssetExifV1;
|
||||||
|
case r'AlbumV1': return SyncEntityType.albumV1;
|
||||||
|
case r'AlbumDeleteV1': return SyncEntityType.albumDeleteV1;
|
||||||
|
case r'AlbumUserV1': return SyncEntityType.albumUserV1;
|
||||||
|
case r'AlbumUserDeleteV1': return SyncEntityType.albumUserDeleteV1;
|
||||||
default:
|
default:
|
||||||
if (!allowNull) {
|
if (!allowNull) {
|
||||||
throw ArgumentError('Unknown enum value to decode: $data');
|
throw ArgumentError('Unknown enum value to decode: $data');
|
||||||
|
6
mobile/openapi/lib/model/sync_request_type.dart
generated
6
mobile/openapi/lib/model/sync_request_type.dart
generated
@ -29,6 +29,8 @@ class SyncRequestType {
|
|||||||
static const assetExifsV1 = SyncRequestType._(r'AssetExifsV1');
|
static const assetExifsV1 = SyncRequestType._(r'AssetExifsV1');
|
||||||
static const partnerAssetsV1 = SyncRequestType._(r'PartnerAssetsV1');
|
static const partnerAssetsV1 = SyncRequestType._(r'PartnerAssetsV1');
|
||||||
static const partnerAssetExifsV1 = SyncRequestType._(r'PartnerAssetExifsV1');
|
static const partnerAssetExifsV1 = SyncRequestType._(r'PartnerAssetExifsV1');
|
||||||
|
static const albumsV1 = SyncRequestType._(r'AlbumsV1');
|
||||||
|
static const albumUsersV1 = SyncRequestType._(r'AlbumUsersV1');
|
||||||
|
|
||||||
/// List of all possible values in this [enum][SyncRequestType].
|
/// List of all possible values in this [enum][SyncRequestType].
|
||||||
static const values = <SyncRequestType>[
|
static const values = <SyncRequestType>[
|
||||||
@ -38,6 +40,8 @@ class SyncRequestType {
|
|||||||
assetExifsV1,
|
assetExifsV1,
|
||||||
partnerAssetsV1,
|
partnerAssetsV1,
|
||||||
partnerAssetExifsV1,
|
partnerAssetExifsV1,
|
||||||
|
albumsV1,
|
||||||
|
albumUsersV1,
|
||||||
];
|
];
|
||||||
|
|
||||||
static SyncRequestType? fromJson(dynamic value) => SyncRequestTypeTypeTransformer().decode(value);
|
static SyncRequestType? fromJson(dynamic value) => SyncRequestTypeTypeTransformer().decode(value);
|
||||||
@ -82,6 +86,8 @@ class SyncRequestTypeTypeTransformer {
|
|||||||
case r'AssetExifsV1': return SyncRequestType.assetExifsV1;
|
case r'AssetExifsV1': return SyncRequestType.assetExifsV1;
|
||||||
case r'PartnerAssetsV1': return SyncRequestType.partnerAssetsV1;
|
case r'PartnerAssetsV1': return SyncRequestType.partnerAssetsV1;
|
||||||
case r'PartnerAssetExifsV1': return SyncRequestType.partnerAssetExifsV1;
|
case r'PartnerAssetExifsV1': return SyncRequestType.partnerAssetExifsV1;
|
||||||
|
case r'AlbumsV1': return SyncRequestType.albumsV1;
|
||||||
|
case r'AlbumUsersV1': return SyncRequestType.albumUsersV1;
|
||||||
default:
|
default:
|
||||||
if (!allowNull) {
|
if (!allowNull) {
|
||||||
throw ArgumentError('Unknown enum value to decode: $data');
|
throw ArgumentError('Unknown enum value to decode: $data');
|
||||||
|
@ -12710,6 +12710,105 @@
|
|||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"SyncAlbumDeleteV1": {
|
||||||
|
"properties": {
|
||||||
|
"albumId": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"albumId"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"SyncAlbumUserDeleteV1": {
|
||||||
|
"properties": {
|
||||||
|
"albumId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"userId": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"albumId",
|
||||||
|
"userId"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"SyncAlbumUserV1": {
|
||||||
|
"properties": {
|
||||||
|
"albumId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"enum": [
|
||||||
|
"editor",
|
||||||
|
"viewer"
|
||||||
|
],
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"userId": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"albumId",
|
||||||
|
"role",
|
||||||
|
"userId"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"SyncAlbumV1": {
|
||||||
|
"properties": {
|
||||||
|
"createdAt": {
|
||||||
|
"format": "date-time",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"isActivityEnabled": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"order": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/AssetOrder"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"ownerId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"thumbnailAssetId": {
|
||||||
|
"nullable": true,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"updatedAt": {
|
||||||
|
"format": "date-time",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"createdAt",
|
||||||
|
"description",
|
||||||
|
"id",
|
||||||
|
"isActivityEnabled",
|
||||||
|
"name",
|
||||||
|
"order",
|
||||||
|
"ownerId",
|
||||||
|
"thumbnailAssetId",
|
||||||
|
"updatedAt"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"SyncAssetDeleteV1": {
|
"SyncAssetDeleteV1": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"assetId": {
|
"assetId": {
|
||||||
@ -12937,7 +13036,11 @@
|
|||||||
"AssetExifV1",
|
"AssetExifV1",
|
||||||
"PartnerAssetV1",
|
"PartnerAssetV1",
|
||||||
"PartnerAssetDeleteV1",
|
"PartnerAssetDeleteV1",
|
||||||
"PartnerAssetExifV1"
|
"PartnerAssetExifV1",
|
||||||
|
"AlbumV1",
|
||||||
|
"AlbumDeleteV1",
|
||||||
|
"AlbumUserV1",
|
||||||
|
"AlbumUserDeleteV1"
|
||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@ -12982,7 +13085,9 @@
|
|||||||
"AssetsV1",
|
"AssetsV1",
|
||||||
"AssetExifsV1",
|
"AssetExifsV1",
|
||||||
"PartnerAssetsV1",
|
"PartnerAssetsV1",
|
||||||
"PartnerAssetExifsV1"
|
"PartnerAssetExifsV1",
|
||||||
|
"AlbumsV1",
|
||||||
|
"AlbumUsersV1"
|
||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
@ -3860,7 +3860,11 @@ export enum SyncEntityType {
|
|||||||
AssetExifV1 = "AssetExifV1",
|
AssetExifV1 = "AssetExifV1",
|
||||||
PartnerAssetV1 = "PartnerAssetV1",
|
PartnerAssetV1 = "PartnerAssetV1",
|
||||||
PartnerAssetDeleteV1 = "PartnerAssetDeleteV1",
|
PartnerAssetDeleteV1 = "PartnerAssetDeleteV1",
|
||||||
PartnerAssetExifV1 = "PartnerAssetExifV1"
|
PartnerAssetExifV1 = "PartnerAssetExifV1",
|
||||||
|
AlbumV1 = "AlbumV1",
|
||||||
|
AlbumDeleteV1 = "AlbumDeleteV1",
|
||||||
|
AlbumUserV1 = "AlbumUserV1",
|
||||||
|
AlbumUserDeleteV1 = "AlbumUserDeleteV1"
|
||||||
}
|
}
|
||||||
export enum SyncRequestType {
|
export enum SyncRequestType {
|
||||||
UsersV1 = "UsersV1",
|
UsersV1 = "UsersV1",
|
||||||
@ -3868,7 +3872,9 @@ export enum SyncRequestType {
|
|||||||
AssetsV1 = "AssetsV1",
|
AssetsV1 = "AssetsV1",
|
||||||
AssetExifsV1 = "AssetExifsV1",
|
AssetExifsV1 = "AssetExifsV1",
|
||||||
PartnerAssetsV1 = "PartnerAssetsV1",
|
PartnerAssetsV1 = "PartnerAssetsV1",
|
||||||
PartnerAssetExifsV1 = "PartnerAssetExifsV1"
|
PartnerAssetExifsV1 = "PartnerAssetExifsV1",
|
||||||
|
AlbumsV1 = "AlbumsV1",
|
||||||
|
AlbumUsersV1 = "AlbumUsersV1"
|
||||||
}
|
}
|
||||||
export enum TranscodeHWAccel {
|
export enum TranscodeHWAccel {
|
||||||
Nvenc = "nvenc",
|
Nvenc = "nvenc",
|
||||||
|
@ -23,6 +23,7 @@
|
|||||||
"test:medium": "vitest --config test/vitest.config.medium.mjs",
|
"test:medium": "vitest --config test/vitest.config.medium.mjs",
|
||||||
"typeorm": "typeorm",
|
"typeorm": "typeorm",
|
||||||
"lifecycle": "node ./dist/utils/lifecycle.js",
|
"lifecycle": "node ./dist/utils/lifecycle.js",
|
||||||
|
"migrations:debug": "node ./dist/bin/migrations.js debug",
|
||||||
"migrations:generate": "node ./dist/bin/migrations.js generate",
|
"migrations:generate": "node ./dist/bin/migrations.js generate",
|
||||||
"migrations:create": "node ./dist/bin/migrations.js create",
|
"migrations:create": "node ./dist/bin/migrations.js create",
|
||||||
"migrations:run": "node ./dist/bin/migrations.js run",
|
"migrations:run": "node ./dist/bin/migrations.js run",
|
||||||
|
@ -125,6 +125,7 @@ const compare = async () => {
|
|||||||
const down = schemaDiff(target, source, {
|
const down = schemaDiff(target, source, {
|
||||||
tables: { ignoreExtra: false },
|
tables: { ignoreExtra: false },
|
||||||
functions: { ignoreExtra: false },
|
functions: { ignoreExtra: false },
|
||||||
|
extension: { ignoreMissing: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
return { up, down };
|
return { up, down };
|
||||||
|
18
server/src/db.d.ts
vendored
18
server/src/db.d.ts
vendored
@ -74,6 +74,20 @@ export interface Albums {
|
|||||||
updateId: Generated<string>;
|
updateId: Generated<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AlbumsAudit {
|
||||||
|
deletedAt: Generated<Timestamp>;
|
||||||
|
id: Generated<string>;
|
||||||
|
albumId: string;
|
||||||
|
userId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AlbumUsersAudit {
|
||||||
|
deletedAt: Generated<Timestamp>;
|
||||||
|
id: Generated<string>;
|
||||||
|
albumId: string;
|
||||||
|
userId: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface AlbumsAssetsAssets {
|
export interface AlbumsAssetsAssets {
|
||||||
albumsId: string;
|
albumsId: string;
|
||||||
assetsId: string;
|
assetsId: string;
|
||||||
@ -84,6 +98,8 @@ export interface AlbumsSharedUsersUsers {
|
|||||||
albumsId: string;
|
albumsId: string;
|
||||||
role: Generated<AlbumUserRole>;
|
role: Generated<AlbumUserRole>;
|
||||||
usersId: string;
|
usersId: string;
|
||||||
|
updatedAt: Generated<Timestamp>;
|
||||||
|
updateId: Generated<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApiKeys {
|
export interface ApiKeys {
|
||||||
@ -466,8 +482,10 @@ export interface VersionHistory {
|
|||||||
export interface DB {
|
export interface DB {
|
||||||
activity: Activity;
|
activity: Activity;
|
||||||
albums: Albums;
|
albums: Albums;
|
||||||
|
albums_audit: AlbumsAudit;
|
||||||
albums_assets_assets: AlbumsAssetsAssets;
|
albums_assets_assets: AlbumsAssetsAssets;
|
||||||
albums_shared_users_users: AlbumsSharedUsersUsers;
|
albums_shared_users_users: AlbumsSharedUsersUsers;
|
||||||
|
album_users_audit: AlbumUsersAudit;
|
||||||
api_keys: ApiKeys;
|
api_keys: ApiKeys;
|
||||||
asset_faces: AssetFaces;
|
asset_faces: AssetFaces;
|
||||||
asset_files: AssetFiles;
|
asset_files: AssetFiles;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { IsEnum, IsInt, IsPositive, IsString } from 'class-validator';
|
import { IsEnum, IsInt, IsPositive, IsString } from 'class-validator';
|
||||||
import { AssetResponseDto } from 'src/dtos/asset-response.dto';
|
import { AssetResponseDto } from 'src/dtos/asset-response.dto';
|
||||||
import { AssetType, AssetVisibility, SyncEntityType, SyncRequestType } from 'src/enum';
|
import { AlbumUserRole, AssetOrder, AssetType, AssetVisibility, SyncEntityType, SyncRequestType } from 'src/enum';
|
||||||
import { Optional, ValidateDate, ValidateUUID } from 'src/validation';
|
import { Optional, ValidateDate, ValidateUUID } from 'src/validation';
|
||||||
|
|
||||||
export class AssetFullSyncDto {
|
export class AssetFullSyncDto {
|
||||||
@ -112,6 +112,34 @@ export class SyncAssetExifV1 {
|
|||||||
fps!: number | null;
|
fps!: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class SyncAlbumDeleteV1 {
|
||||||
|
albumId!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SyncAlbumUserDeleteV1 {
|
||||||
|
albumId!: string;
|
||||||
|
userId!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SyncAlbumUserV1 {
|
||||||
|
albumId!: string;
|
||||||
|
userId!: string;
|
||||||
|
role!: AlbumUserRole;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SyncAlbumV1 {
|
||||||
|
id!: string;
|
||||||
|
ownerId!: string;
|
||||||
|
name!: string;
|
||||||
|
description!: string;
|
||||||
|
createdAt!: Date;
|
||||||
|
updatedAt!: Date;
|
||||||
|
thumbnailAssetId!: string | null;
|
||||||
|
isActivityEnabled!: boolean;
|
||||||
|
@ApiProperty({ enumName: 'AssetOrder', enum: AssetOrder })
|
||||||
|
order!: AssetOrder;
|
||||||
|
}
|
||||||
|
|
||||||
export type SyncItem = {
|
export type SyncItem = {
|
||||||
[SyncEntityType.UserV1]: SyncUserV1;
|
[SyncEntityType.UserV1]: SyncUserV1;
|
||||||
[SyncEntityType.UserDeleteV1]: SyncUserDeleteV1;
|
[SyncEntityType.UserDeleteV1]: SyncUserDeleteV1;
|
||||||
@ -123,10 +151,13 @@ export type SyncItem = {
|
|||||||
[SyncEntityType.PartnerAssetV1]: SyncAssetV1;
|
[SyncEntityType.PartnerAssetV1]: SyncAssetV1;
|
||||||
[SyncEntityType.PartnerAssetDeleteV1]: SyncAssetDeleteV1;
|
[SyncEntityType.PartnerAssetDeleteV1]: SyncAssetDeleteV1;
|
||||||
[SyncEntityType.PartnerAssetExifV1]: SyncAssetExifV1;
|
[SyncEntityType.PartnerAssetExifV1]: SyncAssetExifV1;
|
||||||
|
[SyncEntityType.AlbumV1]: SyncAlbumV1;
|
||||||
|
[SyncEntityType.AlbumDeleteV1]: SyncAlbumDeleteV1;
|
||||||
|
[SyncEntityType.AlbumUserV1]: SyncAlbumUserV1;
|
||||||
|
[SyncEntityType.AlbumUserDeleteV1]: SyncAlbumUserDeleteV1;
|
||||||
};
|
};
|
||||||
|
|
||||||
const responseDtos = [
|
const responseDtos = [
|
||||||
//
|
|
||||||
SyncUserV1,
|
SyncUserV1,
|
||||||
SyncUserDeleteV1,
|
SyncUserDeleteV1,
|
||||||
SyncPartnerV1,
|
SyncPartnerV1,
|
||||||
@ -134,6 +165,10 @@ const responseDtos = [
|
|||||||
SyncAssetV1,
|
SyncAssetV1,
|
||||||
SyncAssetDeleteV1,
|
SyncAssetDeleteV1,
|
||||||
SyncAssetExifV1,
|
SyncAssetExifV1,
|
||||||
|
SyncAlbumV1,
|
||||||
|
SyncAlbumDeleteV1,
|
||||||
|
SyncAlbumUserV1,
|
||||||
|
SyncAlbumUserDeleteV1,
|
||||||
];
|
];
|
||||||
|
|
||||||
export const extraSyncModels = responseDtos;
|
export const extraSyncModels = responseDtos;
|
||||||
|
@ -578,6 +578,8 @@ export enum SyncRequestType {
|
|||||||
AssetExifsV1 = 'AssetExifsV1',
|
AssetExifsV1 = 'AssetExifsV1',
|
||||||
PartnerAssetsV1 = 'PartnerAssetsV1',
|
PartnerAssetsV1 = 'PartnerAssetsV1',
|
||||||
PartnerAssetExifsV1 = 'PartnerAssetExifsV1',
|
PartnerAssetExifsV1 = 'PartnerAssetExifsV1',
|
||||||
|
AlbumsV1 = 'AlbumsV1',
|
||||||
|
AlbumUsersV1 = 'AlbumUsersV1',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum SyncEntityType {
|
export enum SyncEntityType {
|
||||||
@ -594,6 +596,11 @@ export enum SyncEntityType {
|
|||||||
PartnerAssetV1 = 'PartnerAssetV1',
|
PartnerAssetV1 = 'PartnerAssetV1',
|
||||||
PartnerAssetDeleteV1 = 'PartnerAssetDeleteV1',
|
PartnerAssetDeleteV1 = 'PartnerAssetDeleteV1',
|
||||||
PartnerAssetExifV1 = 'PartnerAssetExifV1',
|
PartnerAssetExifV1 = 'PartnerAssetExifV1',
|
||||||
|
|
||||||
|
AlbumV1 = 'AlbumV1',
|
||||||
|
AlbumDeleteV1 = 'AlbumDeleteV1',
|
||||||
|
AlbumUserV1 = 'AlbumUserV1',
|
||||||
|
AlbumUserDeleteV1 = 'AlbumUserDeleteV1',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum NotificationLevel {
|
export enum NotificationLevel {
|
||||||
|
@ -6,7 +6,9 @@ insert into
|
|||||||
values
|
values
|
||||||
($1, $2)
|
($1, $2)
|
||||||
returning
|
returning
|
||||||
*
|
"usersId",
|
||||||
|
"albumsId",
|
||||||
|
"role"
|
||||||
|
|
||||||
-- AlbumUserRepository.update
|
-- AlbumUserRepository.update
|
||||||
update "albums_shared_users_users"
|
update "albums_shared_users_users"
|
||||||
|
@ -246,3 +246,98 @@ where
|
|||||||
and "updatedAt" < now() - interval '1 millisecond'
|
and "updatedAt" < now() - interval '1 millisecond'
|
||||||
order by
|
order by
|
||||||
"updateId" asc
|
"updateId" asc
|
||||||
|
|
||||||
|
-- SyncRepository.getAlbumDeletes
|
||||||
|
select
|
||||||
|
"id",
|
||||||
|
"albumId"
|
||||||
|
from
|
||||||
|
"albums_audit"
|
||||||
|
where
|
||||||
|
"userId" = $1
|
||||||
|
and "deletedAt" < now() - interval '1 millisecond'
|
||||||
|
order by
|
||||||
|
"id" asc
|
||||||
|
|
||||||
|
-- SyncRepository.getAlbumUpserts
|
||||||
|
select distinct
|
||||||
|
on ("albums"."id", "albums"."updateId") "albums"."id",
|
||||||
|
"albums"."ownerId",
|
||||||
|
"albums"."albumName" as "name",
|
||||||
|
"albums"."description",
|
||||||
|
"albums"."createdAt",
|
||||||
|
"albums"."updatedAt",
|
||||||
|
"albums"."albumThumbnailAssetId" as "thumbnailAssetId",
|
||||||
|
"albums"."isActivityEnabled",
|
||||||
|
"albums"."order",
|
||||||
|
"albums"."updateId"
|
||||||
|
from
|
||||||
|
"albums"
|
||||||
|
left join "albums_shared_users_users" as "album_users" on "albums"."id" = "album_users"."albumsId"
|
||||||
|
where
|
||||||
|
"albums"."updatedAt" < now() - interval '1 millisecond'
|
||||||
|
and (
|
||||||
|
"albums"."ownerId" = $1
|
||||||
|
or "album_users"."usersId" = $2
|
||||||
|
)
|
||||||
|
order by
|
||||||
|
"albums"."updateId" asc
|
||||||
|
|
||||||
|
-- SyncRepository.getAlbumUserDeletes
|
||||||
|
select
|
||||||
|
"id",
|
||||||
|
"userId",
|
||||||
|
"albumId"
|
||||||
|
from
|
||||||
|
"album_users_audit"
|
||||||
|
where
|
||||||
|
"albumId" in (
|
||||||
|
select
|
||||||
|
"id"
|
||||||
|
from
|
||||||
|
"albums"
|
||||||
|
where
|
||||||
|
"ownerId" = $1
|
||||||
|
union
|
||||||
|
(
|
||||||
|
select
|
||||||
|
"albumUsers"."albumsId" as "id"
|
||||||
|
from
|
||||||
|
"albums_shared_users_users" as "albumUsers"
|
||||||
|
where
|
||||||
|
"albumUsers"."usersId" = $2
|
||||||
|
)
|
||||||
|
)
|
||||||
|
and "deletedAt" < now() - interval '1 millisecond'
|
||||||
|
order by
|
||||||
|
"id" asc
|
||||||
|
|
||||||
|
-- SyncRepository.getAlbumUserUpserts
|
||||||
|
select
|
||||||
|
"albums_shared_users_users"."albumsId" as "albumId",
|
||||||
|
"albums_shared_users_users"."usersId" as "userId",
|
||||||
|
"albums_shared_users_users"."role",
|
||||||
|
"albums_shared_users_users"."updateId"
|
||||||
|
from
|
||||||
|
"albums_shared_users_users"
|
||||||
|
where
|
||||||
|
"albums_shared_users_users"."updatedAt" < now() - interval '1 millisecond'
|
||||||
|
and "albums_shared_users_users"."albumsId" in (
|
||||||
|
select
|
||||||
|
"id"
|
||||||
|
from
|
||||||
|
"albums"
|
||||||
|
where
|
||||||
|
"ownerId" = $1
|
||||||
|
union
|
||||||
|
(
|
||||||
|
select
|
||||||
|
"albumUsers"."albumsId" as "id"
|
||||||
|
from
|
||||||
|
"albums_shared_users_users" as "albumUsers"
|
||||||
|
where
|
||||||
|
"albumUsers"."usersId" = $2
|
||||||
|
)
|
||||||
|
)
|
||||||
|
order by
|
||||||
|
"albums_shared_users_users"."updateId" asc
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Insertable, Kysely, Selectable, Updateable } from 'kysely';
|
import { Insertable, Kysely, Updateable } from 'kysely';
|
||||||
import { InjectKysely } from 'nestjs-kysely';
|
import { InjectKysely } from 'nestjs-kysely';
|
||||||
import { AlbumsSharedUsersUsers, DB } from 'src/db';
|
import { AlbumsSharedUsersUsers, DB } from 'src/db';
|
||||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||||
@ -15,8 +15,12 @@ export class AlbumUserRepository {
|
|||||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||||
|
|
||||||
@GenerateSql({ params: [{ usersId: DummyValue.UUID, albumsId: DummyValue.UUID }] })
|
@GenerateSql({ params: [{ usersId: DummyValue.UUID, albumsId: DummyValue.UUID }] })
|
||||||
create(albumUser: Insertable<AlbumsSharedUsersUsers>): Promise<Selectable<AlbumsSharedUsersUsers>> {
|
create(albumUser: Insertable<AlbumsSharedUsersUsers>) {
|
||||||
return this.db.insertInto('albums_shared_users_users').values(albumUser).returningAll().executeTakeFirstOrThrow();
|
return this.db
|
||||||
|
.insertInto('albums_shared_users_users')
|
||||||
|
.values(albumUser)
|
||||||
|
.returning(['usersId', 'albumsId', 'role'])
|
||||||
|
.executeTakeFirstOrThrow();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [{ usersId: DummyValue.UUID, albumsId: DummyValue.UUID }, { role: AlbumUserRole.VIEWER }] })
|
@GenerateSql({ params: [{ usersId: DummyValue.UUID, albumsId: DummyValue.UUID }, { role: AlbumUserRole.VIEWER }] })
|
||||||
|
@ -7,8 +7,8 @@ import { DummyValue, GenerateSql } from 'src/decorators';
|
|||||||
import { SyncEntityType } from 'src/enum';
|
import { SyncEntityType } from 'src/enum';
|
||||||
import { SyncAck } from 'src/types';
|
import { SyncAck } from 'src/types';
|
||||||
|
|
||||||
type auditTables = 'users_audit' | 'partners_audit' | 'assets_audit';
|
type AuditTables = 'users_audit' | 'partners_audit' | 'assets_audit' | 'albums_audit' | 'album_users_audit';
|
||||||
type upsertTables = 'users' | 'partners' | 'assets' | 'exif';
|
type UpsertTables = 'users' | 'partners' | 'assets' | 'exif' | 'albums' | 'albums_shared_users_users';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SyncRepository {
|
export class SyncRepository {
|
||||||
@ -110,7 +110,6 @@ export class SyncRepository {
|
|||||||
.selectFrom('assets_audit')
|
.selectFrom('assets_audit')
|
||||||
.select(['id', 'assetId'])
|
.select(['id', 'assetId'])
|
||||||
.where('ownerId', '=', userId)
|
.where('ownerId', '=', userId)
|
||||||
.$if(!!ack, (qb) => qb.where('id', '>', ack!.updateId))
|
|
||||||
.$call((qb) => this.auditTableFilters(qb, ack))
|
.$call((qb) => this.auditTableFilters(qb, ack))
|
||||||
.stream();
|
.stream();
|
||||||
}
|
}
|
||||||
@ -154,19 +153,115 @@ export class SyncRepository {
|
|||||||
.stream();
|
.stream();
|
||||||
}
|
}
|
||||||
|
|
||||||
private auditTableFilters<T extends keyof Pick<DB, auditTables>, D>(qb: SelectQueryBuilder<DB, T, D>, ack?: SyncAck) {
|
@GenerateSql({ params: [DummyValue.UUID], stream: true })
|
||||||
const builder = qb as SelectQueryBuilder<DB, auditTables, D>;
|
getAlbumDeletes(userId: string, ack?: SyncAck) {
|
||||||
|
return this.db
|
||||||
|
.selectFrom('albums_audit')
|
||||||
|
.select(['id', 'albumId'])
|
||||||
|
.where('userId', '=', userId)
|
||||||
|
.$call((qb) => this.auditTableFilters(qb, ack))
|
||||||
|
.stream();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GenerateSql({ params: [DummyValue.UUID], stream: true })
|
||||||
|
getAlbumUpserts(userId: string, ack?: SyncAck) {
|
||||||
|
return this.db
|
||||||
|
.selectFrom('albums')
|
||||||
|
.distinctOn(['albums.id', 'albums.updateId'])
|
||||||
|
.where('albums.updatedAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
|
||||||
|
.$if(!!ack, (qb) => qb.where('albums.updateId', '>', ack!.updateId))
|
||||||
|
.orderBy('albums.updateId', 'asc')
|
||||||
|
.leftJoin('albums_shared_users_users as album_users', 'albums.id', 'album_users.albumsId')
|
||||||
|
.where((eb) => eb.or([eb('albums.ownerId', '=', userId), eb('album_users.usersId', '=', userId)]))
|
||||||
|
.select([
|
||||||
|
'albums.id',
|
||||||
|
'albums.ownerId',
|
||||||
|
'albums.albumName as name',
|
||||||
|
'albums.description',
|
||||||
|
'albums.createdAt',
|
||||||
|
'albums.updatedAt',
|
||||||
|
'albums.albumThumbnailAssetId as thumbnailAssetId',
|
||||||
|
'albums.isActivityEnabled',
|
||||||
|
'albums.order',
|
||||||
|
'albums.updateId',
|
||||||
|
])
|
||||||
|
.stream();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GenerateSql({ params: [DummyValue.UUID], stream: true })
|
||||||
|
getAlbumUserDeletes(userId: string, ack?: SyncAck) {
|
||||||
|
return this.db
|
||||||
|
.selectFrom('album_users_audit')
|
||||||
|
.select(['id', 'userId', 'albumId'])
|
||||||
|
.where((eb) =>
|
||||||
|
eb(
|
||||||
|
'albumId',
|
||||||
|
'in',
|
||||||
|
eb
|
||||||
|
.selectFrom('albums')
|
||||||
|
.select(['id'])
|
||||||
|
.where('ownerId', '=', userId)
|
||||||
|
.union((eb) =>
|
||||||
|
eb.parens(
|
||||||
|
eb
|
||||||
|
.selectFrom('albums_shared_users_users as albumUsers')
|
||||||
|
.select(['albumUsers.albumsId as id'])
|
||||||
|
.where('albumUsers.usersId', '=', userId),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.$call((qb) => this.auditTableFilters(qb, ack))
|
||||||
|
.stream();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GenerateSql({ params: [DummyValue.UUID], stream: true })
|
||||||
|
getAlbumUserUpserts(userId: string, ack?: SyncAck) {
|
||||||
|
return this.db
|
||||||
|
.selectFrom('albums_shared_users_users')
|
||||||
|
.select([
|
||||||
|
'albums_shared_users_users.albumsId as albumId',
|
||||||
|
'albums_shared_users_users.usersId as userId',
|
||||||
|
'albums_shared_users_users.role',
|
||||||
|
'albums_shared_users_users.updateId',
|
||||||
|
])
|
||||||
|
.where('albums_shared_users_users.updatedAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
|
||||||
|
.$if(!!ack, (qb) => qb.where('albums_shared_users_users.updateId', '>', ack!.updateId))
|
||||||
|
.orderBy('albums_shared_users_users.updateId', 'asc')
|
||||||
|
.where((eb) =>
|
||||||
|
eb(
|
||||||
|
'albums_shared_users_users.albumsId',
|
||||||
|
'in',
|
||||||
|
eb
|
||||||
|
.selectFrom('albums')
|
||||||
|
.select(['id'])
|
||||||
|
.where('ownerId', '=', userId)
|
||||||
|
.union((eb) =>
|
||||||
|
eb.parens(
|
||||||
|
eb
|
||||||
|
.selectFrom('albums_shared_users_users as albumUsers')
|
||||||
|
.select(['albumUsers.albumsId as id'])
|
||||||
|
.where('albumUsers.usersId', '=', userId),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.stream();
|
||||||
|
}
|
||||||
|
|
||||||
|
private auditTableFilters<T extends keyof Pick<DB, AuditTables>, D>(qb: SelectQueryBuilder<DB, T, D>, ack?: SyncAck) {
|
||||||
|
const builder = qb as SelectQueryBuilder<DB, AuditTables, D>;
|
||||||
return builder
|
return builder
|
||||||
.where('deletedAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
|
.where('deletedAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
|
||||||
.$if(!!ack, (qb) => qb.where('id', '>', ack!.updateId))
|
.$if(!!ack, (qb) => qb.where('id', '>', ack!.updateId))
|
||||||
.orderBy('id', 'asc') as SelectQueryBuilder<DB, T, D>;
|
.orderBy('id', 'asc') as SelectQueryBuilder<DB, T, D>;
|
||||||
}
|
}
|
||||||
|
|
||||||
private upsertTableFilters<T extends keyof Pick<DB, upsertTables>, D>(
|
private upsertTableFilters<T extends keyof Pick<DB, UpsertTables>, D>(
|
||||||
qb: SelectQueryBuilder<DB, T, D>,
|
qb: SelectQueryBuilder<DB, T, D>,
|
||||||
ack?: SyncAck,
|
ack?: SyncAck,
|
||||||
) {
|
) {
|
||||||
const builder = qb as SelectQueryBuilder<DB, upsertTables, D>;
|
const builder = qb as SelectQueryBuilder<DB, UpsertTables, D>;
|
||||||
return builder
|
return builder
|
||||||
.where('updatedAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
|
.where('updatedAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
|
||||||
.$if(!!ack, (qb) => qb.where('updateId', '>', ack!.updateId))
|
.$if(!!ack, (qb) => qb.where('updateId', '>', ack!.updateId))
|
||||||
|
@ -23,6 +23,19 @@ export const immich_uuid_v7 = registerFunction({
|
|||||||
synchronize: false,
|
synchronize: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const album_user_after_insert = registerFunction({
|
||||||
|
name: 'album_user_after_insert',
|
||||||
|
returnType: 'TRIGGER',
|
||||||
|
language: 'PLPGSQL',
|
||||||
|
body: `
|
||||||
|
BEGIN
|
||||||
|
UPDATE albums SET "updatedAt" = clock_timestamp(), "updateId" = immich_uuid_v7(clock_timestamp())
|
||||||
|
WHERE "id" IN (SELECT DISTINCT "albumsId" FROM inserted_rows);
|
||||||
|
RETURN NULL;
|
||||||
|
END`,
|
||||||
|
synchronize: false,
|
||||||
|
});
|
||||||
|
|
||||||
export const updated_at = registerFunction({
|
export const updated_at = registerFunction({
|
||||||
name: 'updated_at',
|
name: 'updated_at',
|
||||||
returnType: 'TRIGGER',
|
returnType: 'TRIGGER',
|
||||||
@ -114,3 +127,38 @@ export const assets_delete_audit = registerFunction({
|
|||||||
END`,
|
END`,
|
||||||
synchronize: false,
|
synchronize: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const albums_delete_audit = registerFunction({
|
||||||
|
name: 'albums_delete_audit',
|
||||||
|
returnType: 'TRIGGER',
|
||||||
|
language: 'PLPGSQL',
|
||||||
|
body: `
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO albums_audit ("albumId", "userId")
|
||||||
|
SELECT "id", "ownerId"
|
||||||
|
FROM OLD;
|
||||||
|
RETURN NULL;
|
||||||
|
END`,
|
||||||
|
synchronize: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const album_users_delete_audit = registerFunction({
|
||||||
|
name: 'album_users_delete_audit',
|
||||||
|
returnType: 'TRIGGER',
|
||||||
|
language: 'PLPGSQL',
|
||||||
|
body: `
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO albums_audit ("albumId", "userId")
|
||||||
|
SELECT "albumsId", "usersId"
|
||||||
|
FROM OLD;
|
||||||
|
|
||||||
|
IF pg_trigger_depth() = 1 THEN
|
||||||
|
INSERT INTO album_users_audit ("albumId", "userId")
|
||||||
|
SELECT "albumsId", "usersId"
|
||||||
|
FROM OLD;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
RETURN NULL;
|
||||||
|
END`,
|
||||||
|
synchronize: false,
|
||||||
|
});
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
import { asset_face_source_type, asset_visibility_enum, assets_status_enum } from 'src/schema/enums';
|
import { asset_face_source_type, asset_visibility_enum, assets_status_enum } from 'src/schema/enums';
|
||||||
import {
|
import {
|
||||||
|
album_user_after_insert,
|
||||||
|
album_users_delete_audit,
|
||||||
|
albums_delete_audit,
|
||||||
assets_delete_audit,
|
assets_delete_audit,
|
||||||
f_concat_ws,
|
f_concat_ws,
|
||||||
f_unaccent,
|
f_unaccent,
|
||||||
@ -11,6 +14,8 @@ import {
|
|||||||
} from 'src/schema/functions';
|
} from 'src/schema/functions';
|
||||||
import { ActivityTable } from 'src/schema/tables/activity.table';
|
import { ActivityTable } from 'src/schema/tables/activity.table';
|
||||||
import { AlbumAssetTable } from 'src/schema/tables/album-asset.table';
|
import { AlbumAssetTable } from 'src/schema/tables/album-asset.table';
|
||||||
|
import { AlbumAuditTable } from 'src/schema/tables/album-audit.table';
|
||||||
|
import { AlbumUserAuditTable } from 'src/schema/tables/album-user-audit.table';
|
||||||
import { AlbumUserTable } from 'src/schema/tables/album-user.table';
|
import { AlbumUserTable } from 'src/schema/tables/album-user.table';
|
||||||
import { AlbumTable } from 'src/schema/tables/album.table';
|
import { AlbumTable } from 'src/schema/tables/album.table';
|
||||||
import { APIKeyTable } from 'src/schema/tables/api-key.table';
|
import { APIKeyTable } from 'src/schema/tables/api-key.table';
|
||||||
@ -45,15 +50,16 @@ import { UserAuditTable } from 'src/schema/tables/user-audit.table';
|
|||||||
import { UserMetadataTable } from 'src/schema/tables/user-metadata.table';
|
import { UserMetadataTable } from 'src/schema/tables/user-metadata.table';
|
||||||
import { UserTable } from 'src/schema/tables/user.table';
|
import { UserTable } from 'src/schema/tables/user.table';
|
||||||
import { VersionHistoryTable } from 'src/schema/tables/version-history.table';
|
import { VersionHistoryTable } from 'src/schema/tables/version-history.table';
|
||||||
import { ConfigurationParameter, Database, Extensions } from 'src/sql-tools';
|
import { Database, Extensions } from 'src/sql-tools';
|
||||||
|
|
||||||
@Extensions(['uuid-ossp', 'unaccent', 'cube', 'earthdistance', 'pg_trgm', 'plpgsql'])
|
@Extensions(['uuid-ossp', 'unaccent', 'cube', 'earthdistance', 'pg_trgm', 'plpgsql'])
|
||||||
@ConfigurationParameter({ name: 'search_path', value: () => '"$user", public, vectors', scope: 'database' })
|
|
||||||
@Database({ name: 'immich' })
|
@Database({ name: 'immich' })
|
||||||
export class ImmichDatabase {
|
export class ImmichDatabase {
|
||||||
tables = [
|
tables = [
|
||||||
ActivityTable,
|
ActivityTable,
|
||||||
AlbumAssetTable,
|
AlbumAssetTable,
|
||||||
|
AlbumAuditTable,
|
||||||
|
AlbumUserAuditTable,
|
||||||
AlbumUserTable,
|
AlbumUserTable,
|
||||||
AlbumTable,
|
AlbumTable,
|
||||||
APIKeyTable,
|
APIKeyTable,
|
||||||
@ -99,6 +105,9 @@ export class ImmichDatabase {
|
|||||||
users_delete_audit,
|
users_delete_audit,
|
||||||
partners_delete_audit,
|
partners_delete_audit,
|
||||||
assets_delete_audit,
|
assets_delete_audit,
|
||||||
|
albums_delete_audit,
|
||||||
|
album_user_after_insert,
|
||||||
|
album_users_delete_audit,
|
||||||
];
|
];
|
||||||
|
|
||||||
enum = [assets_status_enum, asset_face_source_type, asset_visibility_enum];
|
enum = [assets_status_enum, asset_face_source_type, asset_visibility_enum];
|
||||||
|
@ -0,0 +1,96 @@
|
|||||||
|
import { Kysely, sql } from 'kysely';
|
||||||
|
|
||||||
|
export async function up(db: Kysely<any>): Promise<void> {
|
||||||
|
await sql`CREATE OR REPLACE FUNCTION album_user_after_insert()
|
||||||
|
RETURNS TRIGGER
|
||||||
|
LANGUAGE PLPGSQL
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
UPDATE albums SET "updatedAt" = clock_timestamp(), "updateId" = immich_uuid_v7(clock_timestamp())
|
||||||
|
WHERE "id" IN (SELECT DISTINCT "albumsId" FROM inserted_rows);
|
||||||
|
RETURN NULL;
|
||||||
|
END
|
||||||
|
$$;`.execute(db);
|
||||||
|
await sql`CREATE OR REPLACE FUNCTION albums_delete_audit()
|
||||||
|
RETURNS TRIGGER
|
||||||
|
LANGUAGE PLPGSQL
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO albums_audit ("albumId", "userId")
|
||||||
|
SELECT "id", "ownerId"
|
||||||
|
FROM OLD;
|
||||||
|
RETURN NULL;
|
||||||
|
END
|
||||||
|
$$;`.execute(db);
|
||||||
|
await sql`CREATE OR REPLACE FUNCTION album_users_delete_audit()
|
||||||
|
RETURNS TRIGGER
|
||||||
|
LANGUAGE PLPGSQL
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO albums_audit ("albumId", "userId")
|
||||||
|
SELECT "albumsId", "usersId"
|
||||||
|
FROM OLD;
|
||||||
|
|
||||||
|
IF pg_trigger_depth() = 1 THEN
|
||||||
|
INSERT INTO album_users_audit ("albumId", "userId")
|
||||||
|
SELECT "albumsId", "usersId"
|
||||||
|
FROM OLD;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
RETURN NULL;
|
||||||
|
END
|
||||||
|
$$;`.execute(db);
|
||||||
|
await sql`CREATE TABLE "albums_audit" ("id" uuid NOT NULL DEFAULT immich_uuid_v7(), "albumId" uuid NOT NULL, "userId" uuid NOT NULL, "deletedAt" timestamp with time zone NOT NULL DEFAULT clock_timestamp());`.execute(db);
|
||||||
|
await sql`CREATE TABLE "album_users_audit" ("id" uuid NOT NULL DEFAULT immich_uuid_v7(), "albumId" uuid NOT NULL, "userId" uuid NOT NULL, "deletedAt" timestamp with time zone NOT NULL DEFAULT clock_timestamp());`.execute(db);
|
||||||
|
await sql`ALTER TABLE "albums_audit" ADD CONSTRAINT "PK_c75efea8d4dce316ad29b851a8b" PRIMARY KEY ("id");`.execute(db);
|
||||||
|
await sql`ALTER TABLE "album_users_audit" ADD CONSTRAINT "PK_f479a2e575b7ebc9698362c1688" PRIMARY KEY ("id");`.execute(db);
|
||||||
|
await sql`ALTER TABLE "albums_shared_users_users" ADD "updateId" uuid NOT NULL DEFAULT immich_uuid_v7();`.execute(db);
|
||||||
|
await sql`ALTER TABLE "albums_shared_users_users" ADD "updatedAt" timestamp with time zone NOT NULL DEFAULT now();`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_album_users_update_id" ON "albums_shared_users_users" ("updateId")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_albums_audit_album_id" ON "albums_audit" ("albumId")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_albums_audit_user_id" ON "albums_audit" ("userId")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_albums_audit_deleted_at" ON "albums_audit" ("deletedAt")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_album_users_audit_album_id" ON "album_users_audit" ("albumId")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_album_users_audit_user_id" ON "album_users_audit" ("userId")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_album_users_audit_deleted_at" ON "album_users_audit" ("deletedAt")`.execute(db);
|
||||||
|
await sql`CREATE OR REPLACE TRIGGER "albums_delete_audit"
|
||||||
|
AFTER DELETE ON "albums"
|
||||||
|
REFERENCING OLD TABLE AS "old"
|
||||||
|
FOR EACH STATEMENT
|
||||||
|
WHEN (pg_trigger_depth() = 0)
|
||||||
|
EXECUTE FUNCTION albums_delete_audit();`.execute(db);
|
||||||
|
await sql`CREATE OR REPLACE TRIGGER "album_users_delete_audit"
|
||||||
|
AFTER DELETE ON "albums_shared_users_users"
|
||||||
|
REFERENCING OLD TABLE AS "old"
|
||||||
|
FOR EACH STATEMENT
|
||||||
|
WHEN (pg_trigger_depth() <= 1)
|
||||||
|
EXECUTE FUNCTION album_users_delete_audit();`.execute(db);
|
||||||
|
await sql`CREATE OR REPLACE TRIGGER "album_user_after_insert"
|
||||||
|
AFTER INSERT ON "albums_shared_users_users"
|
||||||
|
REFERENCING NEW TABLE AS "inserted_rows"
|
||||||
|
FOR EACH STATEMENT
|
||||||
|
EXECUTE FUNCTION album_user_after_insert();`.execute(db);
|
||||||
|
await sql`CREATE OR REPLACE TRIGGER "album_users_updated_at"
|
||||||
|
BEFORE UPDATE ON "albums_shared_users_users"
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION updated_at();`.execute(db);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(db: Kysely<any>): Promise<void> {
|
||||||
|
await sql`DROP TRIGGER "albums_delete_audit" ON "albums";`.execute(db);
|
||||||
|
await sql`DROP TRIGGER "album_users_delete_audit" ON "albums_shared_users_users";`.execute(db);
|
||||||
|
await sql`DROP TRIGGER "album_user_after_insert" ON "albums_shared_users_users";`.execute(db);
|
||||||
|
await sql`DROP INDEX "IDX_albums_audit_album_id";`.execute(db);
|
||||||
|
await sql`DROP INDEX "IDX_albums_audit_user_id";`.execute(db);
|
||||||
|
await sql`DROP INDEX "IDX_albums_audit_deleted_at";`.execute(db);
|
||||||
|
await sql`DROP INDEX "IDX_album_users_audit_album_id";`.execute(db);
|
||||||
|
await sql`DROP INDEX "IDX_album_users_audit_user_id";`.execute(db);
|
||||||
|
await sql`DROP INDEX "IDX_album_users_audit_deleted_at";`.execute(db);
|
||||||
|
await sql`ALTER TABLE "albums_audit" DROP CONSTRAINT "PK_c75efea8d4dce316ad29b851a8b";`.execute(db);
|
||||||
|
await sql`ALTER TABLE "album_users_audit" DROP CONSTRAINT "PK_f479a2e575b7ebc9698362c1688";`.execute(db);
|
||||||
|
await sql`DROP TABLE "albums_audit";`.execute(db);
|
||||||
|
await sql`DROP TABLE "album_users_audit";`.execute(db);
|
||||||
|
await sql`DROP FUNCTION album_user_after_insert;`.execute(db);
|
||||||
|
await sql`DROP FUNCTION albums_delete_audit;`.execute(db);
|
||||||
|
await sql`DROP FUNCTION album_users_delete_audit;`.execute(db);
|
||||||
|
}
|
17
server/src/schema/tables/album-audit.table.ts
Normal file
17
server/src/schema/tables/album-audit.table.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { PrimaryGeneratedUuidV7Column } from 'src/decorators';
|
||||||
|
import { Column, CreateDateColumn, Table } from 'src/sql-tools';
|
||||||
|
|
||||||
|
@Table('albums_audit')
|
||||||
|
export class AlbumAuditTable {
|
||||||
|
@PrimaryGeneratedUuidV7Column()
|
||||||
|
id!: string;
|
||||||
|
|
||||||
|
@Column({ type: 'uuid', indexName: 'IDX_albums_audit_album_id' })
|
||||||
|
albumId!: string;
|
||||||
|
|
||||||
|
@Column({ type: 'uuid', indexName: 'IDX_albums_audit_user_id' })
|
||||||
|
userId!: string;
|
||||||
|
|
||||||
|
@CreateDateColumn({ default: () => 'clock_timestamp()', indexName: 'IDX_albums_audit_deleted_at' })
|
||||||
|
deletedAt!: Date;
|
||||||
|
}
|
17
server/src/schema/tables/album-user-audit.table.ts
Normal file
17
server/src/schema/tables/album-user-audit.table.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { PrimaryGeneratedUuidV7Column } from 'src/decorators';
|
||||||
|
import { Column, CreateDateColumn, Table } from 'src/sql-tools';
|
||||||
|
|
||||||
|
@Table('album_users_audit')
|
||||||
|
export class AlbumUserAuditTable {
|
||||||
|
@PrimaryGeneratedUuidV7Column()
|
||||||
|
id!: string;
|
||||||
|
|
||||||
|
@Column({ type: 'uuid', indexName: 'IDX_album_users_audit_album_id' })
|
||||||
|
albumId!: string;
|
||||||
|
|
||||||
|
@Column({ type: 'uuid', indexName: 'IDX_album_users_audit_user_id' })
|
||||||
|
userId!: string;
|
||||||
|
|
||||||
|
@CreateDateColumn({ default: () => 'clock_timestamp()', indexName: 'IDX_album_users_audit_deleted_at' })
|
||||||
|
deletedAt!: Date;
|
||||||
|
}
|
@ -1,12 +1,36 @@
|
|||||||
|
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
||||||
import { AlbumUserRole } from 'src/enum';
|
import { AlbumUserRole } from 'src/enum';
|
||||||
|
import { album_user_after_insert, album_users_delete_audit } from 'src/schema/functions';
|
||||||
import { AlbumTable } from 'src/schema/tables/album.table';
|
import { AlbumTable } from 'src/schema/tables/album.table';
|
||||||
import { UserTable } from 'src/schema/tables/user.table';
|
import { UserTable } from 'src/schema/tables/user.table';
|
||||||
import { Column, ForeignKeyColumn, Index, Table } from 'src/sql-tools';
|
import {
|
||||||
|
AfterDeleteTrigger,
|
||||||
|
AfterInsertTrigger,
|
||||||
|
Column,
|
||||||
|
ForeignKeyColumn,
|
||||||
|
Index,
|
||||||
|
Table,
|
||||||
|
UpdateDateColumn,
|
||||||
|
} from 'src/sql-tools';
|
||||||
|
|
||||||
@Table({ name: 'albums_shared_users_users', primaryConstraintName: 'PK_7df55657e0b2e8b626330a0ebc8' })
|
@Table({ name: 'albums_shared_users_users', primaryConstraintName: 'PK_7df55657e0b2e8b626330a0ebc8' })
|
||||||
// Pre-existing indices from original album <--> user ManyToMany mapping
|
// Pre-existing indices from original album <--> user ManyToMany mapping
|
||||||
@Index({ name: 'IDX_427c350ad49bd3935a50baab73', columns: ['albumsId'] })
|
@Index({ name: 'IDX_427c350ad49bd3935a50baab73', columns: ['albumsId'] })
|
||||||
@Index({ name: 'IDX_f48513bf9bccefd6ff3ad30bd0', columns: ['usersId'] })
|
@Index({ name: 'IDX_f48513bf9bccefd6ff3ad30bd0', columns: ['usersId'] })
|
||||||
|
@UpdatedAtTrigger('album_users_updated_at')
|
||||||
|
@AfterInsertTrigger({
|
||||||
|
name: 'album_user_after_insert',
|
||||||
|
scope: 'statement',
|
||||||
|
referencingNewTableAs: 'inserted_rows',
|
||||||
|
function: album_user_after_insert,
|
||||||
|
})
|
||||||
|
@AfterDeleteTrigger({
|
||||||
|
name: 'album_users_delete_audit',
|
||||||
|
scope: 'statement',
|
||||||
|
function: album_users_delete_audit,
|
||||||
|
referencingOldTableAs: 'old',
|
||||||
|
when: 'pg_trigger_depth() <= 1',
|
||||||
|
})
|
||||||
export class AlbumUserTable {
|
export class AlbumUserTable {
|
||||||
@ForeignKeyColumn(() => AlbumTable, {
|
@ForeignKeyColumn(() => AlbumTable, {
|
||||||
onDelete: 'CASCADE',
|
onDelete: 'CASCADE',
|
||||||
@ -26,4 +50,10 @@ export class AlbumUserTable {
|
|||||||
|
|
||||||
@Column({ type: 'character varying', default: AlbumUserRole.EDITOR })
|
@Column({ type: 'character varying', default: AlbumUserRole.EDITOR })
|
||||||
role!: AlbumUserRole;
|
role!: AlbumUserRole;
|
||||||
|
|
||||||
|
@UpdateIdColumn({ indexName: 'IDX_album_users_update_id' })
|
||||||
|
updateId?: string;
|
||||||
|
|
||||||
|
@UpdateDateColumn()
|
||||||
|
updatedAt!: Date;
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
||||||
import { AssetOrder } from 'src/enum';
|
import { AssetOrder } from 'src/enum';
|
||||||
|
import { albums_delete_audit } from 'src/schema/functions';
|
||||||
import { AssetTable } from 'src/schema/tables/asset.table';
|
import { AssetTable } from 'src/schema/tables/asset.table';
|
||||||
import { UserTable } from 'src/schema/tables/user.table';
|
import { UserTable } from 'src/schema/tables/user.table';
|
||||||
import {
|
import {
|
||||||
|
AfterDeleteTrigger,
|
||||||
Column,
|
Column,
|
||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
DeleteDateColumn,
|
DeleteDateColumn,
|
||||||
@ -14,6 +16,13 @@ import {
|
|||||||
|
|
||||||
@Table({ name: 'albums', primaryConstraintName: 'PK_7f71c7b5bc7c87b8f94c9a93a00' })
|
@Table({ name: 'albums', primaryConstraintName: 'PK_7f71c7b5bc7c87b8f94c9a93a00' })
|
||||||
@UpdatedAtTrigger('albums_updated_at')
|
@UpdatedAtTrigger('albums_updated_at')
|
||||||
|
@AfterDeleteTrigger({
|
||||||
|
name: 'albums_delete_audit',
|
||||||
|
scope: 'statement',
|
||||||
|
function: albums_delete_audit,
|
||||||
|
referencingOldTableAs: 'old',
|
||||||
|
when: 'pg_trigger_depth() = 0',
|
||||||
|
})
|
||||||
export class AlbumTable {
|
export class AlbumTable {
|
||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn()
|
||||||
id!: string;
|
id!: string;
|
||||||
|
@ -24,13 +24,14 @@ import { fromAck, serialize } from 'src/utils/sync';
|
|||||||
|
|
||||||
const FULL_SYNC = { needsFullSync: true, deleted: [], upserted: [] };
|
const FULL_SYNC = { needsFullSync: true, deleted: [], upserted: [] };
|
||||||
export const SYNC_TYPES_ORDER = [
|
export const SYNC_TYPES_ORDER = [
|
||||||
//
|
|
||||||
SyncRequestType.UsersV1,
|
SyncRequestType.UsersV1,
|
||||||
SyncRequestType.PartnersV1,
|
SyncRequestType.PartnersV1,
|
||||||
SyncRequestType.AssetsV1,
|
SyncRequestType.AssetsV1,
|
||||||
SyncRequestType.AssetExifsV1,
|
SyncRequestType.AssetExifsV1,
|
||||||
SyncRequestType.PartnerAssetsV1,
|
SyncRequestType.PartnerAssetsV1,
|
||||||
SyncRequestType.PartnerAssetExifsV1,
|
SyncRequestType.PartnerAssetExifsV1,
|
||||||
|
SyncRequestType.AlbumsV1,
|
||||||
|
SyncRequestType.AlbumUsersV1,
|
||||||
];
|
];
|
||||||
|
|
||||||
const throwSessionRequired = () => {
|
const throwSessionRequired = () => {
|
||||||
@ -206,6 +207,43 @@ export class SyncService extends BaseService {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case SyncRequestType.AlbumsV1: {
|
||||||
|
const deletes = this.syncRepository.getAlbumDeletes(
|
||||||
|
auth.user.id,
|
||||||
|
checkpointMap[SyncEntityType.AlbumDeleteV1],
|
||||||
|
);
|
||||||
|
for await (const { id, ...data } of deletes) {
|
||||||
|
response.write(serialize({ type: SyncEntityType.AlbumDeleteV1, updateId: id, data }));
|
||||||
|
}
|
||||||
|
|
||||||
|
const upserts = this.syncRepository.getAlbumUpserts(auth.user.id, checkpointMap[SyncEntityType.AlbumV1]);
|
||||||
|
for await (const { updateId, ...data } of upserts) {
|
||||||
|
response.write(serialize({ type: SyncEntityType.AlbumV1, updateId, data }));
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case SyncRequestType.AlbumUsersV1: {
|
||||||
|
const deletes = this.syncRepository.getAlbumUserDeletes(
|
||||||
|
auth.user.id,
|
||||||
|
checkpointMap[SyncEntityType.AlbumUserDeleteV1],
|
||||||
|
);
|
||||||
|
for await (const { id, ...data } of deletes) {
|
||||||
|
response.write(serialize({ type: SyncEntityType.AlbumUserDeleteV1, updateId: id, data }));
|
||||||
|
}
|
||||||
|
|
||||||
|
const upserts = this.syncRepository.getAlbumUserUpserts(
|
||||||
|
auth.user.id,
|
||||||
|
checkpointMap[SyncEntityType.AlbumUserV1],
|
||||||
|
);
|
||||||
|
for await (const { updateId, ...data } of upserts) {
|
||||||
|
response.write(serialize({ type: SyncEntityType.AlbumUserV1, updateId, data }));
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
default: {
|
default: {
|
||||||
this.logger.warn(`Unsupported sync type: ${type}`);
|
this.logger.warn(`Unsupported sync type: ${type}`);
|
||||||
break;
|
break;
|
||||||
|
@ -0,0 +1,8 @@
|
|||||||
|
import { TriggerFunction, TriggerFunctionOptions } from 'src/sql-tools/from-code/decorators/trigger-function.decorator';
|
||||||
|
|
||||||
|
export const AfterInsertTrigger = (options: Omit<TriggerFunctionOptions, 'timing' | 'actions'>) =>
|
||||||
|
TriggerFunction({
|
||||||
|
timing: 'after',
|
||||||
|
actions: ['insert'],
|
||||||
|
...options,
|
||||||
|
});
|
@ -1,6 +1,7 @@
|
|||||||
export { schemaDiff } from 'src/sql-tools/diff';
|
export { schemaDiff } from 'src/sql-tools/diff';
|
||||||
export { schemaFromCode } from 'src/sql-tools/from-code';
|
export { schemaFromCode } from 'src/sql-tools/from-code';
|
||||||
export * from 'src/sql-tools/from-code/decorators/after-delete.decorator';
|
export * from 'src/sql-tools/from-code/decorators/after-delete.decorator';
|
||||||
|
export * from 'src/sql-tools/from-code/decorators/after-insert.decorator';
|
||||||
export * from 'src/sql-tools/from-code/decorators/before-update.decorator';
|
export * from 'src/sql-tools/from-code/decorators/before-update.decorator';
|
||||||
export * from 'src/sql-tools/from-code/decorators/check.decorator';
|
export * from 'src/sql-tools/from-code/decorators/check.decorator';
|
||||||
export * from 'src/sql-tools/from-code/decorators/column.decorator';
|
export * from 'src/sql-tools/from-code/decorators/column.decorator';
|
||||||
|
@ -4,9 +4,11 @@ import { DateTime } from 'luxon';
|
|||||||
import { createHash, randomBytes } from 'node:crypto';
|
import { createHash, randomBytes } from 'node:crypto';
|
||||||
import { Writable } from 'node:stream';
|
import { Writable } from 'node:stream';
|
||||||
import { AssetFace } from 'src/database';
|
import { AssetFace } from 'src/database';
|
||||||
import { AssetJobStatus, Assets, DB, FaceSearch, Person, Sessions } from 'src/db';
|
import { Albums, AssetJobStatus, Assets, DB, FaceSearch, Person, Sessions } from 'src/db';
|
||||||
import { AssetType, AssetVisibility, SourceType } from 'src/enum';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
|
import { AssetType, AssetVisibility, SourceType, SyncRequestType } from 'src/enum';
|
||||||
import { ActivityRepository } from 'src/repositories/activity.repository';
|
import { ActivityRepository } from 'src/repositories/activity.repository';
|
||||||
|
import { AlbumUserRepository } from 'src/repositories/album-user.repository';
|
||||||
import { AlbumRepository } from 'src/repositories/album.repository';
|
import { AlbumRepository } from 'src/repositories/album.repository';
|
||||||
import { AssetJobRepository } from 'src/repositories/asset-job.repository';
|
import { AssetJobRepository } from 'src/repositories/asset-job.repository';
|
||||||
import { AssetRepository } from 'src/repositories/asset.repository';
|
import { AssetRepository } from 'src/repositories/asset.repository';
|
||||||
@ -28,8 +30,9 @@ import { UserRepository } from 'src/repositories/user.repository';
|
|||||||
import { VersionHistoryRepository } from 'src/repositories/version-history.repository';
|
import { VersionHistoryRepository } from 'src/repositories/version-history.repository';
|
||||||
import { UserTable } from 'src/schema/tables/user.table';
|
import { UserTable } from 'src/schema/tables/user.table';
|
||||||
import { BaseService } from 'src/services/base.service';
|
import { BaseService } from 'src/services/base.service';
|
||||||
|
import { SyncService } from 'src/services/sync.service';
|
||||||
import { RepositoryInterface } from 'src/types';
|
import { RepositoryInterface } from 'src/types';
|
||||||
import { newDate, newEmbedding, newUuid } from 'test/small.factory';
|
import { factory, newDate, newEmbedding, newUuid } from 'test/small.factory';
|
||||||
import { automock, ServiceOverrides } from 'test/utils';
|
import { automock, ServiceOverrides } from 'test/utils';
|
||||||
import { Mocked } from 'vitest';
|
import { Mocked } from 'vitest';
|
||||||
|
|
||||||
@ -39,6 +42,7 @@ const sha256 = (value: string) => createHash('sha256').update(value).digest('bas
|
|||||||
type RepositoriesTypes = {
|
type RepositoriesTypes = {
|
||||||
activity: ActivityRepository;
|
activity: ActivityRepository;
|
||||||
album: AlbumRepository;
|
album: AlbumRepository;
|
||||||
|
albumUser: AlbumUserRepository;
|
||||||
asset: AssetRepository;
|
asset: AssetRepository;
|
||||||
assetJob: AssetJobRepository;
|
assetJob: AssetJobRepository;
|
||||||
config: ConfigRepository;
|
config: ConfigRepository;
|
||||||
@ -76,6 +80,61 @@ export type Context<R extends RepositoryOptions, S extends BaseService> = {
|
|||||||
getRepository<T extends keyof RepositoriesTypes>(key: T): RepositoriesTypes[T];
|
getRepository<T extends keyof RepositoriesTypes>(key: T): RepositoriesTypes[T];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type SyncTestOptions = {
|
||||||
|
db: Kysely<DB>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const newSyncAuthUser = () => {
|
||||||
|
const user = mediumFactory.userInsert();
|
||||||
|
const session = mediumFactory.sessionInsert({ userId: user.id });
|
||||||
|
|
||||||
|
const auth = factory.auth({
|
||||||
|
session,
|
||||||
|
user: {
|
||||||
|
id: user.id,
|
||||||
|
name: user.name,
|
||||||
|
email: user.email,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
auth,
|
||||||
|
session,
|
||||||
|
user,
|
||||||
|
create: async (db: Kysely<DB>) => {
|
||||||
|
await new UserRepository(db).create(user);
|
||||||
|
await new SessionRepository(db).create(session);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const newSyncTest = (options: SyncTestOptions) => {
|
||||||
|
const { sut, mocks, repos, getRepository } = newMediumService(SyncService, {
|
||||||
|
database: options.db,
|
||||||
|
repos: {
|
||||||
|
sync: 'real',
|
||||||
|
session: 'real',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const testSync = async (auth: AuthDto, types: SyncRequestType[]) => {
|
||||||
|
const stream = mediumFactory.syncStream();
|
||||||
|
// Wait for 2ms to ensure all updates are available and account for setTimeout inaccuracy
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 2));
|
||||||
|
await sut.stream(auth, stream, { types });
|
||||||
|
|
||||||
|
return stream.getResponse();
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
sut,
|
||||||
|
mocks,
|
||||||
|
repos,
|
||||||
|
getRepository,
|
||||||
|
testSync,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const newMediumService = <R extends RepositoryOptions, S extends BaseService>(
|
export const newMediumService = <R extends RepositoryOptions, S extends BaseService>(
|
||||||
Service: ClassConstructor<S>,
|
Service: ClassConstructor<S>,
|
||||||
options: {
|
options: {
|
||||||
@ -125,6 +184,14 @@ export const getRepository = <K extends keyof RepositoriesTypes>(key: K, db: Kys
|
|||||||
return new ActivityRepository(db);
|
return new ActivityRepository(db);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'album': {
|
||||||
|
return new AlbumRepository(db);
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'albumUser': {
|
||||||
|
return new AlbumUserRepository(db);
|
||||||
|
}
|
||||||
|
|
||||||
case 'asset': {
|
case 'asset': {
|
||||||
return new AssetRepository(db);
|
return new AssetRepository(db);
|
||||||
}
|
}
|
||||||
@ -380,6 +447,19 @@ const assetInsert = (asset: Partial<Insertable<Assets>> = {}) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const albumInsert = (album: Partial<Insertable<Albums>> & { ownerId: string }) => {
|
||||||
|
const id = album.id || newUuid();
|
||||||
|
const defaults: Omit<Insertable<Albums>, 'ownerId'> = {
|
||||||
|
albumName: 'Album',
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...defaults,
|
||||||
|
...album,
|
||||||
|
id,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const faceInsert = (face: Partial<Insertable<FaceSearch>> & { faceId: string }) => {
|
const faceInsert = (face: Partial<Insertable<FaceSearch>> & { faceId: string }) => {
|
||||||
const defaults = {
|
const defaults = {
|
||||||
faceId: face.faceId,
|
faceId: face.faceId,
|
||||||
@ -502,6 +582,7 @@ export const mediumFactory = {
|
|||||||
assetInsert,
|
assetInsert,
|
||||||
assetFaceInsert,
|
assetFaceInsert,
|
||||||
assetJobStatusInsert,
|
assetJobStatusInsert,
|
||||||
|
albumInsert,
|
||||||
faceInsert,
|
faceInsert,
|
||||||
personInsert,
|
personInsert,
|
||||||
sessionInsert,
|
sessionInsert,
|
||||||
|
@ -1,910 +0,0 @@
|
|||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
|
||||||
import { SyncEntityType, SyncRequestType } from 'src/enum';
|
|
||||||
import { SYNC_TYPES_ORDER, SyncService } from 'src/services/sync.service';
|
|
||||||
import { mediumFactory, newMediumService } from 'test/medium.factory';
|
|
||||||
import { factory } from 'test/small.factory';
|
|
||||||
import { getKyselyDB } from 'test/utils';
|
|
||||||
|
|
||||||
const setup = async () => {
|
|
||||||
const db = await getKyselyDB();
|
|
||||||
|
|
||||||
const { sut, mocks, repos, getRepository } = newMediumService(SyncService, {
|
|
||||||
database: db,
|
|
||||||
repos: {
|
|
||||||
sync: 'real',
|
|
||||||
session: 'real',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const user = mediumFactory.userInsert();
|
|
||||||
const session = mediumFactory.sessionInsert({ userId: user.id });
|
|
||||||
const auth = factory.auth({
|
|
||||||
session,
|
|
||||||
user: {
|
|
||||||
id: user.id,
|
|
||||||
name: user.name,
|
|
||||||
email: user.email,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await getRepository('user').create(user);
|
|
||||||
await getRepository('session').create(session);
|
|
||||||
|
|
||||||
const testSync = async (auth: AuthDto, types: SyncRequestType[]) => {
|
|
||||||
const stream = mediumFactory.syncStream();
|
|
||||||
// Wait for 1ms to ensure all updates are available
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1));
|
|
||||||
await sut.stream(auth, stream, { types });
|
|
||||||
|
|
||||||
return stream.getResponse();
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
sut,
|
|
||||||
auth,
|
|
||||||
mocks,
|
|
||||||
repos,
|
|
||||||
getRepository,
|
|
||||||
testSync,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
describe(SyncService.name, () => {
|
|
||||||
it('should have all the types in the ordering variable', () => {
|
|
||||||
for (const key in SyncRequestType) {
|
|
||||||
expect(SYNC_TYPES_ORDER).includes(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(SYNC_TYPES_ORDER.length).toBe(Object.keys(SyncRequestType).length);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe.concurrent(SyncEntityType.UserV1, () => {
|
|
||||||
it('should detect and sync the first user', async () => {
|
|
||||||
const { auth, sut, getRepository, testSync } = await setup();
|
|
||||||
|
|
||||||
const userRepo = getRepository('user');
|
|
||||||
const user = await userRepo.get(auth.user.id, { withDeleted: false });
|
|
||||||
if (!user) {
|
|
||||||
expect.fail('First user should exist');
|
|
||||||
}
|
|
||||||
|
|
||||||
const initialSyncResponse = await testSync(auth, [SyncRequestType.UsersV1]);
|
|
||||||
expect(initialSyncResponse).toHaveLength(1);
|
|
||||||
expect(initialSyncResponse).toEqual([
|
|
||||||
{
|
|
||||||
ack: expect.any(String),
|
|
||||||
data: {
|
|
||||||
deletedAt: user.deletedAt,
|
|
||||||
email: user.email,
|
|
||||||
id: user.id,
|
|
||||||
name: user.name,
|
|
||||||
},
|
|
||||||
type: 'UserV1',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
const acks = [initialSyncResponse[0].ack];
|
|
||||||
await sut.setAcks(auth, { acks });
|
|
||||||
const ackSyncResponse = await testSync(auth, [SyncRequestType.UsersV1]);
|
|
||||||
|
|
||||||
expect(ackSyncResponse).toHaveLength(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should detect and sync a soft deleted user', async () => {
|
|
||||||
const { auth, sut, getRepository, testSync } = await setup();
|
|
||||||
|
|
||||||
const deletedAt = new Date().toISOString();
|
|
||||||
const deletedUser = mediumFactory.userInsert({ deletedAt });
|
|
||||||
const deleted = await getRepository('user').create(deletedUser);
|
|
||||||
|
|
||||||
const response = await testSync(auth, [SyncRequestType.UsersV1]);
|
|
||||||
|
|
||||||
expect(response).toHaveLength(2);
|
|
||||||
expect(response).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
{
|
|
||||||
ack: expect.any(String),
|
|
||||||
data: {
|
|
||||||
deletedAt: null,
|
|
||||||
email: auth.user.email,
|
|
||||||
id: auth.user.id,
|
|
||||||
name: auth.user.name,
|
|
||||||
},
|
|
||||||
type: 'UserV1',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ack: expect.any(String),
|
|
||||||
data: {
|
|
||||||
deletedAt,
|
|
||||||
email: deleted.email,
|
|
||||||
id: deleted.id,
|
|
||||||
name: deleted.name,
|
|
||||||
},
|
|
||||||
type: 'UserV1',
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
|
|
||||||
const acks = [response[1].ack];
|
|
||||||
await sut.setAcks(auth, { acks });
|
|
||||||
const ackSyncResponse = await testSync(auth, [SyncRequestType.UsersV1]);
|
|
||||||
|
|
||||||
expect(ackSyncResponse).toHaveLength(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should detect and sync a deleted user', async () => {
|
|
||||||
const { auth, sut, getRepository, testSync } = await setup();
|
|
||||||
|
|
||||||
const userRepo = getRepository('user');
|
|
||||||
const user = mediumFactory.userInsert();
|
|
||||||
await userRepo.create(user);
|
|
||||||
await userRepo.delete({ id: user.id }, true);
|
|
||||||
|
|
||||||
const response = await testSync(auth, [SyncRequestType.UsersV1]);
|
|
||||||
|
|
||||||
expect(response).toHaveLength(2);
|
|
||||||
expect(response).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
{
|
|
||||||
ack: expect.any(String),
|
|
||||||
data: {
|
|
||||||
userId: user.id,
|
|
||||||
},
|
|
||||||
type: 'UserDeleteV1',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ack: expect.any(String),
|
|
||||||
data: {
|
|
||||||
deletedAt: null,
|
|
||||||
email: auth.user.email,
|
|
||||||
id: auth.user.id,
|
|
||||||
name: auth.user.name,
|
|
||||||
},
|
|
||||||
type: 'UserV1',
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
|
|
||||||
const acks = response.map(({ ack }) => ack);
|
|
||||||
await sut.setAcks(auth, { acks });
|
|
||||||
const ackSyncResponse = await testSync(auth, [SyncRequestType.UsersV1]);
|
|
||||||
|
|
||||||
expect(ackSyncResponse).toHaveLength(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should sync a user and then an update to that same user', async () => {
|
|
||||||
const { auth, sut, getRepository, testSync } = await setup();
|
|
||||||
|
|
||||||
const initialSyncResponse = await testSync(auth, [SyncRequestType.UsersV1]);
|
|
||||||
|
|
||||||
expect(initialSyncResponse).toHaveLength(1);
|
|
||||||
expect(initialSyncResponse).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
{
|
|
||||||
ack: expect.any(String),
|
|
||||||
data: {
|
|
||||||
deletedAt: null,
|
|
||||||
email: auth.user.email,
|
|
||||||
id: auth.user.id,
|
|
||||||
name: auth.user.name,
|
|
||||||
},
|
|
||||||
type: 'UserV1',
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
|
|
||||||
const acks = [initialSyncResponse[0].ack];
|
|
||||||
await sut.setAcks(auth, { acks });
|
|
||||||
|
|
||||||
const userRepo = getRepository('user');
|
|
||||||
const updated = await userRepo.update(auth.user.id, { name: 'new name' });
|
|
||||||
const updatedSyncResponse = await testSync(auth, [SyncRequestType.UsersV1]);
|
|
||||||
|
|
||||||
expect(updatedSyncResponse).toHaveLength(1);
|
|
||||||
expect(updatedSyncResponse).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
{
|
|
||||||
ack: expect.any(String),
|
|
||||||
data: {
|
|
||||||
deletedAt: null,
|
|
||||||
email: auth.user.email,
|
|
||||||
id: auth.user.id,
|
|
||||||
name: updated.name,
|
|
||||||
},
|
|
||||||
type: 'UserV1',
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe.concurrent(SyncEntityType.PartnerV1, () => {
|
|
||||||
it('should detect and sync the first partner', async () => {
|
|
||||||
const { auth, sut, getRepository, testSync } = await setup();
|
|
||||||
|
|
||||||
const user1 = auth.user;
|
|
||||||
const userRepo = getRepository('user');
|
|
||||||
const partnerRepo = getRepository('partner');
|
|
||||||
|
|
||||||
const user2 = mediumFactory.userInsert();
|
|
||||||
await userRepo.create(user2);
|
|
||||||
|
|
||||||
const partner = await partnerRepo.create({ sharedById: user2.id, sharedWithId: user1.id });
|
|
||||||
|
|
||||||
const initialSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]);
|
|
||||||
|
|
||||||
expect(initialSyncResponse).toHaveLength(1);
|
|
||||||
expect(initialSyncResponse).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
{
|
|
||||||
ack: expect.any(String),
|
|
||||||
data: {
|
|
||||||
inTimeline: partner.inTimeline,
|
|
||||||
sharedById: partner.sharedById,
|
|
||||||
sharedWithId: partner.sharedWithId,
|
|
||||||
},
|
|
||||||
type: 'PartnerV1',
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
|
|
||||||
const acks = [initialSyncResponse[0].ack];
|
|
||||||
await sut.setAcks(auth, { acks });
|
|
||||||
|
|
||||||
const ackSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]);
|
|
||||||
|
|
||||||
expect(ackSyncResponse).toHaveLength(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should detect and sync a deleted partner', async () => {
|
|
||||||
const { auth, sut, getRepository, testSync } = await setup();
|
|
||||||
|
|
||||||
const userRepo = getRepository('user');
|
|
||||||
const user1 = auth.user;
|
|
||||||
const user2 = mediumFactory.userInsert();
|
|
||||||
await userRepo.create(user2);
|
|
||||||
|
|
||||||
const partnerRepo = getRepository('partner');
|
|
||||||
const partner = await partnerRepo.create({ sharedById: user2.id, sharedWithId: user1.id });
|
|
||||||
await partnerRepo.remove(partner);
|
|
||||||
|
|
||||||
const response = await testSync(auth, [SyncRequestType.PartnersV1]);
|
|
||||||
|
|
||||||
expect(response).toHaveLength(1);
|
|
||||||
expect(response).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
{
|
|
||||||
ack: expect.any(String),
|
|
||||||
data: {
|
|
||||||
sharedById: partner.sharedById,
|
|
||||||
sharedWithId: partner.sharedWithId,
|
|
||||||
},
|
|
||||||
type: 'PartnerDeleteV1',
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
|
|
||||||
const acks = response.map(({ ack }) => ack);
|
|
||||||
await sut.setAcks(auth, { acks });
|
|
||||||
|
|
||||||
const ackSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]);
|
|
||||||
|
|
||||||
expect(ackSyncResponse).toHaveLength(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should detect and sync a partner share both to and from another user', async () => {
|
|
||||||
const { auth, sut, getRepository, testSync } = await setup();
|
|
||||||
|
|
||||||
const userRepo = getRepository('user');
|
|
||||||
const user1 = auth.user;
|
|
||||||
const user2 = await userRepo.create(mediumFactory.userInsert());
|
|
||||||
|
|
||||||
const partnerRepo = getRepository('partner');
|
|
||||||
const partner1 = await partnerRepo.create({ sharedById: user2.id, sharedWithId: user1.id });
|
|
||||||
const partner2 = await partnerRepo.create({ sharedById: user1.id, sharedWithId: user2.id });
|
|
||||||
|
|
||||||
const response = await testSync(auth, [SyncRequestType.PartnersV1]);
|
|
||||||
|
|
||||||
expect(response).toHaveLength(2);
|
|
||||||
expect(response).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
{
|
|
||||||
ack: expect.any(String),
|
|
||||||
data: {
|
|
||||||
inTimeline: partner1.inTimeline,
|
|
||||||
sharedById: partner1.sharedById,
|
|
||||||
sharedWithId: partner1.sharedWithId,
|
|
||||||
},
|
|
||||||
type: 'PartnerV1',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ack: expect.any(String),
|
|
||||||
data: {
|
|
||||||
inTimeline: partner2.inTimeline,
|
|
||||||
sharedById: partner2.sharedById,
|
|
||||||
sharedWithId: partner2.sharedWithId,
|
|
||||||
},
|
|
||||||
type: 'PartnerV1',
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
|
|
||||||
await sut.setAcks(auth, { acks: [response[1].ack] });
|
|
||||||
|
|
||||||
const ackSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]);
|
|
||||||
|
|
||||||
expect(ackSyncResponse).toHaveLength(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should sync a partner and then an update to that same partner', async () => {
|
|
||||||
const { auth, sut, getRepository, testSync } = await setup();
|
|
||||||
|
|
||||||
const userRepo = getRepository('user');
|
|
||||||
const user1 = auth.user;
|
|
||||||
const user2 = await userRepo.create(mediumFactory.userInsert());
|
|
||||||
|
|
||||||
const partnerRepo = getRepository('partner');
|
|
||||||
const partner = await partnerRepo.create({ sharedById: user2.id, sharedWithId: user1.id });
|
|
||||||
|
|
||||||
const initialSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]);
|
|
||||||
|
|
||||||
expect(initialSyncResponse).toHaveLength(1);
|
|
||||||
expect(initialSyncResponse).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
{
|
|
||||||
ack: expect.any(String),
|
|
||||||
data: {
|
|
||||||
inTimeline: partner.inTimeline,
|
|
||||||
sharedById: partner.sharedById,
|
|
||||||
sharedWithId: partner.sharedWithId,
|
|
||||||
},
|
|
||||||
type: 'PartnerV1',
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
|
|
||||||
const acks = [initialSyncResponse[0].ack];
|
|
||||||
await sut.setAcks(auth, { acks });
|
|
||||||
|
|
||||||
const updated = await partnerRepo.update(
|
|
||||||
{ sharedById: partner.sharedById, sharedWithId: partner.sharedWithId },
|
|
||||||
{ inTimeline: true },
|
|
||||||
);
|
|
||||||
|
|
||||||
const updatedSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]);
|
|
||||||
|
|
||||||
expect(updatedSyncResponse).toHaveLength(1);
|
|
||||||
expect(updatedSyncResponse).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
{
|
|
||||||
ack: expect.any(String),
|
|
||||||
data: {
|
|
||||||
inTimeline: updated.inTimeline,
|
|
||||||
sharedById: updated.sharedById,
|
|
||||||
sharedWithId: updated.sharedWithId,
|
|
||||||
},
|
|
||||||
type: 'PartnerV1',
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not sync a partner or partner delete for an unrelated user', async () => {
|
|
||||||
const { auth, getRepository, testSync } = await setup();
|
|
||||||
|
|
||||||
const userRepo = getRepository('user');
|
|
||||||
const user2 = await userRepo.create(mediumFactory.userInsert());
|
|
||||||
const user3 = await userRepo.create(mediumFactory.userInsert());
|
|
||||||
|
|
||||||
const partnerRepo = getRepository('partner');
|
|
||||||
const partner = await partnerRepo.create({ sharedById: user2.id, sharedWithId: user3.id });
|
|
||||||
|
|
||||||
expect(await testSync(auth, [SyncRequestType.PartnersV1])).toHaveLength(0);
|
|
||||||
|
|
||||||
await partnerRepo.remove(partner);
|
|
||||||
|
|
||||||
expect(await testSync(auth, [SyncRequestType.PartnersV1])).toHaveLength(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not sync a partner delete after a user is deleted', async () => {
|
|
||||||
const { auth, getRepository, testSync } = await setup();
|
|
||||||
|
|
||||||
const userRepo = getRepository('user');
|
|
||||||
const user2 = await userRepo.create(mediumFactory.userInsert());
|
|
||||||
|
|
||||||
const partnerRepo = getRepository('partner');
|
|
||||||
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
|
||||||
await userRepo.delete({ id: user2.id }, true);
|
|
||||||
|
|
||||||
expect(await testSync(auth, [SyncRequestType.PartnersV1])).toHaveLength(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe.concurrent(SyncEntityType.AssetV1, () => {
|
|
||||||
it('should detect and sync the first asset', async () => {
|
|
||||||
const { auth, sut, getRepository, testSync } = await setup();
|
|
||||||
|
|
||||||
const checksum = '1115vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA=';
|
|
||||||
const thumbhash = '2225vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA=';
|
|
||||||
const date = new Date().toISOString();
|
|
||||||
|
|
||||||
const assetRepo = getRepository('asset');
|
|
||||||
const asset = mediumFactory.assetInsert({
|
|
||||||
ownerId: auth.user.id,
|
|
||||||
checksum: Buffer.from(checksum, 'base64'),
|
|
||||||
thumbhash: Buffer.from(thumbhash, 'base64'),
|
|
||||||
fileCreatedAt: date,
|
|
||||||
fileModifiedAt: date,
|
|
||||||
localDateTime: date,
|
|
||||||
deletedAt: null,
|
|
||||||
});
|
|
||||||
await assetRepo.create(asset);
|
|
||||||
|
|
||||||
const initialSyncResponse = await testSync(auth, [SyncRequestType.AssetsV1]);
|
|
||||||
|
|
||||||
expect(initialSyncResponse).toHaveLength(1);
|
|
||||||
expect(initialSyncResponse).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
{
|
|
||||||
ack: expect.any(String),
|
|
||||||
data: {
|
|
||||||
id: asset.id,
|
|
||||||
ownerId: asset.ownerId,
|
|
||||||
thumbhash,
|
|
||||||
checksum,
|
|
||||||
deletedAt: asset.deletedAt,
|
|
||||||
fileCreatedAt: asset.fileCreatedAt,
|
|
||||||
fileModifiedAt: asset.fileModifiedAt,
|
|
||||||
isFavorite: asset.isFavorite,
|
|
||||||
localDateTime: asset.localDateTime,
|
|
||||||
type: asset.type,
|
|
||||||
visibility: asset.visibility,
|
|
||||||
},
|
|
||||||
type: 'AssetV1',
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
|
|
||||||
const acks = [initialSyncResponse[0].ack];
|
|
||||||
await sut.setAcks(auth, { acks });
|
|
||||||
|
|
||||||
const ackSyncResponse = await testSync(auth, [SyncRequestType.AssetsV1]);
|
|
||||||
|
|
||||||
expect(ackSyncResponse).toHaveLength(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should detect and sync a deleted asset', async () => {
|
|
||||||
const { auth, sut, getRepository, testSync } = await setup();
|
|
||||||
|
|
||||||
const assetRepo = getRepository('asset');
|
|
||||||
const asset = mediumFactory.assetInsert({ ownerId: auth.user.id });
|
|
||||||
await assetRepo.create(asset);
|
|
||||||
await assetRepo.remove(asset);
|
|
||||||
|
|
||||||
const response = await testSync(auth, [SyncRequestType.AssetsV1]);
|
|
||||||
|
|
||||||
expect(response).toHaveLength(1);
|
|
||||||
expect(response).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
{
|
|
||||||
ack: expect.any(String),
|
|
||||||
data: {
|
|
||||||
assetId: asset.id,
|
|
||||||
},
|
|
||||||
type: 'AssetDeleteV1',
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
|
|
||||||
const acks = response.map(({ ack }) => ack);
|
|
||||||
await sut.setAcks(auth, { acks });
|
|
||||||
|
|
||||||
const ackSyncResponse = await testSync(auth, [SyncRequestType.AssetsV1]);
|
|
||||||
|
|
||||||
expect(ackSyncResponse).toHaveLength(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not sync an asset or asset delete for an unrelated user', async () => {
|
|
||||||
const { auth, getRepository, testSync } = await setup();
|
|
||||||
|
|
||||||
const userRepo = getRepository('user');
|
|
||||||
const user2 = mediumFactory.userInsert();
|
|
||||||
await userRepo.create(user2);
|
|
||||||
|
|
||||||
const sessionRepo = getRepository('session');
|
|
||||||
const session = mediumFactory.sessionInsert({ userId: user2.id });
|
|
||||||
await sessionRepo.create(session);
|
|
||||||
|
|
||||||
const assetRepo = getRepository('asset');
|
|
||||||
const asset = mediumFactory.assetInsert({ ownerId: user2.id });
|
|
||||||
await assetRepo.create(asset);
|
|
||||||
|
|
||||||
const auth2 = factory.auth({ session, user: user2 });
|
|
||||||
|
|
||||||
expect(await testSync(auth2, [SyncRequestType.AssetsV1])).toHaveLength(1);
|
|
||||||
expect(await testSync(auth, [SyncRequestType.AssetsV1])).toHaveLength(0);
|
|
||||||
|
|
||||||
await assetRepo.remove(asset);
|
|
||||||
expect(await testSync(auth2, [SyncRequestType.AssetsV1])).toHaveLength(1);
|
|
||||||
expect(await testSync(auth, [SyncRequestType.AssetsV1])).toHaveLength(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe.concurrent(SyncRequestType.PartnerAssetsV1, () => {
|
|
||||||
it('should detect and sync the first partner asset', async () => {
|
|
||||||
const { auth, sut, getRepository, testSync } = await setup();
|
|
||||||
|
|
||||||
const checksum = '1115vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA=';
|
|
||||||
const thumbhash = '2225vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA=';
|
|
||||||
const date = new Date().toISOString();
|
|
||||||
|
|
||||||
const userRepo = getRepository('user');
|
|
||||||
const user2 = mediumFactory.userInsert();
|
|
||||||
await userRepo.create(user2);
|
|
||||||
|
|
||||||
const assetRepo = getRepository('asset');
|
|
||||||
const asset = mediumFactory.assetInsert({
|
|
||||||
ownerId: user2.id,
|
|
||||||
checksum: Buffer.from(checksum, 'base64'),
|
|
||||||
thumbhash: Buffer.from(thumbhash, 'base64'),
|
|
||||||
fileCreatedAt: date,
|
|
||||||
fileModifiedAt: date,
|
|
||||||
localDateTime: date,
|
|
||||||
deletedAt: null,
|
|
||||||
});
|
|
||||||
await assetRepo.create(asset);
|
|
||||||
|
|
||||||
const partnerRepo = getRepository('partner');
|
|
||||||
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
|
||||||
|
|
||||||
const initialSyncResponse = await testSync(auth, [SyncRequestType.PartnerAssetsV1]);
|
|
||||||
|
|
||||||
expect(initialSyncResponse).toHaveLength(1);
|
|
||||||
expect(initialSyncResponse).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
{
|
|
||||||
ack: expect.any(String),
|
|
||||||
data: {
|
|
||||||
id: asset.id,
|
|
||||||
ownerId: asset.ownerId,
|
|
||||||
thumbhash,
|
|
||||||
checksum,
|
|
||||||
deletedAt: null,
|
|
||||||
fileCreatedAt: date,
|
|
||||||
fileModifiedAt: date,
|
|
||||||
isFavorite: false,
|
|
||||||
localDateTime: date,
|
|
||||||
type: asset.type,
|
|
||||||
visibility: asset.visibility,
|
|
||||||
},
|
|
||||||
type: SyncEntityType.PartnerAssetV1,
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
|
|
||||||
const acks = [initialSyncResponse[0].ack];
|
|
||||||
await sut.setAcks(auth, { acks });
|
|
||||||
|
|
||||||
const ackSyncResponse = await testSync(auth, [SyncRequestType.PartnerAssetsV1]);
|
|
||||||
|
|
||||||
expect(ackSyncResponse).toHaveLength(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should detect and sync a deleted partner asset', async () => {
|
|
||||||
const { auth, sut, getRepository, testSync } = await setup();
|
|
||||||
|
|
||||||
const userRepo = getRepository('user');
|
|
||||||
const user2 = mediumFactory.userInsert();
|
|
||||||
await userRepo.create(user2);
|
|
||||||
const asset = mediumFactory.assetInsert({ ownerId: user2.id });
|
|
||||||
|
|
||||||
const assetRepo = getRepository('asset');
|
|
||||||
await assetRepo.create(asset);
|
|
||||||
|
|
||||||
const partnerRepo = getRepository('partner');
|
|
||||||
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
|
||||||
await assetRepo.remove(asset);
|
|
||||||
|
|
||||||
const response = await testSync(auth, [SyncRequestType.PartnerAssetsV1]);
|
|
||||||
|
|
||||||
expect(response).toHaveLength(1);
|
|
||||||
expect(response).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
{
|
|
||||||
ack: expect.any(String),
|
|
||||||
data: {
|
|
||||||
assetId: asset.id,
|
|
||||||
},
|
|
||||||
type: SyncEntityType.PartnerAssetDeleteV1,
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
|
|
||||||
const acks = response.map(({ ack }) => ack);
|
|
||||||
await sut.setAcks(auth, { acks });
|
|
||||||
|
|
||||||
const ackSyncResponse = await testSync(auth, [SyncRequestType.PartnerAssetsV1]);
|
|
||||||
|
|
||||||
expect(ackSyncResponse).toHaveLength(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not sync a deleted partner asset due to a user delete', async () => {
|
|
||||||
const { auth, getRepository, testSync } = await setup();
|
|
||||||
|
|
||||||
const userRepo = getRepository('user');
|
|
||||||
const user2 = mediumFactory.userInsert();
|
|
||||||
await userRepo.create(user2);
|
|
||||||
|
|
||||||
const partnerRepo = getRepository('partner');
|
|
||||||
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
|
||||||
|
|
||||||
const assetRepo = getRepository('asset');
|
|
||||||
await assetRepo.create(mediumFactory.assetInsert({ ownerId: user2.id }));
|
|
||||||
|
|
||||||
await userRepo.delete({ id: user2.id }, true);
|
|
||||||
|
|
||||||
const response = await testSync(auth, [SyncRequestType.PartnerAssetsV1]);
|
|
||||||
expect(response).toHaveLength(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not sync a deleted partner asset due to a partner delete (unshare)', async () => {
|
|
||||||
const { auth, getRepository, testSync } = await setup();
|
|
||||||
|
|
||||||
const userRepo = getRepository('user');
|
|
||||||
const user2 = mediumFactory.userInsert();
|
|
||||||
await userRepo.create(user2);
|
|
||||||
|
|
||||||
const assetRepo = getRepository('asset');
|
|
||||||
await assetRepo.create(mediumFactory.assetInsert({ ownerId: user2.id }));
|
|
||||||
|
|
||||||
const partnerRepo = getRepository('partner');
|
|
||||||
const partner = { sharedById: user2.id, sharedWithId: auth.user.id };
|
|
||||||
await partnerRepo.create(partner);
|
|
||||||
|
|
||||||
await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(1);
|
|
||||||
|
|
||||||
await partnerRepo.remove(partner);
|
|
||||||
|
|
||||||
await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not sync an asset or asset delete for own user', async () => {
|
|
||||||
const { auth, getRepository, testSync } = await setup();
|
|
||||||
|
|
||||||
const userRepo = getRepository('user');
|
|
||||||
const user2 = mediumFactory.userInsert();
|
|
||||||
await userRepo.create(user2);
|
|
||||||
|
|
||||||
const assetRepo = getRepository('asset');
|
|
||||||
const asset = mediumFactory.assetInsert({ ownerId: auth.user.id });
|
|
||||||
await assetRepo.create(asset);
|
|
||||||
|
|
||||||
const partnerRepo = getRepository('partner');
|
|
||||||
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
|
||||||
|
|
||||||
await expect(testSync(auth, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1);
|
|
||||||
await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0);
|
|
||||||
|
|
||||||
await assetRepo.remove(asset);
|
|
||||||
|
|
||||||
await expect(testSync(auth, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1);
|
|
||||||
await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not sync an asset or asset delete for unrelated user', async () => {
|
|
||||||
const { auth, getRepository, testSync } = await setup();
|
|
||||||
|
|
||||||
const userRepo = getRepository('user');
|
|
||||||
const user2 = mediumFactory.userInsert();
|
|
||||||
await userRepo.create(user2);
|
|
||||||
|
|
||||||
const sessionRepo = getRepository('session');
|
|
||||||
const session = mediumFactory.sessionInsert({ userId: user2.id });
|
|
||||||
await sessionRepo.create(session);
|
|
||||||
|
|
||||||
const auth2 = factory.auth({ session, user: user2 });
|
|
||||||
|
|
||||||
const assetRepo = getRepository('asset');
|
|
||||||
const asset = mediumFactory.assetInsert({ ownerId: user2.id });
|
|
||||||
await assetRepo.create(asset);
|
|
||||||
|
|
||||||
await expect(testSync(auth2, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1);
|
|
||||||
await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0);
|
|
||||||
|
|
||||||
await assetRepo.remove(asset);
|
|
||||||
|
|
||||||
await expect(testSync(auth2, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1);
|
|
||||||
await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe.concurrent(SyncRequestType.AssetExifsV1, () => {
|
|
||||||
it('should detect and sync the first asset exif', async () => {
|
|
||||||
const { auth, sut, getRepository, testSync } = await setup();
|
|
||||||
|
|
||||||
const assetRepo = getRepository('asset');
|
|
||||||
const asset = mediumFactory.assetInsert({ ownerId: auth.user.id });
|
|
||||||
await assetRepo.create(asset);
|
|
||||||
await assetRepo.upsertExif({ assetId: asset.id, make: 'Canon' });
|
|
||||||
|
|
||||||
const initialSyncResponse = await testSync(auth, [SyncRequestType.AssetExifsV1]);
|
|
||||||
|
|
||||||
expect(initialSyncResponse).toHaveLength(1);
|
|
||||||
expect(initialSyncResponse).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
{
|
|
||||||
ack: expect.any(String),
|
|
||||||
data: {
|
|
||||||
assetId: asset.id,
|
|
||||||
city: null,
|
|
||||||
country: null,
|
|
||||||
dateTimeOriginal: null,
|
|
||||||
description: '',
|
|
||||||
exifImageHeight: null,
|
|
||||||
exifImageWidth: null,
|
|
||||||
exposureTime: null,
|
|
||||||
fNumber: null,
|
|
||||||
fileSizeInByte: null,
|
|
||||||
focalLength: null,
|
|
||||||
fps: null,
|
|
||||||
iso: null,
|
|
||||||
latitude: null,
|
|
||||||
lensModel: null,
|
|
||||||
longitude: null,
|
|
||||||
make: 'Canon',
|
|
||||||
model: null,
|
|
||||||
modifyDate: null,
|
|
||||||
orientation: null,
|
|
||||||
profileDescription: null,
|
|
||||||
projectionType: null,
|
|
||||||
rating: null,
|
|
||||||
state: null,
|
|
||||||
timeZone: null,
|
|
||||||
},
|
|
||||||
type: SyncEntityType.AssetExifV1,
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
|
|
||||||
const acks = [initialSyncResponse[0].ack];
|
|
||||||
await sut.setAcks(auth, { acks });
|
|
||||||
|
|
||||||
const ackSyncResponse = await testSync(auth, [SyncRequestType.AssetExifsV1]);
|
|
||||||
|
|
||||||
expect(ackSyncResponse).toHaveLength(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should only sync asset exif for own user', async () => {
|
|
||||||
const { auth, getRepository, testSync } = await setup();
|
|
||||||
|
|
||||||
const userRepo = getRepository('user');
|
|
||||||
const user2 = mediumFactory.userInsert();
|
|
||||||
await userRepo.create(user2);
|
|
||||||
|
|
||||||
const partnerRepo = getRepository('partner');
|
|
||||||
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
|
||||||
|
|
||||||
const assetRepo = getRepository('asset');
|
|
||||||
const asset = mediumFactory.assetInsert({ ownerId: user2.id });
|
|
||||||
await assetRepo.create(asset);
|
|
||||||
await assetRepo.upsertExif({ assetId: asset.id, make: 'Canon' });
|
|
||||||
|
|
||||||
const sessionRepo = getRepository('session');
|
|
||||||
const session = mediumFactory.sessionInsert({ userId: user2.id });
|
|
||||||
await sessionRepo.create(session);
|
|
||||||
|
|
||||||
const auth2 = factory.auth({ session, user: user2 });
|
|
||||||
await expect(testSync(auth2, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(1);
|
|
||||||
await expect(testSync(auth, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe.concurrent(SyncRequestType.PartnerAssetExifsV1, () => {
|
|
||||||
it('should detect and sync the first partner asset exif', async () => {
|
|
||||||
const { auth, sut, getRepository, testSync } = await setup();
|
|
||||||
|
|
||||||
const userRepo = getRepository('user');
|
|
||||||
const user2 = mediumFactory.userInsert();
|
|
||||||
await userRepo.create(user2);
|
|
||||||
|
|
||||||
const partnerRepo = getRepository('partner');
|
|
||||||
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
|
||||||
|
|
||||||
const assetRepo = getRepository('asset');
|
|
||||||
const asset = mediumFactory.assetInsert({ ownerId: user2.id });
|
|
||||||
await assetRepo.create(asset);
|
|
||||||
await assetRepo.upsertExif({ assetId: asset.id, make: 'Canon' });
|
|
||||||
|
|
||||||
const initialSyncResponse = await testSync(auth, [SyncRequestType.PartnerAssetExifsV1]);
|
|
||||||
|
|
||||||
expect(initialSyncResponse).toHaveLength(1);
|
|
||||||
expect(initialSyncResponse).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
{
|
|
||||||
ack: expect.any(String),
|
|
||||||
data: {
|
|
||||||
assetId: asset.id,
|
|
||||||
city: null,
|
|
||||||
country: null,
|
|
||||||
dateTimeOriginal: null,
|
|
||||||
description: '',
|
|
||||||
exifImageHeight: null,
|
|
||||||
exifImageWidth: null,
|
|
||||||
exposureTime: null,
|
|
||||||
fNumber: null,
|
|
||||||
fileSizeInByte: null,
|
|
||||||
focalLength: null,
|
|
||||||
fps: null,
|
|
||||||
iso: null,
|
|
||||||
latitude: null,
|
|
||||||
lensModel: null,
|
|
||||||
longitude: null,
|
|
||||||
make: 'Canon',
|
|
||||||
model: null,
|
|
||||||
modifyDate: null,
|
|
||||||
orientation: null,
|
|
||||||
profileDescription: null,
|
|
||||||
projectionType: null,
|
|
||||||
rating: null,
|
|
||||||
state: null,
|
|
||||||
timeZone: null,
|
|
||||||
},
|
|
||||||
type: SyncEntityType.PartnerAssetExifV1,
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
|
|
||||||
const acks = [initialSyncResponse[0].ack];
|
|
||||||
await sut.setAcks(auth, { acks });
|
|
||||||
|
|
||||||
const ackSyncResponse = await testSync(auth, [SyncRequestType.PartnerAssetExifsV1]);
|
|
||||||
|
|
||||||
expect(ackSyncResponse).toHaveLength(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not sync partner asset exif for own user', async () => {
|
|
||||||
const { auth, getRepository, testSync } = await setup();
|
|
||||||
|
|
||||||
const userRepo = getRepository('user');
|
|
||||||
const user2 = mediumFactory.userInsert();
|
|
||||||
await userRepo.create(user2);
|
|
||||||
|
|
||||||
const partnerRepo = getRepository('partner');
|
|
||||||
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
|
||||||
|
|
||||||
const assetRepo = getRepository('asset');
|
|
||||||
const asset = mediumFactory.assetInsert({ ownerId: auth.user.id });
|
|
||||||
await assetRepo.create(asset);
|
|
||||||
await assetRepo.upsertExif({ assetId: asset.id, make: 'Canon' });
|
|
||||||
|
|
||||||
await expect(testSync(auth, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(1);
|
|
||||||
await expect(testSync(auth, [SyncRequestType.PartnerAssetExifsV1])).resolves.toHaveLength(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not sync partner asset exif for unrelated user', async () => {
|
|
||||||
const { auth, getRepository, testSync } = await setup();
|
|
||||||
|
|
||||||
const userRepo = getRepository('user');
|
|
||||||
|
|
||||||
const user2 = mediumFactory.userInsert();
|
|
||||||
const user3 = mediumFactory.userInsert();
|
|
||||||
await Promise.all([userRepo.create(user2), userRepo.create(user3)]);
|
|
||||||
|
|
||||||
const partnerRepo = getRepository('partner');
|
|
||||||
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
|
||||||
|
|
||||||
const assetRepo = getRepository('asset');
|
|
||||||
const asset = mediumFactory.assetInsert({ ownerId: user3.id });
|
|
||||||
await assetRepo.create(asset);
|
|
||||||
await assetRepo.upsertExif({ assetId: asset.id, make: 'Canon' });
|
|
||||||
|
|
||||||
const sessionRepo = getRepository('session');
|
|
||||||
const session = mediumFactory.sessionInsert({ userId: user3.id });
|
|
||||||
await sessionRepo.create(session);
|
|
||||||
|
|
||||||
const authUser3 = factory.auth({ session, user: user3 });
|
|
||||||
await expect(testSync(authUser3, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(1);
|
|
||||||
await expect(testSync(auth, [SyncRequestType.PartnerAssetExifsV1])).resolves.toHaveLength(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
269
server/test/medium/specs/sync/sync-album-user.spec.ts
Normal file
269
server/test/medium/specs/sync/sync-album-user.spec.ts
Normal file
@ -0,0 +1,269 @@
|
|||||||
|
import { Kysely } from 'kysely';
|
||||||
|
import { DB } from 'src/db';
|
||||||
|
import { AlbumUserRole, SyncEntityType, SyncRequestType } from 'src/enum';
|
||||||
|
import { mediumFactory, newSyncAuthUser, newSyncTest } from 'test/medium.factory';
|
||||||
|
import { getKyselyDB } from 'test/utils';
|
||||||
|
|
||||||
|
let defaultDatabase: Kysely<DB>;
|
||||||
|
|
||||||
|
const setup = async (db?: Kysely<DB>) => {
|
||||||
|
const database = db || defaultDatabase;
|
||||||
|
const result = newSyncTest({ db: database });
|
||||||
|
const { auth, create } = newSyncAuthUser();
|
||||||
|
await create(database);
|
||||||
|
return { ...result, auth };
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
defaultDatabase = await getKyselyDB();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(SyncRequestType.AlbumUsersV1, () => {
|
||||||
|
it('should sync an album user with the correct properties', async () => {
|
||||||
|
const { auth, getRepository, testSync } = await setup();
|
||||||
|
|
||||||
|
const albumRepo = getRepository('album');
|
||||||
|
const albumUserRepo = getRepository('albumUser');
|
||||||
|
const userRepo = getRepository('user');
|
||||||
|
|
||||||
|
const album = mediumFactory.albumInsert({ ownerId: auth.user.id });
|
||||||
|
await albumRepo.create(album, [], []);
|
||||||
|
|
||||||
|
const user = mediumFactory.userInsert();
|
||||||
|
await userRepo.create(user);
|
||||||
|
|
||||||
|
const albumUser = { albumsId: album.id, usersId: user.id, role: AlbumUserRole.EDITOR };
|
||||||
|
await albumUserRepo.create(albumUser);
|
||||||
|
|
||||||
|
await expect(testSync(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: expect.objectContaining({
|
||||||
|
albumId: albumUser.albumsId,
|
||||||
|
role: albumUser.role,
|
||||||
|
userId: albumUser.usersId,
|
||||||
|
}),
|
||||||
|
type: SyncEntityType.AlbumUserV1,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
describe('owner', () => {
|
||||||
|
it('should detect and sync a new shared user', async () => {
|
||||||
|
const { auth, testSync, getRepository } = await setup();
|
||||||
|
|
||||||
|
const albumRepo = getRepository('album');
|
||||||
|
const albumUserRepo = getRepository('albumUser');
|
||||||
|
const userRepo = getRepository('user');
|
||||||
|
|
||||||
|
const user1 = mediumFactory.userInsert();
|
||||||
|
await userRepo.create(user1);
|
||||||
|
|
||||||
|
const album = mediumFactory.albumInsert({ ownerId: auth.user.id });
|
||||||
|
await albumRepo.create(album, [], []);
|
||||||
|
|
||||||
|
const albumUser = { albumsId: album.id, usersId: user1.id, role: AlbumUserRole.EDITOR };
|
||||||
|
await albumUserRepo.create(albumUser);
|
||||||
|
|
||||||
|
await expect(testSync(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: expect.objectContaining({
|
||||||
|
albumId: albumUser.albumsId,
|
||||||
|
role: albumUser.role,
|
||||||
|
userId: albumUser.usersId,
|
||||||
|
}),
|
||||||
|
type: SyncEntityType.AlbumUserV1,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect and sync an updated shared user', async () => {
|
||||||
|
const { auth, testSync, getRepository, sut } = await setup();
|
||||||
|
|
||||||
|
const albumRepo = getRepository('album');
|
||||||
|
const albumUserRepo = getRepository('albumUser');
|
||||||
|
const userRepo = getRepository('user');
|
||||||
|
|
||||||
|
const user1 = mediumFactory.userInsert();
|
||||||
|
await userRepo.create(user1);
|
||||||
|
|
||||||
|
const album = mediumFactory.albumInsert({ ownerId: auth.user.id });
|
||||||
|
await albumRepo.create(album, [], []);
|
||||||
|
|
||||||
|
const albumUser = { albumsId: album.id, usersId: user1.id, role: AlbumUserRole.EDITOR };
|
||||||
|
await albumUserRepo.create(albumUser);
|
||||||
|
|
||||||
|
const initialSyncResponse = await testSync(auth, [SyncRequestType.AlbumUsersV1]);
|
||||||
|
const acks = [initialSyncResponse[0].ack];
|
||||||
|
await sut.setAcks(auth, { acks });
|
||||||
|
|
||||||
|
await expect(testSync(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([]);
|
||||||
|
|
||||||
|
await albumUserRepo.update({ albumsId: album.id, usersId: user1.id }, { role: AlbumUserRole.VIEWER });
|
||||||
|
|
||||||
|
await expect(testSync(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: expect.objectContaining({
|
||||||
|
albumId: albumUser.albumsId,
|
||||||
|
role: AlbumUserRole.VIEWER,
|
||||||
|
userId: albumUser.usersId,
|
||||||
|
}),
|
||||||
|
type: SyncEntityType.AlbumUserV1,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect and sync a deleted shared user', async () => {
|
||||||
|
const { auth, testSync, getRepository, sut } = await setup();
|
||||||
|
|
||||||
|
const albumRepo = getRepository('album');
|
||||||
|
const albumUserRepo = getRepository('albumUser');
|
||||||
|
const userRepo = getRepository('user');
|
||||||
|
|
||||||
|
const user1 = mediumFactory.userInsert();
|
||||||
|
await userRepo.create(user1);
|
||||||
|
|
||||||
|
const album = mediumFactory.albumInsert({ ownerId: auth.user.id });
|
||||||
|
await albumRepo.create(album, [], []);
|
||||||
|
|
||||||
|
const albumUser = { albumsId: album.id, usersId: user1.id, role: AlbumUserRole.EDITOR };
|
||||||
|
await albumUserRepo.create(albumUser);
|
||||||
|
|
||||||
|
const initialSyncResponse = await testSync(auth, [SyncRequestType.AlbumUsersV1]);
|
||||||
|
const acks = [initialSyncResponse[0].ack];
|
||||||
|
await sut.setAcks(auth, { acks });
|
||||||
|
|
||||||
|
await expect(testSync(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([]);
|
||||||
|
|
||||||
|
await albumUserRepo.delete({ albumsId: album.id, usersId: user1.id });
|
||||||
|
|
||||||
|
await expect(testSync(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: expect.objectContaining({
|
||||||
|
albumId: albumUser.albumsId,
|
||||||
|
userId: albumUser.usersId,
|
||||||
|
}),
|
||||||
|
type: SyncEntityType.AlbumUserDeleteV1,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('shared user', () => {
|
||||||
|
it('should detect and sync a new shared user', async () => {
|
||||||
|
const { auth, testSync, getRepository } = await setup();
|
||||||
|
|
||||||
|
const albumRepo = getRepository('album');
|
||||||
|
const albumUserRepo = getRepository('albumUser');
|
||||||
|
const userRepo = getRepository('user');
|
||||||
|
|
||||||
|
const user1 = mediumFactory.userInsert();
|
||||||
|
await userRepo.create(user1);
|
||||||
|
|
||||||
|
const album = mediumFactory.albumInsert({ ownerId: user1.id });
|
||||||
|
await albumRepo.create(album, [], []);
|
||||||
|
|
||||||
|
const albumUser = { albumsId: album.id, usersId: auth.user.id, role: AlbumUserRole.EDITOR };
|
||||||
|
await albumUserRepo.create(albumUser);
|
||||||
|
|
||||||
|
await expect(testSync(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: expect.objectContaining({
|
||||||
|
albumId: albumUser.albumsId,
|
||||||
|
role: albumUser.role,
|
||||||
|
userId: albumUser.usersId,
|
||||||
|
}),
|
||||||
|
type: SyncEntityType.AlbumUserV1,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect and sync an updated shared user', async () => {
|
||||||
|
const { auth, testSync, getRepository, sut } = await setup();
|
||||||
|
|
||||||
|
const albumRepo = getRepository('album');
|
||||||
|
const albumUserRepo = getRepository('albumUser');
|
||||||
|
const userRepo = getRepository('user');
|
||||||
|
|
||||||
|
const owner = mediumFactory.userInsert();
|
||||||
|
const user = mediumFactory.userInsert();
|
||||||
|
await Promise.all([userRepo.create(owner), userRepo.create(user)]);
|
||||||
|
|
||||||
|
const album = mediumFactory.albumInsert({ ownerId: owner.id });
|
||||||
|
await albumRepo.create(
|
||||||
|
album,
|
||||||
|
[],
|
||||||
|
[
|
||||||
|
{ userId: auth.user.id, role: AlbumUserRole.EDITOR },
|
||||||
|
{ userId: user.id, role: AlbumUserRole.EDITOR },
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
const initialSyncResponse = await testSync(auth, [SyncRequestType.AlbumUsersV1]);
|
||||||
|
expect(initialSyncResponse).toHaveLength(2);
|
||||||
|
const acks = [initialSyncResponse[1].ack];
|
||||||
|
await sut.setAcks(auth, { acks });
|
||||||
|
|
||||||
|
await expect(testSync(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([]);
|
||||||
|
|
||||||
|
await albumUserRepo.update({ albumsId: album.id, usersId: user.id }, { role: AlbumUserRole.VIEWER });
|
||||||
|
|
||||||
|
await expect(testSync(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: expect.objectContaining({
|
||||||
|
albumId: album.id,
|
||||||
|
role: AlbumUserRole.VIEWER,
|
||||||
|
userId: user.id,
|
||||||
|
}),
|
||||||
|
type: SyncEntityType.AlbumUserV1,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect and sync a deleted shared user', async () => {
|
||||||
|
const { auth, testSync, getRepository, sut } = await setup();
|
||||||
|
|
||||||
|
const albumRepo = getRepository('album');
|
||||||
|
const albumUserRepo = getRepository('albumUser');
|
||||||
|
const userRepo = getRepository('user');
|
||||||
|
|
||||||
|
const owner = mediumFactory.userInsert();
|
||||||
|
const user = mediumFactory.userInsert();
|
||||||
|
await Promise.all([userRepo.create(owner), userRepo.create(user)]);
|
||||||
|
|
||||||
|
const album = mediumFactory.albumInsert({ ownerId: owner.id });
|
||||||
|
await albumRepo.create(
|
||||||
|
album,
|
||||||
|
[],
|
||||||
|
[
|
||||||
|
{ userId: auth.user.id, role: AlbumUserRole.EDITOR },
|
||||||
|
{ userId: user.id, role: AlbumUserRole.EDITOR },
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
const initialSyncResponse = await testSync(auth, [SyncRequestType.AlbumUsersV1]);
|
||||||
|
expect(initialSyncResponse).toHaveLength(2);
|
||||||
|
const acks = [initialSyncResponse[1].ack];
|
||||||
|
await sut.setAcks(auth, { acks });
|
||||||
|
|
||||||
|
await expect(testSync(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([]);
|
||||||
|
|
||||||
|
await albumUserRepo.delete({ albumsId: album.id, usersId: user.id });
|
||||||
|
|
||||||
|
await expect(testSync(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: expect.objectContaining({
|
||||||
|
albumId: album.id,
|
||||||
|
userId: user.id,
|
||||||
|
}),
|
||||||
|
type: SyncEntityType.AlbumUserDeleteV1,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
220
server/test/medium/specs/sync/sync-album.spec.ts
Normal file
220
server/test/medium/specs/sync/sync-album.spec.ts
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
import { Kysely } from 'kysely';
|
||||||
|
import { DB } from 'src/db';
|
||||||
|
import { AlbumUserRole, SyncEntityType, SyncRequestType } from 'src/enum';
|
||||||
|
import { mediumFactory, newSyncAuthUser, newSyncTest } from 'test/medium.factory';
|
||||||
|
import { getKyselyDB } from 'test/utils';
|
||||||
|
|
||||||
|
let defaultDatabase: Kysely<DB>;
|
||||||
|
|
||||||
|
const setup = async (db?: Kysely<DB>) => {
|
||||||
|
const database = db || defaultDatabase;
|
||||||
|
const result = newSyncTest({ db: database });
|
||||||
|
const { auth, create } = newSyncAuthUser();
|
||||||
|
await create(database);
|
||||||
|
return { ...result, auth };
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
defaultDatabase = await getKyselyDB();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(SyncRequestType.AlbumsV1, () => {
|
||||||
|
it('should sync an album with the correct properties', async () => {
|
||||||
|
const { auth, getRepository, testSync } = await setup();
|
||||||
|
const albumRepo = getRepository('album');
|
||||||
|
const album = mediumFactory.albumInsert({ ownerId: auth.user.id });
|
||||||
|
await albumRepo.create(album, [], []);
|
||||||
|
await expect(testSync(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: expect.objectContaining({
|
||||||
|
id: album.id,
|
||||||
|
name: album.albumName,
|
||||||
|
ownerId: album.ownerId,
|
||||||
|
}),
|
||||||
|
type: SyncEntityType.AlbumV1,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect and sync a new album', async () => {
|
||||||
|
const { auth, getRepository, testSync } = await setup();
|
||||||
|
const albumRepo = getRepository('album');
|
||||||
|
const album = mediumFactory.albumInsert({ ownerId: auth.user.id });
|
||||||
|
await albumRepo.create(album, [], []);
|
||||||
|
await expect(testSync(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: expect.objectContaining({
|
||||||
|
id: album.id,
|
||||||
|
}),
|
||||||
|
type: SyncEntityType.AlbumV1,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect and sync an album delete', async () => {
|
||||||
|
const { auth, getRepository, testSync } = await setup();
|
||||||
|
const albumRepo = getRepository('album');
|
||||||
|
const album = mediumFactory.albumInsert({ ownerId: auth.user.id });
|
||||||
|
await albumRepo.create(album, [], []);
|
||||||
|
await expect(testSync(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: expect.objectContaining({
|
||||||
|
id: album.id,
|
||||||
|
}),
|
||||||
|
type: SyncEntityType.AlbumV1,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
await albumRepo.delete(album.id);
|
||||||
|
await expect(testSync(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: {
|
||||||
|
albumId: album.id,
|
||||||
|
},
|
||||||
|
type: SyncEntityType.AlbumDeleteV1,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('shared albums', () => {
|
||||||
|
it('should detect and sync an album create', async () => {
|
||||||
|
const { auth, getRepository, testSync } = await setup();
|
||||||
|
const albumRepo = getRepository('album');
|
||||||
|
const userRepo = getRepository('user');
|
||||||
|
|
||||||
|
const user2 = mediumFactory.userInsert();
|
||||||
|
await userRepo.create(user2);
|
||||||
|
|
||||||
|
const album = mediumFactory.albumInsert({ ownerId: user2.id });
|
||||||
|
await albumRepo.create(album, [], [{ userId: auth.user.id, role: AlbumUserRole.EDITOR }]);
|
||||||
|
|
||||||
|
await expect(testSync(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: expect.objectContaining({ id: album.id }),
|
||||||
|
type: SyncEntityType.AlbumV1,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect and sync an album share (share before sync)', async () => {
|
||||||
|
const { auth, getRepository, testSync } = await setup();
|
||||||
|
const albumRepo = getRepository('album');
|
||||||
|
const albumUserRepo = getRepository('albumUser');
|
||||||
|
const userRepo = getRepository('user');
|
||||||
|
|
||||||
|
const user2 = mediumFactory.userInsert();
|
||||||
|
await userRepo.create(user2);
|
||||||
|
|
||||||
|
const album = mediumFactory.albumInsert({ ownerId: user2.id });
|
||||||
|
await albumRepo.create(album, [], []);
|
||||||
|
await albumUserRepo.create({ usersId: auth.user.id, albumsId: album.id, role: AlbumUserRole.EDITOR });
|
||||||
|
|
||||||
|
await expect(testSync(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: expect.objectContaining({ id: album.id }),
|
||||||
|
type: SyncEntityType.AlbumV1,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect and sync an album share (share after sync)', async () => {
|
||||||
|
const { auth, getRepository, sut, testSync } = await setup();
|
||||||
|
const albumRepo = getRepository('album');
|
||||||
|
const albumUserRepo = getRepository('albumUser');
|
||||||
|
const userRepo = getRepository('user');
|
||||||
|
|
||||||
|
const user2 = mediumFactory.userInsert();
|
||||||
|
await userRepo.create(user2);
|
||||||
|
|
||||||
|
const userAlbum = mediumFactory.albumInsert({ ownerId: auth.user.id });
|
||||||
|
const user2Album = mediumFactory.albumInsert({ ownerId: user2.id });
|
||||||
|
await Promise.all([albumRepo.create(user2Album, [], []), albumRepo.create(userAlbum, [], [])]);
|
||||||
|
|
||||||
|
const initialSyncResponse = await testSync(auth, [SyncRequestType.AlbumsV1]);
|
||||||
|
|
||||||
|
expect(initialSyncResponse).toEqual([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: expect.objectContaining({ id: userAlbum.id }),
|
||||||
|
type: SyncEntityType.AlbumV1,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const acks = [initialSyncResponse[0].ack];
|
||||||
|
await sut.setAcks(auth, { acks });
|
||||||
|
|
||||||
|
await albumUserRepo.create({ usersId: auth.user.id, albumsId: user2Album.id, role: AlbumUserRole.EDITOR });
|
||||||
|
|
||||||
|
await expect(testSync(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: expect.objectContaining({ id: user2Album.id }),
|
||||||
|
type: SyncEntityType.AlbumV1,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect and sync an album delete`', async () => {
|
||||||
|
const { auth, getRepository, testSync, sut } = await setup();
|
||||||
|
const albumRepo = getRepository('album');
|
||||||
|
const userRepo = getRepository('user');
|
||||||
|
|
||||||
|
const user2 = mediumFactory.userInsert();
|
||||||
|
await userRepo.create(user2);
|
||||||
|
|
||||||
|
const album = mediumFactory.albumInsert({ ownerId: user2.id });
|
||||||
|
await albumRepo.create(album, [], [{ userId: auth.user.id, role: AlbumUserRole.EDITOR }]);
|
||||||
|
|
||||||
|
const initialSyncResponse = await testSync(auth, [SyncRequestType.AlbumsV1]);
|
||||||
|
const acks = [initialSyncResponse[0].ack];
|
||||||
|
await sut.setAcks(auth, { acks });
|
||||||
|
|
||||||
|
await expect(testSync(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([]);
|
||||||
|
|
||||||
|
await albumRepo.delete(album.id);
|
||||||
|
|
||||||
|
await expect(testSync(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: { albumId: album.id },
|
||||||
|
type: SyncEntityType.AlbumDeleteV1,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect and sync an album unshare as an album delete', async () => {
|
||||||
|
const { auth, getRepository, testSync, sut } = await setup();
|
||||||
|
const albumRepo = getRepository('album');
|
||||||
|
const albumUserRepo = getRepository('albumUser');
|
||||||
|
const userRepo = getRepository('user');
|
||||||
|
|
||||||
|
const user2 = mediumFactory.userInsert();
|
||||||
|
await userRepo.create(user2);
|
||||||
|
|
||||||
|
const album = mediumFactory.albumInsert({ ownerId: user2.id });
|
||||||
|
await albumRepo.create(album, [], [{ userId: auth.user.id, role: AlbumUserRole.EDITOR }]);
|
||||||
|
|
||||||
|
const initialSyncResponse = await testSync(auth, [SyncRequestType.AlbumsV1]);
|
||||||
|
const acks = [initialSyncResponse[0].ack];
|
||||||
|
await sut.setAcks(auth, { acks });
|
||||||
|
|
||||||
|
await expect(testSync(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([]);
|
||||||
|
|
||||||
|
await albumUserRepo.delete({ albumsId: album.id, usersId: auth.user.id });
|
||||||
|
|
||||||
|
await expect(testSync(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: { albumId: album.id },
|
||||||
|
type: SyncEntityType.AlbumDeleteV1,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
100
server/test/medium/specs/sync/sync-asset-exif.spec.ts
Normal file
100
server/test/medium/specs/sync/sync-asset-exif.spec.ts
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import { Kysely } from 'kysely';
|
||||||
|
import { DB } from 'src/db';
|
||||||
|
import { SyncEntityType, SyncRequestType } from 'src/enum';
|
||||||
|
import { mediumFactory, newSyncAuthUser, newSyncTest } 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 database = db || defaultDatabase;
|
||||||
|
const result = newSyncTest({ db: database });
|
||||||
|
const { auth, create } = newSyncAuthUser();
|
||||||
|
await create(database);
|
||||||
|
return { ...result, auth };
|
||||||
|
};
|
||||||
|
beforeAll(async () => {
|
||||||
|
defaultDatabase = await getKyselyDB();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe.concurrent(SyncRequestType.AssetExifsV1, () => {
|
||||||
|
it('should detect and sync the first asset exif', async () => {
|
||||||
|
const { auth, sut, getRepository, testSync } = await setup();
|
||||||
|
|
||||||
|
const assetRepo = getRepository('asset');
|
||||||
|
const asset = mediumFactory.assetInsert({ ownerId: auth.user.id });
|
||||||
|
await assetRepo.create(asset);
|
||||||
|
await assetRepo.upsertExif({ assetId: asset.id, make: 'Canon' });
|
||||||
|
|
||||||
|
const initialSyncResponse = await testSync(auth, [SyncRequestType.AssetExifsV1]);
|
||||||
|
|
||||||
|
expect(initialSyncResponse).toHaveLength(1);
|
||||||
|
expect(initialSyncResponse).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: {
|
||||||
|
assetId: asset.id,
|
||||||
|
city: null,
|
||||||
|
country: null,
|
||||||
|
dateTimeOriginal: null,
|
||||||
|
description: '',
|
||||||
|
exifImageHeight: null,
|
||||||
|
exifImageWidth: null,
|
||||||
|
exposureTime: null,
|
||||||
|
fNumber: null,
|
||||||
|
fileSizeInByte: null,
|
||||||
|
focalLength: null,
|
||||||
|
fps: null,
|
||||||
|
iso: null,
|
||||||
|
latitude: null,
|
||||||
|
lensModel: null,
|
||||||
|
longitude: null,
|
||||||
|
make: 'Canon',
|
||||||
|
model: null,
|
||||||
|
modifyDate: null,
|
||||||
|
orientation: null,
|
||||||
|
profileDescription: null,
|
||||||
|
projectionType: null,
|
||||||
|
rating: null,
|
||||||
|
state: null,
|
||||||
|
timeZone: null,
|
||||||
|
},
|
||||||
|
type: SyncEntityType.AssetExifV1,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const acks = [initialSyncResponse[0].ack];
|
||||||
|
await sut.setAcks(auth, { acks });
|
||||||
|
|
||||||
|
const ackSyncResponse = await testSync(auth, [SyncRequestType.AssetExifsV1]);
|
||||||
|
|
||||||
|
expect(ackSyncResponse).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should only sync asset exif for own user', async () => {
|
||||||
|
const { auth, getRepository, testSync } = await setup();
|
||||||
|
|
||||||
|
const userRepo = getRepository('user');
|
||||||
|
const user2 = mediumFactory.userInsert();
|
||||||
|
await userRepo.create(user2);
|
||||||
|
|
||||||
|
const partnerRepo = getRepository('partner');
|
||||||
|
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||||
|
|
||||||
|
const assetRepo = getRepository('asset');
|
||||||
|
const asset = mediumFactory.assetInsert({ ownerId: user2.id });
|
||||||
|
await assetRepo.create(asset);
|
||||||
|
await assetRepo.upsertExif({ assetId: asset.id, make: 'Canon' });
|
||||||
|
|
||||||
|
const sessionRepo = getRepository('session');
|
||||||
|
const session = mediumFactory.sessionInsert({ userId: user2.id });
|
||||||
|
await sessionRepo.create(session);
|
||||||
|
|
||||||
|
const auth2 = factory.auth({ session, user: user2 });
|
||||||
|
await expect(testSync(auth2, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(1);
|
||||||
|
await expect(testSync(auth, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(0);
|
||||||
|
});
|
||||||
|
});
|
130
server/test/medium/specs/sync/sync-asset.spec.ts
Normal file
130
server/test/medium/specs/sync/sync-asset.spec.ts
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
import { Kysely } from 'kysely';
|
||||||
|
import { DB } from 'src/db';
|
||||||
|
import { SyncEntityType, SyncRequestType } from 'src/enum';
|
||||||
|
import { mediumFactory, newSyncAuthUser, newSyncTest } 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 database = db || defaultDatabase;
|
||||||
|
const result = newSyncTest({ db: database });
|
||||||
|
const { auth, create } = newSyncAuthUser();
|
||||||
|
await create(database);
|
||||||
|
return { ...result, auth };
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
defaultDatabase = await getKyselyDB();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe.concurrent(SyncEntityType.AssetV1, () => {
|
||||||
|
it('should detect and sync the first asset', async () => {
|
||||||
|
const { auth, sut, getRepository, testSync } = await setup();
|
||||||
|
|
||||||
|
const checksum = '1115vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA=';
|
||||||
|
const thumbhash = '2225vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA=';
|
||||||
|
const date = new Date().toISOString();
|
||||||
|
|
||||||
|
const assetRepo = getRepository('asset');
|
||||||
|
const asset = mediumFactory.assetInsert({
|
||||||
|
ownerId: auth.user.id,
|
||||||
|
checksum: Buffer.from(checksum, 'base64'),
|
||||||
|
thumbhash: Buffer.from(thumbhash, 'base64'),
|
||||||
|
fileCreatedAt: date,
|
||||||
|
fileModifiedAt: date,
|
||||||
|
localDateTime: date,
|
||||||
|
deletedAt: null,
|
||||||
|
});
|
||||||
|
await assetRepo.create(asset);
|
||||||
|
|
||||||
|
const initialSyncResponse = await testSync(auth, [SyncRequestType.AssetsV1]);
|
||||||
|
|
||||||
|
expect(initialSyncResponse).toHaveLength(1);
|
||||||
|
expect(initialSyncResponse).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: {
|
||||||
|
id: asset.id,
|
||||||
|
ownerId: asset.ownerId,
|
||||||
|
thumbhash,
|
||||||
|
checksum,
|
||||||
|
deletedAt: asset.deletedAt,
|
||||||
|
fileCreatedAt: asset.fileCreatedAt,
|
||||||
|
fileModifiedAt: asset.fileModifiedAt,
|
||||||
|
isFavorite: asset.isFavorite,
|
||||||
|
localDateTime: asset.localDateTime,
|
||||||
|
type: asset.type,
|
||||||
|
visibility: asset.visibility,
|
||||||
|
},
|
||||||
|
type: 'AssetV1',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const acks = [initialSyncResponse[0].ack];
|
||||||
|
await sut.setAcks(auth, { acks });
|
||||||
|
|
||||||
|
const ackSyncResponse = await testSync(auth, [SyncRequestType.AssetsV1]);
|
||||||
|
|
||||||
|
expect(ackSyncResponse).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect and sync a deleted asset', async () => {
|
||||||
|
const { auth, sut, getRepository, testSync } = await setup();
|
||||||
|
|
||||||
|
const assetRepo = getRepository('asset');
|
||||||
|
const asset = mediumFactory.assetInsert({ ownerId: auth.user.id });
|
||||||
|
await assetRepo.create(asset);
|
||||||
|
await assetRepo.remove(asset);
|
||||||
|
|
||||||
|
const response = await testSync(auth, [SyncRequestType.AssetsV1]);
|
||||||
|
|
||||||
|
expect(response).toHaveLength(1);
|
||||||
|
expect(response).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: {
|
||||||
|
assetId: asset.id,
|
||||||
|
},
|
||||||
|
type: 'AssetDeleteV1',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const acks = response.map(({ ack }) => ack);
|
||||||
|
await sut.setAcks(auth, { acks });
|
||||||
|
|
||||||
|
const ackSyncResponse = await testSync(auth, [SyncRequestType.AssetsV1]);
|
||||||
|
|
||||||
|
expect(ackSyncResponse).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not sync an asset or asset delete for an unrelated user', async () => {
|
||||||
|
const { auth, getRepository, testSync } = await setup();
|
||||||
|
|
||||||
|
const userRepo = getRepository('user');
|
||||||
|
const user2 = mediumFactory.userInsert();
|
||||||
|
await userRepo.create(user2);
|
||||||
|
|
||||||
|
const sessionRepo = getRepository('session');
|
||||||
|
const session = mediumFactory.sessionInsert({ userId: user2.id });
|
||||||
|
await sessionRepo.create(session);
|
||||||
|
|
||||||
|
const assetRepo = getRepository('asset');
|
||||||
|
const asset = mediumFactory.assetInsert({ ownerId: user2.id });
|
||||||
|
await assetRepo.create(asset);
|
||||||
|
|
||||||
|
const auth2 = factory.auth({ session, user: user2 });
|
||||||
|
|
||||||
|
expect(await testSync(auth2, [SyncRequestType.AssetsV1])).toHaveLength(1);
|
||||||
|
expect(await testSync(auth, [SyncRequestType.AssetsV1])).toHaveLength(0);
|
||||||
|
|
||||||
|
await assetRepo.remove(asset);
|
||||||
|
expect(await testSync(auth2, [SyncRequestType.AssetsV1])).toHaveLength(1);
|
||||||
|
expect(await testSync(auth, [SyncRequestType.AssetsV1])).toHaveLength(0);
|
||||||
|
});
|
||||||
|
});
|
129
server/test/medium/specs/sync/sync-partner-asset-exif.spec.ts
Normal file
129
server/test/medium/specs/sync/sync-partner-asset-exif.spec.ts
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
import { Kysely } from 'kysely';
|
||||||
|
import { DB } from 'src/db';
|
||||||
|
import { SyncEntityType, SyncRequestType } from 'src/enum';
|
||||||
|
import { mediumFactory, newSyncAuthUser, newSyncTest } 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 database = db || defaultDatabase;
|
||||||
|
const result = newSyncTest({ db: database });
|
||||||
|
const { auth, create } = newSyncAuthUser();
|
||||||
|
await create(database);
|
||||||
|
return { ...result, auth };
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
defaultDatabase = await getKyselyDB();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe.concurrent(SyncRequestType.PartnerAssetExifsV1, () => {
|
||||||
|
it('should detect and sync the first partner asset exif', async () => {
|
||||||
|
const { auth, sut, getRepository, testSync } = await setup();
|
||||||
|
|
||||||
|
const userRepo = getRepository('user');
|
||||||
|
const user2 = mediumFactory.userInsert();
|
||||||
|
await userRepo.create(user2);
|
||||||
|
|
||||||
|
const partnerRepo = getRepository('partner');
|
||||||
|
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||||
|
|
||||||
|
const assetRepo = getRepository('asset');
|
||||||
|
const asset = mediumFactory.assetInsert({ ownerId: user2.id });
|
||||||
|
await assetRepo.create(asset);
|
||||||
|
await assetRepo.upsertExif({ assetId: asset.id, make: 'Canon' });
|
||||||
|
|
||||||
|
const initialSyncResponse = await testSync(auth, [SyncRequestType.PartnerAssetExifsV1]);
|
||||||
|
|
||||||
|
expect(initialSyncResponse).toHaveLength(1);
|
||||||
|
expect(initialSyncResponse).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: {
|
||||||
|
assetId: asset.id,
|
||||||
|
city: null,
|
||||||
|
country: null,
|
||||||
|
dateTimeOriginal: null,
|
||||||
|
description: '',
|
||||||
|
exifImageHeight: null,
|
||||||
|
exifImageWidth: null,
|
||||||
|
exposureTime: null,
|
||||||
|
fNumber: null,
|
||||||
|
fileSizeInByte: null,
|
||||||
|
focalLength: null,
|
||||||
|
fps: null,
|
||||||
|
iso: null,
|
||||||
|
latitude: null,
|
||||||
|
lensModel: null,
|
||||||
|
longitude: null,
|
||||||
|
make: 'Canon',
|
||||||
|
model: null,
|
||||||
|
modifyDate: null,
|
||||||
|
orientation: null,
|
||||||
|
profileDescription: null,
|
||||||
|
projectionType: null,
|
||||||
|
rating: null,
|
||||||
|
state: null,
|
||||||
|
timeZone: null,
|
||||||
|
},
|
||||||
|
type: SyncEntityType.PartnerAssetExifV1,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const acks = [initialSyncResponse[0].ack];
|
||||||
|
await sut.setAcks(auth, { acks });
|
||||||
|
|
||||||
|
const ackSyncResponse = await testSync(auth, [SyncRequestType.PartnerAssetExifsV1]);
|
||||||
|
|
||||||
|
expect(ackSyncResponse).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not sync partner asset exif for own user', async () => {
|
||||||
|
const { auth, getRepository, testSync } = await setup();
|
||||||
|
|
||||||
|
const userRepo = getRepository('user');
|
||||||
|
const user2 = mediumFactory.userInsert();
|
||||||
|
await userRepo.create(user2);
|
||||||
|
|
||||||
|
const partnerRepo = getRepository('partner');
|
||||||
|
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||||
|
|
||||||
|
const assetRepo = getRepository('asset');
|
||||||
|
const asset = mediumFactory.assetInsert({ ownerId: auth.user.id });
|
||||||
|
await assetRepo.create(asset);
|
||||||
|
await assetRepo.upsertExif({ assetId: asset.id, make: 'Canon' });
|
||||||
|
|
||||||
|
await expect(testSync(auth, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(1);
|
||||||
|
await expect(testSync(auth, [SyncRequestType.PartnerAssetExifsV1])).resolves.toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not sync partner asset exif for unrelated user', async () => {
|
||||||
|
const { auth, getRepository, testSync } = await setup();
|
||||||
|
|
||||||
|
const userRepo = getRepository('user');
|
||||||
|
|
||||||
|
const user2 = mediumFactory.userInsert();
|
||||||
|
const user3 = mediumFactory.userInsert();
|
||||||
|
await Promise.all([userRepo.create(user2), userRepo.create(user3)]);
|
||||||
|
|
||||||
|
const partnerRepo = getRepository('partner');
|
||||||
|
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||||
|
|
||||||
|
const assetRepo = getRepository('asset');
|
||||||
|
const asset = mediumFactory.assetInsert({ ownerId: user3.id });
|
||||||
|
await assetRepo.create(asset);
|
||||||
|
await assetRepo.upsertExif({ assetId: asset.id, make: 'Canon' });
|
||||||
|
|
||||||
|
const sessionRepo = getRepository('session');
|
||||||
|
const session = mediumFactory.sessionInsert({ userId: user3.id });
|
||||||
|
await sessionRepo.create(session);
|
||||||
|
|
||||||
|
const authUser3 = factory.auth({ session, user: user3 });
|
||||||
|
await expect(testSync(authUser3, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(1);
|
||||||
|
await expect(testSync(auth, [SyncRequestType.PartnerAssetExifsV1])).resolves.toHaveLength(0);
|
||||||
|
});
|
||||||
|
});
|
208
server/test/medium/specs/sync/sync-partner-asset.spec.ts
Normal file
208
server/test/medium/specs/sync/sync-partner-asset.spec.ts
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
import { Kysely } from 'kysely';
|
||||||
|
import { DB } from 'src/db';
|
||||||
|
import { SyncEntityType, SyncRequestType } from 'src/enum';
|
||||||
|
import { mediumFactory, newSyncAuthUser, newSyncTest } 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 database = db || defaultDatabase;
|
||||||
|
const result = newSyncTest({ db: database });
|
||||||
|
const { auth, create } = newSyncAuthUser();
|
||||||
|
await create(database);
|
||||||
|
return { ...result, auth };
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
defaultDatabase = await getKyselyDB();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe.concurrent(SyncRequestType.PartnerAssetsV1, () => {
|
||||||
|
it('should detect and sync the first partner asset', async () => {
|
||||||
|
const { auth, sut, getRepository, testSync } = await setup();
|
||||||
|
|
||||||
|
const checksum = '1115vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA=';
|
||||||
|
const thumbhash = '2225vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA=';
|
||||||
|
const date = new Date().toISOString();
|
||||||
|
|
||||||
|
const userRepo = getRepository('user');
|
||||||
|
const user2 = mediumFactory.userInsert();
|
||||||
|
await userRepo.create(user2);
|
||||||
|
|
||||||
|
const assetRepo = getRepository('asset');
|
||||||
|
const asset = mediumFactory.assetInsert({
|
||||||
|
ownerId: user2.id,
|
||||||
|
checksum: Buffer.from(checksum, 'base64'),
|
||||||
|
thumbhash: Buffer.from(thumbhash, 'base64'),
|
||||||
|
fileCreatedAt: date,
|
||||||
|
fileModifiedAt: date,
|
||||||
|
localDateTime: date,
|
||||||
|
deletedAt: null,
|
||||||
|
});
|
||||||
|
await assetRepo.create(asset);
|
||||||
|
|
||||||
|
const partnerRepo = getRepository('partner');
|
||||||
|
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||||
|
|
||||||
|
const initialSyncResponse = await testSync(auth, [SyncRequestType.PartnerAssetsV1]);
|
||||||
|
|
||||||
|
expect(initialSyncResponse).toHaveLength(1);
|
||||||
|
expect(initialSyncResponse).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: {
|
||||||
|
id: asset.id,
|
||||||
|
ownerId: asset.ownerId,
|
||||||
|
thumbhash,
|
||||||
|
checksum,
|
||||||
|
deletedAt: null,
|
||||||
|
fileCreatedAt: date,
|
||||||
|
fileModifiedAt: date,
|
||||||
|
isFavorite: false,
|
||||||
|
localDateTime: date,
|
||||||
|
type: asset.type,
|
||||||
|
visibility: asset.visibility,
|
||||||
|
},
|
||||||
|
type: SyncEntityType.PartnerAssetV1,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const acks = [initialSyncResponse[0].ack];
|
||||||
|
await sut.setAcks(auth, { acks });
|
||||||
|
|
||||||
|
const ackSyncResponse = await testSync(auth, [SyncRequestType.PartnerAssetsV1]);
|
||||||
|
|
||||||
|
expect(ackSyncResponse).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect and sync a deleted partner asset', async () => {
|
||||||
|
const { auth, sut, getRepository, testSync } = await setup();
|
||||||
|
|
||||||
|
const userRepo = getRepository('user');
|
||||||
|
const user2 = mediumFactory.userInsert();
|
||||||
|
await userRepo.create(user2);
|
||||||
|
const asset = mediumFactory.assetInsert({ ownerId: user2.id });
|
||||||
|
|
||||||
|
const assetRepo = getRepository('asset');
|
||||||
|
await assetRepo.create(asset);
|
||||||
|
|
||||||
|
const partnerRepo = getRepository('partner');
|
||||||
|
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||||
|
await assetRepo.remove(asset);
|
||||||
|
|
||||||
|
const response = await testSync(auth, [SyncRequestType.PartnerAssetsV1]);
|
||||||
|
|
||||||
|
expect(response).toHaveLength(1);
|
||||||
|
expect(response).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: {
|
||||||
|
assetId: asset.id,
|
||||||
|
},
|
||||||
|
type: SyncEntityType.PartnerAssetDeleteV1,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const acks = response.map(({ ack }) => ack);
|
||||||
|
await sut.setAcks(auth, { acks });
|
||||||
|
|
||||||
|
const ackSyncResponse = await testSync(auth, [SyncRequestType.PartnerAssetsV1]);
|
||||||
|
|
||||||
|
expect(ackSyncResponse).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not sync a deleted partner asset due to a user delete', async () => {
|
||||||
|
const { auth, getRepository, testSync } = await setup();
|
||||||
|
|
||||||
|
const userRepo = getRepository('user');
|
||||||
|
const user2 = mediumFactory.userInsert();
|
||||||
|
await userRepo.create(user2);
|
||||||
|
|
||||||
|
const partnerRepo = getRepository('partner');
|
||||||
|
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||||
|
|
||||||
|
const assetRepo = getRepository('asset');
|
||||||
|
await assetRepo.create(mediumFactory.assetInsert({ ownerId: user2.id }));
|
||||||
|
|
||||||
|
await userRepo.delete({ id: user2.id }, true);
|
||||||
|
|
||||||
|
const response = await testSync(auth, [SyncRequestType.PartnerAssetsV1]);
|
||||||
|
expect(response).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not sync a deleted partner asset due to a partner delete (unshare)', async () => {
|
||||||
|
const { auth, getRepository, testSync } = await setup();
|
||||||
|
|
||||||
|
const userRepo = getRepository('user');
|
||||||
|
const user2 = mediumFactory.userInsert();
|
||||||
|
await userRepo.create(user2);
|
||||||
|
|
||||||
|
const assetRepo = getRepository('asset');
|
||||||
|
await assetRepo.create(mediumFactory.assetInsert({ ownerId: user2.id }));
|
||||||
|
|
||||||
|
const partnerRepo = getRepository('partner');
|
||||||
|
const partner = { sharedById: user2.id, sharedWithId: auth.user.id };
|
||||||
|
await partnerRepo.create(partner);
|
||||||
|
|
||||||
|
await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(1);
|
||||||
|
|
||||||
|
await partnerRepo.remove(partner);
|
||||||
|
|
||||||
|
await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not sync an asset or asset delete for own user', async () => {
|
||||||
|
const { auth, getRepository, testSync } = await setup();
|
||||||
|
|
||||||
|
const userRepo = getRepository('user');
|
||||||
|
const user2 = mediumFactory.userInsert();
|
||||||
|
await userRepo.create(user2);
|
||||||
|
|
||||||
|
const assetRepo = getRepository('asset');
|
||||||
|
const asset = mediumFactory.assetInsert({ ownerId: auth.user.id });
|
||||||
|
await assetRepo.create(asset);
|
||||||
|
|
||||||
|
const partnerRepo = getRepository('partner');
|
||||||
|
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||||
|
|
||||||
|
await expect(testSync(auth, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1);
|
||||||
|
await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0);
|
||||||
|
|
||||||
|
await assetRepo.remove(asset);
|
||||||
|
|
||||||
|
await expect(testSync(auth, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1);
|
||||||
|
await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not sync an asset or asset delete for unrelated user', async () => {
|
||||||
|
const { auth, getRepository, testSync } = await setup();
|
||||||
|
|
||||||
|
const userRepo = getRepository('user');
|
||||||
|
const user2 = mediumFactory.userInsert();
|
||||||
|
await userRepo.create(user2);
|
||||||
|
|
||||||
|
const sessionRepo = getRepository('session');
|
||||||
|
const session = mediumFactory.sessionInsert({ userId: user2.id });
|
||||||
|
await sessionRepo.create(session);
|
||||||
|
|
||||||
|
const auth2 = factory.auth({ session, user: user2 });
|
||||||
|
|
||||||
|
const assetRepo = getRepository('asset');
|
||||||
|
const asset = mediumFactory.assetInsert({ ownerId: user2.id });
|
||||||
|
await assetRepo.create(asset);
|
||||||
|
|
||||||
|
await expect(testSync(auth2, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1);
|
||||||
|
await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0);
|
||||||
|
|
||||||
|
await assetRepo.remove(asset);
|
||||||
|
|
||||||
|
await expect(testSync(auth2, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1);
|
||||||
|
await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0);
|
||||||
|
});
|
||||||
|
});
|
221
server/test/medium/specs/sync/sync-partner.spec.ts
Normal file
221
server/test/medium/specs/sync/sync-partner.spec.ts
Normal file
@ -0,0 +1,221 @@
|
|||||||
|
import { Kysely } from 'kysely';
|
||||||
|
import { DB } from 'src/db';
|
||||||
|
import { SyncEntityType, SyncRequestType } from 'src/enum';
|
||||||
|
import { mediumFactory, newSyncAuthUser, newSyncTest } from 'test/medium.factory';
|
||||||
|
import { getKyselyDB } from 'test/utils';
|
||||||
|
|
||||||
|
let defaultDatabase: Kysely<DB>;
|
||||||
|
|
||||||
|
const setup = async (db?: Kysely<DB>) => {
|
||||||
|
const database = db || defaultDatabase;
|
||||||
|
const result = newSyncTest({ db: database });
|
||||||
|
const { auth, create } = newSyncAuthUser();
|
||||||
|
await create(database);
|
||||||
|
return { ...result, auth };
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
defaultDatabase = await getKyselyDB();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe.concurrent(SyncEntityType.PartnerV1, () => {
|
||||||
|
it('should detect and sync the first partner', async () => {
|
||||||
|
const { auth, sut, getRepository, testSync } = await setup();
|
||||||
|
|
||||||
|
const user1 = auth.user;
|
||||||
|
const userRepo = getRepository('user');
|
||||||
|
const partnerRepo = getRepository('partner');
|
||||||
|
|
||||||
|
const user2 = mediumFactory.userInsert();
|
||||||
|
await userRepo.create(user2);
|
||||||
|
|
||||||
|
const partner = await partnerRepo.create({ sharedById: user2.id, sharedWithId: user1.id });
|
||||||
|
|
||||||
|
const initialSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]);
|
||||||
|
|
||||||
|
expect(initialSyncResponse).toHaveLength(1);
|
||||||
|
expect(initialSyncResponse).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: {
|
||||||
|
inTimeline: partner.inTimeline,
|
||||||
|
sharedById: partner.sharedById,
|
||||||
|
sharedWithId: partner.sharedWithId,
|
||||||
|
},
|
||||||
|
type: 'PartnerV1',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const acks = [initialSyncResponse[0].ack];
|
||||||
|
await sut.setAcks(auth, { acks });
|
||||||
|
|
||||||
|
const ackSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]);
|
||||||
|
|
||||||
|
expect(ackSyncResponse).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect and sync a deleted partner', async () => {
|
||||||
|
const { auth, sut, getRepository, testSync } = await setup();
|
||||||
|
|
||||||
|
const userRepo = getRepository('user');
|
||||||
|
const user1 = auth.user;
|
||||||
|
const user2 = mediumFactory.userInsert();
|
||||||
|
await userRepo.create(user2);
|
||||||
|
|
||||||
|
const partnerRepo = getRepository('partner');
|
||||||
|
const partner = await partnerRepo.create({ sharedById: user2.id, sharedWithId: user1.id });
|
||||||
|
await partnerRepo.remove(partner);
|
||||||
|
|
||||||
|
const response = await testSync(auth, [SyncRequestType.PartnersV1]);
|
||||||
|
|
||||||
|
expect(response).toHaveLength(1);
|
||||||
|
expect(response).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: {
|
||||||
|
sharedById: partner.sharedById,
|
||||||
|
sharedWithId: partner.sharedWithId,
|
||||||
|
},
|
||||||
|
type: 'PartnerDeleteV1',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const acks = response.map(({ ack }) => ack);
|
||||||
|
await sut.setAcks(auth, { acks });
|
||||||
|
|
||||||
|
const ackSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]);
|
||||||
|
|
||||||
|
expect(ackSyncResponse).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect and sync a partner share both to and from another user', async () => {
|
||||||
|
const { auth, sut, getRepository, testSync } = await setup();
|
||||||
|
|
||||||
|
const userRepo = getRepository('user');
|
||||||
|
const user1 = auth.user;
|
||||||
|
const user2 = await userRepo.create(mediumFactory.userInsert());
|
||||||
|
|
||||||
|
const partnerRepo = getRepository('partner');
|
||||||
|
const partner1 = await partnerRepo.create({ sharedById: user2.id, sharedWithId: user1.id });
|
||||||
|
const partner2 = await partnerRepo.create({ sharedById: user1.id, sharedWithId: user2.id });
|
||||||
|
|
||||||
|
const response = await testSync(auth, [SyncRequestType.PartnersV1]);
|
||||||
|
|
||||||
|
expect(response).toHaveLength(2);
|
||||||
|
expect(response).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: {
|
||||||
|
inTimeline: partner1.inTimeline,
|
||||||
|
sharedById: partner1.sharedById,
|
||||||
|
sharedWithId: partner1.sharedWithId,
|
||||||
|
},
|
||||||
|
type: 'PartnerV1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: {
|
||||||
|
inTimeline: partner2.inTimeline,
|
||||||
|
sharedById: partner2.sharedById,
|
||||||
|
sharedWithId: partner2.sharedWithId,
|
||||||
|
},
|
||||||
|
type: 'PartnerV1',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
await sut.setAcks(auth, { acks: [response[1].ack] });
|
||||||
|
|
||||||
|
const ackSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]);
|
||||||
|
|
||||||
|
expect(ackSyncResponse).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should sync a partner and then an update to that same partner', async () => {
|
||||||
|
const { auth, sut, getRepository, testSync } = await setup();
|
||||||
|
|
||||||
|
const userRepo = getRepository('user');
|
||||||
|
const user1 = auth.user;
|
||||||
|
const user2 = await userRepo.create(mediumFactory.userInsert());
|
||||||
|
|
||||||
|
const partnerRepo = getRepository('partner');
|
||||||
|
const partner = await partnerRepo.create({ sharedById: user2.id, sharedWithId: user1.id });
|
||||||
|
|
||||||
|
const initialSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]);
|
||||||
|
|
||||||
|
expect(initialSyncResponse).toHaveLength(1);
|
||||||
|
expect(initialSyncResponse).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: {
|
||||||
|
inTimeline: partner.inTimeline,
|
||||||
|
sharedById: partner.sharedById,
|
||||||
|
sharedWithId: partner.sharedWithId,
|
||||||
|
},
|
||||||
|
type: 'PartnerV1',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const acks = [initialSyncResponse[0].ack];
|
||||||
|
await sut.setAcks(auth, { acks });
|
||||||
|
|
||||||
|
const updated = await partnerRepo.update(
|
||||||
|
{ sharedById: partner.sharedById, sharedWithId: partner.sharedWithId },
|
||||||
|
{ inTimeline: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
const updatedSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]);
|
||||||
|
|
||||||
|
expect(updatedSyncResponse).toHaveLength(1);
|
||||||
|
expect(updatedSyncResponse).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: {
|
||||||
|
inTimeline: updated.inTimeline,
|
||||||
|
sharedById: updated.sharedById,
|
||||||
|
sharedWithId: updated.sharedWithId,
|
||||||
|
},
|
||||||
|
type: 'PartnerV1',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not sync a partner or partner delete for an unrelated user', async () => {
|
||||||
|
const { auth, getRepository, testSync } = await setup();
|
||||||
|
|
||||||
|
const userRepo = getRepository('user');
|
||||||
|
const user2 = await userRepo.create(mediumFactory.userInsert());
|
||||||
|
const user3 = await userRepo.create(mediumFactory.userInsert());
|
||||||
|
|
||||||
|
const partnerRepo = getRepository('partner');
|
||||||
|
const partner = await partnerRepo.create({ sharedById: user2.id, sharedWithId: user3.id });
|
||||||
|
|
||||||
|
expect(await testSync(auth, [SyncRequestType.PartnersV1])).toHaveLength(0);
|
||||||
|
|
||||||
|
await partnerRepo.remove(partner);
|
||||||
|
|
||||||
|
expect(await testSync(auth, [SyncRequestType.PartnersV1])).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not sync a partner delete after a user is deleted', async () => {
|
||||||
|
const { auth, getRepository, testSync } = await setup();
|
||||||
|
|
||||||
|
const userRepo = getRepository('user');
|
||||||
|
const user2 = await userRepo.create(mediumFactory.userInsert());
|
||||||
|
|
||||||
|
const partnerRepo = getRepository('partner');
|
||||||
|
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||||
|
await userRepo.delete({ id: user2.id }, true);
|
||||||
|
|
||||||
|
expect(await testSync(auth, [SyncRequestType.PartnersV1])).toHaveLength(0);
|
||||||
|
});
|
||||||
|
});
|
12
server/test/medium/specs/sync/sync-types.spec.ts
Normal file
12
server/test/medium/specs/sync/sync-types.spec.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { SyncRequestType } from 'src/enum';
|
||||||
|
import { SYNC_TYPES_ORDER } from 'src/services/sync.service';
|
||||||
|
|
||||||
|
describe('types', () => {
|
||||||
|
it('should have all the types in the ordering variable', () => {
|
||||||
|
for (const key in SyncRequestType) {
|
||||||
|
expect(SYNC_TYPES_ORDER).includes(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(SYNC_TYPES_ORDER.length).toBe(Object.keys(SyncRequestType).length);
|
||||||
|
});
|
||||||
|
});
|
179
server/test/medium/specs/sync/sync-user.spec.ts
Normal file
179
server/test/medium/specs/sync/sync-user.spec.ts
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
import { Kysely } from 'kysely';
|
||||||
|
import { DB } from 'src/db';
|
||||||
|
import { SyncEntityType, SyncRequestType } from 'src/enum';
|
||||||
|
import { mediumFactory, newSyncAuthUser, newSyncTest } from 'test/medium.factory';
|
||||||
|
import { getKyselyDB } from 'test/utils';
|
||||||
|
|
||||||
|
let defaultDatabase: Kysely<DB>;
|
||||||
|
|
||||||
|
const setup = async (db?: Kysely<DB>) => {
|
||||||
|
const database = db || defaultDatabase;
|
||||||
|
const result = newSyncTest({ db: database });
|
||||||
|
const { auth, create } = newSyncAuthUser();
|
||||||
|
await create(database);
|
||||||
|
return { ...result, auth };
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
defaultDatabase = await getKyselyDB();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe.concurrent(SyncEntityType.UserV1, () => {
|
||||||
|
it('should detect and sync the first user', async () => {
|
||||||
|
const { auth, sut, getRepository, testSync } = await setup(await getKyselyDB());
|
||||||
|
|
||||||
|
const userRepo = getRepository('user');
|
||||||
|
const user = await userRepo.get(auth.user.id, { withDeleted: false });
|
||||||
|
if (!user) {
|
||||||
|
expect.fail('First user should exist');
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialSyncResponse = await testSync(auth, [SyncRequestType.UsersV1]);
|
||||||
|
expect(initialSyncResponse).toHaveLength(1);
|
||||||
|
expect(initialSyncResponse).toEqual([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: {
|
||||||
|
deletedAt: user.deletedAt,
|
||||||
|
email: user.email,
|
||||||
|
id: user.id,
|
||||||
|
name: user.name,
|
||||||
|
},
|
||||||
|
type: 'UserV1',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const acks = [initialSyncResponse[0].ack];
|
||||||
|
await sut.setAcks(auth, { acks });
|
||||||
|
const ackSyncResponse = await testSync(auth, [SyncRequestType.UsersV1]);
|
||||||
|
|
||||||
|
expect(ackSyncResponse).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect and sync a soft deleted user', async () => {
|
||||||
|
const { auth, sut, getRepository, testSync } = await setup(await getKyselyDB());
|
||||||
|
|
||||||
|
const deletedAt = new Date().toISOString();
|
||||||
|
const deletedUser = mediumFactory.userInsert({ deletedAt });
|
||||||
|
const deleted = await getRepository('user').create(deletedUser);
|
||||||
|
|
||||||
|
const response = await testSync(auth, [SyncRequestType.UsersV1]);
|
||||||
|
|
||||||
|
expect(response).toHaveLength(2);
|
||||||
|
expect(response).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: {
|
||||||
|
deletedAt: null,
|
||||||
|
email: auth.user.email,
|
||||||
|
id: auth.user.id,
|
||||||
|
name: auth.user.name,
|
||||||
|
},
|
||||||
|
type: 'UserV1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: {
|
||||||
|
deletedAt,
|
||||||
|
email: deleted.email,
|
||||||
|
id: deleted.id,
|
||||||
|
name: deleted.name,
|
||||||
|
},
|
||||||
|
type: 'UserV1',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const acks = [response[1].ack];
|
||||||
|
await sut.setAcks(auth, { acks });
|
||||||
|
const ackSyncResponse = await testSync(auth, [SyncRequestType.UsersV1]);
|
||||||
|
|
||||||
|
expect(ackSyncResponse).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect and sync a deleted user', async () => {
|
||||||
|
const { auth, sut, getRepository, testSync } = await setup(await getKyselyDB());
|
||||||
|
|
||||||
|
const userRepo = getRepository('user');
|
||||||
|
const user = mediumFactory.userInsert();
|
||||||
|
await userRepo.create(user);
|
||||||
|
await userRepo.delete({ id: user.id }, true);
|
||||||
|
|
||||||
|
const response = await testSync(auth, [SyncRequestType.UsersV1]);
|
||||||
|
|
||||||
|
expect(response).toHaveLength(2);
|
||||||
|
expect(response).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: {
|
||||||
|
userId: user.id,
|
||||||
|
},
|
||||||
|
type: 'UserDeleteV1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: {
|
||||||
|
deletedAt: null,
|
||||||
|
email: auth.user.email,
|
||||||
|
id: auth.user.id,
|
||||||
|
name: auth.user.name,
|
||||||
|
},
|
||||||
|
type: 'UserV1',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const acks = response.map(({ ack }) => ack);
|
||||||
|
await sut.setAcks(auth, { acks });
|
||||||
|
const ackSyncResponse = await testSync(auth, [SyncRequestType.UsersV1]);
|
||||||
|
|
||||||
|
expect(ackSyncResponse).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should sync a user and then an update to that same user', async () => {
|
||||||
|
const { auth, sut, getRepository, testSync } = await setup(await getKyselyDB());
|
||||||
|
|
||||||
|
const initialSyncResponse = await testSync(auth, [SyncRequestType.UsersV1]);
|
||||||
|
|
||||||
|
expect(initialSyncResponse).toHaveLength(1);
|
||||||
|
expect(initialSyncResponse).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: {
|
||||||
|
deletedAt: null,
|
||||||
|
email: auth.user.email,
|
||||||
|
id: auth.user.id,
|
||||||
|
name: auth.user.name,
|
||||||
|
},
|
||||||
|
type: 'UserV1',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const acks = [initialSyncResponse[0].ack];
|
||||||
|
await sut.setAcks(auth, { acks });
|
||||||
|
|
||||||
|
const userRepo = getRepository('user');
|
||||||
|
const updated = await userRepo.update(auth.user.id, { name: 'new name' });
|
||||||
|
const updatedSyncResponse = await testSync(auth, [SyncRequestType.UsersV1]);
|
||||||
|
|
||||||
|
expect(updatedSyncResponse).toHaveLength(1);
|
||||||
|
expect(updatedSyncResponse).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: {
|
||||||
|
deletedAt: null,
|
||||||
|
email: auth.user.email,
|
||||||
|
id: auth.user.id,
|
||||||
|
name: updated.name,
|
||||||
|
},
|
||||||
|
type: 'UserV1',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
Loading…
x
Reference in New Issue
Block a user