mirror of
https://github.com/immich-app/immich.git
synced 2025-07-09 03:04:16 -04:00
feat: add album asset sync (#19503)
wip: fix album asset exif and some other refactorings feat: add album assets sync feat: album to assets relation sync Co-authored-by: Zack Pollard <zackpollard@ymail.com>
This commit is contained in:
parent
b001ba44f5
commit
881a96cdf9
1
mobile/openapi/README.md
generated
1
mobile/openapi/README.md
generated
@ -466,6 +466,7 @@ Class | Method | HTTP request | Description
|
|||||||
- [SyncAckDto](doc//SyncAckDto.md)
|
- [SyncAckDto](doc//SyncAckDto.md)
|
||||||
- [SyncAckSetDto](doc//SyncAckSetDto.md)
|
- [SyncAckSetDto](doc//SyncAckSetDto.md)
|
||||||
- [SyncAlbumDeleteV1](doc//SyncAlbumDeleteV1.md)
|
- [SyncAlbumDeleteV1](doc//SyncAlbumDeleteV1.md)
|
||||||
|
- [SyncAlbumToAssetV1](doc//SyncAlbumToAssetV1.md)
|
||||||
- [SyncAlbumUserDeleteV1](doc//SyncAlbumUserDeleteV1.md)
|
- [SyncAlbumUserDeleteV1](doc//SyncAlbumUserDeleteV1.md)
|
||||||
- [SyncAlbumUserV1](doc//SyncAlbumUserV1.md)
|
- [SyncAlbumUserV1](doc//SyncAlbumUserV1.md)
|
||||||
- [SyncAlbumV1](doc//SyncAlbumV1.md)
|
- [SyncAlbumV1](doc//SyncAlbumV1.md)
|
||||||
|
1
mobile/openapi/lib/api.dart
generated
1
mobile/openapi/lib/api.dart
generated
@ -249,6 +249,7 @@ 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_delete_v1.dart';
|
||||||
|
part 'model/sync_album_to_asset_v1.dart';
|
||||||
part 'model/sync_album_user_delete_v1.dart';
|
part 'model/sync_album_user_delete_v1.dart';
|
||||||
part 'model/sync_album_user_v1.dart';
|
part 'model/sync_album_user_v1.dart';
|
||||||
part 'model/sync_album_v1.dart';
|
part 'model/sync_album_v1.dart';
|
||||||
|
2
mobile/openapi/lib/api_client.dart
generated
2
mobile/openapi/lib/api_client.dart
generated
@ -554,6 +554,8 @@ class ApiClient {
|
|||||||
return SyncAckSetDto.fromJson(value);
|
return SyncAckSetDto.fromJson(value);
|
||||||
case 'SyncAlbumDeleteV1':
|
case 'SyncAlbumDeleteV1':
|
||||||
return SyncAlbumDeleteV1.fromJson(value);
|
return SyncAlbumDeleteV1.fromJson(value);
|
||||||
|
case 'SyncAlbumToAssetV1':
|
||||||
|
return SyncAlbumToAssetV1.fromJson(value);
|
||||||
case 'SyncAlbumUserDeleteV1':
|
case 'SyncAlbumUserDeleteV1':
|
||||||
return SyncAlbumUserDeleteV1.fromJson(value);
|
return SyncAlbumUserDeleteV1.fromJson(value);
|
||||||
case 'SyncAlbumUserV1':
|
case 'SyncAlbumUserV1':
|
||||||
|
107
mobile/openapi/lib/model/sync_album_to_asset_v1.dart
generated
Normal file
107
mobile/openapi/lib/model/sync_album_to_asset_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 SyncAlbumToAssetV1 {
|
||||||
|
/// Returns a new [SyncAlbumToAssetV1] instance.
|
||||||
|
SyncAlbumToAssetV1({
|
||||||
|
required this.albumId,
|
||||||
|
required this.assetId,
|
||||||
|
});
|
||||||
|
|
||||||
|
String albumId;
|
||||||
|
|
||||||
|
String assetId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => identical(this, other) || other is SyncAlbumToAssetV1 &&
|
||||||
|
other.albumId == albumId &&
|
||||||
|
other.assetId == assetId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
// ignore: unnecessary_parenthesis
|
||||||
|
(albumId.hashCode) +
|
||||||
|
(assetId.hashCode);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'SyncAlbumToAssetV1[albumId=$albumId, assetId=$assetId]';
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final json = <String, dynamic>{};
|
||||||
|
json[r'albumId'] = this.albumId;
|
||||||
|
json[r'assetId'] = this.assetId;
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new [SyncAlbumToAssetV1] instance and imports its values from
|
||||||
|
/// [value] if it's a [Map], null otherwise.
|
||||||
|
// ignore: prefer_constructors_over_static_methods
|
||||||
|
static SyncAlbumToAssetV1? fromJson(dynamic value) {
|
||||||
|
upgradeDto(value, "SyncAlbumToAssetV1");
|
||||||
|
if (value is Map) {
|
||||||
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
|
return SyncAlbumToAssetV1(
|
||||||
|
albumId: mapValueOfType<String>(json, r'albumId')!,
|
||||||
|
assetId: mapValueOfType<String>(json, r'assetId')!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<SyncAlbumToAssetV1> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <SyncAlbumToAssetV1>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = SyncAlbumToAssetV1.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, SyncAlbumToAssetV1> mapFromJson(dynamic json) {
|
||||||
|
final map = <String, SyncAlbumToAssetV1>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
final value = SyncAlbumToAssetV1.fromJson(entry.value);
|
||||||
|
if (value != null) {
|
||||||
|
map[entry.key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// maps a json object with a list of SyncAlbumToAssetV1-objects as value to a dart map
|
||||||
|
static Map<String, List<SyncAlbumToAssetV1>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final map = <String, List<SyncAlbumToAssetV1>>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
json = json.cast<String, dynamic>();
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
map[entry.key] = SyncAlbumToAssetV1.listFromJson(entry.value, growable: growable,);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The list of required keys that must be present in a JSON.
|
||||||
|
static const requiredKeys = <String>{
|
||||||
|
'albumId',
|
||||||
|
'assetId',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
21
mobile/openapi/lib/model/sync_entity_type.dart
generated
21
mobile/openapi/lib/model/sync_entity_type.dart
generated
@ -40,6 +40,13 @@ class SyncEntityType {
|
|||||||
static const albumUserV1 = SyncEntityType._(r'AlbumUserV1');
|
static const albumUserV1 = SyncEntityType._(r'AlbumUserV1');
|
||||||
static const albumUserBackfillV1 = SyncEntityType._(r'AlbumUserBackfillV1');
|
static const albumUserBackfillV1 = SyncEntityType._(r'AlbumUserBackfillV1');
|
||||||
static const albumUserDeleteV1 = SyncEntityType._(r'AlbumUserDeleteV1');
|
static const albumUserDeleteV1 = SyncEntityType._(r'AlbumUserDeleteV1');
|
||||||
|
static const albumAssetV1 = SyncEntityType._(r'AlbumAssetV1');
|
||||||
|
static const albumAssetBackfillV1 = SyncEntityType._(r'AlbumAssetBackfillV1');
|
||||||
|
static const albumAssetExifV1 = SyncEntityType._(r'AlbumAssetExifV1');
|
||||||
|
static const albumAssetExifBackfillV1 = SyncEntityType._(r'AlbumAssetExifBackfillV1');
|
||||||
|
static const albumToAssetV1 = SyncEntityType._(r'AlbumToAssetV1');
|
||||||
|
static const albumToAssetDeleteV1 = SyncEntityType._(r'AlbumToAssetDeleteV1');
|
||||||
|
static const albumToAssetBackfillV1 = SyncEntityType._(r'AlbumToAssetBackfillV1');
|
||||||
static const syncAckV1 = SyncEntityType._(r'SyncAckV1');
|
static const syncAckV1 = SyncEntityType._(r'SyncAckV1');
|
||||||
|
|
||||||
/// List of all possible values in this [enum][SyncEntityType].
|
/// List of all possible values in this [enum][SyncEntityType].
|
||||||
@ -61,6 +68,13 @@ class SyncEntityType {
|
|||||||
albumUserV1,
|
albumUserV1,
|
||||||
albumUserBackfillV1,
|
albumUserBackfillV1,
|
||||||
albumUserDeleteV1,
|
albumUserDeleteV1,
|
||||||
|
albumAssetV1,
|
||||||
|
albumAssetBackfillV1,
|
||||||
|
albumAssetExifV1,
|
||||||
|
albumAssetExifBackfillV1,
|
||||||
|
albumToAssetV1,
|
||||||
|
albumToAssetDeleteV1,
|
||||||
|
albumToAssetBackfillV1,
|
||||||
syncAckV1,
|
syncAckV1,
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -117,6 +131,13 @@ class SyncEntityTypeTypeTransformer {
|
|||||||
case r'AlbumUserV1': return SyncEntityType.albumUserV1;
|
case r'AlbumUserV1': return SyncEntityType.albumUserV1;
|
||||||
case r'AlbumUserBackfillV1': return SyncEntityType.albumUserBackfillV1;
|
case r'AlbumUserBackfillV1': return SyncEntityType.albumUserBackfillV1;
|
||||||
case r'AlbumUserDeleteV1': return SyncEntityType.albumUserDeleteV1;
|
case r'AlbumUserDeleteV1': return SyncEntityType.albumUserDeleteV1;
|
||||||
|
case r'AlbumAssetV1': return SyncEntityType.albumAssetV1;
|
||||||
|
case r'AlbumAssetBackfillV1': return SyncEntityType.albumAssetBackfillV1;
|
||||||
|
case r'AlbumAssetExifV1': return SyncEntityType.albumAssetExifV1;
|
||||||
|
case r'AlbumAssetExifBackfillV1': return SyncEntityType.albumAssetExifBackfillV1;
|
||||||
|
case r'AlbumToAssetV1': return SyncEntityType.albumToAssetV1;
|
||||||
|
case r'AlbumToAssetDeleteV1': return SyncEntityType.albumToAssetDeleteV1;
|
||||||
|
case r'AlbumToAssetBackfillV1': return SyncEntityType.albumToAssetBackfillV1;
|
||||||
case r'SyncAckV1': return SyncEntityType.syncAckV1;
|
case r'SyncAckV1': return SyncEntityType.syncAckV1;
|
||||||
default:
|
default:
|
||||||
if (!allowNull) {
|
if (!allowNull) {
|
||||||
|
9
mobile/openapi/lib/model/sync_request_type.dart
generated
9
mobile/openapi/lib/model/sync_request_type.dart
generated
@ -31,6 +31,9 @@ class SyncRequestType {
|
|||||||
static const partnerAssetExifsV1 = SyncRequestType._(r'PartnerAssetExifsV1');
|
static const partnerAssetExifsV1 = SyncRequestType._(r'PartnerAssetExifsV1');
|
||||||
static const albumsV1 = SyncRequestType._(r'AlbumsV1');
|
static const albumsV1 = SyncRequestType._(r'AlbumsV1');
|
||||||
static const albumUsersV1 = SyncRequestType._(r'AlbumUsersV1');
|
static const albumUsersV1 = SyncRequestType._(r'AlbumUsersV1');
|
||||||
|
static const albumToAssetsV1 = SyncRequestType._(r'AlbumToAssetsV1');
|
||||||
|
static const albumAssetsV1 = SyncRequestType._(r'AlbumAssetsV1');
|
||||||
|
static const albumAssetExifsV1 = SyncRequestType._(r'AlbumAssetExifsV1');
|
||||||
|
|
||||||
/// 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>[
|
||||||
@ -42,6 +45,9 @@ class SyncRequestType {
|
|||||||
partnerAssetExifsV1,
|
partnerAssetExifsV1,
|
||||||
albumsV1,
|
albumsV1,
|
||||||
albumUsersV1,
|
albumUsersV1,
|
||||||
|
albumToAssetsV1,
|
||||||
|
albumAssetsV1,
|
||||||
|
albumAssetExifsV1,
|
||||||
];
|
];
|
||||||
|
|
||||||
static SyncRequestType? fromJson(dynamic value) => SyncRequestTypeTypeTransformer().decode(value);
|
static SyncRequestType? fromJson(dynamic value) => SyncRequestTypeTypeTransformer().decode(value);
|
||||||
@ -88,6 +94,9 @@ class SyncRequestTypeTypeTransformer {
|
|||||||
case r'PartnerAssetExifsV1': return SyncRequestType.partnerAssetExifsV1;
|
case r'PartnerAssetExifsV1': return SyncRequestType.partnerAssetExifsV1;
|
||||||
case r'AlbumsV1': return SyncRequestType.albumsV1;
|
case r'AlbumsV1': return SyncRequestType.albumsV1;
|
||||||
case r'AlbumUsersV1': return SyncRequestType.albumUsersV1;
|
case r'AlbumUsersV1': return SyncRequestType.albumUsersV1;
|
||||||
|
case r'AlbumToAssetsV1': return SyncRequestType.albumToAssetsV1;
|
||||||
|
case r'AlbumAssetsV1': return SyncRequestType.albumAssetsV1;
|
||||||
|
case r'AlbumAssetExifsV1': return SyncRequestType.albumAssetExifsV1;
|
||||||
default:
|
default:
|
||||||
if (!allowNull) {
|
if (!allowNull) {
|
||||||
throw ArgumentError('Unknown enum value to decode: $data');
|
throw ArgumentError('Unknown enum value to decode: $data');
|
||||||
|
@ -13435,6 +13435,10 @@
|
|||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"SyncAckV1": {
|
||||||
|
"properties": {},
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"SyncAlbumDeleteV1": {
|
"SyncAlbumDeleteV1": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"albumId": {
|
"albumId": {
|
||||||
@ -13446,6 +13450,21 @@
|
|||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"SyncAlbumToAssetV1": {
|
||||||
|
"properties": {
|
||||||
|
"albumId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"assetId": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"albumId",
|
||||||
|
"assetId"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"SyncAlbumUserDeleteV1": {
|
"SyncAlbumUserDeleteV1": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"albumId": {
|
"albumId": {
|
||||||
@ -13774,6 +13793,13 @@
|
|||||||
"AlbumUserV1",
|
"AlbumUserV1",
|
||||||
"AlbumUserBackfillV1",
|
"AlbumUserBackfillV1",
|
||||||
"AlbumUserDeleteV1",
|
"AlbumUserDeleteV1",
|
||||||
|
"AlbumAssetV1",
|
||||||
|
"AlbumAssetBackfillV1",
|
||||||
|
"AlbumAssetExifV1",
|
||||||
|
"AlbumAssetExifBackfillV1",
|
||||||
|
"AlbumToAssetV1",
|
||||||
|
"AlbumToAssetDeleteV1",
|
||||||
|
"AlbumToAssetBackfillV1",
|
||||||
"SyncAckV1"
|
"SyncAckV1"
|
||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@ -13821,7 +13847,10 @@
|
|||||||
"PartnerAssetsV1",
|
"PartnerAssetsV1",
|
||||||
"PartnerAssetExifsV1",
|
"PartnerAssetExifsV1",
|
||||||
"AlbumsV1",
|
"AlbumsV1",
|
||||||
"AlbumUsersV1"
|
"AlbumUsersV1",
|
||||||
|
"AlbumToAssetsV1",
|
||||||
|
"AlbumAssetsV1",
|
||||||
|
"AlbumAssetExifsV1"
|
||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
@ -4078,6 +4078,13 @@ export enum SyncEntityType {
|
|||||||
AlbumUserV1 = "AlbumUserV1",
|
AlbumUserV1 = "AlbumUserV1",
|
||||||
AlbumUserBackfillV1 = "AlbumUserBackfillV1",
|
AlbumUserBackfillV1 = "AlbumUserBackfillV1",
|
||||||
AlbumUserDeleteV1 = "AlbumUserDeleteV1",
|
AlbumUserDeleteV1 = "AlbumUserDeleteV1",
|
||||||
|
AlbumAssetV1 = "AlbumAssetV1",
|
||||||
|
AlbumAssetBackfillV1 = "AlbumAssetBackfillV1",
|
||||||
|
AlbumAssetExifV1 = "AlbumAssetExifV1",
|
||||||
|
AlbumAssetExifBackfillV1 = "AlbumAssetExifBackfillV1",
|
||||||
|
AlbumToAssetV1 = "AlbumToAssetV1",
|
||||||
|
AlbumToAssetDeleteV1 = "AlbumToAssetDeleteV1",
|
||||||
|
AlbumToAssetBackfillV1 = "AlbumToAssetBackfillV1",
|
||||||
SyncAckV1 = "SyncAckV1"
|
SyncAckV1 = "SyncAckV1"
|
||||||
}
|
}
|
||||||
export enum SyncRequestType {
|
export enum SyncRequestType {
|
||||||
@ -4088,7 +4095,10 @@ export enum SyncRequestType {
|
|||||||
PartnerAssetsV1 = "PartnerAssetsV1",
|
PartnerAssetsV1 = "PartnerAssetsV1",
|
||||||
PartnerAssetExifsV1 = "PartnerAssetExifsV1",
|
PartnerAssetExifsV1 = "PartnerAssetExifsV1",
|
||||||
AlbumsV1 = "AlbumsV1",
|
AlbumsV1 = "AlbumsV1",
|
||||||
AlbumUsersV1 = "AlbumUsersV1"
|
AlbumUsersV1 = "AlbumUsersV1",
|
||||||
|
AlbumToAssetsV1 = "AlbumToAssetsV1",
|
||||||
|
AlbumAssetsV1 = "AlbumAssetsV1",
|
||||||
|
AlbumAssetExifsV1 = "AlbumAssetExifsV1"
|
||||||
}
|
}
|
||||||
export enum TranscodeHWAccel {
|
export enum TranscodeHWAccel {
|
||||||
Nvenc = "nvenc",
|
Nvenc = "nvenc",
|
||||||
|
@ -340,27 +340,21 @@ export const columns = {
|
|||||||
apiKey: ['id', 'name', 'userId', 'createdAt', 'updatedAt', 'permissions'],
|
apiKey: ['id', 'name', 'userId', 'createdAt', 'updatedAt', 'permissions'],
|
||||||
notification: ['id', 'createdAt', 'level', 'type', 'title', 'description', 'data', 'readAt'],
|
notification: ['id', 'createdAt', 'level', 'type', 'title', 'description', 'data', 'readAt'],
|
||||||
syncAsset: [
|
syncAsset: [
|
||||||
'id',
|
'assets.id',
|
||||||
'ownerId',
|
'assets.ownerId',
|
||||||
'originalFileName',
|
'assets.originalFileName',
|
||||||
'thumbhash',
|
'assets.thumbhash',
|
||||||
'checksum',
|
'assets.checksum',
|
||||||
'fileCreatedAt',
|
'assets.fileCreatedAt',
|
||||||
'fileModifiedAt',
|
'assets.fileModifiedAt',
|
||||||
'localDateTime',
|
'assets.localDateTime',
|
||||||
'type',
|
'assets.type',
|
||||||
'deletedAt',
|
'assets.deletedAt',
|
||||||
'isFavorite',
|
'assets.isFavorite',
|
||||||
'visibility',
|
'assets.visibility',
|
||||||
'updateId',
|
'assets.duration',
|
||||||
'duration',
|
|
||||||
],
|
|
||||||
syncAlbumUser: [
|
|
||||||
'albums_shared_users_users.albumsId as albumId',
|
|
||||||
'albums_shared_users_users.usersId as userId',
|
|
||||||
'albums_shared_users_users.role',
|
|
||||||
'albums_shared_users_users.updateId',
|
|
||||||
],
|
],
|
||||||
|
syncAlbumUser: ['album_users.albumsId as albumId', 'album_users.usersId as userId', 'album_users.role'],
|
||||||
stack: ['stack.id', 'stack.primaryAssetId', 'ownerId'],
|
stack: ['stack.id', 'stack.primaryAssetId', 'ownerId'],
|
||||||
syncAssetExif: [
|
syncAssetExif: [
|
||||||
'exif.assetId',
|
'exif.assetId',
|
||||||
@ -388,7 +382,6 @@ export const columns = {
|
|||||||
'exif.profileDescription',
|
'exif.profileDescription',
|
||||||
'exif.rating',
|
'exif.rating',
|
||||||
'exif.fps',
|
'exif.fps',
|
||||||
'exif.updateId',
|
|
||||||
],
|
],
|
||||||
exif: [
|
exif: [
|
||||||
'exif.assetId',
|
'exif.assetId',
|
||||||
|
10
server/src/db.d.ts
vendored
10
server/src/db.d.ts
vendored
@ -92,6 +92,15 @@ export interface AlbumsAssetsAssets {
|
|||||||
albumsId: string;
|
albumsId: string;
|
||||||
assetsId: string;
|
assetsId: string;
|
||||||
createdAt: Generated<Timestamp>;
|
createdAt: Generated<Timestamp>;
|
||||||
|
updatedAt: Generated<Timestamp>;
|
||||||
|
updateId: Generated<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AlbumAssetsAudit {
|
||||||
|
deletedAt: Generated<Timestamp>;
|
||||||
|
id: Generated<string>;
|
||||||
|
albumId: string;
|
||||||
|
assetId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AlbumsSharedUsersUsers {
|
export interface AlbumsSharedUsersUsers {
|
||||||
@ -487,6 +496,7 @@ export interface DB {
|
|||||||
albums: Albums;
|
albums: Albums;
|
||||||
albums_audit: AlbumsAudit;
|
albums_audit: AlbumsAudit;
|
||||||
albums_assets_assets: AlbumsAssetsAssets;
|
albums_assets_assets: AlbumsAssetsAssets;
|
||||||
|
album_assets_audit: AlbumAssetsAudit;
|
||||||
albums_shared_users_users: AlbumsSharedUsersUsers;
|
albums_shared_users_users: AlbumsSharedUsersUsers;
|
||||||
album_users_audit: AlbumUsersAudit;
|
album_users_audit: AlbumUsersAudit;
|
||||||
api_keys: ApiKeys;
|
api_keys: ApiKeys;
|
||||||
|
@ -145,6 +145,18 @@ export class SyncAlbumV1 {
|
|||||||
order!: AssetOrder;
|
order!: AssetOrder;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class SyncAlbumToAssetV1 {
|
||||||
|
albumId!: string;
|
||||||
|
assetId!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SyncAlbumToAssetDeleteV1 {
|
||||||
|
albumId!: string;
|
||||||
|
assetId!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SyncAckV1 {}
|
||||||
|
|
||||||
export type SyncItem = {
|
export type SyncItem = {
|
||||||
[SyncEntityType.UserV1]: SyncUserV1;
|
[SyncEntityType.UserV1]: SyncUserV1;
|
||||||
[SyncEntityType.UserDeleteV1]: SyncUserDeleteV1;
|
[SyncEntityType.UserDeleteV1]: SyncUserDeleteV1;
|
||||||
@ -163,7 +175,14 @@ export type SyncItem = {
|
|||||||
[SyncEntityType.AlbumUserV1]: SyncAlbumUserV1;
|
[SyncEntityType.AlbumUserV1]: SyncAlbumUserV1;
|
||||||
[SyncEntityType.AlbumUserBackfillV1]: SyncAlbumUserV1;
|
[SyncEntityType.AlbumUserBackfillV1]: SyncAlbumUserV1;
|
||||||
[SyncEntityType.AlbumUserDeleteV1]: SyncAlbumUserDeleteV1;
|
[SyncEntityType.AlbumUserDeleteV1]: SyncAlbumUserDeleteV1;
|
||||||
[SyncEntityType.SyncAckV1]: object;
|
[SyncEntityType.AlbumAssetV1]: SyncAssetV1;
|
||||||
|
[SyncEntityType.AlbumAssetBackfillV1]: SyncAssetV1;
|
||||||
|
[SyncEntityType.AlbumAssetExifV1]: SyncAssetExifV1;
|
||||||
|
[SyncEntityType.AlbumAssetExifBackfillV1]: SyncAssetExifV1;
|
||||||
|
[SyncEntityType.AlbumToAssetV1]: SyncAlbumToAssetV1;
|
||||||
|
[SyncEntityType.AlbumToAssetBackfillV1]: SyncAlbumToAssetV1;
|
||||||
|
[SyncEntityType.AlbumToAssetDeleteV1]: SyncAlbumToAssetDeleteV1;
|
||||||
|
[SyncEntityType.SyncAckV1]: SyncAckV1;
|
||||||
};
|
};
|
||||||
|
|
||||||
const responseDtos = [
|
const responseDtos = [
|
||||||
@ -178,6 +197,8 @@ const responseDtos = [
|
|||||||
SyncAlbumDeleteV1,
|
SyncAlbumDeleteV1,
|
||||||
SyncAlbumUserV1,
|
SyncAlbumUserV1,
|
||||||
SyncAlbumUserDeleteV1,
|
SyncAlbumUserDeleteV1,
|
||||||
|
SyncAlbumToAssetV1,
|
||||||
|
SyncAckV1,
|
||||||
];
|
];
|
||||||
|
|
||||||
export const extraSyncModels = responseDtos;
|
export const extraSyncModels = responseDtos;
|
||||||
|
@ -581,6 +581,9 @@ export enum SyncRequestType {
|
|||||||
PartnerAssetExifsV1 = 'PartnerAssetExifsV1',
|
PartnerAssetExifsV1 = 'PartnerAssetExifsV1',
|
||||||
AlbumsV1 = 'AlbumsV1',
|
AlbumsV1 = 'AlbumsV1',
|
||||||
AlbumUsersV1 = 'AlbumUsersV1',
|
AlbumUsersV1 = 'AlbumUsersV1',
|
||||||
|
AlbumToAssetsV1 = 'AlbumToAssetsV1',
|
||||||
|
AlbumAssetsV1 = 'AlbumAssetsV1',
|
||||||
|
AlbumAssetExifsV1 = 'AlbumAssetExifsV1',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum SyncEntityType {
|
export enum SyncEntityType {
|
||||||
@ -605,6 +608,13 @@ export enum SyncEntityType {
|
|||||||
AlbumUserV1 = 'AlbumUserV1',
|
AlbumUserV1 = 'AlbumUserV1',
|
||||||
AlbumUserBackfillV1 = 'AlbumUserBackfillV1',
|
AlbumUserBackfillV1 = 'AlbumUserBackfillV1',
|
||||||
AlbumUserDeleteV1 = 'AlbumUserDeleteV1',
|
AlbumUserDeleteV1 = 'AlbumUserDeleteV1',
|
||||||
|
AlbumAssetV1 = 'AlbumAssetV1',
|
||||||
|
AlbumAssetBackfillV1 = 'AlbumAssetBackfillV1',
|
||||||
|
AlbumAssetExifV1 = 'AlbumAssetExifV1',
|
||||||
|
AlbumAssetExifBackfillV1 = 'AlbumAssetExifBackfillV1',
|
||||||
|
AlbumToAssetV1 = 'AlbumToAssetV1',
|
||||||
|
AlbumToAssetDeleteV1 = 'AlbumToAssetDeleteV1',
|
||||||
|
AlbumToAssetBackfillV1 = 'AlbumToAssetBackfillV1',
|
||||||
|
|
||||||
SyncAckV1 = 'SyncAckV1',
|
SyncAckV1 = 'SyncAckV1',
|
||||||
}
|
}
|
||||||
|
@ -74,20 +74,20 @@ order by
|
|||||||
|
|
||||||
-- SyncRepository.getAssetUpserts
|
-- SyncRepository.getAssetUpserts
|
||||||
select
|
select
|
||||||
"id",
|
"assets"."id",
|
||||||
"ownerId",
|
"assets"."ownerId",
|
||||||
"originalFileName",
|
"assets"."originalFileName",
|
||||||
"thumbhash",
|
"assets"."thumbhash",
|
||||||
"checksum",
|
"assets"."checksum",
|
||||||
"fileCreatedAt",
|
"assets"."fileCreatedAt",
|
||||||
"fileModifiedAt",
|
"assets"."fileModifiedAt",
|
||||||
"localDateTime",
|
"assets"."localDateTime",
|
||||||
"type",
|
"assets"."type",
|
||||||
"deletedAt",
|
"assets"."deletedAt",
|
||||||
"isFavorite",
|
"assets"."isFavorite",
|
||||||
"visibility",
|
"assets"."visibility",
|
||||||
"updateId",
|
"assets"."duration",
|
||||||
"duration"
|
"assets"."updateId"
|
||||||
from
|
from
|
||||||
"assets"
|
"assets"
|
||||||
where
|
where
|
||||||
@ -111,20 +111,20 @@ order by
|
|||||||
|
|
||||||
-- SyncRepository.getPartnerAssetsBackfill
|
-- SyncRepository.getPartnerAssetsBackfill
|
||||||
select
|
select
|
||||||
"id",
|
"assets"."id",
|
||||||
"ownerId",
|
"assets"."ownerId",
|
||||||
"originalFileName",
|
"assets"."originalFileName",
|
||||||
"thumbhash",
|
"assets"."thumbhash",
|
||||||
"checksum",
|
"assets"."checksum",
|
||||||
"fileCreatedAt",
|
"assets"."fileCreatedAt",
|
||||||
"fileModifiedAt",
|
"assets"."fileModifiedAt",
|
||||||
"localDateTime",
|
"assets"."localDateTime",
|
||||||
"type",
|
"assets"."type",
|
||||||
"deletedAt",
|
"assets"."deletedAt",
|
||||||
"isFavorite",
|
"assets"."isFavorite",
|
||||||
"visibility",
|
"assets"."visibility",
|
||||||
"updateId",
|
"assets"."duration",
|
||||||
"duration"
|
"assets"."updateId"
|
||||||
from
|
from
|
||||||
"assets"
|
"assets"
|
||||||
where
|
where
|
||||||
@ -137,20 +137,20 @@ order by
|
|||||||
|
|
||||||
-- SyncRepository.getPartnerAssetsUpserts
|
-- SyncRepository.getPartnerAssetsUpserts
|
||||||
select
|
select
|
||||||
"id",
|
"assets"."id",
|
||||||
"ownerId",
|
"assets"."ownerId",
|
||||||
"originalFileName",
|
"assets"."originalFileName",
|
||||||
"thumbhash",
|
"assets"."thumbhash",
|
||||||
"checksum",
|
"assets"."checksum",
|
||||||
"fileCreatedAt",
|
"assets"."fileCreatedAt",
|
||||||
"fileModifiedAt",
|
"assets"."fileModifiedAt",
|
||||||
"localDateTime",
|
"assets"."localDateTime",
|
||||||
"type",
|
"assets"."type",
|
||||||
"deletedAt",
|
"assets"."deletedAt",
|
||||||
"isFavorite",
|
"assets"."isFavorite",
|
||||||
"visibility",
|
"assets"."visibility",
|
||||||
"updateId",
|
"assets"."duration",
|
||||||
"duration"
|
"assets"."updateId"
|
||||||
from
|
from
|
||||||
"assets"
|
"assets"
|
||||||
where
|
where
|
||||||
@ -365,6 +365,35 @@ where
|
|||||||
order by
|
order by
|
||||||
"albums"."updateId" asc
|
"albums"."updateId" asc
|
||||||
|
|
||||||
|
-- SyncRepository.getAlbumToAssetDeletes
|
||||||
|
select
|
||||||
|
"id",
|
||||||
|
"assetId",
|
||||||
|
"albumId"
|
||||||
|
from
|
||||||
|
"album_assets_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.getAlbumUserDeletes
|
-- SyncRepository.getAlbumUserDeletes
|
||||||
select
|
select
|
||||||
"id",
|
"id",
|
||||||
@ -409,12 +438,12 @@ order by
|
|||||||
|
|
||||||
-- SyncRepository.getAlbumUsersBackfill
|
-- SyncRepository.getAlbumUsersBackfill
|
||||||
select
|
select
|
||||||
"albums_shared_users_users"."albumsId" as "albumId",
|
"album_users"."albumsId" as "albumId",
|
||||||
"albums_shared_users_users"."usersId" as "userId",
|
"album_users"."usersId" as "userId",
|
||||||
"albums_shared_users_users"."role",
|
"album_users"."role",
|
||||||
"albums_shared_users_users"."updateId"
|
"album_users"."updateId"
|
||||||
from
|
from
|
||||||
"albums_shared_users_users"
|
"albums_shared_users_users" as "album_users"
|
||||||
where
|
where
|
||||||
"albumsId" = $1
|
"albumsId" = $1
|
||||||
and "updatedAt" < now() - interval '1 millisecond'
|
and "updatedAt" < now() - interval '1 millisecond'
|
||||||
@ -425,15 +454,15 @@ order by
|
|||||||
|
|
||||||
-- SyncRepository.getAlbumUserUpserts
|
-- SyncRepository.getAlbumUserUpserts
|
||||||
select
|
select
|
||||||
"albums_shared_users_users"."albumsId" as "albumId",
|
"album_users"."albumsId" as "albumId",
|
||||||
"albums_shared_users_users"."usersId" as "userId",
|
"album_users"."usersId" as "userId",
|
||||||
"albums_shared_users_users"."role",
|
"album_users"."role",
|
||||||
"albums_shared_users_users"."updateId"
|
"album_users"."updateId"
|
||||||
from
|
from
|
||||||
"albums_shared_users_users"
|
"albums_shared_users_users" as "album_users"
|
||||||
where
|
where
|
||||||
"albums_shared_users_users"."updatedAt" < now() - interval '1 millisecond'
|
"album_users"."updatedAt" < now() - interval '1 millisecond'
|
||||||
and "albums_shared_users_users"."albumsId" in (
|
and "album_users"."albumsId" in (
|
||||||
select
|
select
|
||||||
"id"
|
"id"
|
||||||
from
|
from
|
||||||
@ -451,4 +480,175 @@ where
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
order by
|
order by
|
||||||
"albums_shared_users_users"."updateId" asc
|
"album_users"."updateId" asc
|
||||||
|
|
||||||
|
-- SyncRepository.getAlbumAssetsBackfill
|
||||||
|
select
|
||||||
|
"assets"."id",
|
||||||
|
"assets"."ownerId",
|
||||||
|
"assets"."originalFileName",
|
||||||
|
"assets"."thumbhash",
|
||||||
|
"assets"."checksum",
|
||||||
|
"assets"."fileCreatedAt",
|
||||||
|
"assets"."fileModifiedAt",
|
||||||
|
"assets"."localDateTime",
|
||||||
|
"assets"."type",
|
||||||
|
"assets"."deletedAt",
|
||||||
|
"assets"."isFavorite",
|
||||||
|
"assets"."visibility",
|
||||||
|
"assets"."duration",
|
||||||
|
"assets"."updateId"
|
||||||
|
from
|
||||||
|
"assets"
|
||||||
|
inner join "albums_assets_assets" as "album_assets" on "album_assets"."assetsId" = "assets"."id"
|
||||||
|
where
|
||||||
|
"album_assets"."albumsId" = $1
|
||||||
|
and "assets"."updatedAt" < now() - interval '1 millisecond'
|
||||||
|
and "assets"."updateId" <= $2
|
||||||
|
and "assets"."updateId" >= $3
|
||||||
|
order by
|
||||||
|
"assets"."updateId" asc
|
||||||
|
|
||||||
|
-- SyncRepository.getAlbumAssetsUpserts
|
||||||
|
select
|
||||||
|
"assets"."id",
|
||||||
|
"assets"."ownerId",
|
||||||
|
"assets"."originalFileName",
|
||||||
|
"assets"."thumbhash",
|
||||||
|
"assets"."checksum",
|
||||||
|
"assets"."fileCreatedAt",
|
||||||
|
"assets"."fileModifiedAt",
|
||||||
|
"assets"."localDateTime",
|
||||||
|
"assets"."type",
|
||||||
|
"assets"."deletedAt",
|
||||||
|
"assets"."isFavorite",
|
||||||
|
"assets"."visibility",
|
||||||
|
"assets"."duration",
|
||||||
|
"assets"."updateId"
|
||||||
|
from
|
||||||
|
"assets"
|
||||||
|
inner join "albums_assets_assets" as "album_assets" on "album_assets"."assetsId" = "assets"."id"
|
||||||
|
inner join "albums" on "albums"."id" = "album_assets"."albumsId"
|
||||||
|
left join "albums_shared_users_users" as "album_users" on "album_users"."albumsId" = "album_assets"."albumsId"
|
||||||
|
where
|
||||||
|
"assets"."updatedAt" < now() - interval '1 millisecond'
|
||||||
|
and (
|
||||||
|
"albums"."ownerId" = $1
|
||||||
|
or "album_users"."usersId" = $2
|
||||||
|
)
|
||||||
|
order by
|
||||||
|
"assets"."updateId" asc
|
||||||
|
|
||||||
|
-- SyncRepository.getAlbumToAssetBackfill
|
||||||
|
select
|
||||||
|
"album_assets"."assetsId" as "assetId",
|
||||||
|
"album_assets"."albumsId" as "albumId",
|
||||||
|
"album_assets"."updateId"
|
||||||
|
from
|
||||||
|
"albums_assets_assets" as "album_assets"
|
||||||
|
where
|
||||||
|
"album_assets"."albumsId" = $1
|
||||||
|
and "album_assets"."updatedAt" < now() - interval '1 millisecond'
|
||||||
|
and "album_assets"."updateId" <= $2
|
||||||
|
and "album_assets"."updateId" >= $3
|
||||||
|
order by
|
||||||
|
"album_assets"."updateId" asc
|
||||||
|
|
||||||
|
-- SyncRepository.getAlbumToAssetUpserts
|
||||||
|
select
|
||||||
|
"album_assets"."assetsId" as "assetId",
|
||||||
|
"album_assets"."albumsId" as "albumId",
|
||||||
|
"album_assets"."updateId"
|
||||||
|
from
|
||||||
|
"albums_assets_assets" as "album_assets"
|
||||||
|
inner join "albums" on "albums"."id" = "album_assets"."albumsId"
|
||||||
|
left join "albums_shared_users_users" as "album_users" on "album_users"."albumsId" = "album_assets"."albumsId"
|
||||||
|
where
|
||||||
|
"album_assets"."updatedAt" < now() - interval '1 millisecond'
|
||||||
|
and (
|
||||||
|
"albums"."ownerId" = $1
|
||||||
|
or "album_users"."usersId" = $2
|
||||||
|
)
|
||||||
|
order by
|
||||||
|
"album_assets"."updateId" asc
|
||||||
|
|
||||||
|
-- SyncRepository.getAlbumAssetExifsBackfill
|
||||||
|
select
|
||||||
|
"exif"."assetId",
|
||||||
|
"exif"."description",
|
||||||
|
"exif"."exifImageWidth",
|
||||||
|
"exif"."exifImageHeight",
|
||||||
|
"exif"."fileSizeInByte",
|
||||||
|
"exif"."orientation",
|
||||||
|
"exif"."dateTimeOriginal",
|
||||||
|
"exif"."modifyDate",
|
||||||
|
"exif"."timeZone",
|
||||||
|
"exif"."latitude",
|
||||||
|
"exif"."longitude",
|
||||||
|
"exif"."projectionType",
|
||||||
|
"exif"."city",
|
||||||
|
"exif"."state",
|
||||||
|
"exif"."country",
|
||||||
|
"exif"."make",
|
||||||
|
"exif"."model",
|
||||||
|
"exif"."lensModel",
|
||||||
|
"exif"."fNumber",
|
||||||
|
"exif"."focalLength",
|
||||||
|
"exif"."iso",
|
||||||
|
"exif"."exposureTime",
|
||||||
|
"exif"."profileDescription",
|
||||||
|
"exif"."rating",
|
||||||
|
"exif"."fps",
|
||||||
|
"exif"."updateId"
|
||||||
|
from
|
||||||
|
"exif"
|
||||||
|
inner join "albums_assets_assets" as "album_assets" on "album_assets"."assetsId" = "exif"."assetId"
|
||||||
|
where
|
||||||
|
"album_assets"."albumsId" = $1
|
||||||
|
and "exif"."updatedAt" < now() - interval '1 millisecond'
|
||||||
|
and "exif"."updateId" <= $2
|
||||||
|
and "exif"."updateId" >= $3
|
||||||
|
order by
|
||||||
|
"exif"."updateId" asc
|
||||||
|
|
||||||
|
-- SyncRepository.getAlbumAssetExifsUpserts
|
||||||
|
select
|
||||||
|
"exif"."assetId",
|
||||||
|
"exif"."description",
|
||||||
|
"exif"."exifImageWidth",
|
||||||
|
"exif"."exifImageHeight",
|
||||||
|
"exif"."fileSizeInByte",
|
||||||
|
"exif"."orientation",
|
||||||
|
"exif"."dateTimeOriginal",
|
||||||
|
"exif"."modifyDate",
|
||||||
|
"exif"."timeZone",
|
||||||
|
"exif"."latitude",
|
||||||
|
"exif"."longitude",
|
||||||
|
"exif"."projectionType",
|
||||||
|
"exif"."city",
|
||||||
|
"exif"."state",
|
||||||
|
"exif"."country",
|
||||||
|
"exif"."make",
|
||||||
|
"exif"."model",
|
||||||
|
"exif"."lensModel",
|
||||||
|
"exif"."fNumber",
|
||||||
|
"exif"."focalLength",
|
||||||
|
"exif"."iso",
|
||||||
|
"exif"."exposureTime",
|
||||||
|
"exif"."profileDescription",
|
||||||
|
"exif"."rating",
|
||||||
|
"exif"."fps",
|
||||||
|
"exif"."updateId"
|
||||||
|
from
|
||||||
|
"exif"
|
||||||
|
inner join "albums_assets_assets" as "album_assets" on "album_assets"."assetsId" = "exif"."assetId"
|
||||||
|
inner join "albums" on "albums"."id" = "album_assets"."albumsId"
|
||||||
|
left join "albums_shared_users_users" as "album_users" on "album_users"."albumsId" = "album_assets"."albumsId"
|
||||||
|
where
|
||||||
|
"exif"."updatedAt" < now() - interval '1 millisecond'
|
||||||
|
and (
|
||||||
|
"albums"."ownerId" = $1
|
||||||
|
or "album_users"."usersId" = $2
|
||||||
|
)
|
||||||
|
order by
|
||||||
|
"exif"."updateId" asc
|
||||||
|
@ -7,7 +7,13 @@ 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' | 'albums_audit' | 'album_users_audit';
|
type AuditTables =
|
||||||
|
| 'users_audit'
|
||||||
|
| 'partners_audit'
|
||||||
|
| 'assets_audit'
|
||||||
|
| 'albums_audit'
|
||||||
|
| 'album_users_audit'
|
||||||
|
| 'album_assets_audit';
|
||||||
type UpsertTables = 'users' | 'partners' | 'assets' | 'exif' | 'albums' | 'albums_shared_users_users';
|
type UpsertTables = 'users' | 'partners' | 'assets' | 'exif' | 'albums' | 'albums_shared_users_users';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -87,6 +93,7 @@ export class SyncRepository {
|
|||||||
return this.db
|
return this.db
|
||||||
.selectFrom('assets')
|
.selectFrom('assets')
|
||||||
.select(columns.syncAsset)
|
.select(columns.syncAsset)
|
||||||
|
.select('assets.updateId')
|
||||||
.where('ownerId', '=', userId)
|
.where('ownerId', '=', userId)
|
||||||
.$call((qb) => this.upsertTableFilters(qb, ack))
|
.$call((qb) => this.upsertTableFilters(qb, ack))
|
||||||
.stream();
|
.stream();
|
||||||
@ -109,6 +116,7 @@ export class SyncRepository {
|
|||||||
return this.db
|
return this.db
|
||||||
.selectFrom('assets')
|
.selectFrom('assets')
|
||||||
.select(columns.syncAsset)
|
.select(columns.syncAsset)
|
||||||
|
.select('assets.updateId')
|
||||||
.where('ownerId', '=', partnerId)
|
.where('ownerId', '=', partnerId)
|
||||||
.where('updatedAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
|
.where('updatedAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
|
||||||
.where('updateId', '<=', beforeUpdateId)
|
.where('updateId', '<=', beforeUpdateId)
|
||||||
@ -122,6 +130,7 @@ export class SyncRepository {
|
|||||||
return this.db
|
return this.db
|
||||||
.selectFrom('assets')
|
.selectFrom('assets')
|
||||||
.select(columns.syncAsset)
|
.select(columns.syncAsset)
|
||||||
|
.select('assets.updateId')
|
||||||
.where('ownerId', 'in', (eb) =>
|
.where('ownerId', 'in', (eb) =>
|
||||||
eb.selectFrom('partners').select(['sharedById']).where('sharedWithId', '=', userId),
|
eb.selectFrom('partners').select(['sharedById']).where('sharedWithId', '=', userId),
|
||||||
)
|
)
|
||||||
@ -156,6 +165,7 @@ export class SyncRepository {
|
|||||||
return this.db
|
return this.db
|
||||||
.selectFrom('exif')
|
.selectFrom('exif')
|
||||||
.select(columns.syncAssetExif)
|
.select(columns.syncAssetExif)
|
||||||
|
.select('exif.updateId')
|
||||||
.where('assetId', 'in', (eb) => eb.selectFrom('assets').select('id').where('ownerId', '=', userId))
|
.where('assetId', 'in', (eb) => eb.selectFrom('assets').select('id').where('ownerId', '=', userId))
|
||||||
.$call((qb) => this.upsertTableFilters(qb, ack))
|
.$call((qb) => this.upsertTableFilters(qb, ack))
|
||||||
.stream();
|
.stream();
|
||||||
@ -166,6 +176,7 @@ export class SyncRepository {
|
|||||||
return this.db
|
return this.db
|
||||||
.selectFrom('exif')
|
.selectFrom('exif')
|
||||||
.select(columns.syncAssetExif)
|
.select(columns.syncAssetExif)
|
||||||
|
.select('exif.updateId')
|
||||||
.innerJoin('assets', 'assets.id', 'exif.assetId')
|
.innerJoin('assets', 'assets.id', 'exif.assetId')
|
||||||
.where('assets.ownerId', '=', partnerId)
|
.where('assets.ownerId', '=', partnerId)
|
||||||
.where('exif.updatedAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
|
.where('exif.updatedAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
|
||||||
@ -180,6 +191,7 @@ export class SyncRepository {
|
|||||||
return this.db
|
return this.db
|
||||||
.selectFrom('exif')
|
.selectFrom('exif')
|
||||||
.select(columns.syncAssetExif)
|
.select(columns.syncAssetExif)
|
||||||
|
.select('exif.updateId')
|
||||||
.where('assetId', 'in', (eb) =>
|
.where('assetId', 'in', (eb) =>
|
||||||
eb
|
eb
|
||||||
.selectFrom('assets')
|
.selectFrom('assets')
|
||||||
@ -227,6 +239,33 @@ export class SyncRepository {
|
|||||||
.stream();
|
.stream();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GenerateSql({ params: [DummyValue.UUID], stream: true })
|
||||||
|
getAlbumToAssetDeletes(userId: string, ack?: SyncAck) {
|
||||||
|
return this.db
|
||||||
|
.selectFrom('album_assets_audit')
|
||||||
|
.select(['id', 'assetId', '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 })
|
@GenerateSql({ params: [DummyValue.UUID], stream: true })
|
||||||
getAlbumUserDeletes(userId: string, ack?: SyncAck) {
|
getAlbumUserDeletes(userId: string, ack?: SyncAck) {
|
||||||
return this.db
|
return this.db
|
||||||
@ -266,11 +305,12 @@ export class SyncRepository {
|
|||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID, DummyValue.UUID], stream: true })
|
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID, DummyValue.UUID, DummyValue.UUID], stream: true })
|
||||||
getAlbumUsersBackfill(albumId: string, afterUpdateId: string | undefined, beforeUpdateId: string) {
|
getAlbumUsersBackfill(albumId: string, afterUpdateId: string | undefined, beforeUpdateId: string) {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('albums_shared_users_users')
|
.selectFrom('albums_shared_users_users as album_users')
|
||||||
.select(columns.syncAlbumUser)
|
.select(columns.syncAlbumUser)
|
||||||
|
.select('album_users.updateId')
|
||||||
.where('albumsId', '=', albumId)
|
.where('albumsId', '=', albumId)
|
||||||
.where('updatedAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
|
.where('updatedAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
|
||||||
.where('updateId', '<=', beforeUpdateId)
|
.where('updateId', '<=', beforeUpdateId)
|
||||||
@ -282,14 +322,15 @@ export class SyncRepository {
|
|||||||
@GenerateSql({ params: [DummyValue.UUID], stream: true })
|
@GenerateSql({ params: [DummyValue.UUID], stream: true })
|
||||||
getAlbumUserUpserts(userId: string, ack?: SyncAck) {
|
getAlbumUserUpserts(userId: string, ack?: SyncAck) {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('albums_shared_users_users')
|
.selectFrom('albums_shared_users_users as album_users')
|
||||||
.select(columns.syncAlbumUser)
|
.select(columns.syncAlbumUser)
|
||||||
.where('albums_shared_users_users.updatedAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
|
.select('album_users.updateId')
|
||||||
.$if(!!ack, (qb) => qb.where('albums_shared_users_users.updateId', '>', ack!.updateId))
|
.where('album_users.updatedAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
|
||||||
.orderBy('albums_shared_users_users.updateId', 'asc')
|
.$if(!!ack, (qb) => qb.where('album_users.updateId', '>', ack!.updateId))
|
||||||
|
.orderBy('album_users.updateId', 'asc')
|
||||||
.where((eb) =>
|
.where((eb) =>
|
||||||
eb(
|
eb(
|
||||||
'albums_shared_users_users.albumsId',
|
'album_users.albumsId',
|
||||||
'in',
|
'in',
|
||||||
eb
|
eb
|
||||||
.selectFrom('albums')
|
.selectFrom('albums')
|
||||||
@ -308,6 +349,95 @@ export class SyncRepository {
|
|||||||
.stream();
|
.stream();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID, DummyValue.UUID], stream: true })
|
||||||
|
getAlbumAssetsBackfill(albumId: string, afterUpdateId: string | undefined, beforeUpdateId: string) {
|
||||||
|
return this.db
|
||||||
|
.selectFrom('assets')
|
||||||
|
.innerJoin('albums_assets_assets as album_assets', 'album_assets.assetsId', 'assets.id')
|
||||||
|
.select(columns.syncAsset)
|
||||||
|
.select('assets.updateId')
|
||||||
|
.where('album_assets.albumsId', '=', albumId)
|
||||||
|
.where('assets.updatedAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
|
||||||
|
.where('assets.updateId', '<=', beforeUpdateId)
|
||||||
|
.$if(!!afterUpdateId, (eb) => eb.where('assets.updateId', '>=', afterUpdateId!))
|
||||||
|
.orderBy('assets.updateId', 'asc')
|
||||||
|
.stream();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GenerateSql({ params: [DummyValue.UUID], stream: true })
|
||||||
|
getAlbumAssetsUpserts(userId: string, ack?: SyncAck) {
|
||||||
|
return this.db
|
||||||
|
.selectFrom('assets')
|
||||||
|
.innerJoin('albums_assets_assets as album_assets', 'album_assets.assetsId', 'assets.id')
|
||||||
|
.select(columns.syncAsset)
|
||||||
|
.select('assets.updateId')
|
||||||
|
.where('assets.updatedAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
|
||||||
|
.$if(!!ack, (qb) => qb.where('assets.updateId', '>', ack!.updateId))
|
||||||
|
.orderBy('assets.updateId', 'asc')
|
||||||
|
.innerJoin('albums', 'albums.id', 'album_assets.albumsId')
|
||||||
|
.leftJoin('albums_shared_users_users as album_users', 'album_users.albumsId', 'album_assets.albumsId')
|
||||||
|
.where((eb) => eb.or([eb('albums.ownerId', '=', userId), eb('album_users.usersId', '=', userId)]))
|
||||||
|
.stream();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID, DummyValue.UUID], stream: true })
|
||||||
|
getAlbumToAssetBackfill(albumId: string, afterUpdateId: string | undefined, beforeUpdateId: string) {
|
||||||
|
return this.db
|
||||||
|
.selectFrom('albums_assets_assets as album_assets')
|
||||||
|
.select(['album_assets.assetsId as assetId', 'album_assets.albumsId as albumId', 'album_assets.updateId'])
|
||||||
|
.where('album_assets.albumsId', '=', albumId)
|
||||||
|
.where('album_assets.updatedAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
|
||||||
|
.where('album_assets.updateId', '<=', beforeUpdateId)
|
||||||
|
.$if(!!afterUpdateId, (eb) => eb.where('album_assets.updateId', '>=', afterUpdateId!))
|
||||||
|
.orderBy('album_assets.updateId', 'asc')
|
||||||
|
.stream();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GenerateSql({ params: [DummyValue.UUID], stream: true })
|
||||||
|
getAlbumToAssetUpserts(userId: string, ack?: SyncAck) {
|
||||||
|
return this.db
|
||||||
|
.selectFrom('albums_assets_assets as album_assets')
|
||||||
|
.select(['album_assets.assetsId as assetId', 'album_assets.albumsId as albumId', 'album_assets.updateId'])
|
||||||
|
.where('album_assets.updatedAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
|
||||||
|
.$if(!!ack, (qb) => qb.where('album_assets.updateId', '>', ack!.updateId))
|
||||||
|
.orderBy('album_assets.updateId', 'asc')
|
||||||
|
.innerJoin('albums', 'albums.id', 'album_assets.albumsId')
|
||||||
|
.leftJoin('albums_shared_users_users as album_users', 'album_users.albumsId', 'album_assets.albumsId')
|
||||||
|
.where((eb) => eb.or([eb('albums.ownerId', '=', userId), eb('album_users.usersId', '=', userId)]))
|
||||||
|
.stream();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID, DummyValue.UUID], stream: true })
|
||||||
|
getAlbumAssetExifsBackfill(albumId: string, afterUpdateId: string | undefined, beforeUpdateId: string) {
|
||||||
|
return this.db
|
||||||
|
.selectFrom('exif')
|
||||||
|
.innerJoin('albums_assets_assets as album_assets', 'album_assets.assetsId', 'exif.assetId')
|
||||||
|
.select(columns.syncAssetExif)
|
||||||
|
.select('exif.updateId')
|
||||||
|
.where('album_assets.albumsId', '=', albumId)
|
||||||
|
.where('exif.updatedAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
|
||||||
|
.where('exif.updateId', '<=', beforeUpdateId)
|
||||||
|
.$if(!!afterUpdateId, (eb) => eb.where('exif.updateId', '>=', afterUpdateId!))
|
||||||
|
.orderBy('exif.updateId', 'asc')
|
||||||
|
.stream();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GenerateSql({ params: [DummyValue.UUID], stream: true })
|
||||||
|
getAlbumAssetExifsUpserts(userId: string, ack?: SyncAck) {
|
||||||
|
return this.db
|
||||||
|
.selectFrom('exif')
|
||||||
|
.innerJoin('albums_assets_assets as album_assets', 'album_assets.assetsId', 'exif.assetId')
|
||||||
|
.select(columns.syncAssetExif)
|
||||||
|
.select('exif.updateId')
|
||||||
|
.where('exif.updatedAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
|
||||||
|
.$if(!!ack, (qb) => qb.where('exif.updateId', '>', ack!.updateId))
|
||||||
|
.orderBy('exif.updateId', 'asc')
|
||||||
|
.innerJoin('albums', 'albums.id', 'album_assets.albumsId')
|
||||||
|
.leftJoin('albums_shared_users_users as album_users', 'album_users.albumsId', 'album_assets.albumsId')
|
||||||
|
.where((eb) => eb.or([eb('albums.ownerId', '=', userId), eb('album_users.usersId', '=', userId)]))
|
||||||
|
.stream();
|
||||||
|
}
|
||||||
|
|
||||||
private auditTableFilters<T extends keyof Pick<DB, AuditTables>, D>(qb: SelectQueryBuilder<DB, T, D>, ack?: SyncAck) {
|
private auditTableFilters<T extends keyof Pick<DB, AuditTables>, D>(qb: SelectQueryBuilder<DB, T, D>, ack?: SyncAck) {
|
||||||
const builder = qb as SelectQueryBuilder<DB, AuditTables, D>;
|
const builder = qb as SelectQueryBuilder<DB, AuditTables, D>;
|
||||||
return builder
|
return builder
|
||||||
|
@ -142,6 +142,20 @@ export const albums_delete_audit = registerFunction({
|
|||||||
synchronize: false,
|
synchronize: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const album_assets_delete_audit = registerFunction({
|
||||||
|
name: 'album_assets_delete_audit',
|
||||||
|
returnType: 'TRIGGER',
|
||||||
|
language: 'PLPGSQL',
|
||||||
|
body: `
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO album_assets_audit ("albumId", "assetId")
|
||||||
|
SELECT "albumsId", "assetsId" FROM OLD
|
||||||
|
WHERE "albumsId" IN (SELECT "id" FROM albums WHERE "id" IN (SELECT "albumsId" FROM OLD));
|
||||||
|
RETURN NULL;
|
||||||
|
END`,
|
||||||
|
synchronize: false,
|
||||||
|
});
|
||||||
|
|
||||||
export const album_users_delete_audit = registerFunction({
|
export const album_users_delete_audit = registerFunction({
|
||||||
name: 'album_users_delete_audit',
|
name: 'album_users_delete_audit',
|
||||||
returnType: 'TRIGGER',
|
returnType: 'TRIGGER',
|
||||||
|
@ -13,6 +13,7 @@ import {
|
|||||||
users_delete_audit,
|
users_delete_audit,
|
||||||
} 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 { AlbumAssetAuditTable } from 'src/schema/tables/album-asset-audit.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 { AlbumAuditTable } from 'src/schema/tables/album-audit.table';
|
||||||
import { AlbumUserAuditTable } from 'src/schema/tables/album-user-audit.table';
|
import { AlbumUserAuditTable } from 'src/schema/tables/album-user-audit.table';
|
||||||
@ -58,6 +59,7 @@ export class ImmichDatabase {
|
|||||||
tables = [
|
tables = [
|
||||||
ActivityTable,
|
ActivityTable,
|
||||||
AlbumAssetTable,
|
AlbumAssetTable,
|
||||||
|
AlbumAssetAuditTable,
|
||||||
AlbumAuditTable,
|
AlbumAuditTable,
|
||||||
AlbumUserAuditTable,
|
AlbumUserAuditTable,
|
||||||
AlbumUserTable,
|
AlbumUserTable,
|
||||||
|
@ -0,0 +1,18 @@
|
|||||||
|
import { Kysely, sql } from 'kysely';
|
||||||
|
|
||||||
|
export async function up(db: Kysely<any>): Promise<void> {
|
||||||
|
await sql`ALTER TABLE "albums_assets_assets" ADD "updatedAt" timestamp with time zone NOT NULL DEFAULT now();`.execute(db);
|
||||||
|
await sql`ALTER TABLE "albums_assets_assets" ADD "updateId" uuid NOT NULL DEFAULT immich_uuid_v7();`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_album_assets_update_id" ON "albums_assets_assets" ("updateId")`.execute(db);
|
||||||
|
await sql`CREATE OR REPLACE TRIGGER "album_assets_updated_at"
|
||||||
|
BEFORE UPDATE ON "albums_assets_assets"
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION updated_at();`.execute(db);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(db: Kysely<any>): Promise<void> {
|
||||||
|
await sql`DROP INDEX "IDX_album_assets_update_id";`.execute(db);
|
||||||
|
await sql`ALTER TABLE "albums_assets_assets" DROP COLUMN "updatedAt";`.execute(db);
|
||||||
|
await sql`ALTER TABLE "albums_assets_assets" DROP COLUMN "updateId";`.execute(db);
|
||||||
|
await sql`DROP TRIGGER "album_assets_updated_at" ON "albums_assets_assets";`.execute(db);
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
import { Kysely, sql } from 'kysely';
|
||||||
|
|
||||||
|
export async function up(db: Kysely<any>): Promise<void> {
|
||||||
|
await sql`CREATE TABLE "album_assets_audit" ("id" uuid NOT NULL DEFAULT immich_uuid_v7(), "albumId" uuid NOT NULL, "assetId" uuid NOT NULL, "deletedAt" timestamp with time zone NOT NULL DEFAULT clock_timestamp());`.execute(db);
|
||||||
|
await sql`ALTER TABLE "album_assets_audit" ADD CONSTRAINT "PK_32969b576ec8f78d84f37c2eb2d" PRIMARY KEY ("id");`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_album_assets_audit_album_id" ON "album_assets_audit" ("albumId")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_album_assets_audit_asset_id" ON "album_assets_audit" ("assetId")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_album_assets_audit_deleted_at" ON "album_assets_audit" ("deletedAt")`.execute(db);
|
||||||
|
await sql`CREATE OR REPLACE TRIGGER "album_assets_updated_at"
|
||||||
|
BEFORE UPDATE ON "albums_assets_assets"
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION updated_at();`.execute(db);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(db: Kysely<any>): Promise<void> {
|
||||||
|
await sql`DROP TRIGGER "album_assets_updated_at" ON "albums_assets_assets";`.execute(db);
|
||||||
|
await sql`DROP INDEX "IDX_album_assets_audit_album_id";`.execute(db);
|
||||||
|
await sql`DROP INDEX "IDX_album_assets_audit_asset_id";`.execute(db);
|
||||||
|
await sql`DROP INDEX "IDX_album_assets_audit_deleted_at";`.execute(db);
|
||||||
|
await sql`ALTER TABLE "album_assets_audit" DROP CONSTRAINT "PK_32969b576ec8f78d84f37c2eb2d";`.execute(db);
|
||||||
|
await sql`DROP TABLE "album_assets_audit";`.execute(db);
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
import { Kysely, sql } from 'kysely';
|
||||||
|
|
||||||
|
export async function up(db: Kysely<any>): Promise<void> {
|
||||||
|
await sql`CREATE OR REPLACE FUNCTION album_assets_delete_audit()
|
||||||
|
RETURNS TRIGGER
|
||||||
|
LANGUAGE PLPGSQL
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO album_assets_audit ("albumId", "assetId")
|
||||||
|
SELECT "albumsId", "assetsId" FROM OLD
|
||||||
|
WHERE "albumsId" IN (SELECT "id" FROM albums WHERE "id" IN (SELECT "albumsId" FROM OLD));
|
||||||
|
RETURN NULL;
|
||||||
|
END
|
||||||
|
$$;`.execute(db);
|
||||||
|
await sql`ALTER TABLE "album_assets_audit" ADD CONSTRAINT "FK_8047b44b812619a3c75a2839b0d" FOREIGN KEY ("albumId") REFERENCES "albums" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db);
|
||||||
|
await sql`CREATE OR REPLACE TRIGGER "album_assets_delete_audit"
|
||||||
|
AFTER DELETE ON "albums_assets_assets"
|
||||||
|
REFERENCING OLD TABLE AS "old"
|
||||||
|
FOR EACH STATEMENT
|
||||||
|
WHEN (pg_trigger_depth() <= 1)
|
||||||
|
EXECUTE FUNCTION album_assets_delete_audit();`.execute(db);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(db: Kysely<any>): Promise<void> {
|
||||||
|
await sql`DROP TRIGGER "album_assets_delete_audit" ON "albums_assets_assets";`.execute(db);
|
||||||
|
await sql`ALTER TABLE "album_assets_audit" DROP CONSTRAINT "FK_8047b44b812619a3c75a2839b0d";`.execute(db);
|
||||||
|
await sql`DROP FUNCTION album_assets_delete_audit;`.execute(db);
|
||||||
|
}
|
23
server/src/schema/tables/album-asset-audit.table.ts
Normal file
23
server/src/schema/tables/album-asset-audit.table.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { PrimaryGeneratedUuidV7Column } from 'src/decorators';
|
||||||
|
import { AlbumTable } from 'src/schema/tables/album.table';
|
||||||
|
import { Column, CreateDateColumn, ForeignKeyColumn, Table } from 'src/sql-tools';
|
||||||
|
|
||||||
|
@Table('album_assets_audit')
|
||||||
|
export class AlbumAssetAuditTable {
|
||||||
|
@PrimaryGeneratedUuidV7Column()
|
||||||
|
id!: string;
|
||||||
|
|
||||||
|
@ForeignKeyColumn(() => AlbumTable, {
|
||||||
|
type: 'uuid',
|
||||||
|
indexName: 'IDX_album_assets_audit_album_id',
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
onUpdate: 'CASCADE',
|
||||||
|
})
|
||||||
|
albumId!: string;
|
||||||
|
|
||||||
|
@Column({ type: 'uuid', indexName: 'IDX_album_assets_audit_asset_id' })
|
||||||
|
assetId!: string;
|
||||||
|
|
||||||
|
@CreateDateColumn({ default: () => 'clock_timestamp()', indexName: 'IDX_album_assets_audit_deleted_at' })
|
||||||
|
deletedAt!: Date;
|
||||||
|
}
|
@ -1,8 +1,18 @@
|
|||||||
|
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
||||||
|
import { album_assets_delete_audit } from 'src/schema/functions';
|
||||||
import { AlbumTable } from 'src/schema/tables/album.table';
|
import { AlbumTable } from 'src/schema/tables/album.table';
|
||||||
import { AssetTable } from 'src/schema/tables/asset.table';
|
import { AssetTable } from 'src/schema/tables/asset.table';
|
||||||
import { CreateDateColumn, ForeignKeyColumn, Table } from 'src/sql-tools';
|
import { AfterDeleteTrigger, CreateDateColumn, ForeignKeyColumn, Table, UpdateDateColumn } from 'src/sql-tools';
|
||||||
|
|
||||||
@Table({ name: 'albums_assets_assets', primaryConstraintName: 'PK_c67bc36fa845fb7b18e0e398180' })
|
@Table({ name: 'albums_assets_assets', primaryConstraintName: 'PK_c67bc36fa845fb7b18e0e398180' })
|
||||||
|
@UpdatedAtTrigger('album_assets_updated_at')
|
||||||
|
@AfterDeleteTrigger({
|
||||||
|
name: 'album_assets_delete_audit',
|
||||||
|
scope: 'statement',
|
||||||
|
function: album_assets_delete_audit,
|
||||||
|
referencingOldTableAs: 'old',
|
||||||
|
when: 'pg_trigger_depth() <= 1',
|
||||||
|
})
|
||||||
export class AlbumAssetTable {
|
export class AlbumAssetTable {
|
||||||
@ForeignKeyColumn(() => AlbumTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false, primary: true })
|
@ForeignKeyColumn(() => AlbumTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false, primary: true })
|
||||||
albumsId!: string;
|
albumsId!: string;
|
||||||
@ -12,4 +22,10 @@ export class AlbumAssetTable {
|
|||||||
|
|
||||||
@CreateDateColumn()
|
@CreateDateColumn()
|
||||||
createdAt!: Date;
|
createdAt!: Date;
|
||||||
|
|
||||||
|
@UpdateDateColumn()
|
||||||
|
updatedAt!: Date;
|
||||||
|
|
||||||
|
@UpdateIdColumn({ indexName: 'IDX_album_assets_update_id' })
|
||||||
|
updateId!: string;
|
||||||
}
|
}
|
||||||
|
@ -57,11 +57,14 @@ export const SYNC_TYPES_ORDER = [
|
|||||||
SyncRequestType.UsersV1,
|
SyncRequestType.UsersV1,
|
||||||
SyncRequestType.PartnersV1,
|
SyncRequestType.PartnersV1,
|
||||||
SyncRequestType.AssetsV1,
|
SyncRequestType.AssetsV1,
|
||||||
SyncRequestType.AssetExifsV1,
|
|
||||||
SyncRequestType.PartnerAssetsV1,
|
SyncRequestType.PartnerAssetsV1,
|
||||||
SyncRequestType.PartnerAssetExifsV1,
|
|
||||||
SyncRequestType.AlbumsV1,
|
SyncRequestType.AlbumsV1,
|
||||||
SyncRequestType.AlbumUsersV1,
|
SyncRequestType.AlbumUsersV1,
|
||||||
|
SyncRequestType.AlbumAssetsV1,
|
||||||
|
SyncRequestType.AlbumToAssetsV1,
|
||||||
|
SyncRequestType.AssetExifsV1,
|
||||||
|
SyncRequestType.AlbumAssetExifsV1,
|
||||||
|
SyncRequestType.PartnerAssetExifsV1,
|
||||||
];
|
];
|
||||||
|
|
||||||
const throwSessionRequired = () => {
|
const throwSessionRequired = () => {
|
||||||
@ -164,6 +167,21 @@ export class SyncService extends BaseService {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case SyncRequestType.AlbumAssetsV1: {
|
||||||
|
await this.syncAlbumAssetsV1(response, checkpointMap, auth, sessionId);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case SyncRequestType.AlbumToAssetsV1: {
|
||||||
|
await this.syncAlbumToAssetsV1(response, checkpointMap, auth, sessionId);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case SyncRequestType.AlbumAssetExifsV1: {
|
||||||
|
await this.syncAlbumAssetExifsV1(response, checkpointMap, auth, sessionId);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
default: {
|
default: {
|
||||||
this.logger.warn(`Unsupported sync type: ${type}`);
|
this.logger.warn(`Unsupported sync type: ${type}`);
|
||||||
break;
|
break;
|
||||||
@ -380,6 +398,147 @@ export class SyncService extends BaseService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async syncAlbumAssetsV1(response: Writable, checkpointMap: CheckpointMap, auth: AuthDto, sessionId: string) {
|
||||||
|
const backfillType = SyncEntityType.AlbumAssetBackfillV1;
|
||||||
|
const upsertType = SyncEntityType.AlbumAssetV1;
|
||||||
|
|
||||||
|
const backfillCheckpoint = checkpointMap[backfillType];
|
||||||
|
const upsertCheckpoint = checkpointMap[upsertType];
|
||||||
|
|
||||||
|
const albums = await this.syncRepository.getAlbumBackfill(auth.user.id, backfillCheckpoint?.updateId);
|
||||||
|
|
||||||
|
if (upsertCheckpoint) {
|
||||||
|
const endId = upsertCheckpoint.updateId;
|
||||||
|
|
||||||
|
for (const album of albums) {
|
||||||
|
const createId = album.createId;
|
||||||
|
if (isEntityBackfillComplete(createId, backfillCheckpoint)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const startId = getStartId(createId, backfillCheckpoint);
|
||||||
|
const backfill = this.syncRepository.getAlbumAssetsBackfill(album.id, startId, endId);
|
||||||
|
|
||||||
|
for await (const { updateId, ...data } of backfill) {
|
||||||
|
send(response, { type: backfillType, ids: [createId, updateId], data: mapSyncAssetV1(data) });
|
||||||
|
}
|
||||||
|
|
||||||
|
sendEntityBackfillCompleteAck(response, backfillType, createId);
|
||||||
|
}
|
||||||
|
} else if (albums.length > 0) {
|
||||||
|
await this.upsertBackfillCheckpoint({
|
||||||
|
type: backfillType,
|
||||||
|
sessionId,
|
||||||
|
createId: albums.at(-1)!.createId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const upserts = this.syncRepository.getAlbumAssetsUpserts(auth.user.id, checkpointMap[upsertType]);
|
||||||
|
for await (const { updateId, ...data } of upserts) {
|
||||||
|
send(response, { type: upsertType, ids: [updateId], data: mapSyncAssetV1(data) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async syncAlbumAssetExifsV1(
|
||||||
|
response: Writable,
|
||||||
|
checkpointMap: CheckpointMap,
|
||||||
|
auth: AuthDto,
|
||||||
|
sessionId: string,
|
||||||
|
) {
|
||||||
|
const backfillType = SyncEntityType.AlbumAssetExifBackfillV1;
|
||||||
|
const upsertType = SyncEntityType.AlbumAssetExifV1;
|
||||||
|
|
||||||
|
const backfillCheckpoint = checkpointMap[backfillType];
|
||||||
|
const upsertCheckpoint = checkpointMap[upsertType];
|
||||||
|
|
||||||
|
const albums = await this.syncRepository.getAlbumBackfill(auth.user.id, backfillCheckpoint?.updateId);
|
||||||
|
|
||||||
|
if (upsertCheckpoint) {
|
||||||
|
const endId = upsertCheckpoint.updateId;
|
||||||
|
|
||||||
|
for (const album of albums) {
|
||||||
|
const createId = album.createId;
|
||||||
|
if (isEntityBackfillComplete(createId, backfillCheckpoint)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const startId = getStartId(createId, backfillCheckpoint);
|
||||||
|
const backfill = this.syncRepository.getAlbumAssetExifsBackfill(album.id, startId, endId);
|
||||||
|
|
||||||
|
for await (const { updateId, ...data } of backfill) {
|
||||||
|
send(response, { type: backfillType, ids: [createId, updateId], data });
|
||||||
|
}
|
||||||
|
|
||||||
|
sendEntityBackfillCompleteAck(response, backfillType, createId);
|
||||||
|
}
|
||||||
|
} else if (albums.length > 0) {
|
||||||
|
await this.upsertBackfillCheckpoint({
|
||||||
|
type: backfillType,
|
||||||
|
sessionId,
|
||||||
|
createId: albums.at(-1)!.createId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const upserts = this.syncRepository.getAlbumAssetExifsUpserts(auth.user.id, checkpointMap[upsertType]);
|
||||||
|
for await (const { updateId, ...data } of upserts) {
|
||||||
|
send(response, { type: upsertType, ids: [updateId], data });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async syncAlbumToAssetsV1(
|
||||||
|
response: Writable,
|
||||||
|
checkpointMap: CheckpointMap,
|
||||||
|
auth: AuthDto,
|
||||||
|
sessionId: string,
|
||||||
|
) {
|
||||||
|
const backfillType = SyncEntityType.AlbumToAssetBackfillV1;
|
||||||
|
const upsertType = SyncEntityType.AlbumToAssetV1;
|
||||||
|
|
||||||
|
const backfillCheckpoint = checkpointMap[backfillType];
|
||||||
|
const upsertCheckpoint = checkpointMap[upsertType];
|
||||||
|
|
||||||
|
const deletes = this.syncRepository.getAlbumToAssetDeletes(
|
||||||
|
auth.user.id,
|
||||||
|
checkpointMap[SyncEntityType.AlbumToAssetDeleteV1],
|
||||||
|
);
|
||||||
|
for await (const { id, ...data } of deletes) {
|
||||||
|
send(response, { type: SyncEntityType.AlbumToAssetDeleteV1, ids: [id], data });
|
||||||
|
}
|
||||||
|
|
||||||
|
const albums = await this.syncRepository.getAlbumBackfill(auth.user.id, backfillCheckpoint?.updateId);
|
||||||
|
|
||||||
|
if (upsertCheckpoint) {
|
||||||
|
const endId = upsertCheckpoint.updateId;
|
||||||
|
|
||||||
|
for (const album of albums) {
|
||||||
|
const createId = album.createId;
|
||||||
|
if (isEntityBackfillComplete(createId, backfillCheckpoint)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const startId = getStartId(createId, backfillCheckpoint);
|
||||||
|
const backfill = this.syncRepository.getAlbumToAssetBackfill(album.id, startId, endId);
|
||||||
|
|
||||||
|
for await (const { updateId, ...data } of backfill) {
|
||||||
|
send(response, { type: backfillType, ids: [createId, updateId], data });
|
||||||
|
}
|
||||||
|
|
||||||
|
sendEntityBackfillCompleteAck(response, backfillType, createId);
|
||||||
|
}
|
||||||
|
} else if (albums.length > 0) {
|
||||||
|
await this.upsertBackfillCheckpoint({
|
||||||
|
type: backfillType,
|
||||||
|
sessionId,
|
||||||
|
createId: albums.at(-1)!.createId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const upserts = this.syncRepository.getAlbumToAssetUpserts(auth.user.id, checkpointMap[upsertType]);
|
||||||
|
for await (const { updateId, ...data } of upserts) {
|
||||||
|
send(response, { type: upsertType, ids: [updateId], data });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async upsertBackfillCheckpoint(item: { type: SyncEntityType; sessionId: string; createId: string }) {
|
private async upsertBackfillCheckpoint(item: { type: SyncEntityType; sessionId: string; createId: string }) {
|
||||||
const { type, sessionId, createId } = item;
|
const { type, sessionId, createId } = item;
|
||||||
await this.syncRepository.upsertCheckpoints([
|
await this.syncRepository.upsertCheckpoints([
|
||||||
|
222
server/test/medium/specs/sync/sync-album-asset-exif.spec.ts
Normal file
222
server/test/medium/specs/sync/sync-album-asset-exif.spec.ts
Normal file
@ -0,0 +1,222 @@
|
|||||||
|
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 { factory } from 'test/small.factory';
|
||||||
|
import { getKyselyDB, wait } 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.AlbumAssetExifsV1, () => {
|
||||||
|
it('should detect and sync the first album asset exif', async () => {
|
||||||
|
const { auth, sut, getRepository, testSync } = await setup();
|
||||||
|
|
||||||
|
const userRepo = getRepository('user');
|
||||||
|
const user2 = mediumFactory.userInsert();
|
||||||
|
await userRepo.create(user2);
|
||||||
|
|
||||||
|
const albumRepo = getRepository('album');
|
||||||
|
const assetRepo = getRepository('asset');
|
||||||
|
const asset = mediumFactory.assetInsert({ ownerId: user2.id });
|
||||||
|
await assetRepo.create(asset);
|
||||||
|
await assetRepo.upsertExif({ assetId: asset.id, make: 'Canon' });
|
||||||
|
await albumRepo.create({ ownerId: user2.id }, [asset.id], [{ userId: auth.user.id, role: AlbumUserRole.EDITOR }]);
|
||||||
|
|
||||||
|
const initialSyncResponse = await testSync(auth, [SyncRequestType.AlbumAssetExifsV1]);
|
||||||
|
|
||||||
|
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.AlbumAssetExifV1,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const acks = [initialSyncResponse[0].ack];
|
||||||
|
await sut.setAcks(auth, { acks });
|
||||||
|
|
||||||
|
const ackSyncResponse = await testSync(auth, [SyncRequestType.AlbumAssetExifsV1]);
|
||||||
|
|
||||||
|
expect(ackSyncResponse).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should sync album asset exif for own user', async () => {
|
||||||
|
const { auth, getRepository, testSync } = await setup();
|
||||||
|
|
||||||
|
const albumRepo = getRepository('album');
|
||||||
|
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 albumRepo.create({ ownerId: auth.user.id }, [asset.id], []);
|
||||||
|
|
||||||
|
await expect(testSync(auth, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(1);
|
||||||
|
await expect(testSync(auth, [SyncRequestType.AlbumAssetExifsV1])).resolves.toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not sync album 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 userRepo.create(user2);
|
||||||
|
await userRepo.create(user3);
|
||||||
|
|
||||||
|
const albumRepo = getRepository('album');
|
||||||
|
const assetRepo = getRepository('asset');
|
||||||
|
const asset = mediumFactory.assetInsert({ ownerId: user3.id });
|
||||||
|
await assetRepo.create(asset);
|
||||||
|
await assetRepo.upsertExif({ assetId: asset.id, make: 'Canon' });
|
||||||
|
await albumRepo.create({ ownerId: user2.id }, [asset.id], [{ userId: user3.id, role: AlbumUserRole.EDITOR }]);
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should backfill album assets exif when a user shares an album with you', async () => {
|
||||||
|
const { auth, sut, testSync, getRepository } = await setup();
|
||||||
|
|
||||||
|
const userRepo = getRepository('user');
|
||||||
|
const user2 = mediumFactory.userInsert();
|
||||||
|
const user3 = mediumFactory.userInsert();
|
||||||
|
await userRepo.create(user2);
|
||||||
|
await userRepo.create(user3);
|
||||||
|
|
||||||
|
const assetRepo = getRepository('asset');
|
||||||
|
// Asset to check that we do backfill our own assets
|
||||||
|
const asset1Owner = mediumFactory.assetInsert({ ownerId: auth.user.id });
|
||||||
|
const asset1User2 = mediumFactory.assetInsert({ ownerId: user2.id });
|
||||||
|
const asset2User2 = mediumFactory.assetInsert({ ownerId: user2.id });
|
||||||
|
const asset3User2 = mediumFactory.assetInsert({ ownerId: user2.id });
|
||||||
|
await assetRepo.create(asset1Owner);
|
||||||
|
await assetRepo.upsertExif({ assetId: asset1Owner.id, make: 'asset1Owner' });
|
||||||
|
await wait(2);
|
||||||
|
await assetRepo.create(asset1User2);
|
||||||
|
await assetRepo.upsertExif({ assetId: asset1User2.id, make: 'asset1User2' });
|
||||||
|
await wait(2);
|
||||||
|
await assetRepo.create(asset2User2);
|
||||||
|
await assetRepo.upsertExif({ assetId: asset2User2.id, make: 'asset2User2' });
|
||||||
|
await wait(2);
|
||||||
|
await assetRepo.create(asset3User2);
|
||||||
|
await assetRepo.upsertExif({ assetId: asset3User2.id, make: 'asset3User2' });
|
||||||
|
|
||||||
|
const albumRepo = getRepository('album');
|
||||||
|
const album1 = mediumFactory.albumInsert({ ownerId: user2.id });
|
||||||
|
await albumRepo.create(album1, [asset2User2.id], [{ userId: auth.user.id, role: AlbumUserRole.EDITOR }]);
|
||||||
|
|
||||||
|
const response = await testSync(auth, [SyncRequestType.AlbumAssetExifsV1]);
|
||||||
|
expect(response).toHaveLength(1);
|
||||||
|
expect(response).toEqual([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: expect.objectContaining({
|
||||||
|
assetId: asset2User2.id,
|
||||||
|
}),
|
||||||
|
type: SyncEntityType.AlbumAssetExifV1,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// ack initial album asset exif sync
|
||||||
|
const acks = response.map(({ ack }) => ack);
|
||||||
|
await sut.setAcks(auth, { acks });
|
||||||
|
|
||||||
|
// create a second album with
|
||||||
|
const album2 = mediumFactory.albumInsert({ ownerId: user2.id });
|
||||||
|
await albumRepo.create(
|
||||||
|
album2,
|
||||||
|
[asset1User2.id, asset2User2.id, asset3User2.id, asset1Owner.id],
|
||||||
|
[{ userId: auth.user.id, role: AlbumUserRole.EDITOR }],
|
||||||
|
);
|
||||||
|
|
||||||
|
// should backfill the album user
|
||||||
|
const backfillResponse = await testSync(auth, [SyncRequestType.AlbumAssetExifsV1]);
|
||||||
|
expect(backfillResponse).toEqual([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: expect.objectContaining({
|
||||||
|
assetId: asset1Owner.id,
|
||||||
|
}),
|
||||||
|
type: SyncEntityType.AlbumAssetExifBackfillV1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: expect.objectContaining({
|
||||||
|
assetId: asset1User2.id,
|
||||||
|
}),
|
||||||
|
type: SyncEntityType.AlbumAssetExifBackfillV1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: expect.objectContaining({
|
||||||
|
assetId: asset2User2.id,
|
||||||
|
}),
|
||||||
|
type: SyncEntityType.AlbumAssetExifBackfillV1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ack: expect.stringContaining(SyncEntityType.AlbumAssetExifBackfillV1),
|
||||||
|
data: {},
|
||||||
|
type: SyncEntityType.SyncAckV1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: expect.objectContaining({
|
||||||
|
assetId: asset3User2.id,
|
||||||
|
}),
|
||||||
|
type: SyncEntityType.AlbumAssetExifV1,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
await sut.setAcks(auth, { acks: [backfillResponse[3].ack, backfillResponse.at(-1).ack] });
|
||||||
|
|
||||||
|
const finalResponse = await testSync(auth, [SyncRequestType.AlbumAssetExifsV1]);
|
||||||
|
expect(finalResponse).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
218
server/test/medium/specs/sync/sync-album-asset.spec.ts
Normal file
218
server/test/medium/specs/sync/sync-album-asset.spec.ts
Normal file
@ -0,0 +1,218 @@
|
|||||||
|
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 { factory } from 'test/small.factory';
|
||||||
|
import { getKyselyDB, wait } 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.AlbumAssetsV1, () => {
|
||||||
|
it('should detect and sync the first album asset', async () => {
|
||||||
|
const { auth, sut, getRepository, testSync } = await setup();
|
||||||
|
|
||||||
|
const userRepo = getRepository('user');
|
||||||
|
const user2 = mediumFactory.userInsert();
|
||||||
|
await userRepo.create(user2);
|
||||||
|
|
||||||
|
const originalFileName = 'firstAsset';
|
||||||
|
const checksum = '1115vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA=';
|
||||||
|
const thumbhash = '2225vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA=';
|
||||||
|
const date = new Date().toISOString();
|
||||||
|
|
||||||
|
const albumRepo = getRepository('album');
|
||||||
|
const assetRepo = getRepository('asset');
|
||||||
|
const asset = mediumFactory.assetInsert({
|
||||||
|
originalFileName,
|
||||||
|
ownerId: user2.id,
|
||||||
|
checksum: Buffer.from(checksum, 'base64'),
|
||||||
|
thumbhash: Buffer.from(thumbhash, 'base64'),
|
||||||
|
fileCreatedAt: date,
|
||||||
|
fileModifiedAt: date,
|
||||||
|
localDateTime: date,
|
||||||
|
deletedAt: null,
|
||||||
|
duration: '0:10:00.00000',
|
||||||
|
});
|
||||||
|
await assetRepo.create(asset);
|
||||||
|
await albumRepo.create({ ownerId: user2.id }, [asset.id], [{ userId: auth.user.id, role: AlbumUserRole.EDITOR }]);
|
||||||
|
|
||||||
|
const initialSyncResponse = await testSync(auth, [SyncRequestType.AlbumAssetsV1]);
|
||||||
|
|
||||||
|
expect(initialSyncResponse).toHaveLength(1);
|
||||||
|
expect(initialSyncResponse).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: {
|
||||||
|
id: asset.id,
|
||||||
|
originalFileName,
|
||||||
|
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,
|
||||||
|
duration: asset.duration,
|
||||||
|
},
|
||||||
|
type: SyncEntityType.AlbumAssetV1,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const acks = [initialSyncResponse[0].ack];
|
||||||
|
await sut.setAcks(auth, { acks });
|
||||||
|
|
||||||
|
const ackSyncResponse = await testSync(auth, [SyncRequestType.AlbumAssetsV1]);
|
||||||
|
|
||||||
|
expect(ackSyncResponse).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should sync album asset for own user', async () => {
|
||||||
|
const { auth, getRepository, testSync } = await setup();
|
||||||
|
|
||||||
|
const albumRepo = getRepository('album');
|
||||||
|
const assetRepo = getRepository('asset');
|
||||||
|
const asset = mediumFactory.assetInsert({ ownerId: auth.user.id });
|
||||||
|
await assetRepo.create(asset);
|
||||||
|
await albumRepo.create({ ownerId: auth.user.id }, [asset.id], []);
|
||||||
|
|
||||||
|
await expect(testSync(auth, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1);
|
||||||
|
await expect(testSync(auth, [SyncRequestType.AlbumAssetsV1])).resolves.toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not sync album asset for unrelated user', async () => {
|
||||||
|
const { auth, getRepository, testSync } = await setup();
|
||||||
|
|
||||||
|
const userRepo = getRepository('user');
|
||||||
|
const user2 = mediumFactory.userInsert();
|
||||||
|
const user3 = mediumFactory.userInsert();
|
||||||
|
await userRepo.create(user2);
|
||||||
|
await userRepo.create(user3);
|
||||||
|
|
||||||
|
const albumRepo = getRepository('album');
|
||||||
|
const assetRepo = getRepository('asset');
|
||||||
|
const asset = mediumFactory.assetInsert({ ownerId: user3.id });
|
||||||
|
await assetRepo.create(asset);
|
||||||
|
await albumRepo.create({ ownerId: user2.id }, [asset.id], [{ userId: user3.id, role: AlbumUserRole.EDITOR }]);
|
||||||
|
|
||||||
|
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.AssetsV1])).resolves.toHaveLength(1);
|
||||||
|
await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should backfill album assets when a user shares an album with you', async () => {
|
||||||
|
const { auth, sut, testSync, getRepository } = await setup();
|
||||||
|
|
||||||
|
const userRepo = getRepository('user');
|
||||||
|
const user2 = mediumFactory.userInsert();
|
||||||
|
const user3 = mediumFactory.userInsert();
|
||||||
|
await userRepo.create(user2);
|
||||||
|
await userRepo.create(user3);
|
||||||
|
|
||||||
|
const assetRepo = getRepository('asset');
|
||||||
|
// Asset to check that we do backfill our own assets
|
||||||
|
const asset1Owner = mediumFactory.assetInsert({ ownerId: auth.user.id });
|
||||||
|
const asset1User2 = mediumFactory.assetInsert({ ownerId: user2.id });
|
||||||
|
const asset2User2 = mediumFactory.assetInsert({ ownerId: user2.id });
|
||||||
|
const asset3User2 = mediumFactory.assetInsert({ ownerId: user2.id });
|
||||||
|
await assetRepo.create(asset1Owner);
|
||||||
|
await wait(2);
|
||||||
|
await assetRepo.create(asset1User2);
|
||||||
|
await wait(2);
|
||||||
|
await assetRepo.create(asset2User2);
|
||||||
|
await wait(2);
|
||||||
|
await assetRepo.create(asset3User2);
|
||||||
|
|
||||||
|
const albumRepo = getRepository('album');
|
||||||
|
const album1 = mediumFactory.albumInsert({ ownerId: user2.id });
|
||||||
|
await albumRepo.create(album1, [asset2User2.id], [{ userId: auth.user.id, role: AlbumUserRole.EDITOR }]);
|
||||||
|
|
||||||
|
const response = await testSync(auth, [SyncRequestType.AlbumAssetsV1]);
|
||||||
|
expect(response).toHaveLength(1);
|
||||||
|
expect(response).toEqual([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: expect.objectContaining({
|
||||||
|
id: asset2User2.id,
|
||||||
|
}),
|
||||||
|
type: SyncEntityType.AlbumAssetV1,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// ack initial album asset sync
|
||||||
|
const acks = response.map(({ ack }) => ack);
|
||||||
|
await sut.setAcks(auth, { acks });
|
||||||
|
|
||||||
|
// create a second album with
|
||||||
|
const album2 = mediumFactory.albumInsert({ ownerId: user2.id });
|
||||||
|
await albumRepo.create(
|
||||||
|
album2,
|
||||||
|
[asset1User2.id, asset2User2.id, asset3User2.id, asset1Owner.id],
|
||||||
|
[{ userId: auth.user.id, role: AlbumUserRole.EDITOR }],
|
||||||
|
);
|
||||||
|
|
||||||
|
// should backfill the album user
|
||||||
|
const backfillResponse = await testSync(auth, [SyncRequestType.AlbumAssetsV1]);
|
||||||
|
expect(backfillResponse).toEqual([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: expect.objectContaining({
|
||||||
|
id: asset1Owner.id,
|
||||||
|
}),
|
||||||
|
type: SyncEntityType.AlbumAssetBackfillV1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: expect.objectContaining({
|
||||||
|
id: asset1User2.id,
|
||||||
|
}),
|
||||||
|
type: SyncEntityType.AlbumAssetBackfillV1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: expect.objectContaining({
|
||||||
|
id: asset2User2.id,
|
||||||
|
}),
|
||||||
|
type: SyncEntityType.AlbumAssetBackfillV1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ack: expect.stringContaining(SyncEntityType.AlbumAssetBackfillV1),
|
||||||
|
data: {},
|
||||||
|
type: SyncEntityType.SyncAckV1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: expect.objectContaining({
|
||||||
|
id: asset3User2.id,
|
||||||
|
}),
|
||||||
|
type: SyncEntityType.AlbumAssetV1,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
await sut.setAcks(auth, { acks: [backfillResponse[3].ack, backfillResponse.at(-1).ack] });
|
||||||
|
|
||||||
|
const finalResponse = await testSync(auth, [SyncRequestType.AlbumAssetsV1]);
|
||||||
|
expect(finalResponse).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
350
server/test/medium/specs/sync/sync-album-to-asset.spec.ts
Normal file
350
server/test/medium/specs/sync/sync-album-to-asset.spec.ts
Normal file
@ -0,0 +1,350 @@
|
|||||||
|
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, wait } 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.AlbumToAssetsV1, () => {
|
||||||
|
it('should detect and sync the first album to asset relation', async () => {
|
||||||
|
const { auth, sut, getRepository, testSync } = await setup();
|
||||||
|
|
||||||
|
const userRepo = getRepository('user');
|
||||||
|
const user2 = mediumFactory.userInsert();
|
||||||
|
await userRepo.create(user2);
|
||||||
|
|
||||||
|
const albumRepo = getRepository('album');
|
||||||
|
const assetRepo = getRepository('asset');
|
||||||
|
const asset = mediumFactory.assetInsert({ ownerId: user2.id });
|
||||||
|
await assetRepo.create(asset);
|
||||||
|
const album = mediumFactory.albumInsert({ ownerId: user2.id });
|
||||||
|
await albumRepo.create(album, [asset.id], [{ userId: auth.user.id, role: AlbumUserRole.EDITOR }]);
|
||||||
|
|
||||||
|
const initialSyncResponse = await testSync(auth, [SyncRequestType.AlbumToAssetsV1]);
|
||||||
|
|
||||||
|
expect(initialSyncResponse).toHaveLength(1);
|
||||||
|
expect(initialSyncResponse).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: {
|
||||||
|
albumId: album.id,
|
||||||
|
assetId: asset.id,
|
||||||
|
},
|
||||||
|
type: SyncEntityType.AlbumToAssetV1,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const acks = [initialSyncResponse[0].ack];
|
||||||
|
await sut.setAcks(auth, { acks });
|
||||||
|
|
||||||
|
const ackSyncResponse = await testSync(auth, [SyncRequestType.AlbumToAssetsV1]);
|
||||||
|
|
||||||
|
expect(ackSyncResponse).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should sync album to asset for owned albums', async () => {
|
||||||
|
const { auth, sut, getRepository, testSync } = await setup();
|
||||||
|
|
||||||
|
const albumRepo = getRepository('album');
|
||||||
|
const assetRepo = getRepository('asset');
|
||||||
|
const asset = mediumFactory.assetInsert({ ownerId: auth.user.id });
|
||||||
|
await assetRepo.create(asset);
|
||||||
|
const album = mediumFactory.albumInsert({ ownerId: auth.user.id });
|
||||||
|
await albumRepo.create(album, [asset.id], []);
|
||||||
|
|
||||||
|
const initialSyncResponse = await testSync(auth, [SyncRequestType.AlbumToAssetsV1]);
|
||||||
|
|
||||||
|
expect(initialSyncResponse).toHaveLength(1);
|
||||||
|
expect(initialSyncResponse).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: {
|
||||||
|
albumId: album.id,
|
||||||
|
assetId: asset.id,
|
||||||
|
},
|
||||||
|
type: SyncEntityType.AlbumToAssetV1,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const acks = [initialSyncResponse[0].ack];
|
||||||
|
await sut.setAcks(auth, { acks });
|
||||||
|
|
||||||
|
const ackSyncResponse = await testSync(auth, [SyncRequestType.AlbumToAssetsV1]);
|
||||||
|
expect(ackSyncResponse).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect and sync the album to asset for shared albums', async () => {
|
||||||
|
const { auth, sut, getRepository, testSync } = await setup();
|
||||||
|
|
||||||
|
const userRepo = getRepository('user');
|
||||||
|
const user2 = mediumFactory.userInsert();
|
||||||
|
await userRepo.create(user2);
|
||||||
|
|
||||||
|
const albumRepo = getRepository('album');
|
||||||
|
const assetRepo = getRepository('asset');
|
||||||
|
const asset = mediumFactory.assetInsert({ ownerId: auth.user.id });
|
||||||
|
await assetRepo.create(asset);
|
||||||
|
const album = mediumFactory.albumInsert({ ownerId: user2.id });
|
||||||
|
await albumRepo.create(album, [asset.id], [{ userId: auth.user.id, role: AlbumUserRole.EDITOR }]);
|
||||||
|
|
||||||
|
const initialSyncResponse = await testSync(auth, [SyncRequestType.AlbumToAssetsV1]);
|
||||||
|
|
||||||
|
expect(initialSyncResponse).toHaveLength(1);
|
||||||
|
expect(initialSyncResponse).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: {
|
||||||
|
albumId: album.id,
|
||||||
|
assetId: asset.id,
|
||||||
|
},
|
||||||
|
type: SyncEntityType.AlbumToAssetV1,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const acks = [initialSyncResponse[0].ack];
|
||||||
|
await sut.setAcks(auth, { acks });
|
||||||
|
|
||||||
|
const ackSyncResponse = await testSync(auth, [SyncRequestType.AlbumToAssetsV1]);
|
||||||
|
|
||||||
|
expect(ackSyncResponse).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not sync album to asset for an album owned by another user', async () => {
|
||||||
|
const { auth, getRepository, testSync } = await setup();
|
||||||
|
|
||||||
|
const userRepo = getRepository('user');
|
||||||
|
const user2 = mediumFactory.userInsert();
|
||||||
|
await userRepo.create(user2);
|
||||||
|
|
||||||
|
const albumRepo = getRepository('album');
|
||||||
|
const assetRepo = getRepository('asset');
|
||||||
|
const asset = mediumFactory.assetInsert({ ownerId: user2.id });
|
||||||
|
await assetRepo.create(asset);
|
||||||
|
await albumRepo.create({ ownerId: user2.id }, [asset.id], []);
|
||||||
|
|
||||||
|
await expect(testSync(auth, [SyncRequestType.AlbumToAssetsV1])).resolves.toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should backfill album to assets when a user shares an album with you', async () => {
|
||||||
|
const { auth, sut, testSync, getRepository } = await setup();
|
||||||
|
|
||||||
|
const userRepo = getRepository('user');
|
||||||
|
const user2 = mediumFactory.userInsert();
|
||||||
|
await userRepo.create(user2);
|
||||||
|
|
||||||
|
const assetRepo = getRepository('asset');
|
||||||
|
const album1Asset = mediumFactory.assetInsert({ ownerId: user2.id });
|
||||||
|
await assetRepo.create(album1Asset);
|
||||||
|
const album2Asset = mediumFactory.assetInsert({ ownerId: auth.user.id });
|
||||||
|
await assetRepo.create(album2Asset);
|
||||||
|
|
||||||
|
// Backfill album
|
||||||
|
const albumRepo = getRepository('album');
|
||||||
|
const album2 = mediumFactory.albumInsert({ ownerId: user2.id });
|
||||||
|
await albumRepo.create(album2, [album2Asset.id], []);
|
||||||
|
|
||||||
|
await wait(2);
|
||||||
|
|
||||||
|
const album1 = mediumFactory.albumInsert({ ownerId: auth.user.id });
|
||||||
|
await albumRepo.create(album1, [album1Asset.id], []);
|
||||||
|
|
||||||
|
const response = await testSync(auth, [SyncRequestType.AlbumToAssetsV1]);
|
||||||
|
expect(response).toHaveLength(1);
|
||||||
|
expect(response).toEqual([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: {
|
||||||
|
albumId: album1.id,
|
||||||
|
assetId: album1Asset.id,
|
||||||
|
},
|
||||||
|
type: SyncEntityType.AlbumToAssetV1,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// ack initial album to asset sync
|
||||||
|
const acks = response.map(({ ack }) => ack);
|
||||||
|
await sut.setAcks(auth, { acks });
|
||||||
|
|
||||||
|
// add user to backfill album
|
||||||
|
const albumUserRepo = getRepository('albumUser');
|
||||||
|
await albumUserRepo.create({ albumsId: album2.id, usersId: auth.user.id, role: AlbumUserRole.EDITOR });
|
||||||
|
|
||||||
|
// should backfill the album to asset relation
|
||||||
|
const backfillResponse = await testSync(auth, [SyncRequestType.AlbumToAssetsV1]);
|
||||||
|
expect(backfillResponse).toEqual([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: expect.objectContaining({
|
||||||
|
albumId: album2.id,
|
||||||
|
assetId: album2Asset.id,
|
||||||
|
}),
|
||||||
|
type: SyncEntityType.AlbumToAssetBackfillV1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ack: expect.stringContaining(SyncEntityType.AlbumToAssetBackfillV1),
|
||||||
|
data: {},
|
||||||
|
type: SyncEntityType.SyncAckV1,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
await sut.setAcks(auth, { acks: [backfillResponse[1].ack] });
|
||||||
|
|
||||||
|
const finalResponse = await testSync(auth, [SyncRequestType.AlbumToAssetsV1]);
|
||||||
|
expect(finalResponse).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect and sync a deleted album to asset relation', async () => {
|
||||||
|
const { auth, sut, getRepository, testSync } = await setup();
|
||||||
|
|
||||||
|
const albumRepo = getRepository('album');
|
||||||
|
const assetRepo = getRepository('asset');
|
||||||
|
const asset = mediumFactory.assetInsert({ ownerId: auth.user.id });
|
||||||
|
await assetRepo.create(asset);
|
||||||
|
const album = mediumFactory.albumInsert({ ownerId: auth.user.id });
|
||||||
|
await albumRepo.create(album, [asset.id], []);
|
||||||
|
|
||||||
|
const initialSyncResponse = await testSync(auth, [SyncRequestType.AlbumToAssetsV1]);
|
||||||
|
|
||||||
|
expect(initialSyncResponse).toHaveLength(1);
|
||||||
|
expect(initialSyncResponse).toEqual([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: {
|
||||||
|
albumId: album.id,
|
||||||
|
assetId: asset.id,
|
||||||
|
},
|
||||||
|
type: SyncEntityType.AlbumToAssetV1,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const acks = [initialSyncResponse[0].ack];
|
||||||
|
await sut.setAcks(auth, { acks });
|
||||||
|
|
||||||
|
await albumRepo.removeAssetIds(album.id, [asset.id]);
|
||||||
|
|
||||||
|
await wait(2);
|
||||||
|
|
||||||
|
const syncResponse = await testSync(auth, [SyncRequestType.AlbumToAssetsV1]);
|
||||||
|
expect(syncResponse).toHaveLength(1);
|
||||||
|
expect(syncResponse).toEqual([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: {
|
||||||
|
albumId: album.id,
|
||||||
|
assetId: asset.id,
|
||||||
|
},
|
||||||
|
type: SyncEntityType.AlbumToAssetDeleteV1,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
await sut.setAcks(auth, { acks: [syncResponse[0].ack] });
|
||||||
|
|
||||||
|
const ackSyncResponse = await testSync(auth, [SyncRequestType.AlbumToAssetsV1]);
|
||||||
|
expect(ackSyncResponse).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect and sync a deleted album to asset relation when an asset is deleted', async () => {
|
||||||
|
const { auth, sut, getRepository, testSync } = await setup();
|
||||||
|
|
||||||
|
const albumRepo = getRepository('album');
|
||||||
|
const assetRepo = getRepository('asset');
|
||||||
|
const asset = mediumFactory.assetInsert({ ownerId: auth.user.id });
|
||||||
|
await assetRepo.create(asset);
|
||||||
|
const album = mediumFactory.albumInsert({ ownerId: auth.user.id });
|
||||||
|
await albumRepo.create(album, [asset.id], []);
|
||||||
|
|
||||||
|
const initialSyncResponse = await testSync(auth, [SyncRequestType.AlbumToAssetsV1]);
|
||||||
|
|
||||||
|
expect(initialSyncResponse).toHaveLength(1);
|
||||||
|
expect(initialSyncResponse).toEqual([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: {
|
||||||
|
albumId: album.id,
|
||||||
|
assetId: asset.id,
|
||||||
|
},
|
||||||
|
type: SyncEntityType.AlbumToAssetV1,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const acks = [initialSyncResponse[0].ack];
|
||||||
|
await sut.setAcks(auth, { acks });
|
||||||
|
|
||||||
|
await assetRepo.remove({ id: asset.id });
|
||||||
|
|
||||||
|
await wait(2);
|
||||||
|
|
||||||
|
const syncResponse = await testSync(auth, [SyncRequestType.AlbumToAssetsV1]);
|
||||||
|
expect(syncResponse).toHaveLength(1);
|
||||||
|
expect(syncResponse).toEqual([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: {
|
||||||
|
albumId: album.id,
|
||||||
|
assetId: asset.id,
|
||||||
|
},
|
||||||
|
type: SyncEntityType.AlbumToAssetDeleteV1,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
await sut.setAcks(auth, { acks: [syncResponse[0].ack] });
|
||||||
|
|
||||||
|
const ackSyncResponse = await testSync(auth, [SyncRequestType.AlbumToAssetsV1]);
|
||||||
|
expect(ackSyncResponse).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not sync a deleted album to asset relation when the album is deleted', async () => {
|
||||||
|
const { auth, sut, getRepository, testSync } = await setup();
|
||||||
|
|
||||||
|
const albumRepo = getRepository('album');
|
||||||
|
const assetRepo = getRepository('asset');
|
||||||
|
const asset = mediumFactory.assetInsert({ ownerId: auth.user.id });
|
||||||
|
await assetRepo.create(asset);
|
||||||
|
const album = mediumFactory.albumInsert({ ownerId: auth.user.id });
|
||||||
|
await albumRepo.create(album, [asset.id], []);
|
||||||
|
|
||||||
|
const initialSyncResponse = await testSync(auth, [SyncRequestType.AlbumToAssetsV1]);
|
||||||
|
|
||||||
|
expect(initialSyncResponse).toHaveLength(1);
|
||||||
|
expect(initialSyncResponse).toEqual([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: {
|
||||||
|
albumId: album.id,
|
||||||
|
assetId: asset.id,
|
||||||
|
},
|
||||||
|
type: SyncEntityType.AlbumToAssetV1,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const acks = [initialSyncResponse[0].ack];
|
||||||
|
await sut.setAcks(auth, { acks });
|
||||||
|
|
||||||
|
await albumRepo.delete(album.id);
|
||||||
|
|
||||||
|
await wait(2);
|
||||||
|
|
||||||
|
const syncResponse = await testSync(auth, [SyncRequestType.AlbumToAssetsV1]);
|
||||||
|
expect(syncResponse).toHaveLength(0);
|
||||||
|
expect(syncResponse).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
Loading…
x
Reference in New Issue
Block a user