refactor(server): partner search dto (#10902)

* refactor(server): partner search dto

* fix: missed reference

* mobile fix

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
Jason Rasmussen 2024-07-08 16:41:39 -04:00 committed by GitHub
parent 5f25e2ce82
commit 334a709cc6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 136 additions and 40 deletions

View File

@ -14,17 +14,6 @@ final partnerServiceProvider = Provider(
), ),
); );
enum PartnerDirection {
sharedWith("shared-with"),
sharedBy("shared-by");
const PartnerDirection(
this._value,
);
final String _value;
}
class PartnerService { class PartnerService {
final ApiService _apiService; final ApiService _apiService;
final Isar _db; final Isar _db;
@ -34,8 +23,7 @@ class PartnerService {
Future<List<User>?> getPartners(PartnerDirection direction) async { Future<List<User>?> getPartners(PartnerDirection direction) async {
try { try {
final userDtos = final userDtos = await _apiService.partnersApi.getPartners(direction);
await _apiService.partnersApi.getPartners(direction._value);
if (userDtos != null) { if (userDtos != null) {
return userDtos.map((u) => User.fromPartnerDto(u)).toList(); return userDtos.map((u) => User.fromPartnerDto(u)).toList();
} }

View File

@ -73,9 +73,9 @@ class UserService {
Future<List<User>?> getUsersFromServer() async { Future<List<User>?> getUsersFromServer() async {
final List<User>? users = await _getAllUsers(); final List<User>? users = await _getAllUsers();
final List<User>? sharedBy = final List<User>? sharedBy =
await _partnerService.getPartners(PartnerDirection.sharedBy); await _partnerService.getPartners(PartnerDirection.by);
final List<User>? sharedWith = final List<User>? sharedWith =
await _partnerService.getPartners(PartnerDirection.sharedWith); await _partnerService.getPartners(PartnerDirection.with_);
if (users == null || sharedBy == null || sharedWith == null) { if (users == null || sharedBy == null || sharedWith == null) {
_log.warning("Failed to refresh users"); _log.warning("Failed to refresh users");

View File

@ -353,6 +353,7 @@ Class | Method | HTTP request | Description
- [OAuthCallbackDto](doc//OAuthCallbackDto.md) - [OAuthCallbackDto](doc//OAuthCallbackDto.md)
- [OAuthConfigDto](doc//OAuthConfigDto.md) - [OAuthConfigDto](doc//OAuthConfigDto.md)
- [OnThisDayDto](doc//OnThisDayDto.md) - [OnThisDayDto](doc//OnThisDayDto.md)
- [PartnerDirection](doc//PartnerDirection.md)
- [PartnerResponseDto](doc//PartnerResponseDto.md) - [PartnerResponseDto](doc//PartnerResponseDto.md)
- [PathEntityType](doc//PathEntityType.md) - [PathEntityType](doc//PathEntityType.md)
- [PathType](doc//PathType.md) - [PathType](doc//PathType.md)

View File

@ -166,6 +166,7 @@ part 'model/o_auth_authorize_response_dto.dart';
part 'model/o_auth_callback_dto.dart'; part 'model/o_auth_callback_dto.dart';
part 'model/o_auth_config_dto.dart'; part 'model/o_auth_config_dto.dart';
part 'model/on_this_day_dto.dart'; part 'model/on_this_day_dto.dart';
part 'model/partner_direction.dart';
part 'model/partner_response_dto.dart'; part 'model/partner_response_dto.dart';
part 'model/path_entity_type.dart'; part 'model/path_entity_type.dart';
part 'model/path_type.dart'; part 'model/path_type.dart';

View File

@ -67,8 +67,8 @@ class PartnersApi {
/// Performs an HTTP 'GET /partners' operation and returns the [Response]. /// Performs an HTTP 'GET /partners' operation and returns the [Response].
/// Parameters: /// Parameters:
/// ///
/// * [String] direction (required): /// * [PartnerDirection] direction (required):
Future<Response> getPartnersWithHttpInfo(String direction,) async { Future<Response> getPartnersWithHttpInfo(PartnerDirection direction,) async {
// ignore: prefer_const_declarations // ignore: prefer_const_declarations
final path = r'/partners'; final path = r'/partners';
@ -97,8 +97,8 @@ class PartnersApi {
/// Parameters: /// Parameters:
/// ///
/// * [String] direction (required): /// * [PartnerDirection] direction (required):
Future<List<PartnerResponseDto>?> getPartners(String direction,) async { Future<List<PartnerResponseDto>?> getPartners(PartnerDirection direction,) async {
final response = await getPartnersWithHttpInfo(direction,); final response = await getPartnersWithHttpInfo(direction,);
if (response.statusCode >= HttpStatus.badRequest) { if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response)); throw ApiException(response.statusCode, await _decodeBodyBytes(response));

View File

@ -390,6 +390,8 @@ class ApiClient {
return OAuthConfigDto.fromJson(value); return OAuthConfigDto.fromJson(value);
case 'OnThisDayDto': case 'OnThisDayDto':
return OnThisDayDto.fromJson(value); return OnThisDayDto.fromJson(value);
case 'PartnerDirection':
return PartnerDirectionTypeTransformer().decode(value);
case 'PartnerResponseDto': case 'PartnerResponseDto':
return PartnerResponseDto.fromJson(value); return PartnerResponseDto.fromJson(value);
case 'PathEntityType': case 'PathEntityType':

View File

@ -103,6 +103,9 @@ String parameterToString(dynamic value) {
if (value is MemoryType) { if (value is MemoryType) {
return MemoryTypeTypeTransformer().encode(value).toString(); return MemoryTypeTypeTransformer().encode(value).toString();
} }
if (value is PartnerDirection) {
return PartnerDirectionTypeTransformer().encode(value).toString();
}
if (value is PathEntityType) { if (value is PathEntityType) {
return PathEntityTypeTypeTransformer().encode(value).toString(); return PathEntityTypeTypeTransformer().encode(value).toString();
} }

View File

@ -0,0 +1,85 @@
//
// 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 PartnerDirection {
/// Instantiate a new enum with the provided [value].
const PartnerDirection._(this.value);
/// The underlying value of this enum member.
final String value;
@override
String toString() => value;
String toJson() => value;
static const by = PartnerDirection._(r'shared-by');
static const with_ = PartnerDirection._(r'shared-with');
/// List of all possible values in this [enum][PartnerDirection].
static const values = <PartnerDirection>[
by,
with_,
];
static PartnerDirection? fromJson(dynamic value) => PartnerDirectionTypeTransformer().decode(value);
static List<PartnerDirection> listFromJson(dynamic json, {bool growable = false,}) {
final result = <PartnerDirection>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = PartnerDirection.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
}
/// Transformation class that can [encode] an instance of [PartnerDirection] to String,
/// and [decode] dynamic data back to [PartnerDirection].
class PartnerDirectionTypeTransformer {
factory PartnerDirectionTypeTransformer() => _instance ??= const PartnerDirectionTypeTransformer._();
const PartnerDirectionTypeTransformer._();
String encode(PartnerDirection data) => data.value;
/// Decodes a [dynamic value][data] to a PartnerDirection.
///
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
///
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
/// and users are still using an old app with the old code.
PartnerDirection? decode(dynamic data, {bool allowNull = true}) {
if (data != null) {
switch (data) {
case r'shared-by': return PartnerDirection.by;
case r'shared-with': return PartnerDirection.with_;
default:
if (!allowNull) {
throw ArgumentError('Unknown enum value to decode: $data');
}
}
}
return null;
}
/// Singleton [PartnerDirectionTypeTransformer] instance.
static PartnerDirectionTypeTransformer? _instance;
}

View File

@ -3660,11 +3660,7 @@
"required": true, "required": true,
"in": "query", "in": "query",
"schema": { "schema": {
"enum": [ "$ref": "#/components/schemas/PartnerDirection"
"shared-by",
"shared-with"
],
"type": "string"
} }
} }
], ],
@ -9473,6 +9469,13 @@
], ],
"type": "object" "type": "object"
}, },
"PartnerDirection": {
"enum": [
"shared-by",
"shared-with"
],
"type": "string"
},
"PartnerResponseDto": { "PartnerResponseDto": {
"properties": { "properties": {
"avatarColor": { "avatarColor": {

View File

@ -2128,7 +2128,7 @@ export function unlinkOAuthAccount(opts?: Oazapfts.RequestOpts) {
})); }));
} }
export function getPartners({ direction }: { export function getPartners({ direction }: {
direction: "shared-by" | "shared-with"; direction: PartnerDirection;
}, opts?: Oazapfts.RequestOpts) { }, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{ return oazapfts.ok(oazapfts.fetchJson<{
status: 200; status: 200;
@ -3131,6 +3131,10 @@ export enum Type2 {
export enum MemoryType { export enum MemoryType {
OnThisDay = "on_this_day" OnThisDay = "on_this_day"
} }
export enum PartnerDirection {
SharedBy = "shared-by",
SharedWith = "shared-with"
}
export enum PathEntityType { export enum PathEntityType {
Asset = "asset", Asset = "asset",
Person = "person", Person = "person",

View File

@ -1,7 +1,7 @@
import { Body, Controller, Delete, Get, Param, Post, Put, Query } from '@nestjs/common'; import { Body, Controller, Delete, Get, Param, Post, Put, Query } from '@nestjs/common';
import { ApiQuery, ApiTags } from '@nestjs/swagger'; import { ApiQuery, ApiTags } from '@nestjs/swagger';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { PartnerResponseDto, UpdatePartnerDto } from 'src/dtos/partner.dto'; import { PartnerResponseDto, PartnerSearchDto, UpdatePartnerDto } from 'src/dtos/partner.dto';
import { PartnerDirection } from 'src/interfaces/partner.interface'; import { PartnerDirection } from 'src/interfaces/partner.interface';
import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { PartnerService } from 'src/services/partner.service'; import { PartnerService } from 'src/services/partner.service';
@ -16,8 +16,8 @@ export class PartnerController {
@ApiQuery({ name: 'direction', type: 'string', enum: PartnerDirection, required: true }) @ApiQuery({ name: 'direction', type: 'string', enum: PartnerDirection, required: true })
@Authenticated() @Authenticated()
// TODO: remove 'direction' and convert to full query dto // TODO: remove 'direction' and convert to full query dto
getPartners(@Auth() auth: AuthDto, @Query('direction') direction: PartnerDirection): Promise<PartnerResponseDto[]> { getPartners(@Auth() auth: AuthDto, @Query() dto: PartnerSearchDto): Promise<PartnerResponseDto[]> {
return this.service.getAll(auth, direction); return this.service.search(auth, dto);
} }
@Post(':id') @Post(':id')

View File

@ -1,11 +1,19 @@
import { IsNotEmpty } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger';
import { IsEnum, IsNotEmpty } from 'class-validator';
import { UserResponseDto } from 'src/dtos/user.dto'; import { UserResponseDto } from 'src/dtos/user.dto';
import { PartnerDirection } from 'src/interfaces/partner.interface';
export class UpdatePartnerDto { export class UpdatePartnerDto {
@IsNotEmpty() @IsNotEmpty()
inTimeline!: boolean; inTimeline!: boolean;
} }
export class PartnerSearchDto {
@IsEnum(PartnerDirection)
@ApiProperty({ enum: PartnerDirection, enumName: 'PartnerDirection' })
direction!: PartnerDirection;
}
export class PartnerResponseDto extends UserResponseDto { export class PartnerResponseDto extends UserResponseDto {
inTimeline?: boolean; inTimeline?: boolean;
} }

View File

@ -21,16 +21,16 @@ describe(PartnerService.name, () => {
expect(sut).toBeDefined(); expect(sut).toBeDefined();
}); });
describe('getAll', () => { describe('search', () => {
it("should return a list of partners with whom I've shared my library", async () => { it("should return a list of partners with whom I've shared my library", async () => {
partnerMock.getAll.mockResolvedValue([partnerStub.adminToUser1, partnerStub.user1ToAdmin1]); partnerMock.getAll.mockResolvedValue([partnerStub.adminToUser1, partnerStub.user1ToAdmin1]);
await expect(sut.getAll(authStub.user1, PartnerDirection.SharedBy)).resolves.toBeDefined(); await expect(sut.search(authStub.user1, { direction: PartnerDirection.SharedBy })).resolves.toBeDefined();
expect(partnerMock.getAll).toHaveBeenCalledWith(authStub.user1.user.id); expect(partnerMock.getAll).toHaveBeenCalledWith(authStub.user1.user.id);
}); });
it('should return a list of partners who have shared their libraries with me', async () => { it('should return a list of partners who have shared their libraries with me', async () => {
partnerMock.getAll.mockResolvedValue([partnerStub.adminToUser1, partnerStub.user1ToAdmin1]); partnerMock.getAll.mockResolvedValue([partnerStub.adminToUser1, partnerStub.user1ToAdmin1]);
await expect(sut.getAll(authStub.user1, PartnerDirection.SharedWith)).resolves.toBeDefined(); await expect(sut.search(authStub.user1, { direction: PartnerDirection.SharedWith })).resolves.toBeDefined();
expect(partnerMock.getAll).toHaveBeenCalledWith(authStub.user1.user.id); expect(partnerMock.getAll).toHaveBeenCalledWith(authStub.user1.user.id);
}); });
}); });

View File

@ -1,7 +1,7 @@
import { BadRequestException, Inject, Injectable } from '@nestjs/common'; import { BadRequestException, Inject, Injectable } from '@nestjs/common';
import { AccessCore, Permission } from 'src/cores/access.core'; import { AccessCore, Permission } from 'src/cores/access.core';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { PartnerResponseDto, UpdatePartnerDto } from 'src/dtos/partner.dto'; import { PartnerResponseDto, PartnerSearchDto, UpdatePartnerDto } from 'src/dtos/partner.dto';
import { mapUser } from 'src/dtos/user.dto'; import { mapUser } from 'src/dtos/user.dto';
import { PartnerEntity } from 'src/entities/partner.entity'; import { PartnerEntity } from 'src/entities/partner.entity';
import { IAccessRepository } from 'src/interfaces/access.interface'; import { IAccessRepository } from 'src/interfaces/access.interface';
@ -38,7 +38,7 @@ export class PartnerService {
await this.repository.remove(partner); await this.repository.remove(partner);
} }
async getAll(auth: AuthDto, direction: PartnerDirection): Promise<PartnerResponseDto[]> { async search(auth: AuthDto, { direction }: PartnerSearchDto): Promise<PartnerResponseDto[]> {
const partners = await this.repository.getAll(auth.user.id); const partners = await this.repository.getAll(auth.user.id);
const key = direction === PartnerDirection.SharedBy ? 'sharedById' : 'sharedWithId'; const key = direction === PartnerDirection.SharedBy ? 'sharedById' : 'sharedWithId';
return partners return partners

View File

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { searchUsers, getPartners, type UserResponseDto } from '@immich/sdk'; import { searchUsers, getPartners, type UserResponseDto, PartnerDirection } from '@immich/sdk';
import { createEventDispatcher, onMount } from 'svelte'; import { createEventDispatcher, onMount } from 'svelte';
import Button from '../elements/buttons/button.svelte'; import Button from '../elements/buttons/button.svelte';
import UserAvatar from '../shared-components/user-avatar.svelte'; import UserAvatar from '../shared-components/user-avatar.svelte';
@ -21,7 +21,7 @@
users = users.filter((_user) => _user.id !== user.id); users = users.filter((_user) => _user.id !== user.id);
// exclude partners from the list of users available for selection // exclude partners from the list of users available for selection
const partners = await getPartners({ direction: 'shared-by' }); const partners = await getPartners({ direction: PartnerDirection.SharedBy });
const partnerIds = new Set(partners.map((partner) => partner.id)); const partnerIds = new Set(partners.map((partner) => partner.id));
availableUsers = users.filter((user) => !partnerIds.has(user.id)); availableUsers = users.filter((user) => !partnerIds.has(user.id));
}); });

View File

@ -2,6 +2,7 @@
import { import {
createPartner, createPartner,
getPartners, getPartners,
PartnerDirection,
removePartner, removePartner,
updatePartner, updatePartner,
type PartnerResponseDto, type PartnerResponseDto,
@ -40,8 +41,8 @@
partners = []; partners = [];
const [sharedBy, sharedWith] = await Promise.all([ const [sharedBy, sharedWith] = await Promise.all([
getPartners({ direction: 'shared-by' }), getPartners({ direction: PartnerDirection.SharedBy }),
getPartners({ direction: 'shared-with' }), getPartners({ direction: PartnerDirection.SharedWith }),
]); ]);
for (const candidate of sharedBy) { for (const candidate of sharedBy) {

View File

@ -1,12 +1,12 @@
import { authenticate } from '$lib/utils/auth'; import { authenticate } from '$lib/utils/auth';
import { getFormatter } from '$lib/utils/i18n'; import { getFormatter } from '$lib/utils/i18n';
import { getAllAlbums, getPartners } from '@immich/sdk'; import { PartnerDirection, getAllAlbums, getPartners } from '@immich/sdk';
import type { PageLoad } from './$types'; import type { PageLoad } from './$types';
export const load = (async () => { export const load = (async () => {
await authenticate(); await authenticate();
const sharedAlbums = await getAllAlbums({ shared: true }); const sharedAlbums = await getAllAlbums({ shared: true });
const partners = await getPartners({ direction: 'shared-with' }); const partners = await getPartners({ direction: PartnerDirection.SharedWith });
const $t = await getFormatter(); const $t = await getFormatter();
return { return {