mirror of https://github.com/immich-app/immich.git
feat: sync albums and album users (#18377)
parent
58af574241
commit
cd288533a1
@ -0,0 +1,99 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class SyncAlbumDeleteV1 {
|
||||
/// Returns a new [SyncAlbumDeleteV1] instance.
|
||||
SyncAlbumDeleteV1({
|
||||
required this.albumId,
|
||||
});
|
||||
|
||||
String albumId;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is SyncAlbumDeleteV1 &&
|
||||
other.albumId == albumId;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(albumId.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'SyncAlbumDeleteV1[albumId=$albumId]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'albumId'] = this.albumId;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [SyncAlbumDeleteV1] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static SyncAlbumDeleteV1? fromJson(dynamic value) {
|
||||
upgradeDto(value, "SyncAlbumDeleteV1");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return SyncAlbumDeleteV1(
|
||||
albumId: mapValueOfType<String>(json, r'albumId')!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<SyncAlbumDeleteV1> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <SyncAlbumDeleteV1>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = SyncAlbumDeleteV1.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, SyncAlbumDeleteV1> mapFromJson(dynamic json) {
|
||||
final map = <String, SyncAlbumDeleteV1>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = SyncAlbumDeleteV1.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of SyncAlbumDeleteV1-objects as value to a dart map
|
||||
static Map<String, List<SyncAlbumDeleteV1>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<SyncAlbumDeleteV1>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = SyncAlbumDeleteV1.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'albumId',
|
||||
};
|
||||
}
|
||||
|
||||
@ -0,0 +1,107 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class SyncAlbumUserDeleteV1 {
|
||||
/// Returns a new [SyncAlbumUserDeleteV1] instance.
|
||||
SyncAlbumUserDeleteV1({
|
||||
required this.albumId,
|
||||
required this.userId,
|
||||
});
|
||||
|
||||
String albumId;
|
||||
|
||||
String userId;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is SyncAlbumUserDeleteV1 &&
|
||||
other.albumId == albumId &&
|
||||
other.userId == userId;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(albumId.hashCode) +
|
||||
(userId.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'SyncAlbumUserDeleteV1[albumId=$albumId, userId=$userId]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'albumId'] = this.albumId;
|
||||
json[r'userId'] = this.userId;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [SyncAlbumUserDeleteV1] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static SyncAlbumUserDeleteV1? fromJson(dynamic value) {
|
||||
upgradeDto(value, "SyncAlbumUserDeleteV1");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return SyncAlbumUserDeleteV1(
|
||||
albumId: mapValueOfType<String>(json, r'albumId')!,
|
||||
userId: mapValueOfType<String>(json, r'userId')!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<SyncAlbumUserDeleteV1> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <SyncAlbumUserDeleteV1>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = SyncAlbumUserDeleteV1.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, SyncAlbumUserDeleteV1> mapFromJson(dynamic json) {
|
||||
final map = <String, SyncAlbumUserDeleteV1>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = SyncAlbumUserDeleteV1.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of SyncAlbumUserDeleteV1-objects as value to a dart map
|
||||
static Map<String, List<SyncAlbumUserDeleteV1>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<SyncAlbumUserDeleteV1>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = SyncAlbumUserDeleteV1.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'albumId',
|
||||
'userId',
|
||||
};
|
||||
}
|
||||
|
||||
@ -0,0 +1,189 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class SyncAlbumUserV1 {
|
||||
/// Returns a new [SyncAlbumUserV1] instance.
|
||||
SyncAlbumUserV1({
|
||||
required this.albumId,
|
||||
required this.role,
|
||||
required this.userId,
|
||||
});
|
||||
|
||||
String albumId;
|
||||
|
||||
SyncAlbumUserV1RoleEnum role;
|
||||
|
||||
String userId;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is SyncAlbumUserV1 &&
|
||||
other.albumId == albumId &&
|
||||
other.role == role &&
|
||||
other.userId == userId;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(albumId.hashCode) +
|
||||
(role.hashCode) +
|
||||
(userId.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'SyncAlbumUserV1[albumId=$albumId, role=$role, userId=$userId]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'albumId'] = this.albumId;
|
||||
json[r'role'] = this.role;
|
||||
json[r'userId'] = this.userId;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [SyncAlbumUserV1] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static SyncAlbumUserV1? fromJson(dynamic value) {
|
||||
upgradeDto(value, "SyncAlbumUserV1");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return SyncAlbumUserV1(
|
||||
albumId: mapValueOfType<String>(json, r'albumId')!,
|
||||
role: SyncAlbumUserV1RoleEnum.fromJson(json[r'role'])!,
|
||||
userId: mapValueOfType<String>(json, r'userId')!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<SyncAlbumUserV1> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <SyncAlbumUserV1>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = SyncAlbumUserV1.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, SyncAlbumUserV1> mapFromJson(dynamic json) {
|
||||
final map = <String, SyncAlbumUserV1>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = SyncAlbumUserV1.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of SyncAlbumUserV1-objects as value to a dart map
|
||||
static Map<String, List<SyncAlbumUserV1>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<SyncAlbumUserV1>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = SyncAlbumUserV1.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'albumId',
|
||||
'role',
|
||||
'userId',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
class SyncAlbumUserV1RoleEnum {
|
||||
/// Instantiate a new enum with the provided [value].
|
||||
const SyncAlbumUserV1RoleEnum._(this.value);
|
||||
|
||||
/// The underlying value of this enum member.
|
||||
final String value;
|
||||
|
||||
@override
|
||||
String toString() => value;
|
||||
|
||||
String toJson() => value;
|
||||
|
||||
static const editor = SyncAlbumUserV1RoleEnum._(r'editor');
|
||||
static const viewer = SyncAlbumUserV1RoleEnum._(r'viewer');
|
||||
|
||||
/// List of all possible values in this [enum][SyncAlbumUserV1RoleEnum].
|
||||
static const values = <SyncAlbumUserV1RoleEnum>[
|
||||
editor,
|
||||
viewer,
|
||||
];
|
||||
|
||||
static SyncAlbumUserV1RoleEnum? fromJson(dynamic value) => SyncAlbumUserV1RoleEnumTypeTransformer().decode(value);
|
||||
|
||||
static List<SyncAlbumUserV1RoleEnum> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <SyncAlbumUserV1RoleEnum>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = SyncAlbumUserV1RoleEnum.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
}
|
||||
|
||||
/// Transformation class that can [encode] an instance of [SyncAlbumUserV1RoleEnum] to String,
|
||||
/// and [decode] dynamic data back to [SyncAlbumUserV1RoleEnum].
|
||||
class SyncAlbumUserV1RoleEnumTypeTransformer {
|
||||
factory SyncAlbumUserV1RoleEnumTypeTransformer() => _instance ??= const SyncAlbumUserV1RoleEnumTypeTransformer._();
|
||||
|
||||
const SyncAlbumUserV1RoleEnumTypeTransformer._();
|
||||
|
||||
String encode(SyncAlbumUserV1RoleEnum data) => data.value;
|
||||
|
||||
/// Decodes a [dynamic value][data] to a SyncAlbumUserV1RoleEnum.
|
||||
///
|
||||
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
|
||||
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
|
||||
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
|
||||
///
|
||||
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
|
||||
/// and users are still using an old app with the old code.
|
||||
SyncAlbumUserV1RoleEnum? decode(dynamic data, {bool allowNull = true}) {
|
||||
if (data != null) {
|
||||
switch (data) {
|
||||
case r'editor': return SyncAlbumUserV1RoleEnum.editor;
|
||||
case r'viewer': return SyncAlbumUserV1RoleEnum.viewer;
|
||||
default:
|
||||
if (!allowNull) {
|
||||
throw ArgumentError('Unknown enum value to decode: $data');
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Singleton [SyncAlbumUserV1RoleEnumTypeTransformer] instance.
|
||||
static SyncAlbumUserV1RoleEnumTypeTransformer? _instance;
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,167 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class SyncAlbumV1 {
|
||||
/// Returns a new [SyncAlbumV1] instance.
|
||||
SyncAlbumV1({
|
||||
required this.createdAt,
|
||||
required this.description,
|
||||
required this.id,
|
||||
required this.isActivityEnabled,
|
||||
required this.name,
|
||||
required this.order,
|
||||
required this.ownerId,
|
||||
required this.thumbnailAssetId,
|
||||
required this.updatedAt,
|
||||
});
|
||||
|
||||
DateTime createdAt;
|
||||
|
||||
String description;
|
||||
|
||||
String id;
|
||||
|
||||
bool isActivityEnabled;
|
||||
|
||||
String name;
|
||||
|
||||
AssetOrder order;
|
||||
|
||||
String ownerId;
|
||||
|
||||
String? thumbnailAssetId;
|
||||
|
||||
DateTime updatedAt;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is SyncAlbumV1 &&
|
||||
other.createdAt == createdAt &&
|
||||
other.description == description &&
|
||||
other.id == id &&
|
||||
other.isActivityEnabled == isActivityEnabled &&
|
||||
other.name == name &&
|
||||
other.order == order &&
|
||||
other.ownerId == ownerId &&
|
||||
other.thumbnailAssetId == thumbnailAssetId &&
|
||||
other.updatedAt == updatedAt;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(createdAt.hashCode) +
|
||||
(description.hashCode) +
|
||||
(id.hashCode) +
|
||||
(isActivityEnabled.hashCode) +
|
||||
(name.hashCode) +
|
||||
(order.hashCode) +
|
||||
(ownerId.hashCode) +
|
||||
(thumbnailAssetId == null ? 0 : thumbnailAssetId!.hashCode) +
|
||||
(updatedAt.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'SyncAlbumV1[createdAt=$createdAt, description=$description, id=$id, isActivityEnabled=$isActivityEnabled, name=$name, order=$order, ownerId=$ownerId, thumbnailAssetId=$thumbnailAssetId, updatedAt=$updatedAt]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'createdAt'] = this.createdAt.toUtc().toIso8601String();
|
||||
json[r'description'] = this.description;
|
||||
json[r'id'] = this.id;
|
||||
json[r'isActivityEnabled'] = this.isActivityEnabled;
|
||||
json[r'name'] = this.name;
|
||||
json[r'order'] = this.order;
|
||||
json[r'ownerId'] = this.ownerId;
|
||||
if (this.thumbnailAssetId != null) {
|
||||
json[r'thumbnailAssetId'] = this.thumbnailAssetId;
|
||||
} else {
|
||||
// json[r'thumbnailAssetId'] = null;
|
||||
}
|
||||
json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String();
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [SyncAlbumV1] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static SyncAlbumV1? fromJson(dynamic value) {
|
||||
upgradeDto(value, "SyncAlbumV1");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return SyncAlbumV1(
|
||||
createdAt: mapDateTime(json, r'createdAt', r'')!,
|
||||
description: mapValueOfType<String>(json, r'description')!,
|
||||
id: mapValueOfType<String>(json, r'id')!,
|
||||
isActivityEnabled: mapValueOfType<bool>(json, r'isActivityEnabled')!,
|
||||
name: mapValueOfType<String>(json, r'name')!,
|
||||
order: AssetOrder.fromJson(json[r'order'])!,
|
||||
ownerId: mapValueOfType<String>(json, r'ownerId')!,
|
||||
thumbnailAssetId: mapValueOfType<String>(json, r'thumbnailAssetId'),
|
||||
updatedAt: mapDateTime(json, r'updatedAt', r'')!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<SyncAlbumV1> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <SyncAlbumV1>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = SyncAlbumV1.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, SyncAlbumV1> mapFromJson(dynamic json) {
|
||||
final map = <String, SyncAlbumV1>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = SyncAlbumV1.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of SyncAlbumV1-objects as value to a dart map
|
||||
static Map<String, List<SyncAlbumV1>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<SyncAlbumV1>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = SyncAlbumV1.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'createdAt',
|
||||
'description',
|
||||
'id',
|
||||
'isActivityEnabled',
|
||||
'name',
|
||||
'order',
|
||||
'ownerId',
|
||||
'thumbnailAssetId',
|
||||
'updatedAt',
|
||||
};
|
||||
}
|
||||
|
||||
@ -0,0 +1,96 @@
|
||||
import { Kysely, sql } from 'kysely';
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await sql`CREATE OR REPLACE FUNCTION album_user_after_insert()
|
||||
RETURNS TRIGGER
|
||||
LANGUAGE PLPGSQL
|
||||
AS $$
|
||||
BEGIN
|
||||
UPDATE albums SET "updatedAt" = clock_timestamp(), "updateId" = immich_uuid_v7(clock_timestamp())
|
||||
WHERE "id" IN (SELECT DISTINCT "albumsId" FROM inserted_rows);
|
||||
RETURN NULL;
|
||||
END
|
||||
$$;`.execute(db);
|
||||
await sql`CREATE OR REPLACE FUNCTION albums_delete_audit()
|
||||
RETURNS TRIGGER
|
||||
LANGUAGE PLPGSQL
|
||||
AS $$
|
||||
BEGIN
|
||||
INSERT INTO albums_audit ("albumId", "userId")
|
||||
SELECT "id", "ownerId"
|
||||
FROM OLD;
|
||||
RETURN NULL;
|
||||
END
|
||||
$$;`.execute(db);
|
||||
await sql`CREATE OR REPLACE FUNCTION album_users_delete_audit()
|
||||
RETURNS TRIGGER
|
||||
LANGUAGE PLPGSQL
|
||||
AS $$
|
||||
BEGIN
|
||||
INSERT INTO albums_audit ("albumId", "userId")
|
||||
SELECT "albumsId", "usersId"
|
||||
FROM OLD;
|
||||
|
||||
IF pg_trigger_depth() = 1 THEN
|
||||
INSERT INTO album_users_audit ("albumId", "userId")
|
||||
SELECT "albumsId", "usersId"
|
||||
FROM OLD;
|
||||
END IF;
|
||||
|
||||
RETURN NULL;
|
||||
END
|
||||
$$;`.execute(db);
|
||||
await sql`CREATE TABLE "albums_audit" ("id" uuid NOT NULL DEFAULT immich_uuid_v7(), "albumId" uuid NOT NULL, "userId" uuid NOT NULL, "deletedAt" timestamp with time zone NOT NULL DEFAULT clock_timestamp());`.execute(db);
|
||||
await sql`CREATE TABLE "album_users_audit" ("id" uuid NOT NULL DEFAULT immich_uuid_v7(), "albumId" uuid NOT NULL, "userId" uuid NOT NULL, "deletedAt" timestamp with time zone NOT NULL DEFAULT clock_timestamp());`.execute(db);
|
||||
await sql`ALTER TABLE "albums_audit" ADD CONSTRAINT "PK_c75efea8d4dce316ad29b851a8b" PRIMARY KEY ("id");`.execute(db);
|
||||
await sql`ALTER TABLE "album_users_audit" ADD CONSTRAINT "PK_f479a2e575b7ebc9698362c1688" PRIMARY KEY ("id");`.execute(db);
|
||||
await sql`ALTER TABLE "albums_shared_users_users" ADD "updateId" uuid NOT NULL DEFAULT immich_uuid_v7();`.execute(db);
|
||||
await sql`ALTER TABLE "albums_shared_users_users" ADD "updatedAt" timestamp with time zone NOT NULL DEFAULT now();`.execute(db);
|
||||
await sql`CREATE INDEX "IDX_album_users_update_id" ON "albums_shared_users_users" ("updateId")`.execute(db);
|
||||
await sql`CREATE INDEX "IDX_albums_audit_album_id" ON "albums_audit" ("albumId")`.execute(db);
|
||||
await sql`CREATE INDEX "IDX_albums_audit_user_id" ON "albums_audit" ("userId")`.execute(db);
|
||||
await sql`CREATE INDEX "IDX_albums_audit_deleted_at" ON "albums_audit" ("deletedAt")`.execute(db);
|
||||
await sql`CREATE INDEX "IDX_album_users_audit_album_id" ON "album_users_audit" ("albumId")`.execute(db);
|
||||
await sql`CREATE INDEX "IDX_album_users_audit_user_id" ON "album_users_audit" ("userId")`.execute(db);
|
||||
await sql`CREATE INDEX "IDX_album_users_audit_deleted_at" ON "album_users_audit" ("deletedAt")`.execute(db);
|
||||
await sql`CREATE OR REPLACE TRIGGER "albums_delete_audit"
|
||||
AFTER DELETE ON "albums"
|
||||
REFERENCING OLD TABLE AS "old"
|
||||
FOR EACH STATEMENT
|
||||
WHEN (pg_trigger_depth() = 0)
|
||||
EXECUTE FUNCTION albums_delete_audit();`.execute(db);
|
||||
await sql`CREATE OR REPLACE TRIGGER "album_users_delete_audit"
|
||||
AFTER DELETE ON "albums_shared_users_users"
|
||||
REFERENCING OLD TABLE AS "old"
|
||||
FOR EACH STATEMENT
|
||||
WHEN (pg_trigger_depth() <= 1)
|
||||
EXECUTE FUNCTION album_users_delete_audit();`.execute(db);
|
||||
await sql`CREATE OR REPLACE TRIGGER "album_user_after_insert"
|
||||
AFTER INSERT ON "albums_shared_users_users"
|
||||
REFERENCING NEW TABLE AS "inserted_rows"
|
||||
FOR EACH STATEMENT
|
||||
EXECUTE FUNCTION album_user_after_insert();`.execute(db);
|
||||
await sql`CREATE OR REPLACE TRIGGER "album_users_updated_at"
|
||||
BEFORE UPDATE ON "albums_shared_users_users"
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION updated_at();`.execute(db);
|
||||
}
|
||||
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await sql`DROP TRIGGER "albums_delete_audit" ON "albums";`.execute(db);
|
||||
await sql`DROP TRIGGER "album_users_delete_audit" ON "albums_shared_users_users";`.execute(db);
|
||||
await sql`DROP TRIGGER "album_user_after_insert" ON "albums_shared_users_users";`.execute(db);
|
||||
await sql`DROP INDEX "IDX_albums_audit_album_id";`.execute(db);
|
||||
await sql`DROP INDEX "IDX_albums_audit_user_id";`.execute(db);
|
||||
await sql`DROP INDEX "IDX_albums_audit_deleted_at";`.execute(db);
|
||||
await sql`DROP INDEX "IDX_album_users_audit_album_id";`.execute(db);
|
||||
await sql`DROP INDEX "IDX_album_users_audit_user_id";`.execute(db);
|
||||
await sql`DROP INDEX "IDX_album_users_audit_deleted_at";`.execute(db);
|
||||
await sql`ALTER TABLE "albums_audit" DROP CONSTRAINT "PK_c75efea8d4dce316ad29b851a8b";`.execute(db);
|
||||
await sql`ALTER TABLE "album_users_audit" DROP CONSTRAINT "PK_f479a2e575b7ebc9698362c1688";`.execute(db);
|
||||
await sql`DROP TABLE "albums_audit";`.execute(db);
|
||||
await sql`DROP TABLE "album_users_audit";`.execute(db);
|
||||
await sql`DROP FUNCTION album_user_after_insert;`.execute(db);
|
||||
await sql`DROP FUNCTION albums_delete_audit;`.execute(db);
|
||||
await sql`DROP FUNCTION album_users_delete_audit;`.execute(db);
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
import { PrimaryGeneratedUuidV7Column } from 'src/decorators';
|
||||
import { Column, CreateDateColumn, Table } from 'src/sql-tools';
|
||||
|
||||
@Table('albums_audit')
|
||||
export class AlbumAuditTable {
|
||||
@PrimaryGeneratedUuidV7Column()
|
||||
id!: string;
|
||||
|
||||
@Column({ type: 'uuid', indexName: 'IDX_albums_audit_album_id' })
|
||||
albumId!: string;
|
||||
|
||||
@Column({ type: 'uuid', indexName: 'IDX_albums_audit_user_id' })
|
||||
userId!: string;
|
||||
|
||||
@CreateDateColumn({ default: () => 'clock_timestamp()', indexName: 'IDX_albums_audit_deleted_at' })
|
||||
deletedAt!: Date;
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
import { PrimaryGeneratedUuidV7Column } from 'src/decorators';
|
||||
import { Column, CreateDateColumn, Table } from 'src/sql-tools';
|
||||
|
||||
@Table('album_users_audit')
|
||||
export class AlbumUserAuditTable {
|
||||
@PrimaryGeneratedUuidV7Column()
|
||||
id!: string;
|
||||
|
||||
@Column({ type: 'uuid', indexName: 'IDX_album_users_audit_album_id' })
|
||||
albumId!: string;
|
||||
|
||||
@Column({ type: 'uuid', indexName: 'IDX_album_users_audit_user_id' })
|
||||
userId!: string;
|
||||
|
||||
@CreateDateColumn({ default: () => 'clock_timestamp()', indexName: 'IDX_album_users_audit_deleted_at' })
|
||||
deletedAt!: Date;
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
import { TriggerFunction, TriggerFunctionOptions } from 'src/sql-tools/from-code/decorators/trigger-function.decorator';
|
||||
|
||||
export const AfterInsertTrigger = (options: Omit<TriggerFunctionOptions, 'timing' | 'actions'>) =>
|
||||
TriggerFunction({
|
||||
timing: 'after',
|
||||
actions: ['insert'],
|
||||
...options,
|
||||
});
|
||||
@ -1,910 +0,0 @@
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { SyncEntityType, SyncRequestType } from 'src/enum';
|
||||
import { SYNC_TYPES_ORDER, SyncService } from 'src/services/sync.service';
|
||||
import { mediumFactory, newMediumService } from 'test/medium.factory';
|
||||
import { factory } from 'test/small.factory';
|
||||
import { getKyselyDB } from 'test/utils';
|
||||
|
||||
const setup = async () => {
|
||||
const db = await getKyselyDB();
|
||||
|
||||
const { sut, mocks, repos, getRepository } = newMediumService(SyncService, {
|
||||
database: db,
|
||||
repos: {
|
||||
sync: 'real',
|
||||
session: 'real',
|
||||
},
|
||||
});
|
||||
|
||||
const user = mediumFactory.userInsert();
|
||||
const session = mediumFactory.sessionInsert({ userId: user.id });
|
||||
const auth = factory.auth({
|
||||
session,
|
||||
user: {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
},
|
||||
});
|
||||
|
||||
await getRepository('user').create(user);
|
||||
await getRepository('session').create(session);
|
||||
|
||||
const testSync = async (auth: AuthDto, types: SyncRequestType[]) => {
|
||||
const stream = mediumFactory.syncStream();
|
||||
// Wait for 1ms to ensure all updates are available
|
||||
await new Promise((resolve) => setTimeout(resolve, 1));
|
||||
await sut.stream(auth, stream, { types });
|
||||
|
||||
return stream.getResponse();
|
||||
};
|
||||
|
||||
return {
|
||||
sut,
|
||||
auth,
|
||||
mocks,
|
||||
repos,
|
||||
getRepository,
|
||||
testSync,
|
||||
};
|
||||
};
|
||||
|
||||
describe(SyncService.name, () => {
|
||||
it('should have all the types in the ordering variable', () => {
|
||||
for (const key in SyncRequestType) {
|
||||
expect(SYNC_TYPES_ORDER).includes(key);
|
||||
}
|
||||
|
||||
expect(SYNC_TYPES_ORDER.length).toBe(Object.keys(SyncRequestType).length);
|
||||
});
|
||||
|
||||
describe.concurrent(SyncEntityType.UserV1, () => {
|
||||
it('should detect and sync the first user', async () => {
|
||||
const { auth, sut, getRepository, testSync } = await setup();
|
||||
|
||||
const userRepo = getRepository('user');
|
||||
const user = await userRepo.get(auth.user.id, { withDeleted: false });
|
||||
if (!user) {
|
||||
expect.fail('First user should exist');
|
||||
}
|
||||
|
||||
const initialSyncResponse = await testSync(auth, [SyncRequestType.UsersV1]);
|
||||
expect(initialSyncResponse).toHaveLength(1);
|
||||
expect(initialSyncResponse).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
deletedAt: user.deletedAt,
|
||||
email: user.email,
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
},
|
||||
type: 'UserV1',
|
||||
},
|
||||
]);
|
||||
|
||||
const acks = [initialSyncResponse[0].ack];
|
||||
await sut.setAcks(auth, { acks });
|
||||
const ackSyncResponse = await testSync(auth, [SyncRequestType.UsersV1]);
|
||||
|
||||
expect(ackSyncResponse).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should detect and sync a soft deleted user', async () => {
|
||||
const { auth, sut, getRepository, testSync } = await setup();
|
||||
|
||||
const deletedAt = new Date().toISOString();
|
||||
const deletedUser = mediumFactory.userInsert({ deletedAt });
|
||||
const deleted = await getRepository('user').create(deletedUser);
|
||||
|
||||
const response = await testSync(auth, [SyncRequestType.UsersV1]);
|
||||
|
||||
expect(response).toHaveLength(2);
|
||||
expect(response).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
deletedAt: null,
|
||||
email: auth.user.email,
|
||||
id: auth.user.id,
|
||||
name: auth.user.name,
|
||||
},
|
||||
type: 'UserV1',
|
||||
},
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
deletedAt,
|
||||
email: deleted.email,
|
||||
id: deleted.id,
|
||||
name: deleted.name,
|
||||
},
|
||||
type: 'UserV1',
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
const acks = [response[1].ack];
|
||||
await sut.setAcks(auth, { acks });
|
||||
const ackSyncResponse = await testSync(auth, [SyncRequestType.UsersV1]);
|
||||
|
||||
expect(ackSyncResponse).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should detect and sync a deleted user', async () => {
|
||||
const { auth, sut, getRepository, testSync } = await setup();
|
||||
|
||||
const userRepo = getRepository('user');
|
||||
const user = mediumFactory.userInsert();
|
||||
await userRepo.create(user);
|
||||
await userRepo.delete({ id: user.id }, true);
|
||||
|
||||
const response = await testSync(auth, [SyncRequestType.UsersV1]);
|
||||
|
||||
expect(response).toHaveLength(2);
|
||||
expect(response).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
userId: user.id,
|
||||
},
|
||||
type: 'UserDeleteV1',
|
||||
},
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
deletedAt: null,
|
||||
email: auth.user.email,
|
||||
id: auth.user.id,
|
||||
name: auth.user.name,
|
||||
},
|
||||
type: 'UserV1',
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
const acks = response.map(({ ack }) => ack);
|
||||
await sut.setAcks(auth, { acks });
|
||||
const ackSyncResponse = await testSync(auth, [SyncRequestType.UsersV1]);
|
||||
|
||||
expect(ackSyncResponse).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should sync a user and then an update to that same user', async () => {
|
||||
const { auth, sut, getRepository, testSync } = await setup();
|
||||
|
||||
const initialSyncResponse = await testSync(auth, [SyncRequestType.UsersV1]);
|
||||
|
||||
expect(initialSyncResponse).toHaveLength(1);
|
||||
expect(initialSyncResponse).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
deletedAt: null,
|
||||
email: auth.user.email,
|
||||
id: auth.user.id,
|
||||
name: auth.user.name,
|
||||
},
|
||||
type: 'UserV1',
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
const acks = [initialSyncResponse[0].ack];
|
||||
await sut.setAcks(auth, { acks });
|
||||
|
||||
const userRepo = getRepository('user');
|
||||
const updated = await userRepo.update(auth.user.id, { name: 'new name' });
|
||||
const updatedSyncResponse = await testSync(auth, [SyncRequestType.UsersV1]);
|
||||
|
||||
expect(updatedSyncResponse).toHaveLength(1);
|
||||
expect(updatedSyncResponse).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
deletedAt: null,
|
||||
email: auth.user.email,
|
||||
id: auth.user.id,
|
||||
name: updated.name,
|
||||
},
|
||||
type: 'UserV1',
|
||||
},
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe.concurrent(SyncEntityType.PartnerV1, () => {
|
||||
it('should detect and sync the first partner', async () => {
|
||||
const { auth, sut, getRepository, testSync } = await setup();
|
||||
|
||||
const user1 = auth.user;
|
||||
const userRepo = getRepository('user');
|
||||
const partnerRepo = getRepository('partner');
|
||||
|
||||
const user2 = mediumFactory.userInsert();
|
||||
await userRepo.create(user2);
|
||||
|
||||
const partner = await partnerRepo.create({ sharedById: user2.id, sharedWithId: user1.id });
|
||||
|
||||
const initialSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]);
|
||||
|
||||
expect(initialSyncResponse).toHaveLength(1);
|
||||
expect(initialSyncResponse).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
inTimeline: partner.inTimeline,
|
||||
sharedById: partner.sharedById,
|
||||
sharedWithId: partner.sharedWithId,
|
||||
},
|
||||
type: 'PartnerV1',
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
const acks = [initialSyncResponse[0].ack];
|
||||
await sut.setAcks(auth, { acks });
|
||||
|
||||
const ackSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]);
|
||||
|
||||
expect(ackSyncResponse).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should detect and sync a deleted partner', async () => {
|
||||
const { auth, sut, getRepository, testSync } = await setup();
|
||||
|
||||
const userRepo = getRepository('user');
|
||||
const user1 = auth.user;
|
||||
const user2 = mediumFactory.userInsert();
|
||||
await userRepo.create(user2);
|
||||
|
||||
const partnerRepo = getRepository('partner');
|
||||
const partner = await partnerRepo.create({ sharedById: user2.id, sharedWithId: user1.id });
|
||||
await partnerRepo.remove(partner);
|
||||
|
||||
const response = await testSync(auth, [SyncRequestType.PartnersV1]);
|
||||
|
||||
expect(response).toHaveLength(1);
|
||||
expect(response).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
sharedById: partner.sharedById,
|
||||
sharedWithId: partner.sharedWithId,
|
||||
},
|
||||
type: 'PartnerDeleteV1',
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
const acks = response.map(({ ack }) => ack);
|
||||
await sut.setAcks(auth, { acks });
|
||||
|
||||
const ackSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]);
|
||||
|
||||
expect(ackSyncResponse).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should detect and sync a partner share both to and from another user', async () => {
|
||||
const { auth, sut, getRepository, testSync } = await setup();
|
||||
|
||||
const userRepo = getRepository('user');
|
||||
const user1 = auth.user;
|
||||
const user2 = await userRepo.create(mediumFactory.userInsert());
|
||||
|
||||
const partnerRepo = getRepository('partner');
|
||||
const partner1 = await partnerRepo.create({ sharedById: user2.id, sharedWithId: user1.id });
|
||||
const partner2 = await partnerRepo.create({ sharedById: user1.id, sharedWithId: user2.id });
|
||||
|
||||
const response = await testSync(auth, [SyncRequestType.PartnersV1]);
|
||||
|
||||
expect(response).toHaveLength(2);
|
||||
expect(response).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
inTimeline: partner1.inTimeline,
|
||||
sharedById: partner1.sharedById,
|
||||
sharedWithId: partner1.sharedWithId,
|
||||
},
|
||||
type: 'PartnerV1',
|
||||
},
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
inTimeline: partner2.inTimeline,
|
||||
sharedById: partner2.sharedById,
|
||||
sharedWithId: partner2.sharedWithId,
|
||||
},
|
||||
type: 'PartnerV1',
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
await sut.setAcks(auth, { acks: [response[1].ack] });
|
||||
|
||||
const ackSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]);
|
||||
|
||||
expect(ackSyncResponse).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should sync a partner and then an update to that same partner', async () => {
|
||||
const { auth, sut, getRepository, testSync } = await setup();
|
||||
|
||||
const userRepo = getRepository('user');
|
||||
const user1 = auth.user;
|
||||
const user2 = await userRepo.create(mediumFactory.userInsert());
|
||||
|
||||
const partnerRepo = getRepository('partner');
|
||||
const partner = await partnerRepo.create({ sharedById: user2.id, sharedWithId: user1.id });
|
||||
|
||||
const initialSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]);
|
||||
|
||||
expect(initialSyncResponse).toHaveLength(1);
|
||||
expect(initialSyncResponse).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
inTimeline: partner.inTimeline,
|
||||
sharedById: partner.sharedById,
|
||||
sharedWithId: partner.sharedWithId,
|
||||
},
|
||||
type: 'PartnerV1',
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
const acks = [initialSyncResponse[0].ack];
|
||||
await sut.setAcks(auth, { acks });
|
||||
|
||||
const updated = await partnerRepo.update(
|
||||
{ sharedById: partner.sharedById, sharedWithId: partner.sharedWithId },
|
||||
{ inTimeline: true },
|
||||
);
|
||||
|
||||
const updatedSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]);
|
||||
|
||||
expect(updatedSyncResponse).toHaveLength(1);
|
||||
expect(updatedSyncResponse).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
inTimeline: updated.inTimeline,
|
||||
sharedById: updated.sharedById,
|
||||
sharedWithId: updated.sharedWithId,
|
||||
},
|
||||
type: 'PartnerV1',
|
||||
},
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should not sync a partner or partner delete for an unrelated user', async () => {
|
||||
const { auth, getRepository, testSync } = await setup();
|
||||
|
||||
const userRepo = getRepository('user');
|
||||
const user2 = await userRepo.create(mediumFactory.userInsert());
|
||||
const user3 = await userRepo.create(mediumFactory.userInsert());
|
||||
|
||||
const partnerRepo = getRepository('partner');
|
||||
const partner = await partnerRepo.create({ sharedById: user2.id, sharedWithId: user3.id });
|
||||
|
||||
expect(await testSync(auth, [SyncRequestType.PartnersV1])).toHaveLength(0);
|
||||
|
||||
await partnerRepo.remove(partner);
|
||||
|
||||
expect(await testSync(auth, [SyncRequestType.PartnersV1])).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should not sync a partner delete after a user is deleted', async () => {
|
||||
const { auth, getRepository, testSync } = await setup();
|
||||
|
||||
const userRepo = getRepository('user');
|
||||
const user2 = await userRepo.create(mediumFactory.userInsert());
|
||||
|
||||
const partnerRepo = getRepository('partner');
|
||||
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||
await userRepo.delete({ id: user2.id }, true);
|
||||
|
||||
expect(await testSync(auth, [SyncRequestType.PartnersV1])).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe.concurrent(SyncEntityType.AssetV1, () => {
|
||||
it('should detect and sync the first asset', async () => {
|
||||
const { auth, sut, getRepository, testSync } = await setup();
|
||||
|
||||
const checksum = '1115vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA=';
|
||||
const thumbhash = '2225vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA=';
|
||||
const date = new Date().toISOString();
|
||||
|
||||
const assetRepo = getRepository('asset');
|
||||
const asset = mediumFactory.assetInsert({
|
||||
ownerId: auth.user.id,
|
||||
checksum: Buffer.from(checksum, 'base64'),
|
||||
thumbhash: Buffer.from(thumbhash, 'base64'),
|
||||
fileCreatedAt: date,
|
||||
fileModifiedAt: date,
|
||||
localDateTime: date,
|
||||
deletedAt: null,
|
||||
});
|
||||
await assetRepo.create(asset);
|
||||
|
||||
const initialSyncResponse = await testSync(auth, [SyncRequestType.AssetsV1]);
|
||||
|
||||
expect(initialSyncResponse).toHaveLength(1);
|
||||
expect(initialSyncResponse).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
id: asset.id,
|
||||
ownerId: asset.ownerId,
|
||||
thumbhash,
|
||||
checksum,
|
||||
deletedAt: asset.deletedAt,
|
||||
fileCreatedAt: asset.fileCreatedAt,
|
||||
fileModifiedAt: asset.fileModifiedAt,
|
||||
isFavorite: asset.isFavorite,
|
||||
localDateTime: asset.localDateTime,
|
||||
type: asset.type,
|
||||
visibility: asset.visibility,
|
||||
},
|
||||
type: 'AssetV1',
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
const acks = [initialSyncResponse[0].ack];
|
||||
await sut.setAcks(auth, { acks });
|
||||
|
||||
const ackSyncResponse = await testSync(auth, [SyncRequestType.AssetsV1]);
|
||||
|
||||
expect(ackSyncResponse).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should detect and sync a deleted asset', async () => {
|
||||
const { auth, sut, getRepository, testSync } = await setup();
|
||||
|
||||
const assetRepo = getRepository('asset');
|
||||
const asset = mediumFactory.assetInsert({ ownerId: auth.user.id });
|
||||
await assetRepo.create(asset);
|
||||
await assetRepo.remove(asset);
|
||||
|
||||
const response = await testSync(auth, [SyncRequestType.AssetsV1]);
|
||||
|
||||
expect(response).toHaveLength(1);
|
||||
expect(response).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
assetId: asset.id,
|
||||
},
|
||||
type: 'AssetDeleteV1',
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
const acks = response.map(({ ack }) => ack);
|
||||
await sut.setAcks(auth, { acks });
|
||||
|
||||
const ackSyncResponse = await testSync(auth, [SyncRequestType.AssetsV1]);
|
||||
|
||||
expect(ackSyncResponse).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should not sync an asset or asset delete for an unrelated user', async () => {
|
||||
const { auth, getRepository, testSync } = await setup();
|
||||
|
||||
const userRepo = getRepository('user');
|
||||
const user2 = mediumFactory.userInsert();
|
||||
await userRepo.create(user2);
|
||||
|
||||
const sessionRepo = getRepository('session');
|
||||
const session = mediumFactory.sessionInsert({ userId: user2.id });
|
||||
await sessionRepo.create(session);
|
||||
|
||||
const assetRepo = getRepository('asset');
|
||||
const asset = mediumFactory.assetInsert({ ownerId: user2.id });
|
||||
await assetRepo.create(asset);
|
||||
|
||||
const auth2 = factory.auth({ session, user: user2 });
|
||||
|
||||
expect(await testSync(auth2, [SyncRequestType.AssetsV1])).toHaveLength(1);
|
||||
expect(await testSync(auth, [SyncRequestType.AssetsV1])).toHaveLength(0);
|
||||
|
||||
await assetRepo.remove(asset);
|
||||
expect(await testSync(auth2, [SyncRequestType.AssetsV1])).toHaveLength(1);
|
||||
expect(await testSync(auth, [SyncRequestType.AssetsV1])).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe.concurrent(SyncRequestType.PartnerAssetsV1, () => {
|
||||
it('should detect and sync the first partner asset', async () => {
|
||||
const { auth, sut, getRepository, testSync } = await setup();
|
||||
|
||||
const checksum = '1115vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA=';
|
||||
const thumbhash = '2225vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA=';
|
||||
const date = new Date().toISOString();
|
||||
|
||||
const userRepo = getRepository('user');
|
||||
const user2 = mediumFactory.userInsert();
|
||||
await userRepo.create(user2);
|
||||
|
||||
const assetRepo = getRepository('asset');
|
||||
const asset = mediumFactory.assetInsert({
|
||||
ownerId: user2.id,
|
||||
checksum: Buffer.from(checksum, 'base64'),
|
||||
thumbhash: Buffer.from(thumbhash, 'base64'),
|
||||
fileCreatedAt: date,
|
||||
fileModifiedAt: date,
|
||||
localDateTime: date,
|
||||
deletedAt: null,
|
||||
});
|
||||
await assetRepo.create(asset);
|
||||
|
||||
const partnerRepo = getRepository('partner');
|
||||
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||
|
||||
const initialSyncResponse = await testSync(auth, [SyncRequestType.PartnerAssetsV1]);
|
||||
|
||||
expect(initialSyncResponse).toHaveLength(1);
|
||||
expect(initialSyncResponse).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
id: asset.id,
|
||||
ownerId: asset.ownerId,
|
||||
thumbhash,
|
||||
checksum,
|
||||
deletedAt: null,
|
||||
fileCreatedAt: date,
|
||||
fileModifiedAt: date,
|
||||
isFavorite: false,
|
||||
localDateTime: date,
|
||||
type: asset.type,
|
||||
visibility: asset.visibility,
|
||||
},
|
||||
type: SyncEntityType.PartnerAssetV1,
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
const acks = [initialSyncResponse[0].ack];
|
||||
await sut.setAcks(auth, { acks });
|
||||
|
||||
const ackSyncResponse = await testSync(auth, [SyncRequestType.PartnerAssetsV1]);
|
||||
|
||||
expect(ackSyncResponse).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should detect and sync a deleted partner asset', async () => {
|
||||
const { auth, sut, getRepository, testSync } = await setup();
|
||||
|
||||
const userRepo = getRepository('user');
|
||||
const user2 = mediumFactory.userInsert();
|
||||
await userRepo.create(user2);
|
||||
const asset = mediumFactory.assetInsert({ ownerId: user2.id });
|
||||
|
||||
const assetRepo = getRepository('asset');
|
||||
await assetRepo.create(asset);
|
||||
|
||||
const partnerRepo = getRepository('partner');
|
||||
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||
await assetRepo.remove(asset);
|
||||
|
||||
const response = await testSync(auth, [SyncRequestType.PartnerAssetsV1]);
|
||||
|
||||
expect(response).toHaveLength(1);
|
||||
expect(response).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
assetId: asset.id,
|
||||
},
|
||||
type: SyncEntityType.PartnerAssetDeleteV1,
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
const acks = response.map(({ ack }) => ack);
|
||||
await sut.setAcks(auth, { acks });
|
||||
|
||||
const ackSyncResponse = await testSync(auth, [SyncRequestType.PartnerAssetsV1]);
|
||||
|
||||
expect(ackSyncResponse).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should not sync a deleted partner asset due to a user delete', async () => {
|
||||
const { auth, getRepository, testSync } = await setup();
|
||||
|
||||
const userRepo = getRepository('user');
|
||||
const user2 = mediumFactory.userInsert();
|
||||
await userRepo.create(user2);
|
||||
|
||||
const partnerRepo = getRepository('partner');
|
||||
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||
|
||||
const assetRepo = getRepository('asset');
|
||||
await assetRepo.create(mediumFactory.assetInsert({ ownerId: user2.id }));
|
||||
|
||||
await userRepo.delete({ id: user2.id }, true);
|
||||
|
||||
const response = await testSync(auth, [SyncRequestType.PartnerAssetsV1]);
|
||||
expect(response).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should not sync a deleted partner asset due to a partner delete (unshare)', async () => {
|
||||
const { auth, getRepository, testSync } = await setup();
|
||||
|
||||
const userRepo = getRepository('user');
|
||||
const user2 = mediumFactory.userInsert();
|
||||
await userRepo.create(user2);
|
||||
|
||||
const assetRepo = getRepository('asset');
|
||||
await assetRepo.create(mediumFactory.assetInsert({ ownerId: user2.id }));
|
||||
|
||||
const partnerRepo = getRepository('partner');
|
||||
const partner = { sharedById: user2.id, sharedWithId: auth.user.id };
|
||||
await partnerRepo.create(partner);
|
||||
|
||||
await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(1);
|
||||
|
||||
await partnerRepo.remove(partner);
|
||||
|
||||
await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should not sync an asset or asset delete for own user', async () => {
|
||||
const { auth, getRepository, testSync } = await setup();
|
||||
|
||||
const userRepo = getRepository('user');
|
||||
const user2 = mediumFactory.userInsert();
|
||||
await userRepo.create(user2);
|
||||
|
||||
const assetRepo = getRepository('asset');
|
||||
const asset = mediumFactory.assetInsert({ ownerId: auth.user.id });
|
||||
await assetRepo.create(asset);
|
||||
|
||||
const partnerRepo = getRepository('partner');
|
||||
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||
|
||||
await expect(testSync(auth, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1);
|
||||
await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0);
|
||||
|
||||
await assetRepo.remove(asset);
|
||||
|
||||
await expect(testSync(auth, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1);
|
||||
await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should not sync an asset or asset delete for unrelated user', async () => {
|
||||
const { auth, getRepository, testSync } = await setup();
|
||||
|
||||
const userRepo = getRepository('user');
|
||||
const user2 = mediumFactory.userInsert();
|
||||
await userRepo.create(user2);
|
||||
|
||||
const sessionRepo = getRepository('session');
|
||||
const session = mediumFactory.sessionInsert({ userId: user2.id });
|
||||
await sessionRepo.create(session);
|
||||
|
||||
const auth2 = factory.auth({ session, user: user2 });
|
||||
|
||||
const assetRepo = getRepository('asset');
|
||||
const asset = mediumFactory.assetInsert({ ownerId: user2.id });
|
||||
await assetRepo.create(asset);
|
||||
|
||||
await expect(testSync(auth2, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1);
|
||||
await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0);
|
||||
|
||||
await assetRepo.remove(asset);
|
||||
|
||||
await expect(testSync(auth2, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1);
|
||||
await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe.concurrent(SyncRequestType.AssetExifsV1, () => {
|
||||
it('should detect and sync the first asset exif', async () => {
|
||||
const { auth, sut, getRepository, testSync } = await setup();
|
||||
|
||||
const assetRepo = getRepository('asset');
|
||||
const asset = mediumFactory.assetInsert({ ownerId: auth.user.id });
|
||||
await assetRepo.create(asset);
|
||||
await assetRepo.upsertExif({ assetId: asset.id, make: 'Canon' });
|
||||
|
||||
const initialSyncResponse = await testSync(auth, [SyncRequestType.AssetExifsV1]);
|
||||
|
||||
expect(initialSyncResponse).toHaveLength(1);
|
||||
expect(initialSyncResponse).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
assetId: asset.id,
|
||||
city: null,
|
||||
country: null,
|
||||
dateTimeOriginal: null,
|
||||
description: '',
|
||||
exifImageHeight: null,
|
||||
exifImageWidth: null,
|
||||
exposureTime: null,
|
||||
fNumber: null,
|
||||
fileSizeInByte: null,
|
||||
focalLength: null,
|
||||
fps: null,
|
||||
iso: null,
|
||||
latitude: null,
|
||||
lensModel: null,
|
||||
longitude: null,
|
||||
make: 'Canon',
|
||||
model: null,
|
||||
modifyDate: null,
|
||||
orientation: null,
|
||||
profileDescription: null,
|
||||
projectionType: null,
|
||||
rating: null,
|
||||
state: null,
|
||||
timeZone: null,
|
||||
},
|
||||
type: SyncEntityType.AssetExifV1,
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
const acks = [initialSyncResponse[0].ack];
|
||||
await sut.setAcks(auth, { acks });
|
||||
|
||||
const ackSyncResponse = await testSync(auth, [SyncRequestType.AssetExifsV1]);
|
||||
|
||||
expect(ackSyncResponse).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should only sync asset exif for own user', async () => {
|
||||
const { auth, getRepository, testSync } = await setup();
|
||||
|
||||
const userRepo = getRepository('user');
|
||||
const user2 = mediumFactory.userInsert();
|
||||
await userRepo.create(user2);
|
||||
|
||||
const partnerRepo = getRepository('partner');
|
||||
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||
|
||||
const assetRepo = getRepository('asset');
|
||||
const asset = mediumFactory.assetInsert({ ownerId: user2.id });
|
||||
await assetRepo.create(asset);
|
||||
await assetRepo.upsertExif({ assetId: asset.id, make: 'Canon' });
|
||||
|
||||
const sessionRepo = getRepository('session');
|
||||
const session = mediumFactory.sessionInsert({ userId: user2.id });
|
||||
await sessionRepo.create(session);
|
||||
|
||||
const auth2 = factory.auth({ session, user: user2 });
|
||||
await expect(testSync(auth2, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(1);
|
||||
await expect(testSync(auth, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe.concurrent(SyncRequestType.PartnerAssetExifsV1, () => {
|
||||
it('should detect and sync the first partner asset exif', async () => {
|
||||
const { auth, sut, getRepository, testSync } = await setup();
|
||||
|
||||
const userRepo = getRepository('user');
|
||||
const user2 = mediumFactory.userInsert();
|
||||
await userRepo.create(user2);
|
||||
|
||||
const partnerRepo = getRepository('partner');
|
||||
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||
|
||||
const assetRepo = getRepository('asset');
|
||||
const asset = mediumFactory.assetInsert({ ownerId: user2.id });
|
||||
await assetRepo.create(asset);
|
||||
await assetRepo.upsertExif({ assetId: asset.id, make: 'Canon' });
|
||||
|
||||
const initialSyncResponse = await testSync(auth, [SyncRequestType.PartnerAssetExifsV1]);
|
||||
|
||||
expect(initialSyncResponse).toHaveLength(1);
|
||||
expect(initialSyncResponse).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
assetId: asset.id,
|
||||
city: null,
|
||||
country: null,
|
||||
dateTimeOriginal: null,
|
||||
description: '',
|
||||
exifImageHeight: null,
|
||||
exifImageWidth: null,
|
||||
exposureTime: null,
|
||||
fNumber: null,
|
||||
fileSizeInByte: null,
|
||||
focalLength: null,
|
||||
fps: null,
|
||||
iso: null,
|
||||
latitude: null,
|
||||
lensModel: null,
|
||||
longitude: null,
|
||||
make: 'Canon',
|
||||
model: null,
|
||||
modifyDate: null,
|
||||
orientation: null,
|
||||
profileDescription: null,
|
||||
projectionType: null,
|
||||
rating: null,
|
||||
state: null,
|
||||
timeZone: null,
|
||||
},
|
||||
type: SyncEntityType.PartnerAssetExifV1,
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
const acks = [initialSyncResponse[0].ack];
|
||||
await sut.setAcks(auth, { acks });
|
||||
|
||||
const ackSyncResponse = await testSync(auth, [SyncRequestType.PartnerAssetExifsV1]);
|
||||
|
||||
expect(ackSyncResponse).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should not sync partner asset exif for own user', async () => {
|
||||
const { auth, getRepository, testSync } = await setup();
|
||||
|
||||
const userRepo = getRepository('user');
|
||||
const user2 = mediumFactory.userInsert();
|
||||
await userRepo.create(user2);
|
||||
|
||||
const partnerRepo = getRepository('partner');
|
||||
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||
|
||||
const assetRepo = getRepository('asset');
|
||||
const asset = mediumFactory.assetInsert({ ownerId: auth.user.id });
|
||||
await assetRepo.create(asset);
|
||||
await assetRepo.upsertExif({ assetId: asset.id, make: 'Canon' });
|
||||
|
||||
await expect(testSync(auth, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(1);
|
||||
await expect(testSync(auth, [SyncRequestType.PartnerAssetExifsV1])).resolves.toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should not sync partner asset exif for unrelated user', async () => {
|
||||
const { auth, getRepository, testSync } = await setup();
|
||||
|
||||
const userRepo = getRepository('user');
|
||||
|
||||
const user2 = mediumFactory.userInsert();
|
||||
const user3 = mediumFactory.userInsert();
|
||||
await Promise.all([userRepo.create(user2), userRepo.create(user3)]);
|
||||
|
||||
const partnerRepo = getRepository('partner');
|
||||
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||
|
||||
const assetRepo = getRepository('asset');
|
||||
const asset = mediumFactory.assetInsert({ ownerId: user3.id });
|
||||
await assetRepo.create(asset);
|
||||
await assetRepo.upsertExif({ assetId: asset.id, make: 'Canon' });
|
||||
|
||||
const sessionRepo = getRepository('session');
|
||||
const session = mediumFactory.sessionInsert({ userId: user3.id });
|
||||
await sessionRepo.create(session);
|
||||
|
||||
const authUser3 = factory.auth({ session, user: user3 });
|
||||
await expect(testSync(authUser3, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(1);
|
||||
await expect(testSync(auth, [SyncRequestType.PartnerAssetExifsV1])).resolves.toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,269 @@
|
||||
import { Kysely } from 'kysely';
|
||||
import { DB } from 'src/db';
|
||||
import { AlbumUserRole, SyncEntityType, SyncRequestType } from 'src/enum';
|
||||
import { mediumFactory, newSyncAuthUser, newSyncTest } from 'test/medium.factory';
|
||||
import { getKyselyDB } from 'test/utils';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
const setup = async (db?: Kysely<DB>) => {
|
||||
const database = db || defaultDatabase;
|
||||
const result = newSyncTest({ db: database });
|
||||
const { auth, create } = newSyncAuthUser();
|
||||
await create(database);
|
||||
return { ...result, auth };
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
defaultDatabase = await getKyselyDB();
|
||||
});
|
||||
|
||||
describe(SyncRequestType.AlbumUsersV1, () => {
|
||||
it('should sync an album user with the correct properties', async () => {
|
||||
const { auth, getRepository, testSync } = await setup();
|
||||
|
||||
const albumRepo = getRepository('album');
|
||||
const albumUserRepo = getRepository('albumUser');
|
||||
const userRepo = getRepository('user');
|
||||
|
||||
const album = mediumFactory.albumInsert({ ownerId: auth.user.id });
|
||||
await albumRepo.create(album, [], []);
|
||||
|
||||
const user = mediumFactory.userInsert();
|
||||
await userRepo.create(user);
|
||||
|
||||
const albumUser = { albumsId: album.id, usersId: user.id, role: AlbumUserRole.EDITOR };
|
||||
await albumUserRepo.create(albumUser);
|
||||
|
||||
await expect(testSync(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({
|
||||
albumId: albumUser.albumsId,
|
||||
role: albumUser.role,
|
||||
userId: albumUser.usersId,
|
||||
}),
|
||||
type: SyncEntityType.AlbumUserV1,
|
||||
},
|
||||
]);
|
||||
});
|
||||
describe('owner', () => {
|
||||
it('should detect and sync a new shared user', async () => {
|
||||
const { auth, testSync, getRepository } = await setup();
|
||||
|
||||
const albumRepo = getRepository('album');
|
||||
const albumUserRepo = getRepository('albumUser');
|
||||
const userRepo = getRepository('user');
|
||||
|
||||
const user1 = mediumFactory.userInsert();
|
||||
await userRepo.create(user1);
|
||||
|
||||
const album = mediumFactory.albumInsert({ ownerId: auth.user.id });
|
||||
await albumRepo.create(album, [], []);
|
||||
|
||||
const albumUser = { albumsId: album.id, usersId: user1.id, role: AlbumUserRole.EDITOR };
|
||||
await albumUserRepo.create(albumUser);
|
||||
|
||||
await expect(testSync(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({
|
||||
albumId: albumUser.albumsId,
|
||||
role: albumUser.role,
|
||||
userId: albumUser.usersId,
|
||||
}),
|
||||
type: SyncEntityType.AlbumUserV1,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should detect and sync an updated shared user', async () => {
|
||||
const { auth, testSync, getRepository, sut } = await setup();
|
||||
|
||||
const albumRepo = getRepository('album');
|
||||
const albumUserRepo = getRepository('albumUser');
|
||||
const userRepo = getRepository('user');
|
||||
|
||||
const user1 = mediumFactory.userInsert();
|
||||
await userRepo.create(user1);
|
||||
|
||||
const album = mediumFactory.albumInsert({ ownerId: auth.user.id });
|
||||
await albumRepo.create(album, [], []);
|
||||
|
||||
const albumUser = { albumsId: album.id, usersId: user1.id, role: AlbumUserRole.EDITOR };
|
||||
await albumUserRepo.create(albumUser);
|
||||
|
||||
const initialSyncResponse = await testSync(auth, [SyncRequestType.AlbumUsersV1]);
|
||||
const acks = [initialSyncResponse[0].ack];
|
||||
await sut.setAcks(auth, { acks });
|
||||
|
||||
await expect(testSync(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([]);
|
||||
|
||||
await albumUserRepo.update({ albumsId: album.id, usersId: user1.id }, { role: AlbumUserRole.VIEWER });
|
||||
|
||||
await expect(testSync(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({
|
||||
albumId: albumUser.albumsId,
|
||||
role: AlbumUserRole.VIEWER,
|
||||
userId: albumUser.usersId,
|
||||
}),
|
||||
type: SyncEntityType.AlbumUserV1,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should detect and sync a deleted shared user', async () => {
|
||||
const { auth, testSync, getRepository, sut } = await setup();
|
||||
|
||||
const albumRepo = getRepository('album');
|
||||
const albumUserRepo = getRepository('albumUser');
|
||||
const userRepo = getRepository('user');
|
||||
|
||||
const user1 = mediumFactory.userInsert();
|
||||
await userRepo.create(user1);
|
||||
|
||||
const album = mediumFactory.albumInsert({ ownerId: auth.user.id });
|
||||
await albumRepo.create(album, [], []);
|
||||
|
||||
const albumUser = { albumsId: album.id, usersId: user1.id, role: AlbumUserRole.EDITOR };
|
||||
await albumUserRepo.create(albumUser);
|
||||
|
||||
const initialSyncResponse = await testSync(auth, [SyncRequestType.AlbumUsersV1]);
|
||||
const acks = [initialSyncResponse[0].ack];
|
||||
await sut.setAcks(auth, { acks });
|
||||
|
||||
await expect(testSync(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([]);
|
||||
|
||||
await albumUserRepo.delete({ albumsId: album.id, usersId: user1.id });
|
||||
|
||||
await expect(testSync(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({
|
||||
albumId: albumUser.albumsId,
|
||||
userId: albumUser.usersId,
|
||||
}),
|
||||
type: SyncEntityType.AlbumUserDeleteV1,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('shared user', () => {
|
||||
it('should detect and sync a new shared user', async () => {
|
||||
const { auth, testSync, getRepository } = await setup();
|
||||
|
||||
const albumRepo = getRepository('album');
|
||||
const albumUserRepo = getRepository('albumUser');
|
||||
const userRepo = getRepository('user');
|
||||
|
||||
const user1 = mediumFactory.userInsert();
|
||||
await userRepo.create(user1);
|
||||
|
||||
const album = mediumFactory.albumInsert({ ownerId: user1.id });
|
||||
await albumRepo.create(album, [], []);
|
||||
|
||||
const albumUser = { albumsId: album.id, usersId: auth.user.id, role: AlbumUserRole.EDITOR };
|
||||
await albumUserRepo.create(albumUser);
|
||||
|
||||
await expect(testSync(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({
|
||||
albumId: albumUser.albumsId,
|
||||
role: albumUser.role,
|
||||
userId: albumUser.usersId,
|
||||
}),
|
||||
type: SyncEntityType.AlbumUserV1,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should detect and sync an updated shared user', async () => {
|
||||
const { auth, testSync, getRepository, sut } = await setup();
|
||||
|
||||
const albumRepo = getRepository('album');
|
||||
const albumUserRepo = getRepository('albumUser');
|
||||
const userRepo = getRepository('user');
|
||||
|
||||
const owner = mediumFactory.userInsert();
|
||||
const user = mediumFactory.userInsert();
|
||||
await Promise.all([userRepo.create(owner), userRepo.create(user)]);
|
||||
|
||||
const album = mediumFactory.albumInsert({ ownerId: owner.id });
|
||||
await albumRepo.create(
|
||||
album,
|
||||
[],
|
||||
[
|
||||
{ userId: auth.user.id, role: AlbumUserRole.EDITOR },
|
||||
{ userId: user.id, role: AlbumUserRole.EDITOR },
|
||||
],
|
||||
);
|
||||
|
||||
const initialSyncResponse = await testSync(auth, [SyncRequestType.AlbumUsersV1]);
|
||||
expect(initialSyncResponse).toHaveLength(2);
|
||||
const acks = [initialSyncResponse[1].ack];
|
||||
await sut.setAcks(auth, { acks });
|
||||
|
||||
await expect(testSync(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([]);
|
||||
|
||||
await albumUserRepo.update({ albumsId: album.id, usersId: user.id }, { role: AlbumUserRole.VIEWER });
|
||||
|
||||
await expect(testSync(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({
|
||||
albumId: album.id,
|
||||
role: AlbumUserRole.VIEWER,
|
||||
userId: user.id,
|
||||
}),
|
||||
type: SyncEntityType.AlbumUserV1,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should detect and sync a deleted shared user', async () => {
|
||||
const { auth, testSync, getRepository, sut } = await setup();
|
||||
|
||||
const albumRepo = getRepository('album');
|
||||
const albumUserRepo = getRepository('albumUser');
|
||||
const userRepo = getRepository('user');
|
||||
|
||||
const owner = mediumFactory.userInsert();
|
||||
const user = mediumFactory.userInsert();
|
||||
await Promise.all([userRepo.create(owner), userRepo.create(user)]);
|
||||
|
||||
const album = mediumFactory.albumInsert({ ownerId: owner.id });
|
||||
await albumRepo.create(
|
||||
album,
|
||||
[],
|
||||
[
|
||||
{ userId: auth.user.id, role: AlbumUserRole.EDITOR },
|
||||
{ userId: user.id, role: AlbumUserRole.EDITOR },
|
||||
],
|
||||
);
|
||||
|
||||
const initialSyncResponse = await testSync(auth, [SyncRequestType.AlbumUsersV1]);
|
||||
expect(initialSyncResponse).toHaveLength(2);
|
||||
const acks = [initialSyncResponse[1].ack];
|
||||
await sut.setAcks(auth, { acks });
|
||||
|
||||
await expect(testSync(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([]);
|
||||
|
||||
await albumUserRepo.delete({ albumsId: album.id, usersId: user.id });
|
||||
|
||||
await expect(testSync(auth, [SyncRequestType.AlbumUsersV1])).resolves.toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({
|
||||
albumId: album.id,
|
||||
userId: user.id,
|
||||
}),
|
||||
type: SyncEntityType.AlbumUserDeleteV1,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,220 @@
|
||||
import { Kysely } from 'kysely';
|
||||
import { DB } from 'src/db';
|
||||
import { AlbumUserRole, SyncEntityType, SyncRequestType } from 'src/enum';
|
||||
import { mediumFactory, newSyncAuthUser, newSyncTest } from 'test/medium.factory';
|
||||
import { getKyselyDB } from 'test/utils';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
const setup = async (db?: Kysely<DB>) => {
|
||||
const database = db || defaultDatabase;
|
||||
const result = newSyncTest({ db: database });
|
||||
const { auth, create } = newSyncAuthUser();
|
||||
await create(database);
|
||||
return { ...result, auth };
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
defaultDatabase = await getKyselyDB();
|
||||
});
|
||||
|
||||
describe(SyncRequestType.AlbumsV1, () => {
|
||||
it('should sync an album with the correct properties', async () => {
|
||||
const { auth, getRepository, testSync } = await setup();
|
||||
const albumRepo = getRepository('album');
|
||||
const album = mediumFactory.albumInsert({ ownerId: auth.user.id });
|
||||
await albumRepo.create(album, [], []);
|
||||
await expect(testSync(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({
|
||||
id: album.id,
|
||||
name: album.albumName,
|
||||
ownerId: album.ownerId,
|
||||
}),
|
||||
type: SyncEntityType.AlbumV1,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should detect and sync a new album', async () => {
|
||||
const { auth, getRepository, testSync } = await setup();
|
||||
const albumRepo = getRepository('album');
|
||||
const album = mediumFactory.albumInsert({ ownerId: auth.user.id });
|
||||
await albumRepo.create(album, [], []);
|
||||
await expect(testSync(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({
|
||||
id: album.id,
|
||||
}),
|
||||
type: SyncEntityType.AlbumV1,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should detect and sync an album delete', async () => {
|
||||
const { auth, getRepository, testSync } = await setup();
|
||||
const albumRepo = getRepository('album');
|
||||
const album = mediumFactory.albumInsert({ ownerId: auth.user.id });
|
||||
await albumRepo.create(album, [], []);
|
||||
await expect(testSync(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({
|
||||
id: album.id,
|
||||
}),
|
||||
type: SyncEntityType.AlbumV1,
|
||||
},
|
||||
]);
|
||||
|
||||
await albumRepo.delete(album.id);
|
||||
await expect(testSync(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
albumId: album.id,
|
||||
},
|
||||
type: SyncEntityType.AlbumDeleteV1,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
describe('shared albums', () => {
|
||||
it('should detect and sync an album create', async () => {
|
||||
const { auth, getRepository, testSync } = await setup();
|
||||
const albumRepo = getRepository('album');
|
||||
const userRepo = getRepository('user');
|
||||
|
||||
const user2 = mediumFactory.userInsert();
|
||||
await userRepo.create(user2);
|
||||
|
||||
const album = mediumFactory.albumInsert({ ownerId: user2.id });
|
||||
await albumRepo.create(album, [], [{ userId: auth.user.id, role: AlbumUserRole.EDITOR }]);
|
||||
|
||||
await expect(testSync(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({ id: album.id }),
|
||||
type: SyncEntityType.AlbumV1,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should detect and sync an album share (share before sync)', async () => {
|
||||
const { auth, getRepository, testSync } = await setup();
|
||||
const albumRepo = getRepository('album');
|
||||
const albumUserRepo = getRepository('albumUser');
|
||||
const userRepo = getRepository('user');
|
||||
|
||||
const user2 = mediumFactory.userInsert();
|
||||
await userRepo.create(user2);
|
||||
|
||||
const album = mediumFactory.albumInsert({ ownerId: user2.id });
|
||||
await albumRepo.create(album, [], []);
|
||||
await albumUserRepo.create({ usersId: auth.user.id, albumsId: album.id, role: AlbumUserRole.EDITOR });
|
||||
|
||||
await expect(testSync(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({ id: album.id }),
|
||||
type: SyncEntityType.AlbumV1,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should detect and sync an album share (share after sync)', async () => {
|
||||
const { auth, getRepository, sut, testSync } = await setup();
|
||||
const albumRepo = getRepository('album');
|
||||
const albumUserRepo = getRepository('albumUser');
|
||||
const userRepo = getRepository('user');
|
||||
|
||||
const user2 = mediumFactory.userInsert();
|
||||
await userRepo.create(user2);
|
||||
|
||||
const userAlbum = mediumFactory.albumInsert({ ownerId: auth.user.id });
|
||||
const user2Album = mediumFactory.albumInsert({ ownerId: user2.id });
|
||||
await Promise.all([albumRepo.create(user2Album, [], []), albumRepo.create(userAlbum, [], [])]);
|
||||
|
||||
const initialSyncResponse = await testSync(auth, [SyncRequestType.AlbumsV1]);
|
||||
|
||||
expect(initialSyncResponse).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({ id: userAlbum.id }),
|
||||
type: SyncEntityType.AlbumV1,
|
||||
},
|
||||
]);
|
||||
|
||||
const acks = [initialSyncResponse[0].ack];
|
||||
await sut.setAcks(auth, { acks });
|
||||
|
||||
await albumUserRepo.create({ usersId: auth.user.id, albumsId: user2Album.id, role: AlbumUserRole.EDITOR });
|
||||
|
||||
await expect(testSync(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({ id: user2Album.id }),
|
||||
type: SyncEntityType.AlbumV1,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should detect and sync an album delete`', async () => {
|
||||
const { auth, getRepository, testSync, sut } = await setup();
|
||||
const albumRepo = getRepository('album');
|
||||
const userRepo = getRepository('user');
|
||||
|
||||
const user2 = mediumFactory.userInsert();
|
||||
await userRepo.create(user2);
|
||||
|
||||
const album = mediumFactory.albumInsert({ ownerId: user2.id });
|
||||
await albumRepo.create(album, [], [{ userId: auth.user.id, role: AlbumUserRole.EDITOR }]);
|
||||
|
||||
const initialSyncResponse = await testSync(auth, [SyncRequestType.AlbumsV1]);
|
||||
const acks = [initialSyncResponse[0].ack];
|
||||
await sut.setAcks(auth, { acks });
|
||||
|
||||
await expect(testSync(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([]);
|
||||
|
||||
await albumRepo.delete(album.id);
|
||||
|
||||
await expect(testSync(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: { albumId: album.id },
|
||||
type: SyncEntityType.AlbumDeleteV1,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should detect and sync an album unshare as an album delete', async () => {
|
||||
const { auth, getRepository, testSync, sut } = await setup();
|
||||
const albumRepo = getRepository('album');
|
||||
const albumUserRepo = getRepository('albumUser');
|
||||
const userRepo = getRepository('user');
|
||||
|
||||
const user2 = mediumFactory.userInsert();
|
||||
await userRepo.create(user2);
|
||||
|
||||
const album = mediumFactory.albumInsert({ ownerId: user2.id });
|
||||
await albumRepo.create(album, [], [{ userId: auth.user.id, role: AlbumUserRole.EDITOR }]);
|
||||
|
||||
const initialSyncResponse = await testSync(auth, [SyncRequestType.AlbumsV1]);
|
||||
const acks = [initialSyncResponse[0].ack];
|
||||
await sut.setAcks(auth, { acks });
|
||||
|
||||
await expect(testSync(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([]);
|
||||
|
||||
await albumUserRepo.delete({ albumsId: album.id, usersId: auth.user.id });
|
||||
|
||||
await expect(testSync(auth, [SyncRequestType.AlbumsV1])).resolves.toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: { albumId: album.id },
|
||||
type: SyncEntityType.AlbumDeleteV1,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,100 @@
|
||||
import { Kysely } from 'kysely';
|
||||
import { DB } from 'src/db';
|
||||
import { SyncEntityType, SyncRequestType } from 'src/enum';
|
||||
import { mediumFactory, newSyncAuthUser, newSyncTest } from 'test/medium.factory';
|
||||
import { factory } from 'test/small.factory';
|
||||
import { getKyselyDB } from 'test/utils';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
const setup = async (db?: Kysely<DB>) => {
|
||||
const database = db || defaultDatabase;
|
||||
const result = newSyncTest({ db: database });
|
||||
const { auth, create } = newSyncAuthUser();
|
||||
await create(database);
|
||||
return { ...result, auth };
|
||||
};
|
||||
beforeAll(async () => {
|
||||
defaultDatabase = await getKyselyDB();
|
||||
});
|
||||
|
||||
describe.concurrent(SyncRequestType.AssetExifsV1, () => {
|
||||
it('should detect and sync the first asset exif', async () => {
|
||||
const { auth, sut, getRepository, testSync } = await setup();
|
||||
|
||||
const assetRepo = getRepository('asset');
|
||||
const asset = mediumFactory.assetInsert({ ownerId: auth.user.id });
|
||||
await assetRepo.create(asset);
|
||||
await assetRepo.upsertExif({ assetId: asset.id, make: 'Canon' });
|
||||
|
||||
const initialSyncResponse = await testSync(auth, [SyncRequestType.AssetExifsV1]);
|
||||
|
||||
expect(initialSyncResponse).toHaveLength(1);
|
||||
expect(initialSyncResponse).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
assetId: asset.id,
|
||||
city: null,
|
||||
country: null,
|
||||
dateTimeOriginal: null,
|
||||
description: '',
|
||||
exifImageHeight: null,
|
||||
exifImageWidth: null,
|
||||
exposureTime: null,
|
||||
fNumber: null,
|
||||
fileSizeInByte: null,
|
||||
focalLength: null,
|
||||
fps: null,
|
||||
iso: null,
|
||||
latitude: null,
|
||||
lensModel: null,
|
||||
longitude: null,
|
||||
make: 'Canon',
|
||||
model: null,
|
||||
modifyDate: null,
|
||||
orientation: null,
|
||||
profileDescription: null,
|
||||
projectionType: null,
|
||||
rating: null,
|
||||
state: null,
|
||||
timeZone: null,
|
||||
},
|
||||
type: SyncEntityType.AssetExifV1,
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
const acks = [initialSyncResponse[0].ack];
|
||||
await sut.setAcks(auth, { acks });
|
||||
|
||||
const ackSyncResponse = await testSync(auth, [SyncRequestType.AssetExifsV1]);
|
||||
|
||||
expect(ackSyncResponse).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should only sync asset exif for own user', async () => {
|
||||
const { auth, getRepository, testSync } = await setup();
|
||||
|
||||
const userRepo = getRepository('user');
|
||||
const user2 = mediumFactory.userInsert();
|
||||
await userRepo.create(user2);
|
||||
|
||||
const partnerRepo = getRepository('partner');
|
||||
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||
|
||||
const assetRepo = getRepository('asset');
|
||||
const asset = mediumFactory.assetInsert({ ownerId: user2.id });
|
||||
await assetRepo.create(asset);
|
||||
await assetRepo.upsertExif({ assetId: asset.id, make: 'Canon' });
|
||||
|
||||
const sessionRepo = getRepository('session');
|
||||
const session = mediumFactory.sessionInsert({ userId: user2.id });
|
||||
await sessionRepo.create(session);
|
||||
|
||||
const auth2 = factory.auth({ session, user: user2 });
|
||||
await expect(testSync(auth2, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(1);
|
||||
await expect(testSync(auth, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(0);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,130 @@
|
||||
import { Kysely } from 'kysely';
|
||||
import { DB } from 'src/db';
|
||||
import { SyncEntityType, SyncRequestType } from 'src/enum';
|
||||
import { mediumFactory, newSyncAuthUser, newSyncTest } from 'test/medium.factory';
|
||||
import { factory } from 'test/small.factory';
|
||||
import { getKyselyDB } from 'test/utils';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
const setup = async (db?: Kysely<DB>) => {
|
||||
const database = db || defaultDatabase;
|
||||
const result = newSyncTest({ db: database });
|
||||
const { auth, create } = newSyncAuthUser();
|
||||
await create(database);
|
||||
return { ...result, auth };
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
defaultDatabase = await getKyselyDB();
|
||||
});
|
||||
|
||||
describe.concurrent(SyncEntityType.AssetV1, () => {
|
||||
it('should detect and sync the first asset', async () => {
|
||||
const { auth, sut, getRepository, testSync } = await setup();
|
||||
|
||||
const checksum = '1115vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA=';
|
||||
const thumbhash = '2225vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA=';
|
||||
const date = new Date().toISOString();
|
||||
|
||||
const assetRepo = getRepository('asset');
|
||||
const asset = mediumFactory.assetInsert({
|
||||
ownerId: auth.user.id,
|
||||
checksum: Buffer.from(checksum, 'base64'),
|
||||
thumbhash: Buffer.from(thumbhash, 'base64'),
|
||||
fileCreatedAt: date,
|
||||
fileModifiedAt: date,
|
||||
localDateTime: date,
|
||||
deletedAt: null,
|
||||
});
|
||||
await assetRepo.create(asset);
|
||||
|
||||
const initialSyncResponse = await testSync(auth, [SyncRequestType.AssetsV1]);
|
||||
|
||||
expect(initialSyncResponse).toHaveLength(1);
|
||||
expect(initialSyncResponse).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
id: asset.id,
|
||||
ownerId: asset.ownerId,
|
||||
thumbhash,
|
||||
checksum,
|
||||
deletedAt: asset.deletedAt,
|
||||
fileCreatedAt: asset.fileCreatedAt,
|
||||
fileModifiedAt: asset.fileModifiedAt,
|
||||
isFavorite: asset.isFavorite,
|
||||
localDateTime: asset.localDateTime,
|
||||
type: asset.type,
|
||||
visibility: asset.visibility,
|
||||
},
|
||||
type: 'AssetV1',
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
const acks = [initialSyncResponse[0].ack];
|
||||
await sut.setAcks(auth, { acks });
|
||||
|
||||
const ackSyncResponse = await testSync(auth, [SyncRequestType.AssetsV1]);
|
||||
|
||||
expect(ackSyncResponse).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should detect and sync a deleted asset', async () => {
|
||||
const { auth, sut, getRepository, testSync } = await setup();
|
||||
|
||||
const assetRepo = getRepository('asset');
|
||||
const asset = mediumFactory.assetInsert({ ownerId: auth.user.id });
|
||||
await assetRepo.create(asset);
|
||||
await assetRepo.remove(asset);
|
||||
|
||||
const response = await testSync(auth, [SyncRequestType.AssetsV1]);
|
||||
|
||||
expect(response).toHaveLength(1);
|
||||
expect(response).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
assetId: asset.id,
|
||||
},
|
||||
type: 'AssetDeleteV1',
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
const acks = response.map(({ ack }) => ack);
|
||||
await sut.setAcks(auth, { acks });
|
||||
|
||||
const ackSyncResponse = await testSync(auth, [SyncRequestType.AssetsV1]);
|
||||
|
||||
expect(ackSyncResponse).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should not sync an asset or asset delete for an unrelated user', async () => {
|
||||
const { auth, getRepository, testSync } = await setup();
|
||||
|
||||
const userRepo = getRepository('user');
|
||||
const user2 = mediumFactory.userInsert();
|
||||
await userRepo.create(user2);
|
||||
|
||||
const sessionRepo = getRepository('session');
|
||||
const session = mediumFactory.sessionInsert({ userId: user2.id });
|
||||
await sessionRepo.create(session);
|
||||
|
||||
const assetRepo = getRepository('asset');
|
||||
const asset = mediumFactory.assetInsert({ ownerId: user2.id });
|
||||
await assetRepo.create(asset);
|
||||
|
||||
const auth2 = factory.auth({ session, user: user2 });
|
||||
|
||||
expect(await testSync(auth2, [SyncRequestType.AssetsV1])).toHaveLength(1);
|
||||
expect(await testSync(auth, [SyncRequestType.AssetsV1])).toHaveLength(0);
|
||||
|
||||
await assetRepo.remove(asset);
|
||||
expect(await testSync(auth2, [SyncRequestType.AssetsV1])).toHaveLength(1);
|
||||
expect(await testSync(auth, [SyncRequestType.AssetsV1])).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,129 @@
|
||||
import { Kysely } from 'kysely';
|
||||
import { DB } from 'src/db';
|
||||
import { SyncEntityType, SyncRequestType } from 'src/enum';
|
||||
import { mediumFactory, newSyncAuthUser, newSyncTest } from 'test/medium.factory';
|
||||
import { factory } from 'test/small.factory';
|
||||
import { getKyselyDB } from 'test/utils';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
const setup = async (db?: Kysely<DB>) => {
|
||||
const database = db || defaultDatabase;
|
||||
const result = newSyncTest({ db: database });
|
||||
const { auth, create } = newSyncAuthUser();
|
||||
await create(database);
|
||||
return { ...result, auth };
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
defaultDatabase = await getKyselyDB();
|
||||
});
|
||||
|
||||
describe.concurrent(SyncRequestType.PartnerAssetExifsV1, () => {
|
||||
it('should detect and sync the first partner asset exif', async () => {
|
||||
const { auth, sut, getRepository, testSync } = await setup();
|
||||
|
||||
const userRepo = getRepository('user');
|
||||
const user2 = mediumFactory.userInsert();
|
||||
await userRepo.create(user2);
|
||||
|
||||
const partnerRepo = getRepository('partner');
|
||||
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||
|
||||
const assetRepo = getRepository('asset');
|
||||
const asset = mediumFactory.assetInsert({ ownerId: user2.id });
|
||||
await assetRepo.create(asset);
|
||||
await assetRepo.upsertExif({ assetId: asset.id, make: 'Canon' });
|
||||
|
||||
const initialSyncResponse = await testSync(auth, [SyncRequestType.PartnerAssetExifsV1]);
|
||||
|
||||
expect(initialSyncResponse).toHaveLength(1);
|
||||
expect(initialSyncResponse).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
assetId: asset.id,
|
||||
city: null,
|
||||
country: null,
|
||||
dateTimeOriginal: null,
|
||||
description: '',
|
||||
exifImageHeight: null,
|
||||
exifImageWidth: null,
|
||||
exposureTime: null,
|
||||
fNumber: null,
|
||||
fileSizeInByte: null,
|
||||
focalLength: null,
|
||||
fps: null,
|
||||
iso: null,
|
||||
latitude: null,
|
||||
lensModel: null,
|
||||
longitude: null,
|
||||
make: 'Canon',
|
||||
model: null,
|
||||
modifyDate: null,
|
||||
orientation: null,
|
||||
profileDescription: null,
|
||||
projectionType: null,
|
||||
rating: null,
|
||||
state: null,
|
||||
timeZone: null,
|
||||
},
|
||||
type: SyncEntityType.PartnerAssetExifV1,
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
const acks = [initialSyncResponse[0].ack];
|
||||
await sut.setAcks(auth, { acks });
|
||||
|
||||
const ackSyncResponse = await testSync(auth, [SyncRequestType.PartnerAssetExifsV1]);
|
||||
|
||||
expect(ackSyncResponse).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should not sync partner asset exif for own user', async () => {
|
||||
const { auth, getRepository, testSync } = await setup();
|
||||
|
||||
const userRepo = getRepository('user');
|
||||
const user2 = mediumFactory.userInsert();
|
||||
await userRepo.create(user2);
|
||||
|
||||
const partnerRepo = getRepository('partner');
|
||||
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||
|
||||
const assetRepo = getRepository('asset');
|
||||
const asset = mediumFactory.assetInsert({ ownerId: auth.user.id });
|
||||
await assetRepo.create(asset);
|
||||
await assetRepo.upsertExif({ assetId: asset.id, make: 'Canon' });
|
||||
|
||||
await expect(testSync(auth, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(1);
|
||||
await expect(testSync(auth, [SyncRequestType.PartnerAssetExifsV1])).resolves.toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should not sync partner asset exif for unrelated user', async () => {
|
||||
const { auth, getRepository, testSync } = await setup();
|
||||
|
||||
const userRepo = getRepository('user');
|
||||
|
||||
const user2 = mediumFactory.userInsert();
|
||||
const user3 = mediumFactory.userInsert();
|
||||
await Promise.all([userRepo.create(user2), userRepo.create(user3)]);
|
||||
|
||||
const partnerRepo = getRepository('partner');
|
||||
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||
|
||||
const assetRepo = getRepository('asset');
|
||||
const asset = mediumFactory.assetInsert({ ownerId: user3.id });
|
||||
await assetRepo.create(asset);
|
||||
await assetRepo.upsertExif({ assetId: asset.id, make: 'Canon' });
|
||||
|
||||
const sessionRepo = getRepository('session');
|
||||
const session = mediumFactory.sessionInsert({ userId: user3.id });
|
||||
await sessionRepo.create(session);
|
||||
|
||||
const authUser3 = factory.auth({ session, user: user3 });
|
||||
await expect(testSync(authUser3, [SyncRequestType.AssetExifsV1])).resolves.toHaveLength(1);
|
||||
await expect(testSync(auth, [SyncRequestType.PartnerAssetExifsV1])).resolves.toHaveLength(0);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,208 @@
|
||||
import { Kysely } from 'kysely';
|
||||
import { DB } from 'src/db';
|
||||
import { SyncEntityType, SyncRequestType } from 'src/enum';
|
||||
import { mediumFactory, newSyncAuthUser, newSyncTest } from 'test/medium.factory';
|
||||
import { factory } from 'test/small.factory';
|
||||
import { getKyselyDB } from 'test/utils';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
const setup = async (db?: Kysely<DB>) => {
|
||||
const database = db || defaultDatabase;
|
||||
const result = newSyncTest({ db: database });
|
||||
const { auth, create } = newSyncAuthUser();
|
||||
await create(database);
|
||||
return { ...result, auth };
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
defaultDatabase = await getKyselyDB();
|
||||
});
|
||||
|
||||
describe.concurrent(SyncRequestType.PartnerAssetsV1, () => {
|
||||
it('should detect and sync the first partner asset', async () => {
|
||||
const { auth, sut, getRepository, testSync } = await setup();
|
||||
|
||||
const checksum = '1115vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA=';
|
||||
const thumbhash = '2225vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA=';
|
||||
const date = new Date().toISOString();
|
||||
|
||||
const userRepo = getRepository('user');
|
||||
const user2 = mediumFactory.userInsert();
|
||||
await userRepo.create(user2);
|
||||
|
||||
const assetRepo = getRepository('asset');
|
||||
const asset = mediumFactory.assetInsert({
|
||||
ownerId: user2.id,
|
||||
checksum: Buffer.from(checksum, 'base64'),
|
||||
thumbhash: Buffer.from(thumbhash, 'base64'),
|
||||
fileCreatedAt: date,
|
||||
fileModifiedAt: date,
|
||||
localDateTime: date,
|
||||
deletedAt: null,
|
||||
});
|
||||
await assetRepo.create(asset);
|
||||
|
||||
const partnerRepo = getRepository('partner');
|
||||
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||
|
||||
const initialSyncResponse = await testSync(auth, [SyncRequestType.PartnerAssetsV1]);
|
||||
|
||||
expect(initialSyncResponse).toHaveLength(1);
|
||||
expect(initialSyncResponse).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
id: asset.id,
|
||||
ownerId: asset.ownerId,
|
||||
thumbhash,
|
||||
checksum,
|
||||
deletedAt: null,
|
||||
fileCreatedAt: date,
|
||||
fileModifiedAt: date,
|
||||
isFavorite: false,
|
||||
localDateTime: date,
|
||||
type: asset.type,
|
||||
visibility: asset.visibility,
|
||||
},
|
||||
type: SyncEntityType.PartnerAssetV1,
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
const acks = [initialSyncResponse[0].ack];
|
||||
await sut.setAcks(auth, { acks });
|
||||
|
||||
const ackSyncResponse = await testSync(auth, [SyncRequestType.PartnerAssetsV1]);
|
||||
|
||||
expect(ackSyncResponse).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should detect and sync a deleted partner asset', async () => {
|
||||
const { auth, sut, getRepository, testSync } = await setup();
|
||||
|
||||
const userRepo = getRepository('user');
|
||||
const user2 = mediumFactory.userInsert();
|
||||
await userRepo.create(user2);
|
||||
const asset = mediumFactory.assetInsert({ ownerId: user2.id });
|
||||
|
||||
const assetRepo = getRepository('asset');
|
||||
await assetRepo.create(asset);
|
||||
|
||||
const partnerRepo = getRepository('partner');
|
||||
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||
await assetRepo.remove(asset);
|
||||
|
||||
const response = await testSync(auth, [SyncRequestType.PartnerAssetsV1]);
|
||||
|
||||
expect(response).toHaveLength(1);
|
||||
expect(response).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
assetId: asset.id,
|
||||
},
|
||||
type: SyncEntityType.PartnerAssetDeleteV1,
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
const acks = response.map(({ ack }) => ack);
|
||||
await sut.setAcks(auth, { acks });
|
||||
|
||||
const ackSyncResponse = await testSync(auth, [SyncRequestType.PartnerAssetsV1]);
|
||||
|
||||
expect(ackSyncResponse).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should not sync a deleted partner asset due to a user delete', async () => {
|
||||
const { auth, getRepository, testSync } = await setup();
|
||||
|
||||
const userRepo = getRepository('user');
|
||||
const user2 = mediumFactory.userInsert();
|
||||
await userRepo.create(user2);
|
||||
|
||||
const partnerRepo = getRepository('partner');
|
||||
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||
|
||||
const assetRepo = getRepository('asset');
|
||||
await assetRepo.create(mediumFactory.assetInsert({ ownerId: user2.id }));
|
||||
|
||||
await userRepo.delete({ id: user2.id }, true);
|
||||
|
||||
const response = await testSync(auth, [SyncRequestType.PartnerAssetsV1]);
|
||||
expect(response).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should not sync a deleted partner asset due to a partner delete (unshare)', async () => {
|
||||
const { auth, getRepository, testSync } = await setup();
|
||||
|
||||
const userRepo = getRepository('user');
|
||||
const user2 = mediumFactory.userInsert();
|
||||
await userRepo.create(user2);
|
||||
|
||||
const assetRepo = getRepository('asset');
|
||||
await assetRepo.create(mediumFactory.assetInsert({ ownerId: user2.id }));
|
||||
|
||||
const partnerRepo = getRepository('partner');
|
||||
const partner = { sharedById: user2.id, sharedWithId: auth.user.id };
|
||||
await partnerRepo.create(partner);
|
||||
|
||||
await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(1);
|
||||
|
||||
await partnerRepo.remove(partner);
|
||||
|
||||
await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should not sync an asset or asset delete for own user', async () => {
|
||||
const { auth, getRepository, testSync } = await setup();
|
||||
|
||||
const userRepo = getRepository('user');
|
||||
const user2 = mediumFactory.userInsert();
|
||||
await userRepo.create(user2);
|
||||
|
||||
const assetRepo = getRepository('asset');
|
||||
const asset = mediumFactory.assetInsert({ ownerId: auth.user.id });
|
||||
await assetRepo.create(asset);
|
||||
|
||||
const partnerRepo = getRepository('partner');
|
||||
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||
|
||||
await expect(testSync(auth, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1);
|
||||
await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0);
|
||||
|
||||
await assetRepo.remove(asset);
|
||||
|
||||
await expect(testSync(auth, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1);
|
||||
await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should not sync an asset or asset delete for unrelated user', async () => {
|
||||
const { auth, getRepository, testSync } = await setup();
|
||||
|
||||
const userRepo = getRepository('user');
|
||||
const user2 = mediumFactory.userInsert();
|
||||
await userRepo.create(user2);
|
||||
|
||||
const sessionRepo = getRepository('session');
|
||||
const session = mediumFactory.sessionInsert({ userId: user2.id });
|
||||
await sessionRepo.create(session);
|
||||
|
||||
const auth2 = factory.auth({ session, user: user2 });
|
||||
|
||||
const assetRepo = getRepository('asset');
|
||||
const asset = mediumFactory.assetInsert({ ownerId: user2.id });
|
||||
await assetRepo.create(asset);
|
||||
|
||||
await expect(testSync(auth2, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1);
|
||||
await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0);
|
||||
|
||||
await assetRepo.remove(asset);
|
||||
|
||||
await expect(testSync(auth2, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1);
|
||||
await expect(testSync(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toHaveLength(0);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,221 @@
|
||||
import { Kysely } from 'kysely';
|
||||
import { DB } from 'src/db';
|
||||
import { SyncEntityType, SyncRequestType } from 'src/enum';
|
||||
import { mediumFactory, newSyncAuthUser, newSyncTest } from 'test/medium.factory';
|
||||
import { getKyselyDB } from 'test/utils';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
const setup = async (db?: Kysely<DB>) => {
|
||||
const database = db || defaultDatabase;
|
||||
const result = newSyncTest({ db: database });
|
||||
const { auth, create } = newSyncAuthUser();
|
||||
await create(database);
|
||||
return { ...result, auth };
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
defaultDatabase = await getKyselyDB();
|
||||
});
|
||||
|
||||
describe.concurrent(SyncEntityType.PartnerV1, () => {
|
||||
it('should detect and sync the first partner', async () => {
|
||||
const { auth, sut, getRepository, testSync } = await setup();
|
||||
|
||||
const user1 = auth.user;
|
||||
const userRepo = getRepository('user');
|
||||
const partnerRepo = getRepository('partner');
|
||||
|
||||
const user2 = mediumFactory.userInsert();
|
||||
await userRepo.create(user2);
|
||||
|
||||
const partner = await partnerRepo.create({ sharedById: user2.id, sharedWithId: user1.id });
|
||||
|
||||
const initialSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]);
|
||||
|
||||
expect(initialSyncResponse).toHaveLength(1);
|
||||
expect(initialSyncResponse).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
inTimeline: partner.inTimeline,
|
||||
sharedById: partner.sharedById,
|
||||
sharedWithId: partner.sharedWithId,
|
||||
},
|
||||
type: 'PartnerV1',
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
const acks = [initialSyncResponse[0].ack];
|
||||
await sut.setAcks(auth, { acks });
|
||||
|
||||
const ackSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]);
|
||||
|
||||
expect(ackSyncResponse).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should detect and sync a deleted partner', async () => {
|
||||
const { auth, sut, getRepository, testSync } = await setup();
|
||||
|
||||
const userRepo = getRepository('user');
|
||||
const user1 = auth.user;
|
||||
const user2 = mediumFactory.userInsert();
|
||||
await userRepo.create(user2);
|
||||
|
||||
const partnerRepo = getRepository('partner');
|
||||
const partner = await partnerRepo.create({ sharedById: user2.id, sharedWithId: user1.id });
|
||||
await partnerRepo.remove(partner);
|
||||
|
||||
const response = await testSync(auth, [SyncRequestType.PartnersV1]);
|
||||
|
||||
expect(response).toHaveLength(1);
|
||||
expect(response).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
sharedById: partner.sharedById,
|
||||
sharedWithId: partner.sharedWithId,
|
||||
},
|
||||
type: 'PartnerDeleteV1',
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
const acks = response.map(({ ack }) => ack);
|
||||
await sut.setAcks(auth, { acks });
|
||||
|
||||
const ackSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]);
|
||||
|
||||
expect(ackSyncResponse).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should detect and sync a partner share both to and from another user', async () => {
|
||||
const { auth, sut, getRepository, testSync } = await setup();
|
||||
|
||||
const userRepo = getRepository('user');
|
||||
const user1 = auth.user;
|
||||
const user2 = await userRepo.create(mediumFactory.userInsert());
|
||||
|
||||
const partnerRepo = getRepository('partner');
|
||||
const partner1 = await partnerRepo.create({ sharedById: user2.id, sharedWithId: user1.id });
|
||||
const partner2 = await partnerRepo.create({ sharedById: user1.id, sharedWithId: user2.id });
|
||||
|
||||
const response = await testSync(auth, [SyncRequestType.PartnersV1]);
|
||||
|
||||
expect(response).toHaveLength(2);
|
||||
expect(response).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
inTimeline: partner1.inTimeline,
|
||||
sharedById: partner1.sharedById,
|
||||
sharedWithId: partner1.sharedWithId,
|
||||
},
|
||||
type: 'PartnerV1',
|
||||
},
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
inTimeline: partner2.inTimeline,
|
||||
sharedById: partner2.sharedById,
|
||||
sharedWithId: partner2.sharedWithId,
|
||||
},
|
||||
type: 'PartnerV1',
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
await sut.setAcks(auth, { acks: [response[1].ack] });
|
||||
|
||||
const ackSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]);
|
||||
|
||||
expect(ackSyncResponse).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should sync a partner and then an update to that same partner', async () => {
|
||||
const { auth, sut, getRepository, testSync } = await setup();
|
||||
|
||||
const userRepo = getRepository('user');
|
||||
const user1 = auth.user;
|
||||
const user2 = await userRepo.create(mediumFactory.userInsert());
|
||||
|
||||
const partnerRepo = getRepository('partner');
|
||||
const partner = await partnerRepo.create({ sharedById: user2.id, sharedWithId: user1.id });
|
||||
|
||||
const initialSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]);
|
||||
|
||||
expect(initialSyncResponse).toHaveLength(1);
|
||||
expect(initialSyncResponse).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
inTimeline: partner.inTimeline,
|
||||
sharedById: partner.sharedById,
|
||||
sharedWithId: partner.sharedWithId,
|
||||
},
|
||||
type: 'PartnerV1',
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
const acks = [initialSyncResponse[0].ack];
|
||||
await sut.setAcks(auth, { acks });
|
||||
|
||||
const updated = await partnerRepo.update(
|
||||
{ sharedById: partner.sharedById, sharedWithId: partner.sharedWithId },
|
||||
{ inTimeline: true },
|
||||
);
|
||||
|
||||
const updatedSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]);
|
||||
|
||||
expect(updatedSyncResponse).toHaveLength(1);
|
||||
expect(updatedSyncResponse).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
inTimeline: updated.inTimeline,
|
||||
sharedById: updated.sharedById,
|
||||
sharedWithId: updated.sharedWithId,
|
||||
},
|
||||
type: 'PartnerV1',
|
||||
},
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should not sync a partner or partner delete for an unrelated user', async () => {
|
||||
const { auth, getRepository, testSync } = await setup();
|
||||
|
||||
const userRepo = getRepository('user');
|
||||
const user2 = await userRepo.create(mediumFactory.userInsert());
|
||||
const user3 = await userRepo.create(mediumFactory.userInsert());
|
||||
|
||||
const partnerRepo = getRepository('partner');
|
||||
const partner = await partnerRepo.create({ sharedById: user2.id, sharedWithId: user3.id });
|
||||
|
||||
expect(await testSync(auth, [SyncRequestType.PartnersV1])).toHaveLength(0);
|
||||
|
||||
await partnerRepo.remove(partner);
|
||||
|
||||
expect(await testSync(auth, [SyncRequestType.PartnersV1])).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should not sync a partner delete after a user is deleted', async () => {
|
||||
const { auth, getRepository, testSync } = await setup();
|
||||
|
||||
const userRepo = getRepository('user');
|
||||
const user2 = await userRepo.create(mediumFactory.userInsert());
|
||||
|
||||
const partnerRepo = getRepository('partner');
|
||||
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||
await userRepo.delete({ id: user2.id }, true);
|
||||
|
||||
expect(await testSync(auth, [SyncRequestType.PartnersV1])).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,12 @@
|
||||
import { SyncRequestType } from 'src/enum';
|
||||
import { SYNC_TYPES_ORDER } from 'src/services/sync.service';
|
||||
|
||||
describe('types', () => {
|
||||
it('should have all the types in the ordering variable', () => {
|
||||
for (const key in SyncRequestType) {
|
||||
expect(SYNC_TYPES_ORDER).includes(key);
|
||||
}
|
||||
|
||||
expect(SYNC_TYPES_ORDER.length).toBe(Object.keys(SyncRequestType).length);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,179 @@
|
||||
import { Kysely } from 'kysely';
|
||||
import { DB } from 'src/db';
|
||||
import { SyncEntityType, SyncRequestType } from 'src/enum';
|
||||
import { mediumFactory, newSyncAuthUser, newSyncTest } from 'test/medium.factory';
|
||||
import { getKyselyDB } from 'test/utils';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
const setup = async (db?: Kysely<DB>) => {
|
||||
const database = db || defaultDatabase;
|
||||
const result = newSyncTest({ db: database });
|
||||
const { auth, create } = newSyncAuthUser();
|
||||
await create(database);
|
||||
return { ...result, auth };
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
defaultDatabase = await getKyselyDB();
|
||||
});
|
||||
|
||||
describe.concurrent(SyncEntityType.UserV1, () => {
|
||||
it('should detect and sync the first user', async () => {
|
||||
const { auth, sut, getRepository, testSync } = await setup(await getKyselyDB());
|
||||
|
||||
const userRepo = getRepository('user');
|
||||
const user = await userRepo.get(auth.user.id, { withDeleted: false });
|
||||
if (!user) {
|
||||
expect.fail('First user should exist');
|
||||
}
|
||||
|
||||
const initialSyncResponse = await testSync(auth, [SyncRequestType.UsersV1]);
|
||||
expect(initialSyncResponse).toHaveLength(1);
|
||||
expect(initialSyncResponse).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
deletedAt: user.deletedAt,
|
||||
email: user.email,
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
},
|
||||
type: 'UserV1',
|
||||
},
|
||||
]);
|
||||
|
||||
const acks = [initialSyncResponse[0].ack];
|
||||
await sut.setAcks(auth, { acks });
|
||||
const ackSyncResponse = await testSync(auth, [SyncRequestType.UsersV1]);
|
||||
|
||||
expect(ackSyncResponse).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should detect and sync a soft deleted user', async () => {
|
||||
const { auth, sut, getRepository, testSync } = await setup(await getKyselyDB());
|
||||
|
||||
const deletedAt = new Date().toISOString();
|
||||
const deletedUser = mediumFactory.userInsert({ deletedAt });
|
||||
const deleted = await getRepository('user').create(deletedUser);
|
||||
|
||||
const response = await testSync(auth, [SyncRequestType.UsersV1]);
|
||||
|
||||
expect(response).toHaveLength(2);
|
||||
expect(response).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
deletedAt: null,
|
||||
email: auth.user.email,
|
||||
id: auth.user.id,
|
||||
name: auth.user.name,
|
||||
},
|
||||
type: 'UserV1',
|
||||
},
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
deletedAt,
|
||||
email: deleted.email,
|
||||
id: deleted.id,
|
||||
name: deleted.name,
|
||||
},
|
||||
type: 'UserV1',
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
const acks = [response[1].ack];
|
||||
await sut.setAcks(auth, { acks });
|
||||
const ackSyncResponse = await testSync(auth, [SyncRequestType.UsersV1]);
|
||||
|
||||
expect(ackSyncResponse).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should detect and sync a deleted user', async () => {
|
||||
const { auth, sut, getRepository, testSync } = await setup(await getKyselyDB());
|
||||
|
||||
const userRepo = getRepository('user');
|
||||
const user = mediumFactory.userInsert();
|
||||
await userRepo.create(user);
|
||||
await userRepo.delete({ id: user.id }, true);
|
||||
|
||||
const response = await testSync(auth, [SyncRequestType.UsersV1]);
|
||||
|
||||
expect(response).toHaveLength(2);
|
||||
expect(response).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
userId: user.id,
|
||||
},
|
||||
type: 'UserDeleteV1',
|
||||
},
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
deletedAt: null,
|
||||
email: auth.user.email,
|
||||
id: auth.user.id,
|
||||
name: auth.user.name,
|
||||
},
|
||||
type: 'UserV1',
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
const acks = response.map(({ ack }) => ack);
|
||||
await sut.setAcks(auth, { acks });
|
||||
const ackSyncResponse = await testSync(auth, [SyncRequestType.UsersV1]);
|
||||
|
||||
expect(ackSyncResponse).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should sync a user and then an update to that same user', async () => {
|
||||
const { auth, sut, getRepository, testSync } = await setup(await getKyselyDB());
|
||||
|
||||
const initialSyncResponse = await testSync(auth, [SyncRequestType.UsersV1]);
|
||||
|
||||
expect(initialSyncResponse).toHaveLength(1);
|
||||
expect(initialSyncResponse).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
deletedAt: null,
|
||||
email: auth.user.email,
|
||||
id: auth.user.id,
|
||||
name: auth.user.name,
|
||||
},
|
||||
type: 'UserV1',
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
const acks = [initialSyncResponse[0].ack];
|
||||
await sut.setAcks(auth, { acks });
|
||||
|
||||
const userRepo = getRepository('user');
|
||||
const updated = await userRepo.update(auth.user.id, { name: 'new name' });
|
||||
const updatedSyncResponse = await testSync(auth, [SyncRequestType.UsersV1]);
|
||||
|
||||
expect(updatedSyncResponse).toHaveLength(1);
|
||||
expect(updatedSyncResponse).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: {
|
||||
deletedAt: null,
|
||||
email: auth.user.email,
|
||||
id: auth.user.id,
|
||||
name: updated.name,
|
||||
},
|
||||
type: 'UserV1',
|
||||
},
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue