You've already forked immich
mirror of
https://github.com/immich-app/immich.git
synced 2025-07-15 07:14:42 +02:00
feat: pending sync reset flag (#19861)
This commit is contained in:
@ -116,6 +116,7 @@ export const deviceDto = {
|
||||
createdAt: expect.any(String),
|
||||
updatedAt: expect.any(String),
|
||||
current: true,
|
||||
isPendingSyncReset: false,
|
||||
deviceOS: '',
|
||||
deviceType: '',
|
||||
},
|
||||
|
2
mobile/openapi/README.md
generated
2
mobile/openapi/README.md
generated
@ -208,6 +208,7 @@ Class | Method | HTTP request | Description
|
||||
*SessionsApi* | [**deleteSession**](doc//SessionsApi.md#deletesession) | **DELETE** /sessions/{id} |
|
||||
*SessionsApi* | [**getSessions**](doc//SessionsApi.md#getsessions) | **GET** /sessions |
|
||||
*SessionsApi* | [**lockSession**](doc//SessionsApi.md#locksession) | **POST** /sessions/{id}/lock |
|
||||
*SessionsApi* | [**updateSession**](doc//SessionsApi.md#updatesession) | **PUT** /sessions/{id} |
|
||||
*SharedLinksApi* | [**addSharedLinkAssets**](doc//SharedLinksApi.md#addsharedlinkassets) | **PUT** /shared-links/{id}/assets |
|
||||
*SharedLinksApi* | [**createSharedLink**](doc//SharedLinksApi.md#createsharedlink) | **POST** /shared-links |
|
||||
*SharedLinksApi* | [**getAllSharedLinks**](doc//SharedLinksApi.md#getallsharedlinks) | **GET** /shared-links |
|
||||
@ -449,6 +450,7 @@ Class | Method | HTTP request | Description
|
||||
- [SessionCreateResponseDto](doc//SessionCreateResponseDto.md)
|
||||
- [SessionResponseDto](doc//SessionResponseDto.md)
|
||||
- [SessionUnlockDto](doc//SessionUnlockDto.md)
|
||||
- [SessionUpdateDto](doc//SessionUpdateDto.md)
|
||||
- [SharedLinkCreateDto](doc//SharedLinkCreateDto.md)
|
||||
- [SharedLinkEditDto](doc//SharedLinkEditDto.md)
|
||||
- [SharedLinkResponseDto](doc//SharedLinkResponseDto.md)
|
||||
|
1
mobile/openapi/lib/api.dart
generated
1
mobile/openapi/lib/api.dart
generated
@ -232,6 +232,7 @@ part 'model/session_create_dto.dart';
|
||||
part 'model/session_create_response_dto.dart';
|
||||
part 'model/session_response_dto.dart';
|
||||
part 'model/session_unlock_dto.dart';
|
||||
part 'model/session_update_dto.dart';
|
||||
part 'model/shared_link_create_dto.dart';
|
||||
part 'model/shared_link_edit_dto.dart';
|
||||
part 'model/shared_link_response_dto.dart';
|
||||
|
52
mobile/openapi/lib/api/sessions_api.dart
generated
52
mobile/openapi/lib/api/sessions_api.dart
generated
@ -219,4 +219,56 @@ class SessionsApi {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'PUT /sessions/{id}' operation and returns the [Response].
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
///
|
||||
/// * [SessionUpdateDto] sessionUpdateDto (required):
|
||||
Future<Response> updateSessionWithHttpInfo(String id, SessionUpdateDto sessionUpdateDto,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/sessions/{id}'
|
||||
.replaceAll('{id}', id);
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody = sessionUpdateDto;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
const contentTypes = <String>['application/json'];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
apiPath,
|
||||
'PUT',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
///
|
||||
/// * [SessionUpdateDto] sessionUpdateDto (required):
|
||||
Future<SessionResponseDto?> updateSession(String id, SessionUpdateDto sessionUpdateDto,) async {
|
||||
final response = await updateSessionWithHttpInfo(id, sessionUpdateDto,);
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
// When a remote server returns no body with a status of 204, we shall not decode it.
|
||||
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||
// FormatException when trying to decode an empty string.
|
||||
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'SessionResponseDto',) as SessionResponseDto;
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
2
mobile/openapi/lib/api_client.dart
generated
2
mobile/openapi/lib/api_client.dart
generated
@ -520,6 +520,8 @@ class ApiClient {
|
||||
return SessionResponseDto.fromJson(value);
|
||||
case 'SessionUnlockDto':
|
||||
return SessionUnlockDto.fromJson(value);
|
||||
case 'SessionUpdateDto':
|
||||
return SessionUpdateDto.fromJson(value);
|
||||
case 'SharedLinkCreateDto':
|
||||
return SharedLinkCreateDto.fromJson(value);
|
||||
case 'SharedLinkEditDto':
|
||||
|
@ -19,6 +19,7 @@ class SessionCreateResponseDto {
|
||||
required this.deviceType,
|
||||
this.expiresAt,
|
||||
required this.id,
|
||||
required this.isPendingSyncReset,
|
||||
required this.token,
|
||||
required this.updatedAt,
|
||||
});
|
||||
@ -41,6 +42,8 @@ class SessionCreateResponseDto {
|
||||
|
||||
String id;
|
||||
|
||||
bool isPendingSyncReset;
|
||||
|
||||
String token;
|
||||
|
||||
String updatedAt;
|
||||
@ -53,6 +56,7 @@ class SessionCreateResponseDto {
|
||||
other.deviceType == deviceType &&
|
||||
other.expiresAt == expiresAt &&
|
||||
other.id == id &&
|
||||
other.isPendingSyncReset == isPendingSyncReset &&
|
||||
other.token == token &&
|
||||
other.updatedAt == updatedAt;
|
||||
|
||||
@ -65,11 +69,12 @@ class SessionCreateResponseDto {
|
||||
(deviceType.hashCode) +
|
||||
(expiresAt == null ? 0 : expiresAt!.hashCode) +
|
||||
(id.hashCode) +
|
||||
(isPendingSyncReset.hashCode) +
|
||||
(token.hashCode) +
|
||||
(updatedAt.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'SessionCreateResponseDto[createdAt=$createdAt, current=$current, deviceOS=$deviceOS, deviceType=$deviceType, expiresAt=$expiresAt, id=$id, token=$token, updatedAt=$updatedAt]';
|
||||
String toString() => 'SessionCreateResponseDto[createdAt=$createdAt, current=$current, deviceOS=$deviceOS, deviceType=$deviceType, expiresAt=$expiresAt, id=$id, isPendingSyncReset=$isPendingSyncReset, token=$token, updatedAt=$updatedAt]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
@ -83,6 +88,7 @@ class SessionCreateResponseDto {
|
||||
// json[r'expiresAt'] = null;
|
||||
}
|
||||
json[r'id'] = this.id;
|
||||
json[r'isPendingSyncReset'] = this.isPendingSyncReset;
|
||||
json[r'token'] = this.token;
|
||||
json[r'updatedAt'] = this.updatedAt;
|
||||
return json;
|
||||
@ -103,6 +109,7 @@ class SessionCreateResponseDto {
|
||||
deviceType: mapValueOfType<String>(json, r'deviceType')!,
|
||||
expiresAt: mapValueOfType<String>(json, r'expiresAt'),
|
||||
id: mapValueOfType<String>(json, r'id')!,
|
||||
isPendingSyncReset: mapValueOfType<bool>(json, r'isPendingSyncReset')!,
|
||||
token: mapValueOfType<String>(json, r'token')!,
|
||||
updatedAt: mapValueOfType<String>(json, r'updatedAt')!,
|
||||
);
|
||||
@ -157,6 +164,7 @@ class SessionCreateResponseDto {
|
||||
'deviceOS',
|
||||
'deviceType',
|
||||
'id',
|
||||
'isPendingSyncReset',
|
||||
'token',
|
||||
'updatedAt',
|
||||
};
|
||||
|
10
mobile/openapi/lib/model/session_response_dto.dart
generated
10
mobile/openapi/lib/model/session_response_dto.dart
generated
@ -19,6 +19,7 @@ class SessionResponseDto {
|
||||
required this.deviceType,
|
||||
this.expiresAt,
|
||||
required this.id,
|
||||
required this.isPendingSyncReset,
|
||||
required this.updatedAt,
|
||||
});
|
||||
|
||||
@ -40,6 +41,8 @@ class SessionResponseDto {
|
||||
|
||||
String id;
|
||||
|
||||
bool isPendingSyncReset;
|
||||
|
||||
String updatedAt;
|
||||
|
||||
@override
|
||||
@ -50,6 +53,7 @@ class SessionResponseDto {
|
||||
other.deviceType == deviceType &&
|
||||
other.expiresAt == expiresAt &&
|
||||
other.id == id &&
|
||||
other.isPendingSyncReset == isPendingSyncReset &&
|
||||
other.updatedAt == updatedAt;
|
||||
|
||||
@override
|
||||
@ -61,10 +65,11 @@ class SessionResponseDto {
|
||||
(deviceType.hashCode) +
|
||||
(expiresAt == null ? 0 : expiresAt!.hashCode) +
|
||||
(id.hashCode) +
|
||||
(isPendingSyncReset.hashCode) +
|
||||
(updatedAt.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'SessionResponseDto[createdAt=$createdAt, current=$current, deviceOS=$deviceOS, deviceType=$deviceType, expiresAt=$expiresAt, id=$id, updatedAt=$updatedAt]';
|
||||
String toString() => 'SessionResponseDto[createdAt=$createdAt, current=$current, deviceOS=$deviceOS, deviceType=$deviceType, expiresAt=$expiresAt, id=$id, isPendingSyncReset=$isPendingSyncReset, updatedAt=$updatedAt]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
@ -78,6 +83,7 @@ class SessionResponseDto {
|
||||
// json[r'expiresAt'] = null;
|
||||
}
|
||||
json[r'id'] = this.id;
|
||||
json[r'isPendingSyncReset'] = this.isPendingSyncReset;
|
||||
json[r'updatedAt'] = this.updatedAt;
|
||||
return json;
|
||||
}
|
||||
@ -97,6 +103,7 @@ class SessionResponseDto {
|
||||
deviceType: mapValueOfType<String>(json, r'deviceType')!,
|
||||
expiresAt: mapValueOfType<String>(json, r'expiresAt'),
|
||||
id: mapValueOfType<String>(json, r'id')!,
|
||||
isPendingSyncReset: mapValueOfType<bool>(json, r'isPendingSyncReset')!,
|
||||
updatedAt: mapValueOfType<String>(json, r'updatedAt')!,
|
||||
);
|
||||
}
|
||||
@ -150,6 +157,7 @@ class SessionResponseDto {
|
||||
'deviceOS',
|
||||
'deviceType',
|
||||
'id',
|
||||
'isPendingSyncReset',
|
||||
'updatedAt',
|
||||
};
|
||||
}
|
||||
|
108
mobile/openapi/lib/model/session_update_dto.dart
generated
Normal file
108
mobile/openapi/lib/model/session_update_dto.dart
generated
Normal file
@ -0,0 +1,108 @@
|
||||
//
|
||||
// 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 SessionUpdateDto {
|
||||
/// Returns a new [SessionUpdateDto] instance.
|
||||
SessionUpdateDto({
|
||||
this.isPendingSyncReset,
|
||||
});
|
||||
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
bool? isPendingSyncReset;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is SessionUpdateDto &&
|
||||
other.isPendingSyncReset == isPendingSyncReset;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(isPendingSyncReset == null ? 0 : isPendingSyncReset!.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'SessionUpdateDto[isPendingSyncReset=$isPendingSyncReset]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
if (this.isPendingSyncReset != null) {
|
||||
json[r'isPendingSyncReset'] = this.isPendingSyncReset;
|
||||
} else {
|
||||
// json[r'isPendingSyncReset'] = null;
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [SessionUpdateDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static SessionUpdateDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "SessionUpdateDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return SessionUpdateDto(
|
||||
isPendingSyncReset: mapValueOfType<bool>(json, r'isPendingSyncReset'),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<SessionUpdateDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <SessionUpdateDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = SessionUpdateDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, SessionUpdateDto> mapFromJson(dynamic json) {
|
||||
final map = <String, SessionUpdateDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = SessionUpdateDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of SessionUpdateDto-objects as value to a dart map
|
||||
static Map<String, List<SessionUpdateDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<SessionUpdateDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = SessionUpdateDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
};
|
||||
}
|
||||
|
3
mobile/openapi/lib/model/sync_entity_type.dart
generated
3
mobile/openapi/lib/model/sync_entity_type.dart
generated
@ -59,6 +59,7 @@ class SyncEntityType {
|
||||
static const personV1 = SyncEntityType._(r'PersonV1');
|
||||
static const personDeleteV1 = SyncEntityType._(r'PersonDeleteV1');
|
||||
static const syncAckV1 = SyncEntityType._(r'SyncAckV1');
|
||||
static const syncResetV1 = SyncEntityType._(r'SyncResetV1');
|
||||
|
||||
/// List of all possible values in this [enum][SyncEntityType].
|
||||
static const values = <SyncEntityType>[
|
||||
@ -98,6 +99,7 @@ class SyncEntityType {
|
||||
personV1,
|
||||
personDeleteV1,
|
||||
syncAckV1,
|
||||
syncResetV1,
|
||||
];
|
||||
|
||||
static SyncEntityType? fromJson(dynamic value) => SyncEntityTypeTypeTransformer().decode(value);
|
||||
@ -172,6 +174,7 @@ class SyncEntityTypeTypeTransformer {
|
||||
case r'PersonV1': return SyncEntityType.personV1;
|
||||
case r'PersonDeleteV1': return SyncEntityType.personDeleteV1;
|
||||
case r'SyncAckV1': return SyncEntityType.syncAckV1;
|
||||
case r'SyncResetV1': return SyncEntityType.syncResetV1;
|
||||
default:
|
||||
if (!allowNull) {
|
||||
throw ArgumentError('Unknown enum value to decode: $data');
|
||||
|
19
mobile/openapi/lib/model/sync_stream_dto.dart
generated
19
mobile/openapi/lib/model/sync_stream_dto.dart
generated
@ -13,25 +13,41 @@ part of openapi.api;
|
||||
class SyncStreamDto {
|
||||
/// Returns a new [SyncStreamDto] instance.
|
||||
SyncStreamDto({
|
||||
this.reset,
|
||||
this.types = const [],
|
||||
});
|
||||
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
bool? reset;
|
||||
|
||||
List<SyncRequestType> types;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is SyncStreamDto &&
|
||||
other.reset == reset &&
|
||||
_deepEquality.equals(other.types, types);
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(reset == null ? 0 : reset!.hashCode) +
|
||||
(types.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'SyncStreamDto[types=$types]';
|
||||
String toString() => 'SyncStreamDto[reset=$reset, types=$types]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
if (this.reset != null) {
|
||||
json[r'reset'] = this.reset;
|
||||
} else {
|
||||
// json[r'reset'] = null;
|
||||
}
|
||||
json[r'types'] = this.types;
|
||||
return json;
|
||||
}
|
||||
@ -45,6 +61,7 @@ class SyncStreamDto {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return SyncStreamDto(
|
||||
reset: mapValueOfType<bool>(json, r'reset'),
|
||||
types: SyncRequestType.listFromJson(json[r'types']),
|
||||
);
|
||||
}
|
||||
|
@ -6024,6 +6024,56 @@
|
||||
"tags": [
|
||||
"Sessions"
|
||||
]
|
||||
},
|
||||
"put": {
|
||||
"operationId": "updateSession",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/SessionUpdateDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/SessionResponseDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Sessions"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/sessions/{id}/lock": {
|
||||
@ -12790,6 +12840,9 @@
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"isPendingSyncReset": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"token": {
|
||||
"type": "string"
|
||||
},
|
||||
@ -12803,6 +12856,7 @@
|
||||
"deviceOS",
|
||||
"deviceType",
|
||||
"id",
|
||||
"isPendingSyncReset",
|
||||
"token",
|
||||
"updatedAt"
|
||||
],
|
||||
@ -12828,6 +12882,9 @@
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"isPendingSyncReset": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"updatedAt": {
|
||||
"type": "string"
|
||||
}
|
||||
@ -12838,6 +12895,7 @@
|
||||
"deviceOS",
|
||||
"deviceType",
|
||||
"id",
|
||||
"isPendingSyncReset",
|
||||
"updatedAt"
|
||||
],
|
||||
"type": "object"
|
||||
@ -12854,6 +12912,14 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"SessionUpdateDto": {
|
||||
"properties": {
|
||||
"isPendingSyncReset": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"SharedLinkCreateDto": {
|
||||
"properties": {
|
||||
"albumId": {
|
||||
@ -13836,7 +13902,8 @@
|
||||
"StackDeleteV1",
|
||||
"PersonV1",
|
||||
"PersonDeleteV1",
|
||||
"SyncAckV1"
|
||||
"SyncAckV1",
|
||||
"SyncResetV1"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
@ -14074,6 +14141,10 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"SyncResetV1": {
|
||||
"properties": {},
|
||||
"type": "object"
|
||||
},
|
||||
"SyncStackDeleteV1": {
|
||||
"properties": {
|
||||
"stackId": {
|
||||
@ -14116,6 +14187,9 @@
|
||||
},
|
||||
"SyncStreamDto": {
|
||||
"properties": {
|
||||
"reset": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"types": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/SyncRequestType"
|
||||
|
@ -1164,6 +1164,7 @@ export type SessionResponseDto = {
|
||||
deviceType: string;
|
||||
expiresAt?: string;
|
||||
id: string;
|
||||
isPendingSyncReset: boolean;
|
||||
updatedAt: string;
|
||||
};
|
||||
export type SessionCreateDto = {
|
||||
@ -1179,9 +1180,13 @@ export type SessionCreateResponseDto = {
|
||||
deviceType: string;
|
||||
expiresAt?: string;
|
||||
id: string;
|
||||
isPendingSyncReset: boolean;
|
||||
token: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
export type SessionUpdateDto = {
|
||||
isPendingSyncReset?: boolean;
|
||||
};
|
||||
export type SharedLinkResponseDto = {
|
||||
album?: AlbumResponseDto;
|
||||
allowDownload: boolean;
|
||||
@ -1264,6 +1269,7 @@ export type AssetFullSyncDto = {
|
||||
userId?: string;
|
||||
};
|
||||
export type SyncStreamDto = {
|
||||
reset?: boolean;
|
||||
types: SyncRequestType[];
|
||||
};
|
||||
export type DatabaseBackupConfig = {
|
||||
@ -3170,6 +3176,19 @@ export function deleteSession({ id }: {
|
||||
method: "DELETE"
|
||||
}));
|
||||
}
|
||||
export function updateSession({ id, sessionUpdateDto }: {
|
||||
id: string;
|
||||
sessionUpdateDto: SessionUpdateDto;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: SessionResponseDto;
|
||||
}>(`/sessions/${encodeURIComponent(id)}`, oazapfts.json({
|
||||
...opts,
|
||||
method: "PUT",
|
||||
body: sessionUpdateDto
|
||||
})));
|
||||
}
|
||||
export function lockSession({ id }: {
|
||||
id: string;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
@ -4097,7 +4116,8 @@ export enum SyncEntityType {
|
||||
StackDeleteV1 = "StackDeleteV1",
|
||||
PersonV1 = "PersonV1",
|
||||
PersonDeleteV1 = "PersonDeleteV1",
|
||||
SyncAckV1 = "SyncAckV1"
|
||||
SyncAckV1 = "SyncAckV1",
|
||||
SyncResetV1 = "SyncResetV1"
|
||||
}
|
||||
export enum SyncRequestType {
|
||||
AlbumsV1 = "AlbumsV1",
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post } from '@nestjs/common';
|
||||
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { SessionCreateDto, SessionCreateResponseDto, SessionResponseDto } from 'src/dtos/session.dto';
|
||||
import { SessionCreateDto, SessionCreateResponseDto, SessionResponseDto, SessionUpdateDto } from 'src/dtos/session.dto';
|
||||
import { Permission } from 'src/enum';
|
||||
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
||||
import { SessionService } from 'src/services/session.service';
|
||||
@ -31,6 +31,16 @@ export class SessionController {
|
||||
return this.service.deleteAll(auth);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
@Authenticated({ permission: Permission.SESSION_UPDATE })
|
||||
updateSession(
|
||||
@Auth() auth: AuthDto,
|
||||
@Param() { id }: UUIDParamDto,
|
||||
@Body() dto: SessionUpdateDto,
|
||||
): Promise<SessionResponseDto> {
|
||||
return this.service.update(auth, id, dto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@Authenticated({ permission: Permission.SESSION_DELETE })
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
|
@ -201,6 +201,7 @@ export type Album = Selectable<AlbumTable> & {
|
||||
|
||||
export type AuthSession = {
|
||||
id: string;
|
||||
isPendingSyncReset: boolean;
|
||||
hasElevatedPermission: boolean;
|
||||
};
|
||||
|
||||
@ -238,6 +239,7 @@ export type Session = {
|
||||
deviceOS: string;
|
||||
deviceType: string;
|
||||
pinExpiresAt: Date | null;
|
||||
isPendingSyncReset: boolean;
|
||||
};
|
||||
|
||||
export type Exif = Omit<Selectable<ExifTable>, 'updatedAt' | 'updateId'>;
|
||||
@ -311,7 +313,7 @@ export const columns = {
|
||||
'users.quotaSizeInBytes',
|
||||
],
|
||||
authApiKey: ['api_keys.id', 'api_keys.permissions'],
|
||||
authSession: ['sessions.id', 'sessions.updatedAt', 'sessions.pinExpiresAt'],
|
||||
authSession: ['sessions.id', 'sessions.isPendingSyncReset', 'sessions.updatedAt', 'sessions.pinExpiresAt'],
|
||||
authSharedLink: [
|
||||
'shared_links.id',
|
||||
'shared_links.userId',
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { IsInt, IsPositive, IsString } from 'class-validator';
|
||||
import { Session } from 'src/database';
|
||||
import { Optional } from 'src/validation';
|
||||
import { Optional, ValidateBoolean } from 'src/validation';
|
||||
|
||||
export class SessionCreateDto {
|
||||
/**
|
||||
@ -20,6 +20,11 @@ export class SessionCreateDto {
|
||||
deviceOS?: string;
|
||||
}
|
||||
|
||||
export class SessionUpdateDto {
|
||||
@ValidateBoolean({ optional: true })
|
||||
isPendingSyncReset?: boolean;
|
||||
}
|
||||
|
||||
export class SessionResponseDto {
|
||||
id!: string;
|
||||
createdAt!: string;
|
||||
@ -28,6 +33,7 @@ export class SessionResponseDto {
|
||||
current!: boolean;
|
||||
deviceType!: string;
|
||||
deviceOS!: string;
|
||||
isPendingSyncReset!: boolean;
|
||||
}
|
||||
|
||||
export class SessionCreateResponseDto extends SessionResponseDto {
|
||||
@ -42,4 +48,5 @@ export const mapSession = (entity: Session, currentId?: string): SessionResponse
|
||||
current: currentId === entity.id,
|
||||
deviceOS: entity.deviceOS,
|
||||
deviceType: entity.deviceType,
|
||||
isPendingSyncReset: entity.isPendingSyncReset,
|
||||
});
|
||||
|
@ -11,7 +11,7 @@ import {
|
||||
SyncEntityType,
|
||||
SyncRequestType,
|
||||
} from 'src/enum';
|
||||
import { Optional, ValidateDate, ValidateUUID } from 'src/validation';
|
||||
import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation';
|
||||
|
||||
export class AssetFullSyncDto {
|
||||
@ValidateUUID({ optional: true })
|
||||
@ -256,6 +256,9 @@ export class SyncPersonDeleteV1 {
|
||||
@ExtraModel()
|
||||
export class SyncAckV1 {}
|
||||
|
||||
@ExtraModel()
|
||||
export class SyncResetV1 {}
|
||||
|
||||
export type SyncItem = {
|
||||
[SyncEntityType.UserV1]: SyncUserV1;
|
||||
[SyncEntityType.UserDeleteV1]: SyncUserDeleteV1;
|
||||
@ -293,12 +296,16 @@ export type SyncItem = {
|
||||
[SyncEntityType.PersonV1]: SyncPersonV1;
|
||||
[SyncEntityType.PersonDeleteV1]: SyncPersonDeleteV1;
|
||||
[SyncEntityType.SyncAckV1]: SyncAckV1;
|
||||
[SyncEntityType.SyncResetV1]: SyncResetV1;
|
||||
};
|
||||
|
||||
export class SyncStreamDto {
|
||||
@IsEnum(SyncRequestType, { each: true })
|
||||
@ApiProperty({ enumName: 'SyncRequestType', enum: SyncRequestType, isArray: true })
|
||||
types!: SyncRequestType[];
|
||||
|
||||
@ValidateBoolean({ optional: true })
|
||||
reset?: boolean;
|
||||
}
|
||||
|
||||
export class SyncAckDto {
|
||||
|
@ -640,6 +640,7 @@ export enum SyncEntityType {
|
||||
PersonDeleteV1 = 'PersonDeleteV1',
|
||||
|
||||
SyncAckV1 = 'SyncAckV1',
|
||||
SyncResetV1 = 'SyncResetV1',
|
||||
}
|
||||
|
||||
export enum NotificationLevel {
|
||||
|
@ -13,6 +13,7 @@ where
|
||||
-- SessionRepository.getByToken
|
||||
select
|
||||
"sessions"."id",
|
||||
"sessions"."isPendingSyncReset",
|
||||
"sessions"."updatedAt",
|
||||
"sessions"."pinExpiresAt",
|
||||
(
|
||||
@ -71,3 +72,15 @@ set
|
||||
"pinExpiresAt" = $1
|
||||
where
|
||||
"userId" = $2
|
||||
|
||||
-- SessionRepository.resetSyncProgress
|
||||
begin
|
||||
update "sessions"
|
||||
set
|
||||
"isPendingSyncReset" = $1
|
||||
where
|
||||
"id" = $2
|
||||
delete from "session_sync_checkpoints"
|
||||
where
|
||||
"sessionId" = $1
|
||||
commit
|
||||
|
@ -95,4 +95,14 @@ export class SessionRepository {
|
||||
async lockAll(userId: string) {
|
||||
await this.db.updateTable('sessions').set({ pinExpiresAt: null }).where('userId', '=', userId).execute();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
async resetSyncProgress(sessionId: string) {
|
||||
await this.db.transaction().execute((tx) => {
|
||||
return Promise.all([
|
||||
tx.updateTable('sessions').set({ isPendingSyncReset: false }).where('id', '=', sessionId).execute(),
|
||||
tx.deleteFrom('session_sync_checkpoints').where('sessionId', '=', sessionId).execute(),
|
||||
]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,9 @@
|
||||
import { Kysely, sql } from 'kysely';
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await sql`ALTER TABLE "sessions" ADD "isPendingSyncReset" boolean NOT NULL DEFAULT false;`.execute(db);
|
||||
}
|
||||
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await sql`ALTER TABLE "sessions" DROP COLUMN "isPendingSyncReset";`.execute(db);
|
||||
}
|
@ -45,6 +45,9 @@ export class SessionTable {
|
||||
@UpdateIdColumn({ indexName: 'IDX_sessions_update_id' })
|
||||
updateId!: Generated<string>;
|
||||
|
||||
@Column({ type: 'boolean', default: false })
|
||||
isPendingSyncReset!: Generated<boolean>;
|
||||
|
||||
@Column({ type: 'timestamp with time zone', nullable: true })
|
||||
pinExpiresAt!: Timestamp | null;
|
||||
}
|
||||
|
@ -241,6 +241,7 @@ describe(AuthService.name, () => {
|
||||
const sessionWithToken = {
|
||||
id: session.id,
|
||||
updatedAt: session.updatedAt,
|
||||
isPendingSyncReset: false,
|
||||
user: factory.authUser(),
|
||||
pinExpiresAt: null,
|
||||
};
|
||||
@ -255,7 +256,11 @@ describe(AuthService.name, () => {
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
user: sessionWithToken.user,
|
||||
session: { id: session.id, hasElevatedPermission: false },
|
||||
session: {
|
||||
id: session.id,
|
||||
hasElevatedPermission: false,
|
||||
isPendingSyncReset: session.isPendingSyncReset,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -366,6 +371,7 @@ describe(AuthService.name, () => {
|
||||
id: session.id,
|
||||
updatedAt: session.updatedAt,
|
||||
user: factory.authUser(),
|
||||
isPendingSyncReset: false,
|
||||
pinExpiresAt: null,
|
||||
};
|
||||
|
||||
@ -379,7 +385,11 @@ describe(AuthService.name, () => {
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
user: sessionWithToken.user,
|
||||
session: { id: session.id, hasElevatedPermission: false },
|
||||
session: {
|
||||
id: session.id,
|
||||
hasElevatedPermission: false,
|
||||
isPendingSyncReset: session.isPendingSyncReset,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@ -389,6 +399,7 @@ describe(AuthService.name, () => {
|
||||
id: session.id,
|
||||
updatedAt: session.updatedAt,
|
||||
user: factory.authUser(),
|
||||
isPendingSyncReset: false,
|
||||
pinExpiresAt: null,
|
||||
};
|
||||
|
||||
@ -409,6 +420,7 @@ describe(AuthService.name, () => {
|
||||
id: session.id,
|
||||
updatedAt: session.updatedAt,
|
||||
user: factory.authUser(),
|
||||
isPendingSyncReset: false,
|
||||
pinExpiresAt: null,
|
||||
};
|
||||
|
||||
|
@ -466,6 +466,7 @@ export class AuthService extends BaseService {
|
||||
user: session.user,
|
||||
session: {
|
||||
id: session.id,
|
||||
isPendingSyncReset: session.isPendingSyncReset,
|
||||
hasElevatedPermission,
|
||||
},
|
||||
};
|
||||
|
@ -2,7 +2,13 @@ import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { DateTime } from 'luxon';
|
||||
import { OnJob } from 'src/decorators';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { SessionCreateDto, SessionCreateResponseDto, SessionResponseDto, mapSession } from 'src/dtos/session.dto';
|
||||
import {
|
||||
SessionCreateDto,
|
||||
SessionCreateResponseDto,
|
||||
SessionResponseDto,
|
||||
SessionUpdateDto,
|
||||
mapSession,
|
||||
} from 'src/dtos/session.dto';
|
||||
import { JobName, JobStatus, Permission, QueueName } from 'src/enum';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
|
||||
@ -44,6 +50,20 @@ export class SessionService extends BaseService {
|
||||
return sessions.map((session) => mapSession(session, auth.session?.id));
|
||||
}
|
||||
|
||||
async update(auth: AuthDto, id: string, dto: SessionUpdateDto): Promise<SessionResponseDto> {
|
||||
await this.requireAccess({ auth, permission: Permission.SESSION_UPDATE, ids: [id] });
|
||||
|
||||
if (Object.values(dto).filter((prop) => prop !== undefined).length === 0) {
|
||||
throw new BadRequestException('No fields to update');
|
||||
}
|
||||
|
||||
const session = await this.sessionRepository.update(id, {
|
||||
isPendingSyncReset: dto.isPendingSyncReset,
|
||||
});
|
||||
|
||||
return mapSession(session);
|
||||
}
|
||||
|
||||
async delete(auth: AuthDto, id: string): Promise<void> {
|
||||
await this.requireAccess({ auth, permission: Permission.AUTH_DEVICE_DELETE, ids: [id] });
|
||||
await this.sessionRepository.delete(id);
|
||||
|
@ -22,7 +22,7 @@ import { SyncAck } from 'src/types';
|
||||
import { getMyPartnerIds } from 'src/utils/asset.util';
|
||||
import { hexOrBufferToBase64 } from 'src/utils/bytes';
|
||||
import { setIsEqual } from 'src/utils/set';
|
||||
import { fromAck, serialize, SerializeOptions, toAck } from 'src/utils/sync';
|
||||
import { fromAck, mapJsonLine, serialize, SerializeOptions, toAck } from 'src/utils/sync';
|
||||
|
||||
type CheckpointMap = Partial<Record<SyncEntityType, SyncAck>>;
|
||||
type AssetLike = Omit<SyncAssetV1, 'checksum' | 'thumbhash'> & {
|
||||
@ -118,30 +118,42 @@ export class SyncService extends BaseService {
|
||||
}
|
||||
|
||||
async stream(auth: AuthDto, response: Writable, dto: SyncStreamDto) {
|
||||
const sessionId = auth.session?.id;
|
||||
if (!sessionId) {
|
||||
const session = auth.session;
|
||||
if (!session) {
|
||||
return throwSessionRequired();
|
||||
}
|
||||
|
||||
const checkpoints = await this.syncCheckpointRepository.getAll(sessionId);
|
||||
if (dto.reset) {
|
||||
await this.sessionRepository.resetSyncProgress(session.id);
|
||||
session.isPendingSyncReset = false;
|
||||
}
|
||||
|
||||
if (session.isPendingSyncReset) {
|
||||
response.write(mapJsonLine({ type: SyncEntityType.SyncResetV1, data: {} }));
|
||||
response.end();
|
||||
return;
|
||||
}
|
||||
|
||||
const checkpoints = await this.syncCheckpointRepository.getAll(session.id);
|
||||
const checkpointMap: CheckpointMap = Object.fromEntries(checkpoints.map(({ type, ack }) => [type, fromAck(ack)]));
|
||||
|
||||
const handlers: Record<SyncRequestType, () => Promise<void>> = {
|
||||
[SyncRequestType.UsersV1]: () => this.syncUsersV1(response, checkpointMap),
|
||||
[SyncRequestType.PartnersV1]: () => this.syncPartnersV1(response, checkpointMap, auth),
|
||||
[SyncRequestType.AssetsV1]: () => this.syncAssetsV1(response, checkpointMap, auth),
|
||||
[SyncRequestType.AssetExifsV1]: () => this.syncAssetExifsV1(response, checkpointMap, auth),
|
||||
[SyncRequestType.PartnerAssetsV1]: () => this.syncPartnerAssetsV1(response, checkpointMap, auth, sessionId),
|
||||
[SyncRequestType.PartnerAssetsV1]: () => this.syncPartnerAssetsV1(response, checkpointMap, auth, session.id),
|
||||
[SyncRequestType.PartnerAssetExifsV1]: () =>
|
||||
this.syncPartnerAssetExifsV1(response, checkpointMap, auth, sessionId),
|
||||
this.syncPartnerAssetExifsV1(response, checkpointMap, auth, session.id),
|
||||
[SyncRequestType.AlbumsV1]: () => this.syncAlbumsV1(response, checkpointMap, auth),
|
||||
[SyncRequestType.AlbumUsersV1]: () => this.syncAlbumUsersV1(response, checkpointMap, auth, sessionId),
|
||||
[SyncRequestType.AlbumAssetsV1]: () => this.syncAlbumAssetsV1(response, checkpointMap, auth, sessionId),
|
||||
[SyncRequestType.AlbumToAssetsV1]: () => this.syncAlbumToAssetsV1(response, checkpointMap, auth, sessionId),
|
||||
[SyncRequestType.AlbumAssetExifsV1]: () => this.syncAlbumAssetExifsV1(response, checkpointMap, auth, sessionId),
|
||||
[SyncRequestType.AlbumUsersV1]: () => this.syncAlbumUsersV1(response, checkpointMap, auth, session.id),
|
||||
[SyncRequestType.AlbumAssetsV1]: () => this.syncAlbumAssetsV1(response, checkpointMap, auth, session.id),
|
||||
[SyncRequestType.AlbumToAssetsV1]: () => this.syncAlbumToAssetsV1(response, checkpointMap, auth, session.id),
|
||||
[SyncRequestType.AlbumAssetExifsV1]: () => this.syncAlbumAssetExifsV1(response, checkpointMap, auth, session.id),
|
||||
[SyncRequestType.MemoriesV1]: () => this.syncMemoriesV1(response, checkpointMap, auth),
|
||||
[SyncRequestType.MemoryToAssetsV1]: () => this.syncMemoryAssetsV1(response, checkpointMap, auth),
|
||||
[SyncRequestType.StacksV1]: () => this.syncStackV1(response, checkpointMap, auth),
|
||||
[SyncRequestType.PartnerStacksV1]: () => this.syncPartnerStackV1(response, checkpointMap, auth, sessionId),
|
||||
[SyncRequestType.PartnerStacksV1]: () => this.syncPartnerStackV1(response, checkpointMap, auth, session.id),
|
||||
[SyncRequestType.PeopleV1]: () => this.syncPeopleV1(response, checkpointMap, auth),
|
||||
};
|
||||
|
||||
|
@ -234,11 +234,11 @@ export class SyncTestContext extends MediumTestContext<SyncService> {
|
||||
});
|
||||
}
|
||||
|
||||
async syncStream(auth: AuthDto, types: SyncRequestType[]) {
|
||||
async syncStream(auth: AuthDto, types: SyncRequestType[], reset?: boolean) {
|
||||
const stream = mediumFactory.syncStream();
|
||||
// Wait for 2ms to ensure all updates are available and account for setTimeout inaccuracy
|
||||
await wait(2);
|
||||
await this.sut.stream(auth, stream, { types });
|
||||
await this.sut.stream(auth, stream, { types, reset });
|
||||
|
||||
return stream.getResponse();
|
||||
}
|
||||
@ -481,6 +481,7 @@ const sessionInsert = ({
|
||||
const defaults: Insertable<SessionTable> = {
|
||||
id,
|
||||
userId,
|
||||
isPendingSyncReset: false,
|
||||
token: sha256(id),
|
||||
};
|
||||
|
||||
|
63
server/test/medium/specs/sync/sync-reset.spec.ts
Normal file
63
server/test/medium/specs/sync/sync-reset.spec.ts
Normal file
@ -0,0 +1,63 @@
|
||||
import { Kysely } from 'kysely';
|
||||
import { SyncEntityType, SyncRequestType } from 'src/enum';
|
||||
import { DB } from 'src/schema';
|
||||
import { SyncTestContext } from 'test/medium.factory';
|
||||
import { getKyselyDB } from 'test/utils';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
const setup = async (db?: Kysely<DB>) => {
|
||||
const ctx = new SyncTestContext(db || defaultDatabase);
|
||||
const { auth, user, session } = await ctx.newSyncAuthUser();
|
||||
return { auth, user, session, ctx };
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
defaultDatabase = await getKyselyDB();
|
||||
});
|
||||
|
||||
describe(SyncEntityType.SyncResetV1, () => {
|
||||
it('should work', async () => {
|
||||
const { auth, ctx } = await setup();
|
||||
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.AssetsV1]);
|
||||
expect(response).toEqual([]);
|
||||
});
|
||||
|
||||
it('should detect a pending sync reset', async () => {
|
||||
const { auth, ctx } = await setup();
|
||||
|
||||
auth.session!.isPendingSyncReset = true;
|
||||
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.AssetsV1]);
|
||||
expect(response).toEqual([{ type: SyncEntityType.SyncResetV1, data: {} }]);
|
||||
});
|
||||
|
||||
it('should not send other dtos when a reset is pending', async () => {
|
||||
const { auth, user, ctx } = await setup();
|
||||
|
||||
await ctx.newAsset({ ownerId: user.id });
|
||||
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.AssetsV1])).resolves.toHaveLength(1);
|
||||
|
||||
auth.session!.isPendingSyncReset = true;
|
||||
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.AssetsV1])).resolves.toEqual([
|
||||
{ type: SyncEntityType.SyncResetV1, data: {} },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should allow resetting a pending reset when requesting changes ', async () => {
|
||||
const { auth, user, ctx } = await setup();
|
||||
|
||||
await ctx.newAsset({ ownerId: user.id });
|
||||
|
||||
auth.session!.isPendingSyncReset = true;
|
||||
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.AssetsV1], true)).resolves.toEqual([
|
||||
expect.objectContaining({
|
||||
type: SyncEntityType.AssetV1,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
@ -58,7 +58,11 @@ const authFactory = ({
|
||||
}
|
||||
|
||||
if (session) {
|
||||
auth.session = { id: session.id, hasElevatedPermission: false };
|
||||
auth.session = {
|
||||
id: session.id,
|
||||
isPendingSyncReset: false,
|
||||
hasElevatedPermission: false,
|
||||
};
|
||||
}
|
||||
|
||||
if (sharedLink) {
|
||||
@ -131,6 +135,7 @@ const sessionFactory = (session: Partial<Session> = {}) => ({
|
||||
expiresAt: null,
|
||||
userId: newUuid(),
|
||||
pinExpiresAt: newDate(),
|
||||
isPendingSyncReset: false,
|
||||
...session,
|
||||
});
|
||||
|
||||
|
Reference in New Issue
Block a user