mirror of
				https://github.com/immich-app/immich.git
				synced 2025-10-31 10:37:11 -04:00 
			
		
		
		
	
							parent
							
								
									869839f642
								
							
						
					
					
						commit
						fe702ba6d7
					
				
							
								
								
									
										2
									
								
								mobile/openapi/README.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								mobile/openapi/README.md
									
									
									
										generated
									
									
									
								
							| @ -425,6 +425,8 @@ Class | Method | HTTP request | Description | ||||
|  - [SyncAckDto](doc//SyncAckDto.md) | ||||
|  - [SyncAckSetDto](doc//SyncAckSetDto.md) | ||||
|  - [SyncEntityType](doc//SyncEntityType.md) | ||||
|  - [SyncPartnerDeleteV1](doc//SyncPartnerDeleteV1.md) | ||||
|  - [SyncPartnerV1](doc//SyncPartnerV1.md) | ||||
|  - [SyncRequestType](doc//SyncRequestType.md) | ||||
|  - [SyncStreamDto](doc//SyncStreamDto.md) | ||||
|  - [SyncUserDeleteV1](doc//SyncUserDeleteV1.md) | ||||
|  | ||||
							
								
								
									
										2
									
								
								mobile/openapi/lib/api.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								mobile/openapi/lib/api.dart
									
									
									
										generated
									
									
									
								
							| @ -232,6 +232,8 @@ part 'model/sync_ack_delete_dto.dart'; | ||||
| part 'model/sync_ack_dto.dart'; | ||||
| part 'model/sync_ack_set_dto.dart'; | ||||
| part 'model/sync_entity_type.dart'; | ||||
| part 'model/sync_partner_delete_v1.dart'; | ||||
| part 'model/sync_partner_v1.dart'; | ||||
| part 'model/sync_request_type.dart'; | ||||
| part 'model/sync_stream_dto.dart'; | ||||
| part 'model/sync_user_delete_v1.dart'; | ||||
|  | ||||
							
								
								
									
										4
									
								
								mobile/openapi/lib/api_client.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										4
									
								
								mobile/openapi/lib/api_client.dart
									
									
									
										generated
									
									
									
								
							| @ -520,6 +520,10 @@ class ApiClient { | ||||
|           return SyncAckSetDto.fromJson(value); | ||||
|         case 'SyncEntityType': | ||||
|           return SyncEntityTypeTypeTransformer().decode(value); | ||||
|         case 'SyncPartnerDeleteV1': | ||||
|           return SyncPartnerDeleteV1.fromJson(value); | ||||
|         case 'SyncPartnerV1': | ||||
|           return SyncPartnerV1.fromJson(value); | ||||
|         case 'SyncRequestType': | ||||
|           return SyncRequestTypeTypeTransformer().decode(value); | ||||
|         case 'SyncStreamDto': | ||||
|  | ||||
							
								
								
									
										6
									
								
								mobile/openapi/lib/model/sync_entity_type.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										6
									
								
								mobile/openapi/lib/model/sync_entity_type.dart
									
									
									
										generated
									
									
									
								
							| @ -25,11 +25,15 @@ class SyncEntityType { | ||||
| 
 | ||||
|   static const userV1 = SyncEntityType._(r'UserV1'); | ||||
|   static const userDeleteV1 = SyncEntityType._(r'UserDeleteV1'); | ||||
|   static const partnerV1 = SyncEntityType._(r'PartnerV1'); | ||||
|   static const partnerDeleteV1 = SyncEntityType._(r'PartnerDeleteV1'); | ||||
| 
 | ||||
|   /// List of all possible values in this [enum][SyncEntityType]. | ||||
|   static const values = <SyncEntityType>[ | ||||
|     userV1, | ||||
|     userDeleteV1, | ||||
|     partnerV1, | ||||
|     partnerDeleteV1, | ||||
|   ]; | ||||
| 
 | ||||
|   static SyncEntityType? fromJson(dynamic value) => SyncEntityTypeTypeTransformer().decode(value); | ||||
| @ -70,6 +74,8 @@ class SyncEntityTypeTypeTransformer { | ||||
|       switch (data) { | ||||
|         case r'UserV1': return SyncEntityType.userV1; | ||||
|         case r'UserDeleteV1': return SyncEntityType.userDeleteV1; | ||||
|         case r'PartnerV1': return SyncEntityType.partnerV1; | ||||
|         case r'PartnerDeleteV1': return SyncEntityType.partnerDeleteV1; | ||||
|         default: | ||||
|           if (!allowNull) { | ||||
|             throw ArgumentError('Unknown enum value to decode: $data'); | ||||
|  | ||||
							
								
								
									
										107
									
								
								mobile/openapi/lib/model/sync_partner_delete_v1.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								mobile/openapi/lib/model/sync_partner_delete_v1.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @ -0,0 +1,107 @@ | ||||
| // | ||||
| // AUTO-GENERATED FILE, DO NOT MODIFY! | ||||
| // | ||||
| // @dart=2.18 | ||||
| 
 | ||||
| // ignore_for_file: unused_element, unused_import | ||||
| // ignore_for_file: always_put_required_named_parameters_first | ||||
| // ignore_for_file: constant_identifier_names | ||||
| // ignore_for_file: lines_longer_than_80_chars | ||||
| 
 | ||||
| part of openapi.api; | ||||
| 
 | ||||
| class SyncPartnerDeleteV1 { | ||||
|   /// Returns a new [SyncPartnerDeleteV1] instance. | ||||
|   SyncPartnerDeleteV1({ | ||||
|     required this.sharedById, | ||||
|     required this.sharedWithId, | ||||
|   }); | ||||
| 
 | ||||
|   String sharedById; | ||||
| 
 | ||||
|   String sharedWithId; | ||||
| 
 | ||||
|   @override | ||||
|   bool operator ==(Object other) => identical(this, other) || other is SyncPartnerDeleteV1 && | ||||
|     other.sharedById == sharedById && | ||||
|     other.sharedWithId == sharedWithId; | ||||
| 
 | ||||
|   @override | ||||
|   int get hashCode => | ||||
|     // ignore: unnecessary_parenthesis | ||||
|     (sharedById.hashCode) + | ||||
|     (sharedWithId.hashCode); | ||||
| 
 | ||||
|   @override | ||||
|   String toString() => 'SyncPartnerDeleteV1[sharedById=$sharedById, sharedWithId=$sharedWithId]'; | ||||
| 
 | ||||
|   Map<String, dynamic> toJson() { | ||||
|     final json = <String, dynamic>{}; | ||||
|       json[r'sharedById'] = this.sharedById; | ||||
|       json[r'sharedWithId'] = this.sharedWithId; | ||||
|     return json; | ||||
|   } | ||||
| 
 | ||||
|   /// Returns a new [SyncPartnerDeleteV1] instance and imports its values from | ||||
|   /// [value] if it's a [Map], null otherwise. | ||||
|   // ignore: prefer_constructors_over_static_methods | ||||
|   static SyncPartnerDeleteV1? fromJson(dynamic value) { | ||||
|     upgradeDto(value, "SyncPartnerDeleteV1"); | ||||
|     if (value is Map) { | ||||
|       final json = value.cast<String, dynamic>(); | ||||
| 
 | ||||
|       return SyncPartnerDeleteV1( | ||||
|         sharedById: mapValueOfType<String>(json, r'sharedById')!, | ||||
|         sharedWithId: mapValueOfType<String>(json, r'sharedWithId')!, | ||||
|       ); | ||||
|     } | ||||
|     return null; | ||||
|   } | ||||
| 
 | ||||
|   static List<SyncPartnerDeleteV1> listFromJson(dynamic json, {bool growable = false,}) { | ||||
|     final result = <SyncPartnerDeleteV1>[]; | ||||
|     if (json is List && json.isNotEmpty) { | ||||
|       for (final row in json) { | ||||
|         final value = SyncPartnerDeleteV1.fromJson(row); | ||||
|         if (value != null) { | ||||
|           result.add(value); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     return result.toList(growable: growable); | ||||
|   } | ||||
| 
 | ||||
|   static Map<String, SyncPartnerDeleteV1> mapFromJson(dynamic json) { | ||||
|     final map = <String, SyncPartnerDeleteV1>{}; | ||||
|     if (json is Map && json.isNotEmpty) { | ||||
|       json = json.cast<String, dynamic>(); // ignore: parameter_assignments | ||||
|       for (final entry in json.entries) { | ||||
|         final value = SyncPartnerDeleteV1.fromJson(entry.value); | ||||
|         if (value != null) { | ||||
|           map[entry.key] = value; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     return map; | ||||
|   } | ||||
| 
 | ||||
|   // maps a json object with a list of SyncPartnerDeleteV1-objects as value to a dart map | ||||
|   static Map<String, List<SyncPartnerDeleteV1>> mapListFromJson(dynamic json, {bool growable = false,}) { | ||||
|     final map = <String, List<SyncPartnerDeleteV1>>{}; | ||||
|     if (json is Map && json.isNotEmpty) { | ||||
|       // ignore: parameter_assignments | ||||
|       json = json.cast<String, dynamic>(); | ||||
|       for (final entry in json.entries) { | ||||
|         map[entry.key] = SyncPartnerDeleteV1.listFromJson(entry.value, growable: growable,); | ||||
|       } | ||||
|     } | ||||
|     return map; | ||||
|   } | ||||
| 
 | ||||
|   /// The list of required keys that must be present in a JSON. | ||||
|   static const requiredKeys = <String>{ | ||||
|     'sharedById', | ||||
|     'sharedWithId', | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
							
								
								
									
										115
									
								
								mobile/openapi/lib/model/sync_partner_v1.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										115
									
								
								mobile/openapi/lib/model/sync_partner_v1.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @ -0,0 +1,115 @@ | ||||
| // | ||||
| // 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 SyncPartnerV1 { | ||||
|   /// Returns a new [SyncPartnerV1] instance. | ||||
|   SyncPartnerV1({ | ||||
|     required this.inTimeline, | ||||
|     required this.sharedById, | ||||
|     required this.sharedWithId, | ||||
|   }); | ||||
| 
 | ||||
|   bool inTimeline; | ||||
| 
 | ||||
|   String sharedById; | ||||
| 
 | ||||
|   String sharedWithId; | ||||
| 
 | ||||
|   @override | ||||
|   bool operator ==(Object other) => identical(this, other) || other is SyncPartnerV1 && | ||||
|     other.inTimeline == inTimeline && | ||||
|     other.sharedById == sharedById && | ||||
|     other.sharedWithId == sharedWithId; | ||||
| 
 | ||||
|   @override | ||||
|   int get hashCode => | ||||
|     // ignore: unnecessary_parenthesis | ||||
|     (inTimeline.hashCode) + | ||||
|     (sharedById.hashCode) + | ||||
|     (sharedWithId.hashCode); | ||||
| 
 | ||||
|   @override | ||||
|   String toString() => 'SyncPartnerV1[inTimeline=$inTimeline, sharedById=$sharedById, sharedWithId=$sharedWithId]'; | ||||
| 
 | ||||
|   Map<String, dynamic> toJson() { | ||||
|     final json = <String, dynamic>{}; | ||||
|       json[r'inTimeline'] = this.inTimeline; | ||||
|       json[r'sharedById'] = this.sharedById; | ||||
|       json[r'sharedWithId'] = this.sharedWithId; | ||||
|     return json; | ||||
|   } | ||||
| 
 | ||||
|   /// Returns a new [SyncPartnerV1] instance and imports its values from | ||||
|   /// [value] if it's a [Map], null otherwise. | ||||
|   // ignore: prefer_constructors_over_static_methods | ||||
|   static SyncPartnerV1? fromJson(dynamic value) { | ||||
|     upgradeDto(value, "SyncPartnerV1"); | ||||
|     if (value is Map) { | ||||
|       final json = value.cast<String, dynamic>(); | ||||
| 
 | ||||
|       return SyncPartnerV1( | ||||
|         inTimeline: mapValueOfType<bool>(json, r'inTimeline')!, | ||||
|         sharedById: mapValueOfType<String>(json, r'sharedById')!, | ||||
|         sharedWithId: mapValueOfType<String>(json, r'sharedWithId')!, | ||||
|       ); | ||||
|     } | ||||
|     return null; | ||||
|   } | ||||
| 
 | ||||
|   static List<SyncPartnerV1> listFromJson(dynamic json, {bool growable = false,}) { | ||||
|     final result = <SyncPartnerV1>[]; | ||||
|     if (json is List && json.isNotEmpty) { | ||||
|       for (final row in json) { | ||||
|         final value = SyncPartnerV1.fromJson(row); | ||||
|         if (value != null) { | ||||
|           result.add(value); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     return result.toList(growable: growable); | ||||
|   } | ||||
| 
 | ||||
|   static Map<String, SyncPartnerV1> mapFromJson(dynamic json) { | ||||
|     final map = <String, SyncPartnerV1>{}; | ||||
|     if (json is Map && json.isNotEmpty) { | ||||
|       json = json.cast<String, dynamic>(); // ignore: parameter_assignments | ||||
|       for (final entry in json.entries) { | ||||
|         final value = SyncPartnerV1.fromJson(entry.value); | ||||
|         if (value != null) { | ||||
|           map[entry.key] = value; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     return map; | ||||
|   } | ||||
| 
 | ||||
|   // maps a json object with a list of SyncPartnerV1-objects as value to a dart map | ||||
|   static Map<String, List<SyncPartnerV1>> mapListFromJson(dynamic json, {bool growable = false,}) { | ||||
|     final map = <String, List<SyncPartnerV1>>{}; | ||||
|     if (json is Map && json.isNotEmpty) { | ||||
|       // ignore: parameter_assignments | ||||
|       json = json.cast<String, dynamic>(); | ||||
|       for (final entry in json.entries) { | ||||
|         map[entry.key] = SyncPartnerV1.listFromJson(entry.value, growable: growable,); | ||||
|       } | ||||
|     } | ||||
|     return map; | ||||
|   } | ||||
| 
 | ||||
|   /// The list of required keys that must be present in a JSON. | ||||
|   static const requiredKeys = <String>{ | ||||
|     'inTimeline', | ||||
|     'sharedById', | ||||
|     'sharedWithId', | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
							
								
								
									
										3
									
								
								mobile/openapi/lib/model/sync_request_type.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										3
									
								
								mobile/openapi/lib/model/sync_request_type.dart
									
									
									
										generated
									
									
									
								
							| @ -24,10 +24,12 @@ class SyncRequestType { | ||||
|   String toJson() => value; | ||||
| 
 | ||||
|   static const usersV1 = SyncRequestType._(r'UsersV1'); | ||||
|   static const partnersV1 = SyncRequestType._(r'PartnersV1'); | ||||
| 
 | ||||
|   /// List of all possible values in this [enum][SyncRequestType]. | ||||
|   static const values = <SyncRequestType>[ | ||||
|     usersV1, | ||||
|     partnersV1, | ||||
|   ]; | ||||
| 
 | ||||
|   static SyncRequestType? fromJson(dynamic value) => SyncRequestTypeTypeTransformer().decode(value); | ||||
| @ -67,6 +69,7 @@ class SyncRequestTypeTypeTransformer { | ||||
|     if (data != null) { | ||||
|       switch (data) { | ||||
|         case r'UsersV1': return SyncRequestType.usersV1; | ||||
|         case r'PartnersV1': return SyncRequestType.partnersV1; | ||||
|         default: | ||||
|           if (!allowNull) { | ||||
|             throw ArgumentError('Unknown enum value to decode: $data'); | ||||
|  | ||||
| @ -12052,13 +12052,50 @@ | ||||
|       "SyncEntityType": { | ||||
|         "enum": [ | ||||
|           "UserV1", | ||||
|           "UserDeleteV1" | ||||
|           "UserDeleteV1", | ||||
|           "PartnerV1", | ||||
|           "PartnerDeleteV1" | ||||
|         ], | ||||
|         "type": "string" | ||||
|       }, | ||||
|       "SyncPartnerDeleteV1": { | ||||
|         "properties": { | ||||
|           "sharedById": { | ||||
|             "type": "string" | ||||
|           }, | ||||
|           "sharedWithId": { | ||||
|             "type": "string" | ||||
|           } | ||||
|         }, | ||||
|         "required": [ | ||||
|           "sharedById", | ||||
|           "sharedWithId" | ||||
|         ], | ||||
|         "type": "object" | ||||
|       }, | ||||
|       "SyncPartnerV1": { | ||||
|         "properties": { | ||||
|           "inTimeline": { | ||||
|             "type": "boolean" | ||||
|           }, | ||||
|           "sharedById": { | ||||
|             "type": "string" | ||||
|           }, | ||||
|           "sharedWithId": { | ||||
|             "type": "string" | ||||
|           } | ||||
|         }, | ||||
|         "required": [ | ||||
|           "inTimeline", | ||||
|           "sharedById", | ||||
|           "sharedWithId" | ||||
|         ], | ||||
|         "type": "object" | ||||
|       }, | ||||
|       "SyncRequestType": { | ||||
|         "enum": [ | ||||
|           "UsersV1" | ||||
|           "UsersV1", | ||||
|           "PartnersV1" | ||||
|         ], | ||||
|         "type": "string" | ||||
|       }, | ||||
|  | ||||
| @ -3645,10 +3645,13 @@ export enum Error2 { | ||||
| } | ||||
| export enum SyncEntityType { | ||||
|     UserV1 = "UserV1", | ||||
|     UserDeleteV1 = "UserDeleteV1" | ||||
|     UserDeleteV1 = "UserDeleteV1", | ||||
|     PartnerV1 = "PartnerV1", | ||||
|     PartnerDeleteV1 = "PartnerDeleteV1" | ||||
| } | ||||
| export enum SyncRequestType { | ||||
|     UsersV1 = "UsersV1" | ||||
|     UsersV1 = "UsersV1", | ||||
|     PartnersV1 = "PartnersV1" | ||||
| } | ||||
| export enum TranscodeHWAccel { | ||||
|     Nvenc = "nvenc", | ||||
|  | ||||
							
								
								
									
										9
									
								
								server/src/db.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										9
									
								
								server/src/db.d.ts
									
									
									
									
										vendored
									
									
								
							| @ -272,6 +272,13 @@ export interface NaturalearthCountries { | ||||
|   type: string; | ||||
| } | ||||
| 
 | ||||
| export interface PartnersAudit { | ||||
|   deletedAt: Generated<Timestamp>; | ||||
|   id: Generated<string>; | ||||
|   sharedById: string; | ||||
|   sharedWithId: string; | ||||
| } | ||||
| 
 | ||||
| export interface Partners { | ||||
|   createdAt: Generated<Timestamp>; | ||||
|   inTimeline: Generated<boolean>; | ||||
| @ -316,7 +323,6 @@ export interface SessionSyncCheckpoints { | ||||
|   updateId: Generated<string>; | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| export interface SharedLinkAsset { | ||||
|   assetsId: string; | ||||
|   sharedLinksId: string; | ||||
| @ -462,6 +468,7 @@ export interface DB { | ||||
|   migrations: Migrations; | ||||
|   move_history: MoveHistory; | ||||
|   naturalearth_countries: NaturalearthCountries; | ||||
|   partners_audit: PartnersAudit; | ||||
|   partners: Partners; | ||||
|   person: Person; | ||||
|   sessions: Sessions; | ||||
|  | ||||
| @ -45,15 +45,30 @@ export class SyncUserDeleteV1 { | ||||
|   userId!: string; | ||||
| } | ||||
| 
 | ||||
| export class SyncPartnerV1 { | ||||
|   sharedById!: string; | ||||
|   sharedWithId!: string; | ||||
|   inTimeline!: boolean; | ||||
| } | ||||
| 
 | ||||
| export class SyncPartnerDeleteV1 { | ||||
|   sharedById!: string; | ||||
|   sharedWithId!: string; | ||||
| } | ||||
| 
 | ||||
| export type SyncItem = { | ||||
|   [SyncEntityType.UserV1]: SyncUserV1; | ||||
|   [SyncEntityType.UserDeleteV1]: SyncUserDeleteV1; | ||||
|   [SyncEntityType.PartnerV1]: SyncPartnerV1; | ||||
|   [SyncEntityType.PartnerDeleteV1]: SyncPartnerDeleteV1; | ||||
| }; | ||||
| 
 | ||||
| const responseDtos = [ | ||||
|   //
 | ||||
|   SyncUserV1, | ||||
|   SyncUserDeleteV1, | ||||
|   SyncPartnerV1, | ||||
|   SyncPartnerDeleteV1, | ||||
| ]; | ||||
| 
 | ||||
| export const extraSyncModels = responseDtos; | ||||
|  | ||||
							
								
								
									
										19
									
								
								server/src/entities/partner-audit.entity.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								server/src/entities/partner-audit.entity.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,19 @@ | ||||
| import { Column, CreateDateColumn, Entity, Index, PrimaryColumn } from 'typeorm'; | ||||
| 
 | ||||
| @Entity('partners_audit') | ||||
| export class PartnerAuditEntity { | ||||
|   @PrimaryColumn({ type: 'uuid', nullable: false, default: () => 'immich_uuid_v7()' }) | ||||
|   id!: string; | ||||
| 
 | ||||
|   @Index('IDX_partners_audit_shared_by_id') | ||||
|   @Column({ type: 'uuid' }) | ||||
|   sharedById!: string; | ||||
| 
 | ||||
|   @Index('IDX_partners_audit_shared_with_id') | ||||
|   @Column({ type: 'uuid' }) | ||||
|   sharedWithId!: string; | ||||
| 
 | ||||
|   @Index('IDX_partners_audit_deleted_at') | ||||
|   @CreateDateColumn({ type: 'timestamptz', default: () => 'clock_timestamp()' }) | ||||
|   deletedAt!: Date; | ||||
| } | ||||
| @ -548,9 +548,12 @@ export enum DatabaseLock { | ||||
| 
 | ||||
| export enum SyncRequestType { | ||||
|   UsersV1 = 'UsersV1', | ||||
|   PartnersV1 = 'PartnersV1', | ||||
| } | ||||
| 
 | ||||
| export enum SyncEntityType { | ||||
|   UserV1 = 'UserV1', | ||||
|   UserDeleteV1 = 'UserDeleteV1', | ||||
|   PartnerV1 = 'PartnerV1', | ||||
|   PartnerDeleteV1 = 'PartnerDeleteV1', | ||||
| } | ||||
|  | ||||
| @ -0,0 +1,38 @@ | ||||
| import { MigrationInterface, QueryRunner } from "typeorm"; | ||||
| 
 | ||||
| export class CreatePartnersAuditTable1740739778549 implements MigrationInterface { | ||||
|     name = 'CreatePartnersAuditTable1740739778549' | ||||
| 
 | ||||
|     public async up(queryRunner: QueryRunner): Promise<void> { | ||||
|         await queryRunner.query(`CREATE TABLE "partners_audit" ("id" uuid NOT NULL DEFAULT immich_uuid_v7(), "sharedById" uuid NOT NULL, "sharedWithId" uuid NOT NULL, "deletedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT clock_timestamp(), CONSTRAINT "PK_952b50217ff78198a7e380f0359" PRIMARY KEY ("id"))`); | ||||
|         await queryRunner.query(`CREATE INDEX "IDX_partners_audit_shared_by_id" ON "partners_audit" ("sharedById") `); | ||||
|         await queryRunner.query(`CREATE INDEX "IDX_partners_audit_shared_with_id" ON "partners_audit" ("sharedWithId") `); | ||||
|         await queryRunner.query(`CREATE INDEX "IDX_partners_audit_deleted_at" ON "partners_audit" ("deletedAt") `); | ||||
|         await queryRunner.query(`CREATE OR REPLACE FUNCTION partners_delete_audit() RETURNS TRIGGER AS
 | ||||
|               $$ | ||||
|                BEGIN | ||||
|                 INSERT INTO partners_audit ("sharedById", "sharedWithId") | ||||
|                 SELECT "sharedById", "sharedWithId" | ||||
|                 FROM OLD; | ||||
|                 RETURN NULL; | ||||
|                END; | ||||
|               $$ LANGUAGE plpgsql` | ||||
|         ); | ||||
|         await queryRunner.query(`CREATE OR REPLACE TRIGGER partners_delete_audit
 | ||||
|                AFTER DELETE ON partners | ||||
|                REFERENCING OLD TABLE AS OLD | ||||
|                FOR EACH STATEMENT | ||||
|                EXECUTE FUNCTION partners_delete_audit(); | ||||
|             `);
 | ||||
|     } | ||||
| 
 | ||||
|     public async down(queryRunner: QueryRunner): Promise<void> { | ||||
|         await queryRunner.query(`DROP INDEX "public"."IDX_partners_audit_deleted_at"`); | ||||
|         await queryRunner.query(`DROP INDEX "public"."IDX_partners_audit_shared_with_id"`); | ||||
|         await queryRunner.query(`DROP INDEX "public"."IDX_partners_audit_shared_by_id"`); | ||||
|         await queryRunner.query(`DROP TRIGGER partners_delete_audit`); | ||||
|         await queryRunner.query(`DROP FUNCTION partners_delete_audit`); | ||||
|         await queryRunner.query(`DROP TABLE "partners_audit"`); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| @ -56,4 +56,26 @@ export class SyncRepository { | ||||
|       .orderBy(['id asc']) | ||||
|       .stream(); | ||||
|   } | ||||
| 
 | ||||
|   getPartnerUpserts(userId: string, ack?: SyncAck) { | ||||
|     return this.db | ||||
|       .selectFrom('partners') | ||||
|       .select(['sharedById', 'sharedWithId', 'inTimeline', 'updateId']) | ||||
|       .$if(!!ack, (qb) => qb.where('updateId', '>', ack!.updateId)) | ||||
|       .where((eb) => eb.or([eb('sharedById', '=', userId), eb('sharedWithId', '=', userId)])) | ||||
|       .where('updatedAt', '<', sql.raw<Date>("now() - interval '1 millisecond'")) | ||||
|       .orderBy(['updateId asc']) | ||||
|       .stream(); | ||||
|   } | ||||
| 
 | ||||
|   getPartnerDeletes(userId: string, ack?: SyncAck) { | ||||
|     return this.db | ||||
|       .selectFrom('partners_audit') | ||||
|       .select(['id', 'sharedById', 'sharedWithId']) | ||||
|       .$if(!!ack, (qb) => qb.where('id', '>', ack!.updateId)) | ||||
|       .where((eb) => eb.or([eb('sharedById', '=', userId), eb('sharedWithId', '=', userId)])) | ||||
|       .where('deletedAt', '<', sql.raw<Date>("now() - interval '1 millisecond'")) | ||||
|       .orderBy(['id asc']) | ||||
|       .stream(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -25,6 +25,7 @@ const FULL_SYNC = { needsFullSync: true, deleted: [], upserted: [] }; | ||||
| const SYNC_TYPES_ORDER = [ | ||||
|   //
 | ||||
|   SyncRequestType.UsersV1, | ||||
|   SyncRequestType.PartnersV1, | ||||
| ]; | ||||
| 
 | ||||
| const throwSessionRequired = () => { | ||||
| @ -81,8 +82,6 @@ export class SyncService extends BaseService { | ||||
|       checkpoints.map(({ type, ack }) => [type, fromAck(ack)]), | ||||
|     ); | ||||
| 
 | ||||
|     // TODO pre-filter/sort list based on optimal sync order
 | ||||
| 
 | ||||
|     for (const type of SYNC_TYPES_ORDER.filter((type) => dto.types.includes(type))) { | ||||
|       switch (type) { | ||||
|         case SyncRequestType.UsersV1: { | ||||
| @ -99,6 +98,23 @@ export class SyncService extends BaseService { | ||||
|           break; | ||||
|         } | ||||
| 
 | ||||
|         case SyncRequestType.PartnersV1: { | ||||
|           const deletes = this.syncRepository.getPartnerDeletes( | ||||
|             auth.user.id, | ||||
|             checkpointMap[SyncEntityType.PartnerDeleteV1], | ||||
|           ); | ||||
|           for await (const { id, ...data } of deletes) { | ||||
|             response.write(serialize({ type: SyncEntityType.PartnerDeleteV1, updateId: id, data })); | ||||
|           } | ||||
| 
 | ||||
|           const upserts = this.syncRepository.getPartnerUpserts(auth.user.id, checkpointMap[SyncEntityType.PartnerV1]); | ||||
|           for await (const { updateId, ...data } of upserts) { | ||||
|             response.write(serialize({ type: SyncEntityType.PartnerV1, updateId, data })); | ||||
|           } | ||||
| 
 | ||||
|           break; | ||||
|         } | ||||
| 
 | ||||
|         default: { | ||||
|           this.logger.warn(`Unsupported sync type: ${type}`); | ||||
|           break; | ||||
|  | ||||
| @ -1,11 +1,12 @@ | ||||
| import { Insertable, Kysely } from 'kysely'; | ||||
| import { randomBytes, randomUUID } from 'node:crypto'; | ||||
| import { Writable } from 'node:stream'; | ||||
| import { Assets, DB, Sessions, Users } from 'src/db'; | ||||
| import { Assets, DB, Partners, Sessions, Users } from 'src/db'; | ||||
| import { AuthDto } from 'src/dtos/auth.dto'; | ||||
| import { AssetType } from 'src/enum'; | ||||
| import { AlbumRepository } from 'src/repositories/album.repository'; | ||||
| import { AssetRepository } from 'src/repositories/asset.repository'; | ||||
| import { PartnerRepository } from 'src/repositories/partner.repository'; | ||||
| import { SessionRepository } from 'src/repositories/session.repository'; | ||||
| import { SyncRepository } from 'src/repositories/sync.repository'; | ||||
| import { UserRepository } from 'src/repositories/user.repository'; | ||||
| @ -30,6 +31,7 @@ class CustomWritable extends Writable { | ||||
| type Asset = Insertable<Assets>; | ||||
| type User = Partial<Insertable<Users>>; | ||||
| type Session = Omit<Insertable<Sessions>, 'token'> & { token?: string }; | ||||
| type Partner = Insertable<Partners>; | ||||
| 
 | ||||
| export const newUuid = () => randomUUID() as string; | ||||
| 
 | ||||
| @ -37,6 +39,7 @@ export class TestFactory { | ||||
|   private assets: Asset[] = []; | ||||
|   private sessions: Session[] = []; | ||||
|   private users: User[] = []; | ||||
|   private partners: Partner[] = []; | ||||
| 
 | ||||
|   private constructor(private context: TestContext) {} | ||||
| 
 | ||||
| @ -100,6 +103,17 @@ export class TestFactory { | ||||
|     }; | ||||
|   } | ||||
| 
 | ||||
|   static partner(partner: Partner) { | ||||
|     const defaults = { | ||||
|       inTimeline: true, | ||||
|     }; | ||||
| 
 | ||||
|     return { | ||||
|       ...defaults, | ||||
|       ...partner, | ||||
|     }; | ||||
|   } | ||||
| 
 | ||||
|   withAsset(asset: Asset) { | ||||
|     this.assets.push(asset); | ||||
|     return this; | ||||
| @ -115,6 +129,11 @@ export class TestFactory { | ||||
|     return this; | ||||
|   } | ||||
| 
 | ||||
|   withPartner(partner: Partner) { | ||||
|     this.partners.push(partner); | ||||
|     return this; | ||||
|   } | ||||
| 
 | ||||
|   async create() { | ||||
|     for (const asset of this.assets) { | ||||
|       await this.context.createAsset(asset); | ||||
| @ -124,6 +143,10 @@ export class TestFactory { | ||||
|       await this.context.createUser(user); | ||||
|     } | ||||
| 
 | ||||
|     for (const partner of this.partners) { | ||||
|       await this.context.createPartner(partner); | ||||
|     } | ||||
| 
 | ||||
|     for (const session of this.sessions) { | ||||
|       await this.context.createSession(session); | ||||
|     } | ||||
| @ -138,6 +161,7 @@ export class TestContext { | ||||
|   albumRepository: AlbumRepository; | ||||
|   sessionRepository: SessionRepository; | ||||
|   syncRepository: SyncRepository; | ||||
|   partnerRepository: PartnerRepository; | ||||
| 
 | ||||
|   private constructor(private db: Kysely<DB>) { | ||||
|     this.userRepository = new UserRepository(this.db); | ||||
| @ -145,6 +169,7 @@ export class TestContext { | ||||
|     this.albumRepository = new AlbumRepository(this.db); | ||||
|     this.sessionRepository = new SessionRepository(this.db); | ||||
|     this.syncRepository = new SyncRepository(this.db); | ||||
|     this.partnerRepository = new PartnerRepository(this.db); | ||||
|   } | ||||
| 
 | ||||
|   static from(db: Kysely<DB>) { | ||||
| @ -159,6 +184,10 @@ export class TestContext { | ||||
|     return this.userRepository.create(TestFactory.user(user)); | ||||
|   } | ||||
| 
 | ||||
|   createPartner(partner: Partner) { | ||||
|     return this.partnerRepository.create(TestFactory.partner(partner)); | ||||
|   } | ||||
| 
 | ||||
|   createAsset(asset: Asset) { | ||||
|     return this.assetRepository.create(TestFactory.asset(asset)); | ||||
|   } | ||||
|  | ||||
| @ -17,6 +17,8 @@ const setup = async () => { | ||||
| 
 | ||||
|   const testSync = async (auth: AuthDto, types: SyncRequestType[]) => { | ||||
|     const stream = TestFactory.stream(); | ||||
|     // 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(); | ||||
| @ -186,4 +188,178 @@ describe(SyncService.name, () => { | ||||
|       ); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe.concurrent('partners', () => { | ||||
|     it('should detect and sync the first partner', async () => { | ||||
|       const { auth, context, sut, testSync } = await setup(); | ||||
| 
 | ||||
|       const user1 = auth.user; | ||||
|       const user2 = await context.createUser(); | ||||
| 
 | ||||
|       const partner = await context.createPartner({ 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, context, sut, testSync } = await setup(); | ||||
| 
 | ||||
|       const user1 = auth.user; | ||||
|       const user2 = await context.createUser(); | ||||
| 
 | ||||
|       const partner = await context.createPartner({ sharedById: user2.id, sharedWithId: user1.id }); | ||||
|       await context.partnerRepository.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, context, sut, testSync } = await setup(); | ||||
| 
 | ||||
|       const user1 = auth.user; | ||||
|       const user2 = await context.createUser(); | ||||
| 
 | ||||
|       const partner1 = await context.createPartner({ sharedById: user2.id, sharedWithId: user1.id }); | ||||
|       const partner2 = await context.createPartner({ 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, context, sut, testSync } = await setup(); | ||||
| 
 | ||||
|       const user1 = auth.user; | ||||
|       const user2 = await context.createUser(); | ||||
| 
 | ||||
|       const partner = await context.createPartner({ 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 context.partnerRepository.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 for an unrelated user', async () => { | ||||
|       const { auth, context, testSync } = await setup(); | ||||
| 
 | ||||
|       const user2 = await context.createUser(); | ||||
|       const user3 = await context.createUser(); | ||||
| 
 | ||||
|       await context.createPartner({ sharedById: user2.id, sharedWithId: user3.id }); | ||||
| 
 | ||||
|       const response = await testSync(auth, [SyncRequestType.PartnersV1]); | ||||
| 
 | ||||
|       expect(response).toHaveLength(0); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| @ -9,5 +9,7 @@ export const newSyncRepositoryMock = (): Mocked<RepositoryInterface<SyncReposito | ||||
|     deleteCheckpoints: vitest.fn(), | ||||
|     getUserUpserts: vitest.fn(), | ||||
|     getUserDeletes: vitest.fn(), | ||||
|     getPartnerUpserts: vitest.fn(), | ||||
|     getPartnerDeletes: vitest.fn(), | ||||
|   }; | ||||
| }; | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user