feat: sync auth user (#20067)

This commit is contained in:
Jason Rasmussen 2025-07-23 09:59:33 -04:00 committed by GitHub
parent ab597155fa
commit 92384c28de
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 507 additions and 41 deletions

@ -1 +1 @@
Subproject commit 18736fc27a80c99c68e856cdb4f842bc81ed3445 Subproject commit 37f60ea537c0228f5f92e4f42dc42f0bb39a6d7f

View File

@ -479,6 +479,7 @@ Class | Method | HTTP request | Description
- [SyncAssetFaceDeleteV1](doc//SyncAssetFaceDeleteV1.md) - [SyncAssetFaceDeleteV1](doc//SyncAssetFaceDeleteV1.md)
- [SyncAssetFaceV1](doc//SyncAssetFaceV1.md) - [SyncAssetFaceV1](doc//SyncAssetFaceV1.md)
- [SyncAssetV1](doc//SyncAssetV1.md) - [SyncAssetV1](doc//SyncAssetV1.md)
- [SyncAuthUserV1](doc//SyncAuthUserV1.md)
- [SyncEntityType](doc//SyncEntityType.md) - [SyncEntityType](doc//SyncEntityType.md)
- [SyncMemoryAssetDeleteV1](doc//SyncMemoryAssetDeleteV1.md) - [SyncMemoryAssetDeleteV1](doc//SyncMemoryAssetDeleteV1.md)
- [SyncMemoryAssetV1](doc//SyncMemoryAssetV1.md) - [SyncMemoryAssetV1](doc//SyncMemoryAssetV1.md)

View File

@ -260,6 +260,7 @@ part 'model/sync_asset_exif_v1.dart';
part 'model/sync_asset_face_delete_v1.dart'; part 'model/sync_asset_face_delete_v1.dart';
part 'model/sync_asset_face_v1.dart'; part 'model/sync_asset_face_v1.dart';
part 'model/sync_asset_v1.dart'; part 'model/sync_asset_v1.dart';
part 'model/sync_auth_user_v1.dart';
part 'model/sync_entity_type.dart'; part 'model/sync_entity_type.dart';
part 'model/sync_memory_asset_delete_v1.dart'; part 'model/sync_memory_asset_delete_v1.dart';
part 'model/sync_memory_asset_v1.dart'; part 'model/sync_memory_asset_v1.dart';

View File

@ -576,6 +576,8 @@ class ApiClient {
return SyncAssetFaceV1.fromJson(value); return SyncAssetFaceV1.fromJson(value);
case 'SyncAssetV1': case 'SyncAssetV1':
return SyncAssetV1.fromJson(value); return SyncAssetV1.fromJson(value);
case 'SyncAuthUserV1':
return SyncAuthUserV1.fromJson(value);
case 'SyncEntityType': case 'SyncEntityType':
return SyncEntityTypeTypeTransformer().decode(value); return SyncEntityTypeTypeTransformer().decode(value);
case 'SyncMemoryAssetDeleteV1': case 'SyncMemoryAssetDeleteV1':

View File

@ -0,0 +1,215 @@
//
// 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 SyncAuthUserV1 {
/// Returns a new [SyncAuthUserV1] instance.
SyncAuthUserV1({
required this.avatarColor,
required this.deletedAt,
required this.email,
required this.hasProfileImage,
required this.id,
required this.isAdmin,
required this.name,
required this.oauthId,
required this.pinCode,
required this.profileChangedAt,
required this.quotaSizeInBytes,
required this.quotaUsageInBytes,
required this.storageLabel,
});
UserAvatarColor? avatarColor;
DateTime? deletedAt;
String email;
bool hasProfileImage;
String id;
bool isAdmin;
String name;
String oauthId;
String? pinCode;
DateTime profileChangedAt;
int? quotaSizeInBytes;
int quotaUsageInBytes;
String? storageLabel;
@override
bool operator ==(Object other) => identical(this, other) || other is SyncAuthUserV1 &&
other.avatarColor == avatarColor &&
other.deletedAt == deletedAt &&
other.email == email &&
other.hasProfileImage == hasProfileImage &&
other.id == id &&
other.isAdmin == isAdmin &&
other.name == name &&
other.oauthId == oauthId &&
other.pinCode == pinCode &&
other.profileChangedAt == profileChangedAt &&
other.quotaSizeInBytes == quotaSizeInBytes &&
other.quotaUsageInBytes == quotaUsageInBytes &&
other.storageLabel == storageLabel;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(avatarColor == null ? 0 : avatarColor!.hashCode) +
(deletedAt == null ? 0 : deletedAt!.hashCode) +
(email.hashCode) +
(hasProfileImage.hashCode) +
(id.hashCode) +
(isAdmin.hashCode) +
(name.hashCode) +
(oauthId.hashCode) +
(pinCode == null ? 0 : pinCode!.hashCode) +
(profileChangedAt.hashCode) +
(quotaSizeInBytes == null ? 0 : quotaSizeInBytes!.hashCode) +
(quotaUsageInBytes.hashCode) +
(storageLabel == null ? 0 : storageLabel!.hashCode);
@override
String toString() => 'SyncAuthUserV1[avatarColor=$avatarColor, deletedAt=$deletedAt, email=$email, hasProfileImage=$hasProfileImage, id=$id, isAdmin=$isAdmin, name=$name, oauthId=$oauthId, pinCode=$pinCode, profileChangedAt=$profileChangedAt, quotaSizeInBytes=$quotaSizeInBytes, quotaUsageInBytes=$quotaUsageInBytes, storageLabel=$storageLabel]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
if (this.avatarColor != null) {
json[r'avatarColor'] = this.avatarColor;
} else {
// json[r'avatarColor'] = null;
}
if (this.deletedAt != null) {
json[r'deletedAt'] = this.deletedAt!.toUtc().toIso8601String();
} else {
// json[r'deletedAt'] = null;
}
json[r'email'] = this.email;
json[r'hasProfileImage'] = this.hasProfileImage;
json[r'id'] = this.id;
json[r'isAdmin'] = this.isAdmin;
json[r'name'] = this.name;
json[r'oauthId'] = this.oauthId;
if (this.pinCode != null) {
json[r'pinCode'] = this.pinCode;
} else {
// json[r'pinCode'] = null;
}
json[r'profileChangedAt'] = this.profileChangedAt.toUtc().toIso8601String();
if (this.quotaSizeInBytes != null) {
json[r'quotaSizeInBytes'] = this.quotaSizeInBytes;
} else {
// json[r'quotaSizeInBytes'] = null;
}
json[r'quotaUsageInBytes'] = this.quotaUsageInBytes;
if (this.storageLabel != null) {
json[r'storageLabel'] = this.storageLabel;
} else {
// json[r'storageLabel'] = null;
}
return json;
}
/// Returns a new [SyncAuthUserV1] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static SyncAuthUserV1? fromJson(dynamic value) {
upgradeDto(value, "SyncAuthUserV1");
if (value is Map) {
final json = value.cast<String, dynamic>();
return SyncAuthUserV1(
avatarColor: UserAvatarColor.fromJson(json[r'avatarColor']),
deletedAt: mapDateTime(json, r'deletedAt', r''),
email: mapValueOfType<String>(json, r'email')!,
hasProfileImage: mapValueOfType<bool>(json, r'hasProfileImage')!,
id: mapValueOfType<String>(json, r'id')!,
isAdmin: mapValueOfType<bool>(json, r'isAdmin')!,
name: mapValueOfType<String>(json, r'name')!,
oauthId: mapValueOfType<String>(json, r'oauthId')!,
pinCode: mapValueOfType<String>(json, r'pinCode'),
profileChangedAt: mapDateTime(json, r'profileChangedAt', r'')!,
quotaSizeInBytes: mapValueOfType<int>(json, r'quotaSizeInBytes'),
quotaUsageInBytes: mapValueOfType<int>(json, r'quotaUsageInBytes')!,
storageLabel: mapValueOfType<String>(json, r'storageLabel'),
);
}
return null;
}
static List<SyncAuthUserV1> listFromJson(dynamic json, {bool growable = false,}) {
final result = <SyncAuthUserV1>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = SyncAuthUserV1.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, SyncAuthUserV1> mapFromJson(dynamic json) {
final map = <String, SyncAuthUserV1>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = SyncAuthUserV1.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of SyncAuthUserV1-objects as value to a dart map
static Map<String, List<SyncAuthUserV1>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<SyncAuthUserV1>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = SyncAuthUserV1.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'avatarColor',
'deletedAt',
'email',
'hasProfileImage',
'id',
'isAdmin',
'name',
'oauthId',
'pinCode',
'profileChangedAt',
'quotaSizeInBytes',
'quotaUsageInBytes',
'storageLabel',
};
}

View File

@ -23,6 +23,7 @@ class SyncEntityType {
String toJson() => value; String toJson() => value;
static const authUserV1 = SyncEntityType._(r'AuthUserV1');
static const userV1 = SyncEntityType._(r'UserV1'); static const userV1 = SyncEntityType._(r'UserV1');
static const userDeleteV1 = SyncEntityType._(r'UserDeleteV1'); static const userDeleteV1 = SyncEntityType._(r'UserDeleteV1');
static const assetV1 = SyncEntityType._(r'AssetV1'); static const assetV1 = SyncEntityType._(r'AssetV1');
@ -67,6 +68,7 @@ class SyncEntityType {
/// List of all possible values in this [enum][SyncEntityType]. /// List of all possible values in this [enum][SyncEntityType].
static const values = <SyncEntityType>[ static const values = <SyncEntityType>[
authUserV1,
userV1, userV1,
userDeleteV1, userDeleteV1,
assetV1, assetV1,
@ -146,6 +148,7 @@ class SyncEntityTypeTypeTransformer {
SyncEntityType? decode(dynamic data, {bool allowNull = true}) { SyncEntityType? decode(dynamic data, {bool allowNull = true}) {
if (data != null) { if (data != null) {
switch (data) { switch (data) {
case r'AuthUserV1': return SyncEntityType.authUserV1;
case r'UserV1': return SyncEntityType.userV1; case r'UserV1': return SyncEntityType.userV1;
case r'UserDeleteV1': return SyncEntityType.userDeleteV1; case r'UserDeleteV1': return SyncEntityType.userDeleteV1;
case r'AssetV1': return SyncEntityType.assetV1; case r'AssetV1': return SyncEntityType.assetV1;

View File

@ -30,6 +30,7 @@ class SyncRequestType {
static const albumAssetExifsV1 = SyncRequestType._(r'AlbumAssetExifsV1'); static const albumAssetExifsV1 = SyncRequestType._(r'AlbumAssetExifsV1');
static const assetsV1 = SyncRequestType._(r'AssetsV1'); static const assetsV1 = SyncRequestType._(r'AssetsV1');
static const assetExifsV1 = SyncRequestType._(r'AssetExifsV1'); static const assetExifsV1 = SyncRequestType._(r'AssetExifsV1');
static const authUsersV1 = SyncRequestType._(r'AuthUsersV1');
static const memoriesV1 = SyncRequestType._(r'MemoriesV1'); static const memoriesV1 = SyncRequestType._(r'MemoriesV1');
static const memoryToAssetsV1 = SyncRequestType._(r'MemoryToAssetsV1'); static const memoryToAssetsV1 = SyncRequestType._(r'MemoryToAssetsV1');
static const partnersV1 = SyncRequestType._(r'PartnersV1'); static const partnersV1 = SyncRequestType._(r'PartnersV1');
@ -51,6 +52,7 @@ class SyncRequestType {
albumAssetExifsV1, albumAssetExifsV1,
assetsV1, assetsV1,
assetExifsV1, assetExifsV1,
authUsersV1,
memoriesV1, memoriesV1,
memoryToAssetsV1, memoryToAssetsV1,
partnersV1, partnersV1,
@ -107,6 +109,7 @@ class SyncRequestTypeTypeTransformer {
case r'AlbumAssetExifsV1': return SyncRequestType.albumAssetExifsV1; case r'AlbumAssetExifsV1': return SyncRequestType.albumAssetExifsV1;
case r'AssetsV1': return SyncRequestType.assetsV1; case r'AssetsV1': return SyncRequestType.assetsV1;
case r'AssetExifsV1': return SyncRequestType.assetExifsV1; case r'AssetExifsV1': return SyncRequestType.assetExifsV1;
case r'AuthUsersV1': return SyncRequestType.authUsersV1;
case r'MemoriesV1': return SyncRequestType.memoriesV1; case r'MemoriesV1': return SyncRequestType.memoriesV1;
case r'MemoryToAssetsV1': return SyncRequestType.memoryToAssetsV1; case r'MemoryToAssetsV1': return SyncRequestType.memoryToAssetsV1;
case r'PartnersV1': return SyncRequestType.partnersV1; case r'PartnersV1': return SyncRequestType.partnersV1;

View File

@ -13,12 +13,15 @@ part of openapi.api;
class SyncUserV1 { class SyncUserV1 {
/// Returns a new [SyncUserV1] instance. /// Returns a new [SyncUserV1] instance.
SyncUserV1({ SyncUserV1({
required this.avatarColor,
required this.deletedAt, required this.deletedAt,
required this.email, required this.email,
required this.id, required this.id,
required this.name, required this.name,
}); });
UserAvatarColor? avatarColor;
DateTime? deletedAt; DateTime? deletedAt;
String email; String email;
@ -29,6 +32,7 @@ class SyncUserV1 {
@override @override
bool operator ==(Object other) => identical(this, other) || other is SyncUserV1 && bool operator ==(Object other) => identical(this, other) || other is SyncUserV1 &&
other.avatarColor == avatarColor &&
other.deletedAt == deletedAt && other.deletedAt == deletedAt &&
other.email == email && other.email == email &&
other.id == id && other.id == id &&
@ -37,16 +41,22 @@ class SyncUserV1 {
@override @override
int get hashCode => int get hashCode =>
// ignore: unnecessary_parenthesis // ignore: unnecessary_parenthesis
(avatarColor == null ? 0 : avatarColor!.hashCode) +
(deletedAt == null ? 0 : deletedAt!.hashCode) + (deletedAt == null ? 0 : deletedAt!.hashCode) +
(email.hashCode) + (email.hashCode) +
(id.hashCode) + (id.hashCode) +
(name.hashCode); (name.hashCode);
@override @override
String toString() => 'SyncUserV1[deletedAt=$deletedAt, email=$email, id=$id, name=$name]'; String toString() => 'SyncUserV1[avatarColor=$avatarColor, deletedAt=$deletedAt, email=$email, id=$id, name=$name]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
if (this.avatarColor != null) {
json[r'avatarColor'] = this.avatarColor;
} else {
// json[r'avatarColor'] = null;
}
if (this.deletedAt != null) { if (this.deletedAt != null) {
json[r'deletedAt'] = this.deletedAt!.toUtc().toIso8601String(); json[r'deletedAt'] = this.deletedAt!.toUtc().toIso8601String();
} else { } else {
@ -67,6 +77,7 @@ class SyncUserV1 {
final json = value.cast<String, dynamic>(); final json = value.cast<String, dynamic>();
return SyncUserV1( return SyncUserV1(
avatarColor: UserAvatarColor.fromJson(json[r'avatarColor']),
deletedAt: mapDateTime(json, r'deletedAt', r''), deletedAt: mapDateTime(json, r'deletedAt', r''),
email: mapValueOfType<String>(json, r'email')!, email: mapValueOfType<String>(json, r'email')!,
id: mapValueOfType<String>(json, r'id')!, id: mapValueOfType<String>(json, r'id')!,
@ -118,6 +129,7 @@ class SyncUserV1 {
/// The list of required keys that must be present in a JSON. /// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{ static const requiredKeys = <String>{
'avatarColor',
'deletedAt', 'deletedAt',
'email', 'email',
'id', 'id',

View File

@ -9,6 +9,7 @@ abstract final class SyncStreamStub {
email: "admin@admin", email: "admin@admin",
id: "1", id: "1",
name: "Admin", name: "Admin",
avatarColor: null,
), ),
ack: "1", ack: "1",
); );
@ -19,6 +20,7 @@ abstract final class SyncStreamStub {
email: "user@user", email: "user@user",
id: "5", id: "5",
name: "User", name: "User",
avatarColor: null,
), ),
ack: "5", ack: "5",
); );

View File

@ -13978,8 +13978,79 @@
], ],
"type": "object" "type": "object"
}, },
"SyncAuthUserV1": {
"properties": {
"avatarColor": {
"allOf": [
{
"$ref": "#/components/schemas/UserAvatarColor"
}
],
"nullable": true
},
"deletedAt": {
"format": "date-time",
"nullable": true,
"type": "string"
},
"email": {
"type": "string"
},
"hasProfileImage": {
"type": "boolean"
},
"id": {
"type": "string"
},
"isAdmin": {
"type": "boolean"
},
"name": {
"type": "string"
},
"oauthId": {
"type": "string"
},
"pinCode": {
"nullable": true,
"type": "string"
},
"profileChangedAt": {
"format": "date-time",
"type": "string"
},
"quotaSizeInBytes": {
"nullable": true,
"type": "integer"
},
"quotaUsageInBytes": {
"type": "integer"
},
"storageLabel": {
"nullable": true,
"type": "string"
}
},
"required": [
"avatarColor",
"deletedAt",
"email",
"hasProfileImage",
"id",
"isAdmin",
"name",
"oauthId",
"pinCode",
"profileChangedAt",
"quotaSizeInBytes",
"quotaUsageInBytes",
"storageLabel"
],
"type": "object"
},
"SyncEntityType": { "SyncEntityType": {
"enum": [ "enum": [
"AuthUserV1",
"UserV1", "UserV1",
"UserDeleteV1", "UserDeleteV1",
"AssetV1", "AssetV1",
@ -14242,6 +14313,7 @@
"AlbumAssetExifsV1", "AlbumAssetExifsV1",
"AssetsV1", "AssetsV1",
"AssetExifsV1", "AssetExifsV1",
"AuthUsersV1",
"MemoriesV1", "MemoriesV1",
"MemoryToAssetsV1", "MemoryToAssetsV1",
"PartnersV1", "PartnersV1",
@ -14364,6 +14436,14 @@
}, },
"SyncUserV1": { "SyncUserV1": {
"properties": { "properties": {
"avatarColor": {
"allOf": [
{
"$ref": "#/components/schemas/UserAvatarColor"
}
],
"nullable": true
},
"deletedAt": { "deletedAt": {
"format": "date-time", "format": "date-time",
"nullable": true, "nullable": true,
@ -14380,6 +14460,7 @@
} }
}, },
"required": [ "required": [
"avatarColor",
"deletedAt", "deletedAt",
"email", "email",
"id", "id",

View File

@ -4099,6 +4099,7 @@ export enum Error2 {
NotFound = "not_found" NotFound = "not_found"
} }
export enum SyncEntityType { export enum SyncEntityType {
AuthUserV1 = "AuthUserV1",
UserV1 = "UserV1", UserV1 = "UserV1",
UserDeleteV1 = "UserDeleteV1", UserDeleteV1 = "UserDeleteV1",
AssetV1 = "AssetV1", AssetV1 = "AssetV1",
@ -4149,6 +4150,7 @@ export enum SyncRequestType {
AlbumAssetExifsV1 = "AlbumAssetExifsV1", AlbumAssetExifsV1 = "AlbumAssetExifsV1",
AssetsV1 = "AssetsV1", AssetsV1 = "AssetsV1",
AssetExifsV1 = "AssetExifsV1", AssetExifsV1 = "AssetExifsV1",
AuthUsersV1 = "AuthUsersV1",
MemoriesV1 = "MemoriesV1", MemoriesV1 = "MemoriesV1",
MemoryToAssetsV1 = "MemoryToAssetsV1", MemoryToAssetsV1 = "MemoryToAssetsV1",
PartnersV1 = "PartnersV1", PartnersV1 = "PartnersV1",

View File

@ -356,6 +356,7 @@ export const columns = {
], ],
syncAlbumUser: ['album_user.albumsId as albumId', 'album_user.usersId as userId', 'album_user.role'], syncAlbumUser: ['album_user.albumsId as albumId', 'album_user.usersId as userId', 'album_user.role'],
syncStack: ['stack.id', 'stack.createdAt', 'stack.updatedAt', 'stack.primaryAssetId', 'stack.ownerId'], syncStack: ['stack.id', 'stack.createdAt', 'stack.updatedAt', 'stack.primaryAssetId', 'stack.ownerId'],
syncUser: ['id', 'name', 'email', 'avatarColor', 'deletedAt', 'updateId'],
stack: ['stack.id', 'stack.primaryAssetId', 'ownerId'], stack: ['stack.id', 'stack.primaryAssetId', 'ownerId'],
syncAssetExif: [ syncAssetExif: [
'asset_exif.assetId', 'asset_exif.assetId',

View File

@ -10,6 +10,7 @@ import {
MemoryType, MemoryType,
SyncEntityType, SyncEntityType,
SyncRequestType, SyncRequestType,
UserAvatarColor,
UserMetadataKey, UserMetadataKey,
} from 'src/enum'; } from 'src/enum';
import { UserMetadata } from 'src/types'; import { UserMetadata } from 'src/types';
@ -58,9 +59,25 @@ export class SyncUserV1 {
id!: string; id!: string;
name!: string; name!: string;
email!: string; email!: string;
@ValidateEnum({ enum: UserAvatarColor, name: 'UserAvatarColor', nullable: true })
avatarColor!: UserAvatarColor | null;
deletedAt!: Date | null; deletedAt!: Date | null;
} }
@ExtraModel()
export class SyncAuthUserV1 extends SyncUserV1 {
isAdmin!: boolean;
pinCode!: string | null;
oauthId!: string;
storageLabel!: string | null;
@ApiProperty({ type: 'integer' })
quotaSizeInBytes!: number | null;
@ApiProperty({ type: 'integer' })
quotaUsageInBytes!: number;
hasProfileImage!: boolean;
profileChangedAt!: Date;
}
@ExtraModel() @ExtraModel()
export class SyncUserDeleteV1 { export class SyncUserDeleteV1 {
userId!: string; userId!: string;
@ -301,6 +318,7 @@ export class SyncAckV1 {}
export class SyncResetV1 {} export class SyncResetV1 {}
export type SyncItem = { export type SyncItem = {
[SyncEntityType.AuthUserV1]: SyncAuthUserV1;
[SyncEntityType.UserV1]: SyncUserV1; [SyncEntityType.UserV1]: SyncUserV1;
[SyncEntityType.UserDeleteV1]: SyncUserDeleteV1; [SyncEntityType.UserDeleteV1]: SyncUserDeleteV1;
[SyncEntityType.PartnerV1]: SyncPartnerV1; [SyncEntityType.PartnerV1]: SyncPartnerV1;

View File

@ -559,6 +559,7 @@ export enum SyncRequestType {
AlbumAssetExifsV1 = 'AlbumAssetExifsV1', AlbumAssetExifsV1 = 'AlbumAssetExifsV1',
AssetsV1 = 'AssetsV1', AssetsV1 = 'AssetsV1',
AssetExifsV1 = 'AssetExifsV1', AssetExifsV1 = 'AssetExifsV1',
AuthUsersV1 = 'AuthUsersV1',
MemoriesV1 = 'MemoriesV1', MemoriesV1 = 'MemoriesV1',
MemoryToAssetsV1 = 'MemoryToAssetsV1', MemoryToAssetsV1 = 'MemoryToAssetsV1',
PartnersV1 = 'PartnersV1', PartnersV1 = 'PartnersV1',
@ -573,6 +574,8 @@ export enum SyncRequestType {
} }
export enum SyncEntityType { export enum SyncEntityType {
AuthUserV1 = 'AuthUserV1',
UserV1 = 'UserV1', UserV1 = 'UserV1',
UserDeleteV1 = 'UserDeleteV1', UserDeleteV1 = 'UserDeleteV1',

View File

@ -444,6 +444,29 @@ where
order by order by
"asset_face"."updateId" asc "asset_face"."updateId" asc
-- SyncRepository.authUser.getUpserts
select
"id",
"name",
"email",
"avatarColor",
"deletedAt",
"updateId",
"isAdmin",
"pinCode",
"oauthId",
"storageLabel",
"quotaSizeInBytes",
"quotaUsageInBytes",
"profileImagePath",
"profileChangedAt"
from
"user"
where
"updatedAt" < now() - interval '1 millisecond'
order by
"updateId" asc
-- SyncRepository.memory.getDeletes -- SyncRepository.memory.getDeletes
select select
"id", "id",
@ -871,6 +894,7 @@ select
"id", "id",
"name", "name",
"email", "email",
"avatarColor",
"deletedAt", "deletedAt",
"updateId" "updateId"
from from

View File

@ -43,6 +43,7 @@ export class SyncRepository {
asset: AssetSync; asset: AssetSync;
assetExif: AssetExifSync; assetExif: AssetExifSync;
assetFace: AssetFaceSync; assetFace: AssetFaceSync;
authUser: AuthUserSync;
memory: MemorySync; memory: MemorySync;
memoryToAsset: MemoryToAssetSync; memoryToAsset: MemoryToAssetSync;
partner: PartnerSync; partner: PartnerSync;
@ -63,6 +64,7 @@ export class SyncRepository {
this.asset = new AssetSync(this.db); this.asset = new AssetSync(this.db);
this.assetExif = new AssetExifSync(this.db); this.assetExif = new AssetExifSync(this.db);
this.assetFace = new AssetFaceSync(this.db); this.assetFace = new AssetFaceSync(this.db);
this.authUser = new AuthUserSync(this.db);
this.memory = new MemorySync(this.db); this.memory = new MemorySync(this.db);
this.memoryToAsset = new MemoryToAssetSync(this.db); this.memoryToAsset = new MemoryToAssetSync(this.db);
this.partner = new PartnerSync(this.db); this.partner = new PartnerSync(this.db);
@ -367,6 +369,27 @@ class AssetSync extends BaseSync {
} }
} }
class AuthUserSync extends BaseSync {
@GenerateSql({ params: [], stream: true })
getUpserts(ack?: SyncAck) {
return this.db
.selectFrom('user')
.select(columns.syncUser)
.select([
'isAdmin',
'pinCode',
'oauthId',
'storageLabel',
'quotaSizeInBytes',
'quotaUsageInBytes',
'profileImagePath',
'profileChangedAt',
])
.$call(this.upsertTableFilters(ack))
.stream();
}
}
class PersonSync extends BaseSync { class PersonSync extends BaseSync {
@GenerateSql({ params: [DummyValue.UUID], stream: true }) @GenerateSql({ params: [DummyValue.UUID], stream: true })
getDeletes(userId: string, ack?: SyncAck) { getDeletes(userId: string, ack?: SyncAck) {
@ -693,11 +716,7 @@ class UserSync extends BaseSync {
@GenerateSql({ params: [], stream: true }) @GenerateSql({ params: [], stream: true })
getUpserts(ack?: SyncAck) { getUpserts(ack?: SyncAck) {
return this.db return this.db.selectFrom('user').select(columns.syncUser).$call(this.upsertTableFilters(ack)).stream();
.selectFrom('user')
.select(['id', 'name', 'email', 'deletedAt', 'updateId'])
.$call(this.upsertTableFilters(ack))
.stream();
} }
} }

View File

@ -54,6 +54,7 @@ const sendEntityBackfillCompleteAck = (response: Writable, ackType: SyncEntityTy
const FULL_SYNC = { needsFullSync: true, deleted: [], upserted: [] }; const FULL_SYNC = { needsFullSync: true, deleted: [], upserted: [] };
export const SYNC_TYPES_ORDER = [ export const SYNC_TYPES_ORDER = [
SyncRequestType.AuthUsersV1,
SyncRequestType.UsersV1, SyncRequestType.UsersV1,
SyncRequestType.PartnersV1, SyncRequestType.PartnersV1,
SyncRequestType.AssetsV1, SyncRequestType.AssetsV1,
@ -140,6 +141,7 @@ export class SyncService extends BaseService {
const checkpointMap: CheckpointMap = Object.fromEntries(checkpoints.map(({ type, ack }) => [type, fromAck(ack)])); const checkpointMap: CheckpointMap = Object.fromEntries(checkpoints.map(({ type, ack }) => [type, fromAck(ack)]));
const handlers: Record<SyncRequestType, () => Promise<void>> = { const handlers: Record<SyncRequestType, () => Promise<void>> = {
[SyncRequestType.AuthUsersV1]: () => this.syncAuthUsersV1(response, checkpointMap),
[SyncRequestType.UsersV1]: () => this.syncUsersV1(response, checkpointMap), [SyncRequestType.UsersV1]: () => this.syncUsersV1(response, checkpointMap),
[SyncRequestType.PartnersV1]: () => this.syncPartnersV1(response, checkpointMap, auth), [SyncRequestType.PartnersV1]: () => this.syncPartnersV1(response, checkpointMap, auth),
[SyncRequestType.AssetsV1]: () => this.syncAssetsV1(response, checkpointMap, auth), [SyncRequestType.AssetsV1]: () => this.syncAssetsV1(response, checkpointMap, auth),
@ -169,6 +171,14 @@ export class SyncService extends BaseService {
response.end(); response.end();
} }
private async syncAuthUsersV1(response: Writable, checkpointMap: CheckpointMap) {
const upsertType = SyncEntityType.AuthUserV1;
const upserts = this.syncRepository.authUser.getUpserts(checkpointMap[upsertType]);
for await (const { updateId, profileImagePath, ...data } of upserts) {
send(response, { type: upsertType, ids: [updateId], data: { ...data, hasProfileImage: !!profileImagePath } });
}
}
private async syncUsersV1(response: Writable, checkpointMap: CheckpointMap) { private async syncUsersV1(response: Writable, checkpointMap: CheckpointMap) {
const deleteType = SyncEntityType.UserDeleteV1; const deleteType = SyncEntityType.UserDeleteV1;
const deletes = this.syncRepository.user.getDeletes(checkpointMap[deleteType]); const deletes = this.syncRepository.user.getDeletes(checkpointMap[deleteType]);

View File

@ -507,7 +507,14 @@ const userInsert = (user: Partial<Insertable<UserTable>> = {}) => {
deletedAt: null, deletedAt: null,
isAdmin: false, isAdmin: false,
profileImagePath: '', profileImagePath: '',
profileChangedAt: newDate(),
shouldChangePassword: true, shouldChangePassword: true,
storageLabel: null,
pinCode: null,
oauthId: '',
avatarColor: null,
quotaSizeInBytes: null,
quotaUsageInBytes: 0,
}; };
return { ...defaults, ...user, id }; return { ...defaults, ...user, id };

View File

@ -0,0 +1,87 @@
import { Kysely } from 'kysely';
import { SyncEntityType, SyncRequestType } from 'src/enum';
import { UserRepository } from 'src/repositories/user.repository';
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.AuthUserV1, () => {
it('should detect and sync the first user', async () => {
const { auth, user, ctx } = await setup(await getKyselyDB());
const response = await ctx.syncStream(auth, [SyncRequestType.AuthUsersV1]);
expect(response).toHaveLength(1);
expect(response).toEqual([
{
ack: expect.any(String),
data: {
id: user.id,
isAdmin: user.isAdmin,
deletedAt: user.deletedAt,
name: user.name,
avatarColor: user.avatarColor,
email: user.email,
pinCode: user.pinCode,
hasProfileImage: false,
profileChangedAt: (user.profileChangedAt as Date).toISOString(),
oauthId: user.oauthId,
quotaSizeInBytes: user.quotaSizeInBytes,
quotaUsageInBytes: user.quotaUsageInBytes,
storageLabel: user.storageLabel,
},
type: 'AuthUserV1',
},
]);
await ctx.syncAckAll(auth, response);
await expect(ctx.syncStream(auth, [SyncRequestType.AuthUsersV1])).resolves.toEqual([]);
});
it('should sync a change and then another change to that same user', async () => {
const { auth, user, ctx } = await setup(await getKyselyDB());
const userRepo = ctx.get(UserRepository);
const response = await ctx.syncStream(auth, [SyncRequestType.AuthUsersV1]);
expect(response).toHaveLength(1);
expect(response).toEqual([
{
ack: expect.any(String),
data: expect.objectContaining({
id: user.id,
isAdmin: false,
}),
type: 'AuthUserV1',
},
]);
await ctx.syncAckAll(auth, response);
await userRepo.update(user.id, { isAdmin: true });
const newResponse = await ctx.syncStream(auth, [SyncRequestType.AuthUsersV1]);
expect(newResponse).toHaveLength(1);
expect(newResponse).toEqual([
{
ack: expect.any(String),
data: expect.objectContaining({
id: user.id,
isAdmin: true,
}),
type: 'AuthUserV1',
},
]);
});
});

View File

@ -37,6 +37,7 @@ describe(SyncEntityType.UserV1, () => {
email: user.email, email: user.email,
id: user.id, id: user.id,
name: user.name, name: user.name,
avatarColor: user.avatarColor,
}, },
type: 'UserV1', type: 'UserV1',
}, },
@ -49,8 +50,7 @@ describe(SyncEntityType.UserV1, () => {
it('should detect and sync a soft deleted user', async () => { it('should detect and sync a soft deleted user', async () => {
const { auth, ctx } = await setup(await getKyselyDB()); const { auth, ctx } = await setup(await getKyselyDB());
const deletedAt = new Date().toISOString(); const { user: deleted } = await ctx.newUser({ deletedAt: new Date().toISOString() });
const { user: deleted } = await ctx.newUser({ deletedAt });
const response = await ctx.syncStream(auth, [SyncRequestType.UsersV1]); const response = await ctx.syncStream(auth, [SyncRequestType.UsersV1]);
@ -59,22 +59,12 @@ describe(SyncEntityType.UserV1, () => {
expect.arrayContaining([ expect.arrayContaining([
{ {
ack: expect.any(String), ack: expect.any(String),
data: { data: expect.objectContaining({ id: auth.user.id }),
deletedAt: null,
email: auth.user.email,
id: auth.user.id,
name: auth.user.name,
},
type: 'UserV1', type: 'UserV1',
}, },
{ {
ack: expect.any(String), ack: expect.any(String),
data: { data: expect.objectContaining({ id: deleted.id }),
deletedAt,
email: deleted.email,
id: deleted.id,
name: deleted.name,
},
type: 'UserV1', type: 'UserV1',
}, },
]), ]),
@ -85,7 +75,7 @@ describe(SyncEntityType.UserV1, () => {
}); });
it('should detect and sync a deleted user', async () => { it('should detect and sync a deleted user', async () => {
const { auth, ctx } = await setup(await getKyselyDB()); const { auth, user: authUser, ctx } = await setup(await getKyselyDB());
const userRepo = ctx.get(UserRepository); const userRepo = ctx.get(UserRepository);
@ -104,12 +94,7 @@ describe(SyncEntityType.UserV1, () => {
}, },
{ {
ack: expect.any(String), ack: expect.any(String),
data: { data: expect.objectContaining({ id: authUser.id }),
deletedAt: null,
email: auth.user.email,
id: auth.user.id,
name: auth.user.name,
},
type: 'UserV1', type: 'UserV1',
}, },
]); ]);
@ -119,7 +104,7 @@ describe(SyncEntityType.UserV1, () => {
}); });
it('should sync a user and then an update to that same user', async () => { it('should sync a user and then an update to that same user', async () => {
const { auth, ctx } = await setup(await getKyselyDB()); const { auth, user, ctx } = await setup(await getKyselyDB());
const userRepo = ctx.get(UserRepository); const userRepo = ctx.get(UserRepository);
@ -128,12 +113,7 @@ describe(SyncEntityType.UserV1, () => {
expect(response).toEqual([ expect(response).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
data: { data: expect.objectContaining({ id: user.id }),
deletedAt: null,
email: auth.user.email,
id: auth.user.id,
name: auth.user.name,
},
type: 'UserV1', type: 'UserV1',
}, },
]); ]);
@ -147,12 +127,7 @@ describe(SyncEntityType.UserV1, () => {
expect(newResponse).toEqual([ expect(newResponse).toEqual([
{ {
ack: expect.any(String), ack: expect.any(String),
data: { data: expect.objectContaining({ id: user.id, name: updated.name }),
deletedAt: null,
email: auth.user.email,
id: auth.user.id,
name: updated.name,
},
type: 'UserV1', type: 'UserV1',
}, },
]); ]);