mirror of
https://github.com/immich-app/immich.git
synced 2025-05-24 01:12:58 -04:00
feat: sync assets, partner assets, exif, and partner exif (#16658)
* feat: sync assets, partner assets, exif, and partner exif Co-authored-by: Zack Pollard <zack@futo.org> Co-authored-by: Alex Tran <alex.tran1502@gmail.com> * refactor: remove duplicate where clause and orderBy statements in sync queries * fix: asset deletes not filtering by ownerId --------- Co-authored-by: Zack Pollard <zack@futo.org> Co-authored-by: Alex Tran <alex.tran1502@gmail.com> Co-authored-by: Zack Pollard <zackpollard@ymail.com>
This commit is contained in:
parent
e97df503f2
commit
a96bba4b26
3
mobile/openapi/README.md
generated
3
mobile/openapi/README.md
generated
@ -424,6 +424,9 @@ Class | Method | HTTP request | Description
|
|||||||
- [SyncAckDeleteDto](doc//SyncAckDeleteDto.md)
|
- [SyncAckDeleteDto](doc//SyncAckDeleteDto.md)
|
||||||
- [SyncAckDto](doc//SyncAckDto.md)
|
- [SyncAckDto](doc//SyncAckDto.md)
|
||||||
- [SyncAckSetDto](doc//SyncAckSetDto.md)
|
- [SyncAckSetDto](doc//SyncAckSetDto.md)
|
||||||
|
- [SyncAssetDeleteV1](doc//SyncAssetDeleteV1.md)
|
||||||
|
- [SyncAssetExifV1](doc//SyncAssetExifV1.md)
|
||||||
|
- [SyncAssetV1](doc//SyncAssetV1.md)
|
||||||
- [SyncEntityType](doc//SyncEntityType.md)
|
- [SyncEntityType](doc//SyncEntityType.md)
|
||||||
- [SyncPartnerDeleteV1](doc//SyncPartnerDeleteV1.md)
|
- [SyncPartnerDeleteV1](doc//SyncPartnerDeleteV1.md)
|
||||||
- [SyncPartnerV1](doc//SyncPartnerV1.md)
|
- [SyncPartnerV1](doc//SyncPartnerV1.md)
|
||||||
|
3
mobile/openapi/lib/api.dart
generated
3
mobile/openapi/lib/api.dart
generated
@ -231,6 +231,9 @@ part 'model/stack_update_dto.dart';
|
|||||||
part 'model/sync_ack_delete_dto.dart';
|
part 'model/sync_ack_delete_dto.dart';
|
||||||
part 'model/sync_ack_dto.dart';
|
part 'model/sync_ack_dto.dart';
|
||||||
part 'model/sync_ack_set_dto.dart';
|
part 'model/sync_ack_set_dto.dart';
|
||||||
|
part 'model/sync_asset_delete_v1.dart';
|
||||||
|
part 'model/sync_asset_exif_v1.dart';
|
||||||
|
part 'model/sync_asset_v1.dart';
|
||||||
part 'model/sync_entity_type.dart';
|
part 'model/sync_entity_type.dart';
|
||||||
part 'model/sync_partner_delete_v1.dart';
|
part 'model/sync_partner_delete_v1.dart';
|
||||||
part 'model/sync_partner_v1.dart';
|
part 'model/sync_partner_v1.dart';
|
||||||
|
6
mobile/openapi/lib/api_client.dart
generated
6
mobile/openapi/lib/api_client.dart
generated
@ -518,6 +518,12 @@ class ApiClient {
|
|||||||
return SyncAckDto.fromJson(value);
|
return SyncAckDto.fromJson(value);
|
||||||
case 'SyncAckSetDto':
|
case 'SyncAckSetDto':
|
||||||
return SyncAckSetDto.fromJson(value);
|
return SyncAckSetDto.fromJson(value);
|
||||||
|
case 'SyncAssetDeleteV1':
|
||||||
|
return SyncAssetDeleteV1.fromJson(value);
|
||||||
|
case 'SyncAssetExifV1':
|
||||||
|
return SyncAssetExifV1.fromJson(value);
|
||||||
|
case 'SyncAssetV1':
|
||||||
|
return SyncAssetV1.fromJson(value);
|
||||||
case 'SyncEntityType':
|
case 'SyncEntityType':
|
||||||
return SyncEntityTypeTypeTransformer().decode(value);
|
return SyncEntityTypeTypeTransformer().decode(value);
|
||||||
case 'SyncPartnerDeleteV1':
|
case 'SyncPartnerDeleteV1':
|
||||||
|
99
mobile/openapi/lib/model/sync_asset_delete_v1.dart
generated
Normal file
99
mobile/openapi/lib/model/sync_asset_delete_v1.dart
generated
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
//
|
||||||
|
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||||
|
//
|
||||||
|
// @dart=2.18
|
||||||
|
|
||||||
|
// ignore_for_file: unused_element, unused_import
|
||||||
|
// ignore_for_file: always_put_required_named_parameters_first
|
||||||
|
// ignore_for_file: constant_identifier_names
|
||||||
|
// ignore_for_file: lines_longer_than_80_chars
|
||||||
|
|
||||||
|
part of openapi.api;
|
||||||
|
|
||||||
|
class SyncAssetDeleteV1 {
|
||||||
|
/// Returns a new [SyncAssetDeleteV1] instance.
|
||||||
|
SyncAssetDeleteV1({
|
||||||
|
required this.assetId,
|
||||||
|
});
|
||||||
|
|
||||||
|
String assetId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => identical(this, other) || other is SyncAssetDeleteV1 &&
|
||||||
|
other.assetId == assetId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
// ignore: unnecessary_parenthesis
|
||||||
|
(assetId.hashCode);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'SyncAssetDeleteV1[assetId=$assetId]';
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final json = <String, dynamic>{};
|
||||||
|
json[r'assetId'] = this.assetId;
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new [SyncAssetDeleteV1] instance and imports its values from
|
||||||
|
/// [value] if it's a [Map], null otherwise.
|
||||||
|
// ignore: prefer_constructors_over_static_methods
|
||||||
|
static SyncAssetDeleteV1? fromJson(dynamic value) {
|
||||||
|
upgradeDto(value, "SyncAssetDeleteV1");
|
||||||
|
if (value is Map) {
|
||||||
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
|
return SyncAssetDeleteV1(
|
||||||
|
assetId: mapValueOfType<String>(json, r'assetId')!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<SyncAssetDeleteV1> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <SyncAssetDeleteV1>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = SyncAssetDeleteV1.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, SyncAssetDeleteV1> mapFromJson(dynamic json) {
|
||||||
|
final map = <String, SyncAssetDeleteV1>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
final value = SyncAssetDeleteV1.fromJson(entry.value);
|
||||||
|
if (value != null) {
|
||||||
|
map[entry.key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// maps a json object with a list of SyncAssetDeleteV1-objects as value to a dart map
|
||||||
|
static Map<String, List<SyncAssetDeleteV1>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final map = <String, List<SyncAssetDeleteV1>>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
json = json.cast<String, dynamic>();
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
map[entry.key] = SyncAssetDeleteV1.listFromJson(entry.value, growable: growable,);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The list of required keys that must be present in a JSON.
|
||||||
|
static const requiredKeys = <String>{
|
||||||
|
'assetId',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
387
mobile/openapi/lib/model/sync_asset_exif_v1.dart
generated
Normal file
387
mobile/openapi/lib/model/sync_asset_exif_v1.dart
generated
Normal file
@ -0,0 +1,387 @@
|
|||||||
|
//
|
||||||
|
// 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 SyncAssetExifV1 {
|
||||||
|
/// Returns a new [SyncAssetExifV1] instance.
|
||||||
|
SyncAssetExifV1({
|
||||||
|
required this.assetId,
|
||||||
|
required this.city,
|
||||||
|
required this.country,
|
||||||
|
required this.dateTimeOriginal,
|
||||||
|
required this.description,
|
||||||
|
required this.exifImageHeight,
|
||||||
|
required this.exifImageWidth,
|
||||||
|
required this.exposureTime,
|
||||||
|
required this.fNumber,
|
||||||
|
required this.fileSizeInByte,
|
||||||
|
required this.focalLength,
|
||||||
|
required this.fps,
|
||||||
|
required this.iso,
|
||||||
|
required this.latitude,
|
||||||
|
required this.lensModel,
|
||||||
|
required this.longitude,
|
||||||
|
required this.make,
|
||||||
|
required this.model,
|
||||||
|
required this.modifyDate,
|
||||||
|
required this.orientation,
|
||||||
|
required this.profileDescription,
|
||||||
|
required this.projectionType,
|
||||||
|
required this.rating,
|
||||||
|
required this.state,
|
||||||
|
required this.timeZone,
|
||||||
|
});
|
||||||
|
|
||||||
|
String assetId;
|
||||||
|
|
||||||
|
String? city;
|
||||||
|
|
||||||
|
String? country;
|
||||||
|
|
||||||
|
DateTime? dateTimeOriginal;
|
||||||
|
|
||||||
|
String? description;
|
||||||
|
|
||||||
|
int? exifImageHeight;
|
||||||
|
|
||||||
|
int? exifImageWidth;
|
||||||
|
|
||||||
|
String? exposureTime;
|
||||||
|
|
||||||
|
int? fNumber;
|
||||||
|
|
||||||
|
int? fileSizeInByte;
|
||||||
|
|
||||||
|
int? focalLength;
|
||||||
|
|
||||||
|
int? fps;
|
||||||
|
|
||||||
|
int? iso;
|
||||||
|
|
||||||
|
int? latitude;
|
||||||
|
|
||||||
|
String? lensModel;
|
||||||
|
|
||||||
|
int? longitude;
|
||||||
|
|
||||||
|
String? make;
|
||||||
|
|
||||||
|
String? model;
|
||||||
|
|
||||||
|
DateTime? modifyDate;
|
||||||
|
|
||||||
|
String? orientation;
|
||||||
|
|
||||||
|
String? profileDescription;
|
||||||
|
|
||||||
|
String? projectionType;
|
||||||
|
|
||||||
|
int? rating;
|
||||||
|
|
||||||
|
String? state;
|
||||||
|
|
||||||
|
String? timeZone;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => identical(this, other) || other is SyncAssetExifV1 &&
|
||||||
|
other.assetId == assetId &&
|
||||||
|
other.city == city &&
|
||||||
|
other.country == country &&
|
||||||
|
other.dateTimeOriginal == dateTimeOriginal &&
|
||||||
|
other.description == description &&
|
||||||
|
other.exifImageHeight == exifImageHeight &&
|
||||||
|
other.exifImageWidth == exifImageWidth &&
|
||||||
|
other.exposureTime == exposureTime &&
|
||||||
|
other.fNumber == fNumber &&
|
||||||
|
other.fileSizeInByte == fileSizeInByte &&
|
||||||
|
other.focalLength == focalLength &&
|
||||||
|
other.fps == fps &&
|
||||||
|
other.iso == iso &&
|
||||||
|
other.latitude == latitude &&
|
||||||
|
other.lensModel == lensModel &&
|
||||||
|
other.longitude == longitude &&
|
||||||
|
other.make == make &&
|
||||||
|
other.model == model &&
|
||||||
|
other.modifyDate == modifyDate &&
|
||||||
|
other.orientation == orientation &&
|
||||||
|
other.profileDescription == profileDescription &&
|
||||||
|
other.projectionType == projectionType &&
|
||||||
|
other.rating == rating &&
|
||||||
|
other.state == state &&
|
||||||
|
other.timeZone == timeZone;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
// ignore: unnecessary_parenthesis
|
||||||
|
(assetId.hashCode) +
|
||||||
|
(city == null ? 0 : city!.hashCode) +
|
||||||
|
(country == null ? 0 : country!.hashCode) +
|
||||||
|
(dateTimeOriginal == null ? 0 : dateTimeOriginal!.hashCode) +
|
||||||
|
(description == null ? 0 : description!.hashCode) +
|
||||||
|
(exifImageHeight == null ? 0 : exifImageHeight!.hashCode) +
|
||||||
|
(exifImageWidth == null ? 0 : exifImageWidth!.hashCode) +
|
||||||
|
(exposureTime == null ? 0 : exposureTime!.hashCode) +
|
||||||
|
(fNumber == null ? 0 : fNumber!.hashCode) +
|
||||||
|
(fileSizeInByte == null ? 0 : fileSizeInByte!.hashCode) +
|
||||||
|
(focalLength == null ? 0 : focalLength!.hashCode) +
|
||||||
|
(fps == null ? 0 : fps!.hashCode) +
|
||||||
|
(iso == null ? 0 : iso!.hashCode) +
|
||||||
|
(latitude == null ? 0 : latitude!.hashCode) +
|
||||||
|
(lensModel == null ? 0 : lensModel!.hashCode) +
|
||||||
|
(longitude == null ? 0 : longitude!.hashCode) +
|
||||||
|
(make == null ? 0 : make!.hashCode) +
|
||||||
|
(model == null ? 0 : model!.hashCode) +
|
||||||
|
(modifyDate == null ? 0 : modifyDate!.hashCode) +
|
||||||
|
(orientation == null ? 0 : orientation!.hashCode) +
|
||||||
|
(profileDescription == null ? 0 : profileDescription!.hashCode) +
|
||||||
|
(projectionType == null ? 0 : projectionType!.hashCode) +
|
||||||
|
(rating == null ? 0 : rating!.hashCode) +
|
||||||
|
(state == null ? 0 : state!.hashCode) +
|
||||||
|
(timeZone == null ? 0 : timeZone!.hashCode);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'SyncAssetExifV1[assetId=$assetId, city=$city, country=$country, dateTimeOriginal=$dateTimeOriginal, description=$description, exifImageHeight=$exifImageHeight, exifImageWidth=$exifImageWidth, exposureTime=$exposureTime, fNumber=$fNumber, fileSizeInByte=$fileSizeInByte, focalLength=$focalLength, fps=$fps, iso=$iso, latitude=$latitude, lensModel=$lensModel, longitude=$longitude, make=$make, model=$model, modifyDate=$modifyDate, orientation=$orientation, profileDescription=$profileDescription, projectionType=$projectionType, rating=$rating, state=$state, timeZone=$timeZone]';
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final json = <String, dynamic>{};
|
||||||
|
json[r'assetId'] = this.assetId;
|
||||||
|
if (this.city != null) {
|
||||||
|
json[r'city'] = this.city;
|
||||||
|
} else {
|
||||||
|
// json[r'city'] = null;
|
||||||
|
}
|
||||||
|
if (this.country != null) {
|
||||||
|
json[r'country'] = this.country;
|
||||||
|
} else {
|
||||||
|
// json[r'country'] = null;
|
||||||
|
}
|
||||||
|
if (this.dateTimeOriginal != null) {
|
||||||
|
json[r'dateTimeOriginal'] = this.dateTimeOriginal!.toUtc().toIso8601String();
|
||||||
|
} else {
|
||||||
|
// json[r'dateTimeOriginal'] = null;
|
||||||
|
}
|
||||||
|
if (this.description != null) {
|
||||||
|
json[r'description'] = this.description;
|
||||||
|
} else {
|
||||||
|
// json[r'description'] = null;
|
||||||
|
}
|
||||||
|
if (this.exifImageHeight != null) {
|
||||||
|
json[r'exifImageHeight'] = this.exifImageHeight;
|
||||||
|
} else {
|
||||||
|
// json[r'exifImageHeight'] = null;
|
||||||
|
}
|
||||||
|
if (this.exifImageWidth != null) {
|
||||||
|
json[r'exifImageWidth'] = this.exifImageWidth;
|
||||||
|
} else {
|
||||||
|
// json[r'exifImageWidth'] = null;
|
||||||
|
}
|
||||||
|
if (this.exposureTime != null) {
|
||||||
|
json[r'exposureTime'] = this.exposureTime;
|
||||||
|
} else {
|
||||||
|
// json[r'exposureTime'] = null;
|
||||||
|
}
|
||||||
|
if (this.fNumber != null) {
|
||||||
|
json[r'fNumber'] = this.fNumber;
|
||||||
|
} else {
|
||||||
|
// json[r'fNumber'] = null;
|
||||||
|
}
|
||||||
|
if (this.fileSizeInByte != null) {
|
||||||
|
json[r'fileSizeInByte'] = this.fileSizeInByte;
|
||||||
|
} else {
|
||||||
|
// json[r'fileSizeInByte'] = null;
|
||||||
|
}
|
||||||
|
if (this.focalLength != null) {
|
||||||
|
json[r'focalLength'] = this.focalLength;
|
||||||
|
} else {
|
||||||
|
// json[r'focalLength'] = null;
|
||||||
|
}
|
||||||
|
if (this.fps != null) {
|
||||||
|
json[r'fps'] = this.fps;
|
||||||
|
} else {
|
||||||
|
// json[r'fps'] = null;
|
||||||
|
}
|
||||||
|
if (this.iso != null) {
|
||||||
|
json[r'iso'] = this.iso;
|
||||||
|
} else {
|
||||||
|
// json[r'iso'] = null;
|
||||||
|
}
|
||||||
|
if (this.latitude != null) {
|
||||||
|
json[r'latitude'] = this.latitude;
|
||||||
|
} else {
|
||||||
|
// json[r'latitude'] = null;
|
||||||
|
}
|
||||||
|
if (this.lensModel != null) {
|
||||||
|
json[r'lensModel'] = this.lensModel;
|
||||||
|
} else {
|
||||||
|
// json[r'lensModel'] = null;
|
||||||
|
}
|
||||||
|
if (this.longitude != null) {
|
||||||
|
json[r'longitude'] = this.longitude;
|
||||||
|
} else {
|
||||||
|
// json[r'longitude'] = null;
|
||||||
|
}
|
||||||
|
if (this.make != null) {
|
||||||
|
json[r'make'] = this.make;
|
||||||
|
} else {
|
||||||
|
// json[r'make'] = null;
|
||||||
|
}
|
||||||
|
if (this.model != null) {
|
||||||
|
json[r'model'] = this.model;
|
||||||
|
} else {
|
||||||
|
// json[r'model'] = null;
|
||||||
|
}
|
||||||
|
if (this.modifyDate != null) {
|
||||||
|
json[r'modifyDate'] = this.modifyDate!.toUtc().toIso8601String();
|
||||||
|
} else {
|
||||||
|
// json[r'modifyDate'] = null;
|
||||||
|
}
|
||||||
|
if (this.orientation != null) {
|
||||||
|
json[r'orientation'] = this.orientation;
|
||||||
|
} else {
|
||||||
|
// json[r'orientation'] = null;
|
||||||
|
}
|
||||||
|
if (this.profileDescription != null) {
|
||||||
|
json[r'profileDescription'] = this.profileDescription;
|
||||||
|
} else {
|
||||||
|
// json[r'profileDescription'] = null;
|
||||||
|
}
|
||||||
|
if (this.projectionType != null) {
|
||||||
|
json[r'projectionType'] = this.projectionType;
|
||||||
|
} else {
|
||||||
|
// json[r'projectionType'] = null;
|
||||||
|
}
|
||||||
|
if (this.rating != null) {
|
||||||
|
json[r'rating'] = this.rating;
|
||||||
|
} else {
|
||||||
|
// json[r'rating'] = null;
|
||||||
|
}
|
||||||
|
if (this.state != null) {
|
||||||
|
json[r'state'] = this.state;
|
||||||
|
} else {
|
||||||
|
// json[r'state'] = null;
|
||||||
|
}
|
||||||
|
if (this.timeZone != null) {
|
||||||
|
json[r'timeZone'] = this.timeZone;
|
||||||
|
} else {
|
||||||
|
// json[r'timeZone'] = null;
|
||||||
|
}
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new [SyncAssetExifV1] instance and imports its values from
|
||||||
|
/// [value] if it's a [Map], null otherwise.
|
||||||
|
// ignore: prefer_constructors_over_static_methods
|
||||||
|
static SyncAssetExifV1? fromJson(dynamic value) {
|
||||||
|
upgradeDto(value, "SyncAssetExifV1");
|
||||||
|
if (value is Map) {
|
||||||
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
|
return SyncAssetExifV1(
|
||||||
|
assetId: mapValueOfType<String>(json, r'assetId')!,
|
||||||
|
city: mapValueOfType<String>(json, r'city'),
|
||||||
|
country: mapValueOfType<String>(json, r'country'),
|
||||||
|
dateTimeOriginal: mapDateTime(json, r'dateTimeOriginal', r''),
|
||||||
|
description: mapValueOfType<String>(json, r'description'),
|
||||||
|
exifImageHeight: mapValueOfType<int>(json, r'exifImageHeight'),
|
||||||
|
exifImageWidth: mapValueOfType<int>(json, r'exifImageWidth'),
|
||||||
|
exposureTime: mapValueOfType<String>(json, r'exposureTime'),
|
||||||
|
fNumber: mapValueOfType<int>(json, r'fNumber'),
|
||||||
|
fileSizeInByte: mapValueOfType<int>(json, r'fileSizeInByte'),
|
||||||
|
focalLength: mapValueOfType<int>(json, r'focalLength'),
|
||||||
|
fps: mapValueOfType<int>(json, r'fps'),
|
||||||
|
iso: mapValueOfType<int>(json, r'iso'),
|
||||||
|
latitude: mapValueOfType<int>(json, r'latitude'),
|
||||||
|
lensModel: mapValueOfType<String>(json, r'lensModel'),
|
||||||
|
longitude: mapValueOfType<int>(json, r'longitude'),
|
||||||
|
make: mapValueOfType<String>(json, r'make'),
|
||||||
|
model: mapValueOfType<String>(json, r'model'),
|
||||||
|
modifyDate: mapDateTime(json, r'modifyDate', r''),
|
||||||
|
orientation: mapValueOfType<String>(json, r'orientation'),
|
||||||
|
profileDescription: mapValueOfType<String>(json, r'profileDescription'),
|
||||||
|
projectionType: mapValueOfType<String>(json, r'projectionType'),
|
||||||
|
rating: mapValueOfType<int>(json, r'rating'),
|
||||||
|
state: mapValueOfType<String>(json, r'state'),
|
||||||
|
timeZone: mapValueOfType<String>(json, r'timeZone'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<SyncAssetExifV1> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <SyncAssetExifV1>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = SyncAssetExifV1.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, SyncAssetExifV1> mapFromJson(dynamic json) {
|
||||||
|
final map = <String, SyncAssetExifV1>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
final value = SyncAssetExifV1.fromJson(entry.value);
|
||||||
|
if (value != null) {
|
||||||
|
map[entry.key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// maps a json object with a list of SyncAssetExifV1-objects as value to a dart map
|
||||||
|
static Map<String, List<SyncAssetExifV1>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final map = <String, List<SyncAssetExifV1>>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
json = json.cast<String, dynamic>();
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
map[entry.key] = SyncAssetExifV1.listFromJson(entry.value, growable: growable,);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The list of required keys that must be present in a JSON.
|
||||||
|
static const requiredKeys = <String>{
|
||||||
|
'assetId',
|
||||||
|
'city',
|
||||||
|
'country',
|
||||||
|
'dateTimeOriginal',
|
||||||
|
'description',
|
||||||
|
'exifImageHeight',
|
||||||
|
'exifImageWidth',
|
||||||
|
'exposureTime',
|
||||||
|
'fNumber',
|
||||||
|
'fileSizeInByte',
|
||||||
|
'focalLength',
|
||||||
|
'fps',
|
||||||
|
'iso',
|
||||||
|
'latitude',
|
||||||
|
'lensModel',
|
||||||
|
'longitude',
|
||||||
|
'make',
|
||||||
|
'model',
|
||||||
|
'modifyDate',
|
||||||
|
'orientation',
|
||||||
|
'profileDescription',
|
||||||
|
'projectionType',
|
||||||
|
'rating',
|
||||||
|
'state',
|
||||||
|
'timeZone',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
279
mobile/openapi/lib/model/sync_asset_v1.dart
generated
Normal file
279
mobile/openapi/lib/model/sync_asset_v1.dart
generated
Normal file
@ -0,0 +1,279 @@
|
|||||||
|
//
|
||||||
|
// 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 SyncAssetV1 {
|
||||||
|
/// Returns a new [SyncAssetV1] instance.
|
||||||
|
SyncAssetV1({
|
||||||
|
required this.checksum,
|
||||||
|
required this.deletedAt,
|
||||||
|
required this.fileCreatedAt,
|
||||||
|
required this.fileModifiedAt,
|
||||||
|
required this.id,
|
||||||
|
required this.isFavorite,
|
||||||
|
required this.isVisible,
|
||||||
|
required this.localDateTime,
|
||||||
|
required this.ownerId,
|
||||||
|
required this.thumbhash,
|
||||||
|
required this.type,
|
||||||
|
});
|
||||||
|
|
||||||
|
String checksum;
|
||||||
|
|
||||||
|
DateTime? deletedAt;
|
||||||
|
|
||||||
|
DateTime? fileCreatedAt;
|
||||||
|
|
||||||
|
DateTime? fileModifiedAt;
|
||||||
|
|
||||||
|
String id;
|
||||||
|
|
||||||
|
bool isFavorite;
|
||||||
|
|
||||||
|
bool isVisible;
|
||||||
|
|
||||||
|
DateTime? localDateTime;
|
||||||
|
|
||||||
|
String ownerId;
|
||||||
|
|
||||||
|
String? thumbhash;
|
||||||
|
|
||||||
|
SyncAssetV1TypeEnum type;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => identical(this, other) || other is SyncAssetV1 &&
|
||||||
|
other.checksum == checksum &&
|
||||||
|
other.deletedAt == deletedAt &&
|
||||||
|
other.fileCreatedAt == fileCreatedAt &&
|
||||||
|
other.fileModifiedAt == fileModifiedAt &&
|
||||||
|
other.id == id &&
|
||||||
|
other.isFavorite == isFavorite &&
|
||||||
|
other.isVisible == isVisible &&
|
||||||
|
other.localDateTime == localDateTime &&
|
||||||
|
other.ownerId == ownerId &&
|
||||||
|
other.thumbhash == thumbhash &&
|
||||||
|
other.type == type;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
// ignore: unnecessary_parenthesis
|
||||||
|
(checksum.hashCode) +
|
||||||
|
(deletedAt == null ? 0 : deletedAt!.hashCode) +
|
||||||
|
(fileCreatedAt == null ? 0 : fileCreatedAt!.hashCode) +
|
||||||
|
(fileModifiedAt == null ? 0 : fileModifiedAt!.hashCode) +
|
||||||
|
(id.hashCode) +
|
||||||
|
(isFavorite.hashCode) +
|
||||||
|
(isVisible.hashCode) +
|
||||||
|
(localDateTime == null ? 0 : localDateTime!.hashCode) +
|
||||||
|
(ownerId.hashCode) +
|
||||||
|
(thumbhash == null ? 0 : thumbhash!.hashCode) +
|
||||||
|
(type.hashCode);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'SyncAssetV1[checksum=$checksum, deletedAt=$deletedAt, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, id=$id, isFavorite=$isFavorite, isVisible=$isVisible, localDateTime=$localDateTime, ownerId=$ownerId, thumbhash=$thumbhash, type=$type]';
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final json = <String, dynamic>{};
|
||||||
|
json[r'checksum'] = this.checksum;
|
||||||
|
if (this.deletedAt != null) {
|
||||||
|
json[r'deletedAt'] = this.deletedAt!.toUtc().toIso8601String();
|
||||||
|
} else {
|
||||||
|
// json[r'deletedAt'] = null;
|
||||||
|
}
|
||||||
|
if (this.fileCreatedAt != null) {
|
||||||
|
json[r'fileCreatedAt'] = this.fileCreatedAt!.toUtc().toIso8601String();
|
||||||
|
} else {
|
||||||
|
// json[r'fileCreatedAt'] = null;
|
||||||
|
}
|
||||||
|
if (this.fileModifiedAt != null) {
|
||||||
|
json[r'fileModifiedAt'] = this.fileModifiedAt!.toUtc().toIso8601String();
|
||||||
|
} else {
|
||||||
|
// json[r'fileModifiedAt'] = null;
|
||||||
|
}
|
||||||
|
json[r'id'] = this.id;
|
||||||
|
json[r'isFavorite'] = this.isFavorite;
|
||||||
|
json[r'isVisible'] = this.isVisible;
|
||||||
|
if (this.localDateTime != null) {
|
||||||
|
json[r'localDateTime'] = this.localDateTime!.toUtc().toIso8601String();
|
||||||
|
} else {
|
||||||
|
// json[r'localDateTime'] = null;
|
||||||
|
}
|
||||||
|
json[r'ownerId'] = this.ownerId;
|
||||||
|
if (this.thumbhash != null) {
|
||||||
|
json[r'thumbhash'] = this.thumbhash;
|
||||||
|
} else {
|
||||||
|
// json[r'thumbhash'] = null;
|
||||||
|
}
|
||||||
|
json[r'type'] = this.type;
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new [SyncAssetV1] instance and imports its values from
|
||||||
|
/// [value] if it's a [Map], null otherwise.
|
||||||
|
// ignore: prefer_constructors_over_static_methods
|
||||||
|
static SyncAssetV1? fromJson(dynamic value) {
|
||||||
|
upgradeDto(value, "SyncAssetV1");
|
||||||
|
if (value is Map) {
|
||||||
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
|
return SyncAssetV1(
|
||||||
|
checksum: mapValueOfType<String>(json, r'checksum')!,
|
||||||
|
deletedAt: mapDateTime(json, r'deletedAt', r''),
|
||||||
|
fileCreatedAt: mapDateTime(json, r'fileCreatedAt', r''),
|
||||||
|
fileModifiedAt: mapDateTime(json, r'fileModifiedAt', r''),
|
||||||
|
id: mapValueOfType<String>(json, r'id')!,
|
||||||
|
isFavorite: mapValueOfType<bool>(json, r'isFavorite')!,
|
||||||
|
isVisible: mapValueOfType<bool>(json, r'isVisible')!,
|
||||||
|
localDateTime: mapDateTime(json, r'localDateTime', r''),
|
||||||
|
ownerId: mapValueOfType<String>(json, r'ownerId')!,
|
||||||
|
thumbhash: mapValueOfType<String>(json, r'thumbhash'),
|
||||||
|
type: SyncAssetV1TypeEnum.fromJson(json[r'type'])!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<SyncAssetV1> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <SyncAssetV1>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = SyncAssetV1.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, SyncAssetV1> mapFromJson(dynamic json) {
|
||||||
|
final map = <String, SyncAssetV1>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
final value = SyncAssetV1.fromJson(entry.value);
|
||||||
|
if (value != null) {
|
||||||
|
map[entry.key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// maps a json object with a list of SyncAssetV1-objects as value to a dart map
|
||||||
|
static Map<String, List<SyncAssetV1>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final map = <String, List<SyncAssetV1>>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
json = json.cast<String, dynamic>();
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
map[entry.key] = SyncAssetV1.listFromJson(entry.value, growable: growable,);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The list of required keys that must be present in a JSON.
|
||||||
|
static const requiredKeys = <String>{
|
||||||
|
'checksum',
|
||||||
|
'deletedAt',
|
||||||
|
'fileCreatedAt',
|
||||||
|
'fileModifiedAt',
|
||||||
|
'id',
|
||||||
|
'isFavorite',
|
||||||
|
'isVisible',
|
||||||
|
'localDateTime',
|
||||||
|
'ownerId',
|
||||||
|
'thumbhash',
|
||||||
|
'type',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class SyncAssetV1TypeEnum {
|
||||||
|
/// Instantiate a new enum with the provided [value].
|
||||||
|
const SyncAssetV1TypeEnum._(this.value);
|
||||||
|
|
||||||
|
/// The underlying value of this enum member.
|
||||||
|
final String value;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => value;
|
||||||
|
|
||||||
|
String toJson() => value;
|
||||||
|
|
||||||
|
static const IMAGE = SyncAssetV1TypeEnum._(r'IMAGE');
|
||||||
|
static const VIDEO = SyncAssetV1TypeEnum._(r'VIDEO');
|
||||||
|
static const AUDIO = SyncAssetV1TypeEnum._(r'AUDIO');
|
||||||
|
static const OTHER = SyncAssetV1TypeEnum._(r'OTHER');
|
||||||
|
|
||||||
|
/// List of all possible values in this [enum][SyncAssetV1TypeEnum].
|
||||||
|
static const values = <SyncAssetV1TypeEnum>[
|
||||||
|
IMAGE,
|
||||||
|
VIDEO,
|
||||||
|
AUDIO,
|
||||||
|
OTHER,
|
||||||
|
];
|
||||||
|
|
||||||
|
static SyncAssetV1TypeEnum? fromJson(dynamic value) => SyncAssetV1TypeEnumTypeTransformer().decode(value);
|
||||||
|
|
||||||
|
static List<SyncAssetV1TypeEnum> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <SyncAssetV1TypeEnum>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = SyncAssetV1TypeEnum.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transformation class that can [encode] an instance of [SyncAssetV1TypeEnum] to String,
|
||||||
|
/// and [decode] dynamic data back to [SyncAssetV1TypeEnum].
|
||||||
|
class SyncAssetV1TypeEnumTypeTransformer {
|
||||||
|
factory SyncAssetV1TypeEnumTypeTransformer() => _instance ??= const SyncAssetV1TypeEnumTypeTransformer._();
|
||||||
|
|
||||||
|
const SyncAssetV1TypeEnumTypeTransformer._();
|
||||||
|
|
||||||
|
String encode(SyncAssetV1TypeEnum data) => data.value;
|
||||||
|
|
||||||
|
/// Decodes a [dynamic value][data] to a SyncAssetV1TypeEnum.
|
||||||
|
///
|
||||||
|
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
|
||||||
|
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
|
||||||
|
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
|
||||||
|
///
|
||||||
|
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
|
||||||
|
/// and users are still using an old app with the old code.
|
||||||
|
SyncAssetV1TypeEnum? decode(dynamic data, {bool allowNull = true}) {
|
||||||
|
if (data != null) {
|
||||||
|
switch (data) {
|
||||||
|
case r'IMAGE': return SyncAssetV1TypeEnum.IMAGE;
|
||||||
|
case r'VIDEO': return SyncAssetV1TypeEnum.VIDEO;
|
||||||
|
case r'AUDIO': return SyncAssetV1TypeEnum.AUDIO;
|
||||||
|
case r'OTHER': return SyncAssetV1TypeEnum.OTHER;
|
||||||
|
default:
|
||||||
|
if (!allowNull) {
|
||||||
|
throw ArgumentError('Unknown enum value to decode: $data');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Singleton [SyncAssetV1TypeEnumTypeTransformer] instance.
|
||||||
|
static SyncAssetV1TypeEnumTypeTransformer? _instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
18
mobile/openapi/lib/model/sync_entity_type.dart
generated
18
mobile/openapi/lib/model/sync_entity_type.dart
generated
@ -27,6 +27,12 @@ class SyncEntityType {
|
|||||||
static const userDeleteV1 = SyncEntityType._(r'UserDeleteV1');
|
static const userDeleteV1 = SyncEntityType._(r'UserDeleteV1');
|
||||||
static const partnerV1 = SyncEntityType._(r'PartnerV1');
|
static const partnerV1 = SyncEntityType._(r'PartnerV1');
|
||||||
static const partnerDeleteV1 = SyncEntityType._(r'PartnerDeleteV1');
|
static const partnerDeleteV1 = SyncEntityType._(r'PartnerDeleteV1');
|
||||||
|
static const assetV1 = SyncEntityType._(r'AssetV1');
|
||||||
|
static const assetDeleteV1 = SyncEntityType._(r'AssetDeleteV1');
|
||||||
|
static const assetExifV1 = SyncEntityType._(r'AssetExifV1');
|
||||||
|
static const partnerAssetV1 = SyncEntityType._(r'PartnerAssetV1');
|
||||||
|
static const partnerAssetDeleteV1 = SyncEntityType._(r'PartnerAssetDeleteV1');
|
||||||
|
static const partnerAssetExifV1 = SyncEntityType._(r'PartnerAssetExifV1');
|
||||||
|
|
||||||
/// List of all possible values in this [enum][SyncEntityType].
|
/// List of all possible values in this [enum][SyncEntityType].
|
||||||
static const values = <SyncEntityType>[
|
static const values = <SyncEntityType>[
|
||||||
@ -34,6 +40,12 @@ class SyncEntityType {
|
|||||||
userDeleteV1,
|
userDeleteV1,
|
||||||
partnerV1,
|
partnerV1,
|
||||||
partnerDeleteV1,
|
partnerDeleteV1,
|
||||||
|
assetV1,
|
||||||
|
assetDeleteV1,
|
||||||
|
assetExifV1,
|
||||||
|
partnerAssetV1,
|
||||||
|
partnerAssetDeleteV1,
|
||||||
|
partnerAssetExifV1,
|
||||||
];
|
];
|
||||||
|
|
||||||
static SyncEntityType? fromJson(dynamic value) => SyncEntityTypeTypeTransformer().decode(value);
|
static SyncEntityType? fromJson(dynamic value) => SyncEntityTypeTypeTransformer().decode(value);
|
||||||
@ -76,6 +88,12 @@ class SyncEntityTypeTypeTransformer {
|
|||||||
case r'UserDeleteV1': return SyncEntityType.userDeleteV1;
|
case r'UserDeleteV1': return SyncEntityType.userDeleteV1;
|
||||||
case r'PartnerV1': return SyncEntityType.partnerV1;
|
case r'PartnerV1': return SyncEntityType.partnerV1;
|
||||||
case r'PartnerDeleteV1': return SyncEntityType.partnerDeleteV1;
|
case r'PartnerDeleteV1': return SyncEntityType.partnerDeleteV1;
|
||||||
|
case r'AssetV1': return SyncEntityType.assetV1;
|
||||||
|
case r'AssetDeleteV1': return SyncEntityType.assetDeleteV1;
|
||||||
|
case r'AssetExifV1': return SyncEntityType.assetExifV1;
|
||||||
|
case r'PartnerAssetV1': return SyncEntityType.partnerAssetV1;
|
||||||
|
case r'PartnerAssetDeleteV1': return SyncEntityType.partnerAssetDeleteV1;
|
||||||
|
case r'PartnerAssetExifV1': return SyncEntityType.partnerAssetExifV1;
|
||||||
default:
|
default:
|
||||||
if (!allowNull) {
|
if (!allowNull) {
|
||||||
throw ArgumentError('Unknown enum value to decode: $data');
|
throw ArgumentError('Unknown enum value to decode: $data');
|
||||||
|
12
mobile/openapi/lib/model/sync_request_type.dart
generated
12
mobile/openapi/lib/model/sync_request_type.dart
generated
@ -25,11 +25,19 @@ class SyncRequestType {
|
|||||||
|
|
||||||
static const usersV1 = SyncRequestType._(r'UsersV1');
|
static const usersV1 = SyncRequestType._(r'UsersV1');
|
||||||
static const partnersV1 = SyncRequestType._(r'PartnersV1');
|
static const partnersV1 = SyncRequestType._(r'PartnersV1');
|
||||||
|
static const assetsV1 = SyncRequestType._(r'AssetsV1');
|
||||||
|
static const assetExifsV1 = SyncRequestType._(r'AssetExifsV1');
|
||||||
|
static const partnerAssetsV1 = SyncRequestType._(r'PartnerAssetsV1');
|
||||||
|
static const partnerAssetExifsV1 = SyncRequestType._(r'PartnerAssetExifsV1');
|
||||||
|
|
||||||
/// 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>[
|
||||||
usersV1,
|
usersV1,
|
||||||
partnersV1,
|
partnersV1,
|
||||||
|
assetsV1,
|
||||||
|
assetExifsV1,
|
||||||
|
partnerAssetsV1,
|
||||||
|
partnerAssetExifsV1,
|
||||||
];
|
];
|
||||||
|
|
||||||
static SyncRequestType? fromJson(dynamic value) => SyncRequestTypeTypeTransformer().decode(value);
|
static SyncRequestType? fromJson(dynamic value) => SyncRequestTypeTypeTransformer().decode(value);
|
||||||
@ -70,6 +78,10 @@ class SyncRequestTypeTypeTransformer {
|
|||||||
switch (data) {
|
switch (data) {
|
||||||
case r'UsersV1': return SyncRequestType.usersV1;
|
case r'UsersV1': return SyncRequestType.usersV1;
|
||||||
case r'PartnersV1': return SyncRequestType.partnersV1;
|
case r'PartnersV1': return SyncRequestType.partnersV1;
|
||||||
|
case r'AssetsV1': return SyncRequestType.assetsV1;
|
||||||
|
case r'AssetExifsV1': return SyncRequestType.assetExifsV1;
|
||||||
|
case r'PartnerAssetsV1': return SyncRequestType.partnerAssetsV1;
|
||||||
|
case r'PartnerAssetExifsV1': return SyncRequestType.partnerAssetExifsV1;
|
||||||
default:
|
default:
|
||||||
if (!allowNull) {
|
if (!allowNull) {
|
||||||
throw ArgumentError('Unknown enum value to decode: $data');
|
throw ArgumentError('Unknown enum value to decode: $data');
|
||||||
|
@ -12049,12 +12049,228 @@
|
|||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"SyncAssetDeleteV1": {
|
||||||
|
"properties": {
|
||||||
|
"assetId": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"assetId"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"SyncAssetExifV1": {
|
||||||
|
"properties": {
|
||||||
|
"assetId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"city": {
|
||||||
|
"nullable": true,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"country": {
|
||||||
|
"nullable": true,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"dateTimeOriginal": {
|
||||||
|
"format": "date-time",
|
||||||
|
"nullable": true,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"nullable": true,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"exifImageHeight": {
|
||||||
|
"nullable": true,
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"exifImageWidth": {
|
||||||
|
"nullable": true,
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"exposureTime": {
|
||||||
|
"nullable": true,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"fNumber": {
|
||||||
|
"nullable": true,
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"fileSizeInByte": {
|
||||||
|
"nullable": true,
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"focalLength": {
|
||||||
|
"nullable": true,
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"fps": {
|
||||||
|
"nullable": true,
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"iso": {
|
||||||
|
"nullable": true,
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"latitude": {
|
||||||
|
"nullable": true,
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"lensModel": {
|
||||||
|
"nullable": true,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"longitude": {
|
||||||
|
"nullable": true,
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"make": {
|
||||||
|
"nullable": true,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"model": {
|
||||||
|
"nullable": true,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"modifyDate": {
|
||||||
|
"format": "date-time",
|
||||||
|
"nullable": true,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"orientation": {
|
||||||
|
"nullable": true,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"profileDescription": {
|
||||||
|
"nullable": true,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"projectionType": {
|
||||||
|
"nullable": true,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"rating": {
|
||||||
|
"nullable": true,
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"state": {
|
||||||
|
"nullable": true,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"timeZone": {
|
||||||
|
"nullable": true,
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"assetId",
|
||||||
|
"city",
|
||||||
|
"country",
|
||||||
|
"dateTimeOriginal",
|
||||||
|
"description",
|
||||||
|
"exifImageHeight",
|
||||||
|
"exifImageWidth",
|
||||||
|
"exposureTime",
|
||||||
|
"fNumber",
|
||||||
|
"fileSizeInByte",
|
||||||
|
"focalLength",
|
||||||
|
"fps",
|
||||||
|
"iso",
|
||||||
|
"latitude",
|
||||||
|
"lensModel",
|
||||||
|
"longitude",
|
||||||
|
"make",
|
||||||
|
"model",
|
||||||
|
"modifyDate",
|
||||||
|
"orientation",
|
||||||
|
"profileDescription",
|
||||||
|
"projectionType",
|
||||||
|
"rating",
|
||||||
|
"state",
|
||||||
|
"timeZone"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"SyncAssetV1": {
|
||||||
|
"properties": {
|
||||||
|
"checksum": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"deletedAt": {
|
||||||
|
"format": "date-time",
|
||||||
|
"nullable": true,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"fileCreatedAt": {
|
||||||
|
"format": "date-time",
|
||||||
|
"nullable": true,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"fileModifiedAt": {
|
||||||
|
"format": "date-time",
|
||||||
|
"nullable": true,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"isFavorite": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"isVisible": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"localDateTime": {
|
||||||
|
"format": "date-time",
|
||||||
|
"nullable": true,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"ownerId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"thumbhash": {
|
||||||
|
"nullable": true,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"enum": [
|
||||||
|
"IMAGE",
|
||||||
|
"VIDEO",
|
||||||
|
"AUDIO",
|
||||||
|
"OTHER"
|
||||||
|
],
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"checksum",
|
||||||
|
"deletedAt",
|
||||||
|
"fileCreatedAt",
|
||||||
|
"fileModifiedAt",
|
||||||
|
"id",
|
||||||
|
"isFavorite",
|
||||||
|
"isVisible",
|
||||||
|
"localDateTime",
|
||||||
|
"ownerId",
|
||||||
|
"thumbhash",
|
||||||
|
"type"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"SyncEntityType": {
|
"SyncEntityType": {
|
||||||
"enum": [
|
"enum": [
|
||||||
"UserV1",
|
"UserV1",
|
||||||
"UserDeleteV1",
|
"UserDeleteV1",
|
||||||
"PartnerV1",
|
"PartnerV1",
|
||||||
"PartnerDeleteV1"
|
"PartnerDeleteV1",
|
||||||
|
"AssetV1",
|
||||||
|
"AssetDeleteV1",
|
||||||
|
"AssetExifV1",
|
||||||
|
"PartnerAssetV1",
|
||||||
|
"PartnerAssetDeleteV1",
|
||||||
|
"PartnerAssetExifV1"
|
||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@ -12095,7 +12311,11 @@
|
|||||||
"SyncRequestType": {
|
"SyncRequestType": {
|
||||||
"enum": [
|
"enum": [
|
||||||
"UsersV1",
|
"UsersV1",
|
||||||
"PartnersV1"
|
"PartnersV1",
|
||||||
|
"AssetsV1",
|
||||||
|
"AssetExifsV1",
|
||||||
|
"PartnerAssetsV1",
|
||||||
|
"PartnerAssetExifsV1"
|
||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
@ -3647,11 +3647,21 @@ export enum SyncEntityType {
|
|||||||
UserV1 = "UserV1",
|
UserV1 = "UserV1",
|
||||||
UserDeleteV1 = "UserDeleteV1",
|
UserDeleteV1 = "UserDeleteV1",
|
||||||
PartnerV1 = "PartnerV1",
|
PartnerV1 = "PartnerV1",
|
||||||
PartnerDeleteV1 = "PartnerDeleteV1"
|
PartnerDeleteV1 = "PartnerDeleteV1",
|
||||||
|
AssetV1 = "AssetV1",
|
||||||
|
AssetDeleteV1 = "AssetDeleteV1",
|
||||||
|
AssetExifV1 = "AssetExifV1",
|
||||||
|
PartnerAssetV1 = "PartnerAssetV1",
|
||||||
|
PartnerAssetDeleteV1 = "PartnerAssetDeleteV1",
|
||||||
|
PartnerAssetExifV1 = "PartnerAssetExifV1"
|
||||||
}
|
}
|
||||||
export enum SyncRequestType {
|
export enum SyncRequestType {
|
||||||
UsersV1 = "UsersV1",
|
UsersV1 = "UsersV1",
|
||||||
PartnersV1 = "PartnersV1"
|
PartnersV1 = "PartnersV1",
|
||||||
|
AssetsV1 = "AssetsV1",
|
||||||
|
AssetExifsV1 = "AssetExifsV1",
|
||||||
|
PartnerAssetsV1 = "PartnerAssetsV1",
|
||||||
|
PartnerAssetExifsV1 = "PartnerAssetExifsV1"
|
||||||
}
|
}
|
||||||
export enum TranscodeHWAccel {
|
export enum TranscodeHWAccel {
|
||||||
Nvenc = "nvenc",
|
Nvenc = "nvenc",
|
||||||
|
@ -117,4 +117,46 @@ export const columns = {
|
|||||||
userDto: ['id', 'name', 'email', 'profileImagePath', 'profileChangedAt'],
|
userDto: ['id', 'name', 'email', 'profileImagePath', 'profileChangedAt'],
|
||||||
tagDto: ['id', 'value', 'createdAt', 'updatedAt', 'color', 'parentId'],
|
tagDto: ['id', 'value', 'createdAt', 'updatedAt', 'color', 'parentId'],
|
||||||
apiKey: ['id', 'name', 'userId', 'createdAt', 'updatedAt', 'permissions'],
|
apiKey: ['id', 'name', 'userId', 'createdAt', 'updatedAt', 'permissions'],
|
||||||
|
syncAsset: [
|
||||||
|
'id',
|
||||||
|
'ownerId',
|
||||||
|
'thumbhash',
|
||||||
|
'checksum',
|
||||||
|
'fileCreatedAt',
|
||||||
|
'fileModifiedAt',
|
||||||
|
'localDateTime',
|
||||||
|
'type',
|
||||||
|
'deletedAt',
|
||||||
|
'isFavorite',
|
||||||
|
'isVisible',
|
||||||
|
'updateId',
|
||||||
|
],
|
||||||
|
syncAssetExif: [
|
||||||
|
'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',
|
||||||
|
],
|
||||||
} as const;
|
} as const;
|
||||||
|
10
server/src/db.d.ts
vendored
10
server/src/db.d.ts
vendored
@ -119,6 +119,13 @@ export interface AssetJobStatus {
|
|||||||
thumbnailAt: Timestamp | null;
|
thumbnailAt: Timestamp | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AssetsAudit {
|
||||||
|
deletedAt: Generated<Timestamp>;
|
||||||
|
id: Generated<string>;
|
||||||
|
assetId: string;
|
||||||
|
ownerId: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Assets {
|
export interface Assets {
|
||||||
checksum: Buffer;
|
checksum: Buffer;
|
||||||
createdAt: Generated<Timestamp>;
|
createdAt: Generated<Timestamp>;
|
||||||
@ -168,6 +175,8 @@ export interface Audit {
|
|||||||
|
|
||||||
export interface Exif {
|
export interface Exif {
|
||||||
assetId: string;
|
assetId: string;
|
||||||
|
updateId: Generated<string>;
|
||||||
|
updatedAt: Generated<Timestamp>;
|
||||||
autoStackId: string | null;
|
autoStackId: string | null;
|
||||||
bitsPerSample: number | null;
|
bitsPerSample: number | null;
|
||||||
city: string | null;
|
city: string | null;
|
||||||
@ -459,6 +468,7 @@ export interface DB {
|
|||||||
asset_job_status: AssetJobStatus;
|
asset_job_status: AssetJobStatus;
|
||||||
asset_stack: AssetStack;
|
asset_stack: AssetStack;
|
||||||
assets: Assets;
|
assets: Assets;
|
||||||
|
assets_audit: AssetsAudit;
|
||||||
audit: Audit;
|
audit: Audit;
|
||||||
exif: Exif;
|
exif: Exif;
|
||||||
face_search: FaceSearch;
|
face_search: FaceSearch;
|
||||||
|
@ -102,7 +102,7 @@ const mapStack = (entity: AssetEntity) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// if an asset is jsonified in the DB before being returned, its buffer fields will be hex-encoded strings
|
// if an asset is jsonified in the DB before being returned, its buffer fields will be hex-encoded strings
|
||||||
const hexOrBufferToBase64 = (encoded: string | Buffer) => {
|
export const hexOrBufferToBase64 = (encoded: string | Buffer) => {
|
||||||
if (typeof encoded === 'string') {
|
if (typeof encoded === 'string') {
|
||||||
return Buffer.from(encoded.slice(2), 'hex').toString('base64');
|
return Buffer.from(encoded.slice(2), 'hex').toString('base64');
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { IsEnum, IsInt, IsPositive, IsString } from 'class-validator';
|
import { IsEnum, IsInt, IsPositive, IsString } from 'class-validator';
|
||||||
import { AssetResponseDto } from 'src/dtos/asset-response.dto';
|
import { AssetResponseDto } from 'src/dtos/asset-response.dto';
|
||||||
import { SyncEntityType, SyncRequestType } from 'src/enum';
|
import { AssetType, SyncEntityType, SyncRequestType } from 'src/enum';
|
||||||
import { Optional, ValidateDate, ValidateUUID } from 'src/validation';
|
import { Optional, ValidateDate, ValidateUUID } from 'src/validation';
|
||||||
|
|
||||||
export class AssetFullSyncDto {
|
export class AssetFullSyncDto {
|
||||||
@ -56,11 +56,73 @@ export class SyncPartnerDeleteV1 {
|
|||||||
sharedWithId!: string;
|
sharedWithId!: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class SyncAssetV1 {
|
||||||
|
id!: string;
|
||||||
|
ownerId!: string;
|
||||||
|
thumbhash!: string | null;
|
||||||
|
checksum!: string;
|
||||||
|
fileCreatedAt!: Date | null;
|
||||||
|
fileModifiedAt!: Date | null;
|
||||||
|
localDateTime!: Date | null;
|
||||||
|
type!: AssetType;
|
||||||
|
deletedAt!: Date | null;
|
||||||
|
isFavorite!: boolean;
|
||||||
|
isVisible!: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SyncAssetDeleteV1 {
|
||||||
|
assetId!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SyncAssetExifV1 {
|
||||||
|
assetId!: string;
|
||||||
|
description!: string | null;
|
||||||
|
@ApiProperty({ type: 'integer' })
|
||||||
|
exifImageWidth!: number | null;
|
||||||
|
@ApiProperty({ type: 'integer' })
|
||||||
|
exifImageHeight!: number | null;
|
||||||
|
@ApiProperty({ type: 'integer' })
|
||||||
|
fileSizeInByte!: number | null;
|
||||||
|
orientation!: string | null;
|
||||||
|
dateTimeOriginal!: Date | null;
|
||||||
|
modifyDate!: Date | null;
|
||||||
|
timeZone!: string | null;
|
||||||
|
@ApiProperty({ type: 'integer' })
|
||||||
|
latitude!: number | null;
|
||||||
|
@ApiProperty({ type: 'integer' })
|
||||||
|
longitude!: number | null;
|
||||||
|
projectionType!: string | null;
|
||||||
|
city!: string | null;
|
||||||
|
state!: string | null;
|
||||||
|
country!: string | null;
|
||||||
|
make!: string | null;
|
||||||
|
model!: string | null;
|
||||||
|
lensModel!: string | null;
|
||||||
|
@ApiProperty({ type: 'integer' })
|
||||||
|
fNumber!: number | null;
|
||||||
|
@ApiProperty({ type: 'integer' })
|
||||||
|
focalLength!: number | null;
|
||||||
|
@ApiProperty({ type: 'integer' })
|
||||||
|
iso!: number | null;
|
||||||
|
exposureTime!: string | null;
|
||||||
|
profileDescription!: string | null;
|
||||||
|
@ApiProperty({ type: 'integer' })
|
||||||
|
rating!: number | null;
|
||||||
|
@ApiProperty({ type: 'integer' })
|
||||||
|
fps!: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
export type SyncItem = {
|
export type SyncItem = {
|
||||||
[SyncEntityType.UserV1]: SyncUserV1;
|
[SyncEntityType.UserV1]: SyncUserV1;
|
||||||
[SyncEntityType.UserDeleteV1]: SyncUserDeleteV1;
|
[SyncEntityType.UserDeleteV1]: SyncUserDeleteV1;
|
||||||
[SyncEntityType.PartnerV1]: SyncPartnerV1;
|
[SyncEntityType.PartnerV1]: SyncPartnerV1;
|
||||||
[SyncEntityType.PartnerDeleteV1]: SyncPartnerDeleteV1;
|
[SyncEntityType.PartnerDeleteV1]: SyncPartnerDeleteV1;
|
||||||
|
[SyncEntityType.AssetV1]: SyncAssetV1;
|
||||||
|
[SyncEntityType.AssetDeleteV1]: SyncAssetDeleteV1;
|
||||||
|
[SyncEntityType.AssetExifV1]: SyncAssetExifV1;
|
||||||
|
[SyncEntityType.PartnerAssetV1]: SyncAssetV1;
|
||||||
|
[SyncEntityType.PartnerAssetDeleteV1]: SyncAssetDeleteV1;
|
||||||
|
[SyncEntityType.PartnerAssetExifV1]: SyncAssetExifV1;
|
||||||
};
|
};
|
||||||
|
|
||||||
const responseDtos = [
|
const responseDtos = [
|
||||||
@ -69,6 +131,9 @@ const responseDtos = [
|
|||||||
SyncUserDeleteV1,
|
SyncUserDeleteV1,
|
||||||
SyncPartnerV1,
|
SyncPartnerV1,
|
||||||
SyncPartnerDeleteV1,
|
SyncPartnerDeleteV1,
|
||||||
|
SyncAssetV1,
|
||||||
|
SyncAssetDeleteV1,
|
||||||
|
SyncAssetExifV1,
|
||||||
];
|
];
|
||||||
|
|
||||||
export const extraSyncModels = responseDtos;
|
export const extraSyncModels = responseDtos;
|
||||||
|
19
server/src/entities/asset-audit.entity.ts
Normal file
19
server/src/entities/asset-audit.entity.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { Column, CreateDateColumn, Entity, Index, PrimaryColumn } from 'typeorm';
|
||||||
|
|
||||||
|
@Entity('assets_audit')
|
||||||
|
export class AssetAuditEntity {
|
||||||
|
@PrimaryColumn({ type: 'uuid', nullable: false, default: () => 'immich_uuid_v7()' })
|
||||||
|
id!: string;
|
||||||
|
|
||||||
|
@Index('IDX_assets_audit_asset_id')
|
||||||
|
@Column({ type: 'uuid' })
|
||||||
|
assetId!: string;
|
||||||
|
|
||||||
|
@Index('IDX_assets_audit_owner_id')
|
||||||
|
@Column({ type: 'uuid' })
|
||||||
|
ownerId!: string;
|
||||||
|
|
||||||
|
@Index('IDX_assets_audit_deleted_at')
|
||||||
|
@CreateDateColumn({ type: 'timestamptz', default: () => 'clock_timestamp()' })
|
||||||
|
deletedAt!: Date;
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
import { AssetEntity } from 'src/entities/asset.entity';
|
import { AssetEntity } from 'src/entities/asset.entity';
|
||||||
import { Index, JoinColumn, OneToOne, PrimaryColumn } from 'typeorm';
|
import { Index, JoinColumn, OneToOne, PrimaryColumn, UpdateDateColumn } from 'typeorm';
|
||||||
import { Column } from 'typeorm/decorator/columns/Column.js';
|
import { Column } from 'typeorm/decorator/columns/Column.js';
|
||||||
import { Entity } from 'typeorm/decorator/entity/Entity.js';
|
import { Entity } from 'typeorm/decorator/entity/Entity.js';
|
||||||
|
|
||||||
@ -12,6 +12,13 @@ export class ExifEntity {
|
|||||||
@PrimaryColumn()
|
@PrimaryColumn()
|
||||||
assetId!: string;
|
assetId!: string;
|
||||||
|
|
||||||
|
@UpdateDateColumn({ type: 'timestamptz', default: () => 'clock_timestamp()' })
|
||||||
|
updatedAt?: Date;
|
||||||
|
|
||||||
|
@Index('IDX_asset_exif_update_id')
|
||||||
|
@Column({ type: 'uuid', nullable: false, default: () => 'immich_uuid_v7()' })
|
||||||
|
updateId?: string;
|
||||||
|
|
||||||
/* General info */
|
/* General info */
|
||||||
@Column({ type: 'text', default: '' })
|
@Column({ type: 'text', default: '' })
|
||||||
description!: string; // or caption
|
description!: string; // or caption
|
||||||
|
@ -549,11 +549,24 @@ export enum DatabaseLock {
|
|||||||
export enum SyncRequestType {
|
export enum SyncRequestType {
|
||||||
UsersV1 = 'UsersV1',
|
UsersV1 = 'UsersV1',
|
||||||
PartnersV1 = 'PartnersV1',
|
PartnersV1 = 'PartnersV1',
|
||||||
|
AssetsV1 = 'AssetsV1',
|
||||||
|
AssetExifsV1 = 'AssetExifsV1',
|
||||||
|
PartnerAssetsV1 = 'PartnerAssetsV1',
|
||||||
|
PartnerAssetExifsV1 = 'PartnerAssetExifsV1',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum SyncEntityType {
|
export enum SyncEntityType {
|
||||||
UserV1 = 'UserV1',
|
UserV1 = 'UserV1',
|
||||||
UserDeleteV1 = 'UserDeleteV1',
|
UserDeleteV1 = 'UserDeleteV1',
|
||||||
|
|
||||||
PartnerV1 = 'PartnerV1',
|
PartnerV1 = 'PartnerV1',
|
||||||
PartnerDeleteV1 = 'PartnerDeleteV1',
|
PartnerDeleteV1 = 'PartnerDeleteV1',
|
||||||
|
|
||||||
|
AssetV1 = 'AssetV1',
|
||||||
|
AssetDeleteV1 = 'AssetDeleteV1',
|
||||||
|
AssetExifV1 = 'AssetExifV1',
|
||||||
|
|
||||||
|
PartnerAssetV1 = 'PartnerAssetV1',
|
||||||
|
PartnerAssetDeleteV1 = 'PartnerAssetDeleteV1',
|
||||||
|
PartnerAssetExifV1 = 'PartnerAssetExifV1',
|
||||||
}
|
}
|
||||||
|
37
server/src/migrations/1741191762113-AssetAuditTable.ts
Normal file
37
server/src/migrations/1741191762113-AssetAuditTable.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||||
|
|
||||||
|
export class AssetAuditTable1741191762113 implements MigrationInterface {
|
||||||
|
name = 'AssetAuditTable1741191762113'
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`CREATE TABLE "assets_audit" ("id" uuid NOT NULL DEFAULT immich_uuid_v7(), "assetId" uuid NOT NULL, "ownerId" uuid NOT NULL, "deletedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT clock_timestamp(), CONSTRAINT "PK_99bd5c015f81a641927a32b4212" PRIMARY KEY ("id"))`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_assets_audit_asset_id" ON "assets_audit" ("assetId") `);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_assets_audit_owner_id" ON "assets_audit" ("ownerId") `);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_assets_audit_deleted_at" ON "assets_audit" ("deletedAt") `);
|
||||||
|
await queryRunner.query(`CREATE OR REPLACE FUNCTION assets_delete_audit() RETURNS TRIGGER AS
|
||||||
|
$$
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO assets_audit ("assetId", "ownerId")
|
||||||
|
SELECT "id", "ownerId"
|
||||||
|
FROM OLD;
|
||||||
|
RETURN NULL;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql`
|
||||||
|
);
|
||||||
|
await queryRunner.query(`CREATE OR REPLACE TRIGGER assets_delete_audit
|
||||||
|
AFTER DELETE ON assets
|
||||||
|
REFERENCING OLD TABLE AS OLD
|
||||||
|
FOR EACH STATEMENT
|
||||||
|
EXECUTE FUNCTION assets_delete_audit();
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`DROP TRIGGER assets_delete_audit`);
|
||||||
|
await queryRunner.query(`DROP FUNCTION assets_delete_audit`);
|
||||||
|
await queryRunner.query(`DROP INDEX "IDX_assets_audit_deleted_at"`);
|
||||||
|
await queryRunner.query(`DROP INDEX "IDX_assets_audit_owner_id"`);
|
||||||
|
await queryRunner.query(`DROP INDEX "IDX_assets_audit_asset_id"`);
|
||||||
|
await queryRunner.query(`DROP TABLE "assets_audit"`);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,50 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
export class FixAssetAndUserCascadeConditions1741280328985 implements MigrationInterface {
|
||||||
|
name = 'FixAssetAndUserCascadeConditions1741280328985';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`
|
||||||
|
CREATE OR REPLACE TRIGGER assets_delete_audit
|
||||||
|
AFTER DELETE ON assets
|
||||||
|
REFERENCING OLD TABLE AS OLD
|
||||||
|
FOR EACH STATEMENT
|
||||||
|
WHEN (pg_trigger_depth() = 0)
|
||||||
|
EXECUTE FUNCTION assets_delete_audit();`);
|
||||||
|
await queryRunner.query(`
|
||||||
|
CREATE OR REPLACE TRIGGER users_delete_audit
|
||||||
|
AFTER DELETE ON users
|
||||||
|
REFERENCING OLD TABLE AS OLD
|
||||||
|
FOR EACH STATEMENT
|
||||||
|
WHEN (pg_trigger_depth() = 0)
|
||||||
|
EXECUTE FUNCTION users_delete_audit();`);
|
||||||
|
await queryRunner.query(`
|
||||||
|
CREATE OR REPLACE TRIGGER partners_delete_audit
|
||||||
|
AFTER DELETE ON partners
|
||||||
|
REFERENCING OLD TABLE AS OLD
|
||||||
|
FOR EACH STATEMENT
|
||||||
|
WHEN (pg_trigger_depth() = 0)
|
||||||
|
EXECUTE FUNCTION partners_delete_audit();`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`
|
||||||
|
CREATE OR REPLACE TRIGGER assets_delete_audit
|
||||||
|
AFTER DELETE ON assets
|
||||||
|
REFERENCING OLD TABLE AS OLD
|
||||||
|
FOR EACH STATEMENT
|
||||||
|
EXECUTE FUNCTION assets_delete_audit();`);
|
||||||
|
await queryRunner.query(`
|
||||||
|
CREATE OR REPLACE TRIGGER users_delete_audit
|
||||||
|
AFTER DELETE ON users
|
||||||
|
REFERENCING OLD TABLE AS OLD
|
||||||
|
FOR EACH STATEMENT
|
||||||
|
EXECUTE FUNCTION users_delete_audit();`);
|
||||||
|
await queryRunner.query(`
|
||||||
|
CREATE OR REPLACE TRIGGER partners_delete_audit
|
||||||
|
AFTER DELETE ON partners
|
||||||
|
REFERENCING OLD TABLE AS OLD
|
||||||
|
FOR EACH STATEMENT
|
||||||
|
EXECUTE FUNCTION partners_delete_audit();`);
|
||||||
|
}
|
||||||
|
}
|
25
server/src/migrations/1741281344519-AddExifUpdateId.ts
Normal file
25
server/src/migrations/1741281344519-AddExifUpdateId.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
export class AddExifUpdateId1741281344519 implements MigrationInterface {
|
||||||
|
name = 'AddExifUpdateId1741281344519';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "exif" ADD "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT clock_timestamp()`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(`ALTER TABLE "exif" ADD "updateId" uuid NOT NULL DEFAULT immich_uuid_v7()`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_asset_exif_update_id" ON "exif" ("updateId") `);
|
||||||
|
await queryRunner.query(`
|
||||||
|
create trigger asset_exif_updated_at
|
||||||
|
before update on exif
|
||||||
|
for each row execute procedure updated_at()
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_asset_exif_update_id"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "exif" DROP COLUMN "updateId"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "exif" DROP COLUMN "updatedAt"`);
|
||||||
|
await queryRunner.query(`DROP TRIGGER asset_exif_updated_at on exif`);
|
||||||
|
}
|
||||||
|
}
|
@ -420,8 +420,8 @@ from
|
|||||||
) as "stacked_assets" on "asset_stack"."id" is not null
|
) as "stacked_assets" on "asset_stack"."id" is not null
|
||||||
where
|
where
|
||||||
"assets"."ownerId" = $1::uuid
|
"assets"."ownerId" = $1::uuid
|
||||||
and "isVisible" = $2
|
and "assets"."isVisible" = $2
|
||||||
and "updatedAt" <= $3
|
and "assets"."updatedAt" <= $3
|
||||||
and "assets"."id" > $4
|
and "assets"."id" > $4
|
||||||
order by
|
order by
|
||||||
"assets"."id"
|
"assets"."id"
|
||||||
@ -450,7 +450,7 @@ from
|
|||||||
) as "stacked_assets" on "asset_stack"."id" is not null
|
) as "stacked_assets" on "asset_stack"."id" is not null
|
||||||
where
|
where
|
||||||
"assets"."ownerId" = any ($1::uuid[])
|
"assets"."ownerId" = any ($1::uuid[])
|
||||||
and "isVisible" = $2
|
and "assets"."isVisible" = $2
|
||||||
and "updatedAt" > $3
|
and "assets"."updatedAt" > $3
|
||||||
limit
|
limit
|
||||||
$4
|
$4
|
||||||
|
@ -551,7 +551,7 @@ export class AssetRepository {
|
|||||||
return this.getById(asset.id, { exifInfo: true, faces: { person: true } }) as Promise<AssetEntity>;
|
return this.getById(asset.id, { exifInfo: true, faces: { person: true } }) as Promise<AssetEntity>;
|
||||||
}
|
}
|
||||||
|
|
||||||
async remove(asset: AssetEntity): Promise<void> {
|
async remove(asset: { id: string }): Promise<void> {
|
||||||
await this.db.deleteFrom('assets').where('id', '=', asUuid(asset.id)).execute();
|
await this.db.deleteFrom('assets').where('id', '=', asUuid(asset.id)).execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -968,8 +968,8 @@ export class AssetRepository {
|
|||||||
)
|
)
|
||||||
.select((eb) => eb.fn.toJson(eb.table('stacked_assets')).as('stack'))
|
.select((eb) => eb.fn.toJson(eb.table('stacked_assets')).as('stack'))
|
||||||
.where('assets.ownerId', '=', asUuid(ownerId))
|
.where('assets.ownerId', '=', asUuid(ownerId))
|
||||||
.where('isVisible', '=', true)
|
.where('assets.isVisible', '=', true)
|
||||||
.where('updatedAt', '<=', updatedUntil)
|
.where('assets.updatedAt', '<=', updatedUntil)
|
||||||
.$if(!!lastId, (qb) => qb.where('assets.id', '>', lastId!))
|
.$if(!!lastId, (qb) => qb.where('assets.id', '>', lastId!))
|
||||||
.orderBy('assets.id')
|
.orderBy('assets.id')
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
@ -996,8 +996,8 @@ export class AssetRepository {
|
|||||||
)
|
)
|
||||||
.select((eb) => eb.fn.toJson(eb.table('stacked_assets')).as('stack'))
|
.select((eb) => eb.fn.toJson(eb.table('stacked_assets')).as('stack'))
|
||||||
.where('assets.ownerId', '=', anyUuid(options.userIds))
|
.where('assets.ownerId', '=', anyUuid(options.userIds))
|
||||||
.where('isVisible', '=', true)
|
.where('assets.isVisible', '=', true)
|
||||||
.where('updatedAt', '>', options.updatedAfter)
|
.where('assets.updatedAt', '>', options.updatedAfter)
|
||||||
.limit(options.limit)
|
.limit(options.limit)
|
||||||
.execute() as any as Promise<AssetEntity[]>;
|
.execute() as any as Promise<AssetEntity[]>;
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,14 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Insertable, Kysely, sql } from 'kysely';
|
import { Insertable, Kysely, SelectQueryBuilder, sql } from 'kysely';
|
||||||
import { InjectKysely } from 'nestjs-kysely';
|
import { InjectKysely } from 'nestjs-kysely';
|
||||||
|
import { columns } from 'src/database';
|
||||||
import { DB, SessionSyncCheckpoints } from 'src/db';
|
import { DB, SessionSyncCheckpoints } from 'src/db';
|
||||||
import { SyncEntityType } from 'src/enum';
|
import { SyncEntityType } from 'src/enum';
|
||||||
import { SyncAck } from 'src/types';
|
import { SyncAck } from 'src/types';
|
||||||
|
|
||||||
|
type auditTables = 'users_audit' | 'partners_audit' | 'assets_audit';
|
||||||
|
type upsertTables = 'users' | 'partners' | 'assets' | 'exif';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SyncRepository {
|
export class SyncRepository {
|
||||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||||
@ -41,9 +45,7 @@ export class SyncRepository {
|
|||||||
return this.db
|
return this.db
|
||||||
.selectFrom('users')
|
.selectFrom('users')
|
||||||
.select(['id', 'name', 'email', 'deletedAt', 'updateId'])
|
.select(['id', 'name', 'email', 'deletedAt', 'updateId'])
|
||||||
.$if(!!ack, (qb) => qb.where('updateId', '>', ack!.updateId))
|
.$call((qb) => this.upsertTableFilters(qb, ack))
|
||||||
.where('updatedAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
|
|
||||||
.orderBy(['updateId asc'])
|
|
||||||
.stream();
|
.stream();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,9 +53,7 @@ export class SyncRepository {
|
|||||||
return this.db
|
return this.db
|
||||||
.selectFrom('users_audit')
|
.selectFrom('users_audit')
|
||||||
.select(['id', 'userId'])
|
.select(['id', 'userId'])
|
||||||
.$if(!!ack, (qb) => qb.where('id', '>', ack!.updateId))
|
.$call((qb) => this.auditTableFilters(qb, ack))
|
||||||
.where('deletedAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
|
|
||||||
.orderBy(['id asc'])
|
|
||||||
.stream();
|
.stream();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,10 +61,8 @@ export class SyncRepository {
|
|||||||
return this.db
|
return this.db
|
||||||
.selectFrom('partners')
|
.selectFrom('partners')
|
||||||
.select(['sharedById', 'sharedWithId', 'inTimeline', 'updateId'])
|
.select(['sharedById', 'sharedWithId', 'inTimeline', 'updateId'])
|
||||||
.$if(!!ack, (qb) => qb.where('updateId', '>', ack!.updateId))
|
|
||||||
.where((eb) => eb.or([eb('sharedById', '=', userId), eb('sharedWithId', '=', userId)]))
|
.where((eb) => eb.or([eb('sharedById', '=', userId), eb('sharedWithId', '=', userId)]))
|
||||||
.where('updatedAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
|
.$call((qb) => this.upsertTableFilters(qb, ack))
|
||||||
.orderBy(['updateId asc'])
|
|
||||||
.stream();
|
.stream();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,10 +70,93 @@ export class SyncRepository {
|
|||||||
return this.db
|
return this.db
|
||||||
.selectFrom('partners_audit')
|
.selectFrom('partners_audit')
|
||||||
.select(['id', 'sharedById', 'sharedWithId'])
|
.select(['id', 'sharedById', 'sharedWithId'])
|
||||||
.$if(!!ack, (qb) => qb.where('id', '>', ack!.updateId))
|
|
||||||
.where((eb) => eb.or([eb('sharedById', '=', userId), eb('sharedWithId', '=', userId)]))
|
.where((eb) => eb.or([eb('sharedById', '=', userId), eb('sharedWithId', '=', userId)]))
|
||||||
.where('deletedAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
|
.$call((qb) => this.auditTableFilters(qb, ack))
|
||||||
.orderBy(['id asc'])
|
|
||||||
.stream();
|
.stream();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getAssetUpserts(userId: string, ack?: SyncAck) {
|
||||||
|
return this.db
|
||||||
|
.selectFrom('assets')
|
||||||
|
.select(columns.syncAsset)
|
||||||
|
.where('ownerId', '=', userId)
|
||||||
|
.$call((qb) => this.upsertTableFilters(qb, ack))
|
||||||
|
.stream();
|
||||||
|
}
|
||||||
|
|
||||||
|
getPartnerAssetsUpserts(userId: string, ack?: SyncAck) {
|
||||||
|
return this.db
|
||||||
|
.selectFrom('assets')
|
||||||
|
.select(columns.syncAsset)
|
||||||
|
.where('ownerId', 'in', (eb) =>
|
||||||
|
eb.selectFrom('partners').select(['sharedById']).where('sharedWithId', '=', userId),
|
||||||
|
)
|
||||||
|
.$call((qb) => this.upsertTableFilters(qb, ack))
|
||||||
|
.stream();
|
||||||
|
}
|
||||||
|
|
||||||
|
getAssetDeletes(userId: string, ack?: SyncAck) {
|
||||||
|
return this.db
|
||||||
|
.selectFrom('assets_audit')
|
||||||
|
.select(['id', 'assetId'])
|
||||||
|
.where('ownerId', '=', userId)
|
||||||
|
.$if(!!ack, (qb) => qb.where('id', '>', ack!.updateId))
|
||||||
|
.$call((qb) => this.auditTableFilters(qb, ack))
|
||||||
|
.stream();
|
||||||
|
}
|
||||||
|
|
||||||
|
getPartnerAssetDeletes(userId: string, ack?: SyncAck) {
|
||||||
|
return this.db
|
||||||
|
.selectFrom('assets_audit')
|
||||||
|
.select(['id', 'assetId'])
|
||||||
|
.where('ownerId', 'in', (eb) =>
|
||||||
|
eb.selectFrom('partners').select(['sharedById']).where('sharedWithId', '=', userId),
|
||||||
|
)
|
||||||
|
.$call((qb) => this.auditTableFilters(qb, ack))
|
||||||
|
.stream();
|
||||||
|
}
|
||||||
|
|
||||||
|
getAssetExifsUpserts(userId: string, ack?: SyncAck) {
|
||||||
|
return this.db
|
||||||
|
.selectFrom('exif')
|
||||||
|
.select(columns.syncAssetExif)
|
||||||
|
.where('assetId', 'in', (eb) => eb.selectFrom('assets').select('id').where('ownerId', '=', userId))
|
||||||
|
.$call((qb) => this.upsertTableFilters(qb, ack))
|
||||||
|
.stream();
|
||||||
|
}
|
||||||
|
|
||||||
|
getPartnerAssetExifsUpserts(userId: string, ack?: SyncAck) {
|
||||||
|
return this.db
|
||||||
|
.selectFrom('exif')
|
||||||
|
.select(columns.syncAssetExif)
|
||||||
|
.where('assetId', 'in', (eb) =>
|
||||||
|
eb
|
||||||
|
.selectFrom('assets')
|
||||||
|
.select('id')
|
||||||
|
.where('ownerId', 'in', (eb) =>
|
||||||
|
eb.selectFrom('partners').select(['sharedById']).where('sharedWithId', '=', userId),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.$call((qb) => this.upsertTableFilters(qb, ack))
|
||||||
|
.stream();
|
||||||
|
}
|
||||||
|
|
||||||
|
private auditTableFilters<T extends keyof Pick<DB, auditTables>, D>(qb: SelectQueryBuilder<DB, T, D>, ack?: SyncAck) {
|
||||||
|
const builder = qb as SelectQueryBuilder<DB, auditTables, D>;
|
||||||
|
return builder
|
||||||
|
.where('deletedAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
|
||||||
|
.$if(!!ack, (qb) => qb.where('id', '>', ack!.updateId))
|
||||||
|
.orderBy(['id asc']) as SelectQueryBuilder<DB, T, D>;
|
||||||
|
}
|
||||||
|
|
||||||
|
private upsertTableFilters<T extends keyof Pick<DB, upsertTables>, D>(
|
||||||
|
qb: SelectQueryBuilder<DB, T, D>,
|
||||||
|
ack?: SyncAck,
|
||||||
|
) {
|
||||||
|
const builder = qb as SelectQueryBuilder<DB, upsertTables, D>;
|
||||||
|
return builder
|
||||||
|
.where('updatedAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
|
||||||
|
.$if(!!ack, (qb) => qb.where('updateId', '>', ack!.updateId))
|
||||||
|
.orderBy(['updateId asc']) as SelectQueryBuilder<DB, T, D>;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ import { DateTime } from 'luxon';
|
|||||||
import { Writable } from 'node:stream';
|
import { Writable } from 'node:stream';
|
||||||
import { AUDIT_LOG_MAX_DURATION } from 'src/constants';
|
import { AUDIT_LOG_MAX_DURATION } from 'src/constants';
|
||||||
import { SessionSyncCheckpoints } from 'src/db';
|
import { SessionSyncCheckpoints } from 'src/db';
|
||||||
import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
|
import { AssetResponseDto, hexOrBufferToBase64, mapAsset } from 'src/dtos/asset-response.dto';
|
||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import {
|
import {
|
||||||
AssetDeltaSyncDto,
|
AssetDeltaSyncDto,
|
||||||
@ -22,10 +22,14 @@ import { setIsEqual } from 'src/utils/set';
|
|||||||
import { fromAck, serialize } from 'src/utils/sync';
|
import { fromAck, serialize } from 'src/utils/sync';
|
||||||
|
|
||||||
const FULL_SYNC = { needsFullSync: true, deleted: [], upserted: [] };
|
const FULL_SYNC = { needsFullSync: true, deleted: [], upserted: [] };
|
||||||
const SYNC_TYPES_ORDER = [
|
export const SYNC_TYPES_ORDER = [
|
||||||
//
|
//
|
||||||
SyncRequestType.UsersV1,
|
SyncRequestType.UsersV1,
|
||||||
SyncRequestType.PartnersV1,
|
SyncRequestType.PartnersV1,
|
||||||
|
SyncRequestType.AssetsV1,
|
||||||
|
SyncRequestType.AssetExifsV1,
|
||||||
|
SyncRequestType.PartnerAssetsV1,
|
||||||
|
SyncRequestType.PartnerAssetExifsV1,
|
||||||
];
|
];
|
||||||
|
|
||||||
const throwSessionRequired = () => {
|
const throwSessionRequired = () => {
|
||||||
@ -49,17 +53,22 @@ export class SyncService extends BaseService {
|
|||||||
return throwSessionRequired();
|
return throwSessionRequired();
|
||||||
}
|
}
|
||||||
|
|
||||||
const checkpoints: Insertable<SessionSyncCheckpoints>[] = [];
|
const checkpoints: Record<string, Insertable<SessionSyncCheckpoints>> = {};
|
||||||
for (const ack of dto.acks) {
|
for (const ack of dto.acks) {
|
||||||
const { type } = fromAck(ack);
|
const { type } = fromAck(ack);
|
||||||
// TODO proper ack validation via class validator
|
// TODO proper ack validation via class validator
|
||||||
if (!Object.values(SyncEntityType).includes(type)) {
|
if (!Object.values(SyncEntityType).includes(type)) {
|
||||||
throw new BadRequestException(`Invalid ack type: ${type}`);
|
throw new BadRequestException(`Invalid ack type: ${type}`);
|
||||||
}
|
}
|
||||||
checkpoints.push({ sessionId, type, ack });
|
|
||||||
|
if (checkpoints[type]) {
|
||||||
|
throw new BadRequestException('Only one ack per type is allowed');
|
||||||
|
}
|
||||||
|
|
||||||
|
checkpoints[type] = { sessionId, type, ack };
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.syncRepository.upsertCheckpoints(checkpoints);
|
await this.syncRepository.upsertCheckpoints(Object.values(checkpoints));
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteAcks(auth: AuthDto, dto: SyncAckDeleteDto) {
|
async deleteAcks(auth: AuthDto, dto: SyncAckDeleteDto) {
|
||||||
@ -115,6 +124,87 @@ export class SyncService extends BaseService {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case SyncRequestType.AssetsV1: {
|
||||||
|
const deletes = this.syncRepository.getAssetDeletes(
|
||||||
|
auth.user.id,
|
||||||
|
checkpointMap[SyncEntityType.AssetDeleteV1],
|
||||||
|
);
|
||||||
|
for await (const { id, ...data } of deletes) {
|
||||||
|
response.write(serialize({ type: SyncEntityType.AssetDeleteV1, updateId: id, data }));
|
||||||
|
}
|
||||||
|
|
||||||
|
const upserts = this.syncRepository.getAssetUpserts(auth.user.id, checkpointMap[SyncEntityType.AssetV1]);
|
||||||
|
for await (const { updateId, checksum, thumbhash, ...data } of upserts) {
|
||||||
|
response.write(
|
||||||
|
serialize({
|
||||||
|
type: SyncEntityType.AssetV1,
|
||||||
|
updateId,
|
||||||
|
data: {
|
||||||
|
...data,
|
||||||
|
checksum: hexOrBufferToBase64(checksum),
|
||||||
|
thumbhash: thumbhash ? hexOrBufferToBase64(thumbhash) : null,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case SyncRequestType.PartnerAssetsV1: {
|
||||||
|
const deletes = this.syncRepository.getPartnerAssetDeletes(
|
||||||
|
auth.user.id,
|
||||||
|
checkpointMap[SyncEntityType.PartnerAssetDeleteV1],
|
||||||
|
);
|
||||||
|
for await (const { id, ...data } of deletes) {
|
||||||
|
response.write(serialize({ type: SyncEntityType.PartnerAssetDeleteV1, updateId: id, data }));
|
||||||
|
}
|
||||||
|
|
||||||
|
const upserts = this.syncRepository.getPartnerAssetsUpserts(
|
||||||
|
auth.user.id,
|
||||||
|
checkpointMap[SyncEntityType.PartnerAssetV1],
|
||||||
|
);
|
||||||
|
for await (const { updateId, checksum, thumbhash, ...data } of upserts) {
|
||||||
|
response.write(
|
||||||
|
serialize({
|
||||||
|
type: SyncEntityType.PartnerAssetV1,
|
||||||
|
updateId,
|
||||||
|
data: {
|
||||||
|
...data,
|
||||||
|
checksum: hexOrBufferToBase64(checksum),
|
||||||
|
thumbhash: thumbhash ? hexOrBufferToBase64(thumbhash) : null,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case SyncRequestType.AssetExifsV1: {
|
||||||
|
const upserts = this.syncRepository.getAssetExifsUpserts(
|
||||||
|
auth.user.id,
|
||||||
|
checkpointMap[SyncEntityType.AssetExifV1],
|
||||||
|
);
|
||||||
|
for await (const { updateId, ...data } of upserts) {
|
||||||
|
response.write(serialize({ type: SyncEntityType.AssetExifV1, updateId, data }));
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case SyncRequestType.PartnerAssetExifsV1: {
|
||||||
|
const upserts = this.syncRepository.getPartnerAssetExifsUpserts(
|
||||||
|
auth.user.id,
|
||||||
|
checkpointMap[SyncEntityType.PartnerAssetExifV1],
|
||||||
|
);
|
||||||
|
for await (const { updateId, ...data } of upserts) {
|
||||||
|
response.write(serialize({ type: SyncEntityType.PartnerAssetExifV1, updateId, data }));
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
default: {
|
default: {
|
||||||
this.logger.warn(`Unsupported sync type: ${type}`);
|
this.logger.warn(`Unsupported sync type: ${type}`);
|
||||||
break;
|
break;
|
||||||
|
@ -55,7 +55,7 @@ class CustomWritable extends Writable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Asset = Insertable<Assets>;
|
type Asset = Partial<Insertable<Assets>>;
|
||||||
type User = Partial<Insertable<Users>>;
|
type User = Partial<Insertable<Users>>;
|
||||||
type Session = Omit<Insertable<Sessions>, 'token'> & { token?: string };
|
type Session = Omit<Insertable<Sessions>, 'token'> & { token?: string };
|
||||||
type Partner = Insertable<Partners>;
|
type Partner = Insertable<Partners>;
|
||||||
@ -160,10 +160,6 @@ export class TestFactory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async create() {
|
async create() {
|
||||||
for (const asset of this.assets) {
|
|
||||||
await this.context.createAsset(asset);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const user of this.users) {
|
for (const user of this.users) {
|
||||||
await this.context.createUser(user);
|
await this.context.createUser(user);
|
||||||
}
|
}
|
||||||
@ -176,6 +172,10 @@ export class TestFactory {
|
|||||||
await this.context.createSession(session);
|
await this.context.createSession(session);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const asset of this.assets) {
|
||||||
|
await this.context.createAsset(asset);
|
||||||
|
}
|
||||||
|
|
||||||
return this.context;
|
return this.context;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -212,7 +212,7 @@ export class TestContext {
|
|||||||
versionHistory: VersionHistoryRepository;
|
versionHistory: VersionHistoryRepository;
|
||||||
view: ViewRepository;
|
view: ViewRepository;
|
||||||
|
|
||||||
private constructor(private db: Kysely<DB>) {
|
private constructor(public db: Kysely<DB>) {
|
||||||
const logger = newLoggingRepositoryMock() as unknown as LoggingRepository;
|
const logger = newLoggingRepositoryMock() as unknown as LoggingRepository;
|
||||||
const config = new ConfigRepository();
|
const config = new ConfigRepository();
|
||||||
|
|
||||||
|
74
server/test/medium/specs/audit.database.spec.ts
Normal file
74
server/test/medium/specs/audit.database.spec.ts
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import { TestContext, TestFactory } from 'test/factory';
|
||||||
|
import { getKyselyDB } from 'test/utils';
|
||||||
|
|
||||||
|
describe('audit', () => {
|
||||||
|
let context: TestContext;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const db = await getKyselyDB();
|
||||||
|
context = await TestContext.from(db).create();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('partners_audit', () => {
|
||||||
|
it('should not cascade user deletes to partners_audit', async () => {
|
||||||
|
const user1 = TestFactory.user();
|
||||||
|
const user2 = TestFactory.user();
|
||||||
|
|
||||||
|
await context
|
||||||
|
.getFactory()
|
||||||
|
.withUser(user1)
|
||||||
|
.withUser(user2)
|
||||||
|
.withPartner({ sharedById: user1.id, sharedWithId: user2.id })
|
||||||
|
.create();
|
||||||
|
|
||||||
|
await context.user.delete(user1, true);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
context.db.selectFrom('partners_audit').select(['id']).where('sharedById', '=', user1.id).execute(),
|
||||||
|
).resolves.toHaveLength(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('assets_audit', () => {
|
||||||
|
it('should not cascade user deletes to assets_audit', async () => {
|
||||||
|
const user = TestFactory.user();
|
||||||
|
const asset = TestFactory.asset({ ownerId: user.id });
|
||||||
|
|
||||||
|
await context.getFactory().withUser(user).withAsset(asset).create();
|
||||||
|
|
||||||
|
await context.user.delete(user, true);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
context.db.selectFrom('assets_audit').select(['id']).where('assetId', '=', asset.id).execute(),
|
||||||
|
).resolves.toHaveLength(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('exif', () => {
|
||||||
|
it('should automatically set updatedAt and updateId when the row is updated', async () => {
|
||||||
|
const user = TestFactory.user();
|
||||||
|
const asset = TestFactory.asset({ ownerId: user.id });
|
||||||
|
const exif = { assetId: asset.id, make: 'Canon' };
|
||||||
|
|
||||||
|
await context.getFactory().withUser(user).withAsset(asset).create();
|
||||||
|
await context.asset.upsertExif(exif);
|
||||||
|
|
||||||
|
const before = await context.db
|
||||||
|
.selectFrom('exif')
|
||||||
|
.select(['updatedAt', 'updateId'])
|
||||||
|
.where('assetId', '=', asset.id)
|
||||||
|
.executeTakeFirstOrThrow();
|
||||||
|
|
||||||
|
await context.asset.upsertExif({ assetId: asset.id, make: 'Canon 2' });
|
||||||
|
|
||||||
|
const after = await context.db
|
||||||
|
.selectFrom('exif')
|
||||||
|
.select(['updatedAt', 'updateId'])
|
||||||
|
.where('assetId', '=', asset.id)
|
||||||
|
.executeTakeFirstOrThrow();
|
||||||
|
|
||||||
|
expect(before.updateId).not.toEqual(after.updateId);
|
||||||
|
expect(before.updatedAt).not.toEqual(after.updatedAt);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -1,6 +1,6 @@
|
|||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import { SyncRequestType } from 'src/enum';
|
import { SyncEntityType, SyncRequestType } from 'src/enum';
|
||||||
import { SyncService } from 'src/services/sync.service';
|
import { SYNC_TYPES_ORDER, SyncService } from 'src/services/sync.service';
|
||||||
import { TestContext, TestFactory } from 'test/factory';
|
import { TestContext, TestFactory } from 'test/factory';
|
||||||
import { getKyselyDB, newTestService } from 'test/utils';
|
import { getKyselyDB, newTestService } from 'test/utils';
|
||||||
|
|
||||||
@ -33,7 +33,15 @@ const setup = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
describe(SyncService.name, () => {
|
describe(SyncService.name, () => {
|
||||||
describe.concurrent('users', () => {
|
it('should have all the types in the ordering variable', () => {
|
||||||
|
for (const key in SyncRequestType) {
|
||||||
|
expect(SYNC_TYPES_ORDER).includes(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(SYNC_TYPES_ORDER.length).toBe(Object.keys(SyncRequestType).length);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe.concurrent(SyncEntityType.UserV1, () => {
|
||||||
it('should detect and sync the first user', async () => {
|
it('should detect and sync the first user', async () => {
|
||||||
const { context, auth, sut, testSync } = await setup();
|
const { context, auth, sut, testSync } = await setup();
|
||||||
|
|
||||||
@ -189,7 +197,7 @@ describe(SyncService.name, () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe.concurrent('partners', () => {
|
describe.concurrent(SyncEntityType.PartnerV1, () => {
|
||||||
it('should detect and sync the first partner', async () => {
|
it('should detect and sync the first partner', async () => {
|
||||||
const { auth, context, sut, testSync } = await setup();
|
const { auth, context, sut, testSync } = await setup();
|
||||||
|
|
||||||
@ -349,7 +357,7 @@ describe(SyncService.name, () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not sync a partner for an unrelated user', async () => {
|
it('should not sync a partner or partner delete for an unrelated user', async () => {
|
||||||
const { auth, context, testSync } = await setup();
|
const { auth, context, testSync } = await setup();
|
||||||
|
|
||||||
const user2 = await context.createUser();
|
const user2 = await context.createUser();
|
||||||
@ -357,9 +365,436 @@ describe(SyncService.name, () => {
|
|||||||
|
|
||||||
await context.createPartner({ sharedById: user2.id, sharedWithId: user3.id });
|
await context.createPartner({ sharedById: user2.id, sharedWithId: user3.id });
|
||||||
|
|
||||||
const response = await testSync(auth, [SyncRequestType.PartnersV1]);
|
expect(await testSync(auth, [SyncRequestType.PartnersV1])).toHaveLength(0);
|
||||||
|
|
||||||
|
await context.partner.remove({ sharedById: user2.id, sharedWithId: user3.id });
|
||||||
|
|
||||||
|
expect(await testSync(auth, [SyncRequestType.PartnersV1])).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not sync a partner delete after a user is deleted', async () => {
|
||||||
|
const { auth, context, testSync } = await setup();
|
||||||
|
|
||||||
|
const user2 = await context.createUser();
|
||||||
|
await context.createPartner({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||||
|
await context.user.delete({ id: user2.id }, true);
|
||||||
|
|
||||||
|
expect(await testSync(auth, [SyncRequestType.PartnersV1])).toHaveLength(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe.concurrent(SyncEntityType.AssetV1, () => {
|
||||||
|
it('should detect and sync the first asset', async () => {
|
||||||
|
const { auth, context, sut, testSync } = await setup();
|
||||||
|
|
||||||
|
const checksum = '1115vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA=';
|
||||||
|
const thumbhash = '2225vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA=';
|
||||||
|
const date = new Date().toISOString();
|
||||||
|
|
||||||
|
const asset = TestFactory.asset({
|
||||||
|
ownerId: auth.user.id,
|
||||||
|
checksum: Buffer.from(checksum, 'base64'),
|
||||||
|
thumbhash: Buffer.from(thumbhash, 'base64'),
|
||||||
|
fileCreatedAt: date,
|
||||||
|
fileModifiedAt: date,
|
||||||
|
deletedAt: null,
|
||||||
|
});
|
||||||
|
await context.createAsset(asset);
|
||||||
|
|
||||||
|
const initialSyncResponse = await testSync(auth, [SyncRequestType.AssetsV1]);
|
||||||
|
|
||||||
|
expect(initialSyncResponse).toHaveLength(1);
|
||||||
|
expect(initialSyncResponse).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: {
|
||||||
|
id: asset.id,
|
||||||
|
ownerId: asset.ownerId,
|
||||||
|
thumbhash,
|
||||||
|
checksum,
|
||||||
|
deletedAt: null,
|
||||||
|
fileCreatedAt: date,
|
||||||
|
fileModifiedAt: date,
|
||||||
|
isFavorite: false,
|
||||||
|
isVisible: true,
|
||||||
|
localDateTime: null,
|
||||||
|
type: asset.type,
|
||||||
|
},
|
||||||
|
type: 'AssetV1',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const acks = [initialSyncResponse[0].ack];
|
||||||
|
await sut.setAcks(auth, { acks });
|
||||||
|
|
||||||
|
const ackSyncResponse = await testSync(auth, [SyncRequestType.AssetsV1]);
|
||||||
|
|
||||||
|
expect(ackSyncResponse).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect and sync a deleted asset', async () => {
|
||||||
|
const { auth, context, sut, testSync } = await setup();
|
||||||
|
|
||||||
|
const asset = TestFactory.asset({ ownerId: auth.user.id });
|
||||||
|
await context.createAsset(asset);
|
||||||
|
await context.asset.remove(asset);
|
||||||
|
|
||||||
|
const response = await testSync(auth, [SyncRequestType.AssetsV1]);
|
||||||
|
|
||||||
|
expect(response).toHaveLength(1);
|
||||||
|
expect(response).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: {
|
||||||
|
assetId: asset.id,
|
||||||
|
},
|
||||||
|
type: 'AssetDeleteV1',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const acks = response.map(({ ack }) => ack);
|
||||||
|
await sut.setAcks(auth, { acks });
|
||||||
|
|
||||||
|
const ackSyncResponse = await testSync(auth, [SyncRequestType.AssetsV1]);
|
||||||
|
|
||||||
|
expect(ackSyncResponse).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not sync an asset or asset delete for an unrelated user', async () => {
|
||||||
|
const { auth, context, testSync } = await setup();
|
||||||
|
|
||||||
|
const user2 = await context.createUser();
|
||||||
|
const session = TestFactory.session({ userId: user2.id });
|
||||||
|
const auth2 = TestFactory.auth({ session, user: user2 });
|
||||||
|
|
||||||
|
const asset = TestFactory.asset({ ownerId: user2.id });
|
||||||
|
await context.createAsset(asset);
|
||||||
|
|
||||||
|
expect(await testSync(auth2, [SyncRequestType.AssetsV1])).toHaveLength(1);
|
||||||
|
expect(await testSync(auth, [SyncRequestType.AssetsV1])).toHaveLength(0);
|
||||||
|
|
||||||
|
await context.asset.remove(asset);
|
||||||
|
expect(await testSync(auth2, [SyncRequestType.AssetsV1])).toHaveLength(1);
|
||||||
|
expect(await testSync(auth, [SyncRequestType.AssetsV1])).toHaveLength(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe.concurrent(SyncRequestType.PartnerAssetsV1, () => {
|
||||||
|
it('should detect and sync the first partner asset', async () => {
|
||||||
|
const { auth, context, sut, testSync } = await setup();
|
||||||
|
|
||||||
|
const checksum = '1115vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA=';
|
||||||
|
const thumbhash = '2225vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA=';
|
||||||
|
const date = new Date().toISOString();
|
||||||
|
|
||||||
|
const user2 = await context.createUser();
|
||||||
|
|
||||||
|
const asset = TestFactory.asset({
|
||||||
|
ownerId: user2.id,
|
||||||
|
checksum: Buffer.from(checksum, 'base64'),
|
||||||
|
thumbhash: Buffer.from(thumbhash, 'base64'),
|
||||||
|
fileCreatedAt: date,
|
||||||
|
fileModifiedAt: date,
|
||||||
|
deletedAt: null,
|
||||||
|
});
|
||||||
|
await context.createAsset(asset);
|
||||||
|
await context.partner.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||||
|
|
||||||
|
const initialSyncResponse = await testSync(auth, [SyncRequestType.PartnerAssetsV1]);
|
||||||
|
|
||||||
|
expect(initialSyncResponse).toHaveLength(1);
|
||||||
|
expect(initialSyncResponse).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: {
|
||||||
|
id: asset.id,
|
||||||
|
ownerId: asset.ownerId,
|
||||||
|
thumbhash,
|
||||||
|
checksum,
|
||||||
|
deletedAt: null,
|
||||||
|
fileCreatedAt: date,
|
||||||
|
fileModifiedAt: date,
|
||||||
|
isFavorite: false,
|
||||||
|
isVisible: true,
|
||||||
|
localDateTime: null,
|
||||||
|
type: asset.type,
|
||||||
|
},
|
||||||
|
type: SyncEntityType.PartnerAssetV1,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const acks = [initialSyncResponse[0].ack];
|
||||||
|
await sut.setAcks(auth, { acks });
|
||||||
|
|
||||||
|
const ackSyncResponse = await testSync(auth, [SyncRequestType.PartnerAssetsV1]);
|
||||||
|
|
||||||
|
expect(ackSyncResponse).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect and sync a deleted partner asset', async () => {
|
||||||
|
const { auth, context, sut, testSync } = await setup();
|
||||||
|
|
||||||
|
const user2 = await context.createUser();
|
||||||
|
const asset = TestFactory.asset({ ownerId: user2.id });
|
||||||
|
await context.createAsset(asset);
|
||||||
|
await context.partner.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||||
|
await context.asset.remove(asset);
|
||||||
|
|
||||||
|
const response = await testSync(auth, [SyncRequestType.PartnerAssetsV1]);
|
||||||
|
|
||||||
|
expect(response).toHaveLength(1);
|
||||||
|
expect(response).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: {
|
||||||
|
assetId: asset.id,
|
||||||
|
},
|
||||||
|
type: SyncEntityType.PartnerAssetDeleteV1,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const acks = response.map(({ ack }) => ack);
|
||||||
|
await sut.setAcks(auth, { acks });
|
||||||
|
|
||||||
|
const ackSyncResponse = await testSync(auth, [SyncRequestType.PartnerAssetsV1]);
|
||||||
|
|
||||||
|
expect(ackSyncResponse).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not sync a deleted partner asset due to a user delete', async () => {
|
||||||
|
const { auth, context, testSync } = await setup();
|
||||||
|
|
||||||
|
const user2 = await context.createUser();
|
||||||
|
await context.partner.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||||
|
await context.createAsset({ ownerId: user2.id });
|
||||||
|
await context.user.delete({ id: user2.id }, true);
|
||||||
|
|
||||||
|
const response = await testSync(auth, [SyncRequestType.PartnerAssetsV1]);
|
||||||
|
|
||||||
expect(response).toHaveLength(0);
|
expect(response).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not sync a deleted partner asset due to a partner delete (unshare)', async () => {
|
||||||
|
const { auth, context, testSync } = await setup();
|
||||||
|
|
||||||
|
const user2 = await context.createUser();
|
||||||
|
await context.createAsset({ ownerId: user2.id });
|
||||||
|
const partner = { sharedById: user2.id, sharedWithId: auth.user.id };
|
||||||
|
await context.partner.create(partner);
|
||||||
|
|
||||||
|
await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(1);
|
||||||
|
|
||||||
|
await context.partner.remove(partner);
|
||||||
|
|
||||||
|
await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not sync an asset or asset delete for own user', async () => {
|
||||||
|
const { auth, context, testSync } = await setup();
|
||||||
|
|
||||||
|
const user2 = await context.createUser();
|
||||||
|
const asset = await context.createAsset({ ownerId: auth.user.id });
|
||||||
|
const partner = { sharedById: user2.id, sharedWithId: auth.user.id };
|
||||||
|
await context.partner.create(partner);
|
||||||
|
|
||||||
|
await expect(testSync(auth, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1);
|
||||||
|
await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0);
|
||||||
|
|
||||||
|
await context.asset.remove(asset);
|
||||||
|
|
||||||
|
await expect(testSync(auth, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1);
|
||||||
|
await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not sync an asset or asset delete for unrelated user', async () => {
|
||||||
|
const { auth, context, testSync } = await setup();
|
||||||
|
|
||||||
|
const user2 = await context.createUser();
|
||||||
|
const session = TestFactory.session({ userId: user2.id });
|
||||||
|
const auth2 = TestFactory.auth({ session, user: user2 });
|
||||||
|
const asset = await context.createAsset({ ownerId: user2.id });
|
||||||
|
|
||||||
|
await expect(testSync(auth2, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1);
|
||||||
|
await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0);
|
||||||
|
|
||||||
|
await context.asset.remove(asset);
|
||||||
|
|
||||||
|
await expect(testSync(auth2, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1);
|
||||||
|
await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe.concurrent(SyncRequestType.AssetExifsV1, () => {
|
||||||
|
it('should detect and sync the first asset exif', async () => {
|
||||||
|
const { auth, context, sut, testSync } = await setup();
|
||||||
|
|
||||||
|
const asset = TestFactory.asset({ ownerId: auth.user.id });
|
||||||
|
const exif = { assetId: asset.id, make: 'Canon' };
|
||||||
|
|
||||||
|
await context.createAsset(asset);
|
||||||
|
await context.asset.upsertExif(exif);
|
||||||
|
|
||||||
|
const initialSyncResponse = await testSync(auth, [SyncRequestType.AssetExifsV1]);
|
||||||
|
|
||||||
|
expect(initialSyncResponse).toHaveLength(1);
|
||||||
|
expect(initialSyncResponse).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: {
|
||||||
|
assetId: asset.id,
|
||||||
|
city: null,
|
||||||
|
country: null,
|
||||||
|
dateTimeOriginal: null,
|
||||||
|
description: '',
|
||||||
|
exifImageHeight: null,
|
||||||
|
exifImageWidth: null,
|
||||||
|
exposureTime: null,
|
||||||
|
fNumber: null,
|
||||||
|
fileSizeInByte: null,
|
||||||
|
focalLength: null,
|
||||||
|
fps: null,
|
||||||
|
iso: null,
|
||||||
|
latitude: null,
|
||||||
|
lensModel: null,
|
||||||
|
longitude: null,
|
||||||
|
make: 'Canon',
|
||||||
|
model: null,
|
||||||
|
modifyDate: null,
|
||||||
|
orientation: null,
|
||||||
|
profileDescription: null,
|
||||||
|
projectionType: null,
|
||||||
|
rating: null,
|
||||||
|
state: null,
|
||||||
|
timeZone: null,
|
||||||
|
},
|
||||||
|
type: SyncEntityType.AssetExifV1,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const acks = [initialSyncResponse[0].ack];
|
||||||
|
await sut.setAcks(auth, { acks });
|
||||||
|
|
||||||
|
const ackSyncResponse = await testSync(auth, [SyncRequestType.AssetExifsV1]);
|
||||||
|
|
||||||
|
expect(ackSyncResponse).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should only sync asset exif for own user', async () => {
|
||||||
|
const { auth, context, testSync } = await setup();
|
||||||
|
|
||||||
|
const user2 = await context.createUser();
|
||||||
|
const session = TestFactory.session({ userId: user2.id });
|
||||||
|
const auth2 = TestFactory.auth({ session, user: user2 });
|
||||||
|
|
||||||
|
await context.partner.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||||
|
const asset = TestFactory.asset({ ownerId: user2.id });
|
||||||
|
const exif = { assetId: asset.id, make: 'Canon' };
|
||||||
|
|
||||||
|
await context.createAsset(asset);
|
||||||
|
await context.asset.upsertExif(exif);
|
||||||
|
|
||||||
|
await expect(testSync(auth2, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(1);
|
||||||
|
await expect(testSync(auth, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe.concurrent(SyncRequestType.PartnerAssetExifsV1, () => {
|
||||||
|
it('should detect and sync the first partner asset exif', async () => {
|
||||||
|
const { auth, context, sut, testSync } = await setup();
|
||||||
|
|
||||||
|
const user2 = await context.createUser();
|
||||||
|
await context.partner.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||||
|
const asset = TestFactory.asset({ ownerId: user2.id });
|
||||||
|
await context.createAsset(asset);
|
||||||
|
const exif = { assetId: asset.id, make: 'Canon' };
|
||||||
|
await context.asset.upsertExif(exif);
|
||||||
|
|
||||||
|
const initialSyncResponse = await testSync(auth, [SyncRequestType.PartnerAssetExifsV1]);
|
||||||
|
|
||||||
|
expect(initialSyncResponse).toHaveLength(1);
|
||||||
|
expect(initialSyncResponse).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
{
|
||||||
|
ack: expect.any(String),
|
||||||
|
data: {
|
||||||
|
assetId: asset.id,
|
||||||
|
city: null,
|
||||||
|
country: null,
|
||||||
|
dateTimeOriginal: null,
|
||||||
|
description: '',
|
||||||
|
exifImageHeight: null,
|
||||||
|
exifImageWidth: null,
|
||||||
|
exposureTime: null,
|
||||||
|
fNumber: null,
|
||||||
|
fileSizeInByte: null,
|
||||||
|
focalLength: null,
|
||||||
|
fps: null,
|
||||||
|
iso: null,
|
||||||
|
latitude: null,
|
||||||
|
lensModel: null,
|
||||||
|
longitude: null,
|
||||||
|
make: 'Canon',
|
||||||
|
model: null,
|
||||||
|
modifyDate: null,
|
||||||
|
orientation: null,
|
||||||
|
profileDescription: null,
|
||||||
|
projectionType: null,
|
||||||
|
rating: null,
|
||||||
|
state: null,
|
||||||
|
timeZone: null,
|
||||||
|
},
|
||||||
|
type: SyncEntityType.PartnerAssetExifV1,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const acks = [initialSyncResponse[0].ack];
|
||||||
|
await sut.setAcks(auth, { acks });
|
||||||
|
|
||||||
|
const ackSyncResponse = await testSync(auth, [SyncRequestType.PartnerAssetExifsV1]);
|
||||||
|
|
||||||
|
expect(ackSyncResponse).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not sync partner asset exif for own user', async () => {
|
||||||
|
const { auth, context, testSync } = await setup();
|
||||||
|
|
||||||
|
const user2 = await context.createUser();
|
||||||
|
await context.partner.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||||
|
const asset = TestFactory.asset({ ownerId: auth.user.id });
|
||||||
|
const exif = { assetId: asset.id, make: 'Canon' };
|
||||||
|
await context.createAsset(asset);
|
||||||
|
await context.asset.upsertExif(exif);
|
||||||
|
|
||||||
|
await expect(testSync(auth, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(1);
|
||||||
|
await expect(testSync(auth, [SyncRequestType.PartnerAssetExifsV1])).resolves.toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not sync partner asset exif for unrelated user', async () => {
|
||||||
|
const { auth, context, testSync } = await setup();
|
||||||
|
|
||||||
|
const user2 = await context.createUser();
|
||||||
|
const user3 = await context.createUser();
|
||||||
|
const session = TestFactory.session({ userId: user3.id });
|
||||||
|
const authUser3 = TestFactory.auth({ session, user: user3 });
|
||||||
|
await context.partner.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||||
|
const asset = TestFactory.asset({ ownerId: user3.id });
|
||||||
|
const exif = { assetId: asset.id, make: 'Canon' };
|
||||||
|
await context.createAsset(asset);
|
||||||
|
await context.asset.upsertExif(exif);
|
||||||
|
|
||||||
|
await expect(testSync(authUser3, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(1);
|
||||||
|
await expect(testSync(auth, [SyncRequestType.PartnerAssetExifsV1])).resolves.toHaveLength(0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -11,5 +11,11 @@ export const newSyncRepositoryMock = (): Mocked<RepositoryInterface<SyncReposito
|
|||||||
getUserDeletes: vitest.fn(),
|
getUserDeletes: vitest.fn(),
|
||||||
getPartnerUpserts: vitest.fn(),
|
getPartnerUpserts: vitest.fn(),
|
||||||
getPartnerDeletes: vitest.fn(),
|
getPartnerDeletes: vitest.fn(),
|
||||||
|
getPartnerAssetsUpserts: vitest.fn(),
|
||||||
|
getPartnerAssetDeletes: vitest.fn(),
|
||||||
|
getAssetDeletes: vitest.fn(),
|
||||||
|
getAssetUpserts: vitest.fn(),
|
||||||
|
getAssetExifsUpserts: vitest.fn(),
|
||||||
|
getPartnerAssetExifsUpserts: vitest.fn(),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user