feat(web): add search filter for camera lens model. (#21792)

This commit is contained in:
Dag Stuan 2025-10-24 20:41:34 +02:00 committed by GitHub
parent d9cddeb0f1
commit 78fb815cdb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 133 additions and 15 deletions

View file

@ -249,6 +249,7 @@ export enum SearchSuggestionType {
CITY = 'city',
CAMERA_MAKE = 'camera-make',
CAMERA_MODEL = 'camera-model',
CAMERA_LENS_MODEL = 'camera-lens-model',
}
export class SearchSuggestionRequestDto {
@ -271,6 +272,10 @@ export class SearchSuggestionRequestDto {
@Optional()
model?: string;
@IsString()
@Optional()
lensModel?: string;
@ValidateBoolean({ optional: true })
@PropertyLifecycle({ addedAt: 'v111.0.0' })
includeNull?: boolean;

View file

@ -290,3 +290,15 @@ where
and "visibility" = $2
and "deletedAt" is null
and "model" is not null
-- SearchRepository.getCameraLensModels
select distinct
on ("lensModel") "lensModel"
from
"asset_exif"
inner join "asset" on "asset"."id" = "asset_exif"."assetId"
where
"ownerId" = any ($1::uuid[])
and "visibility" = $2
and "deletedAt" is null
and "lensModel" is not null

View file

@ -160,10 +160,17 @@ export interface GetCitiesOptions extends GetStatesOptions {
export interface GetCameraModelsOptions {
make?: string;
lensModel?: string;
}
export interface GetCameraMakesOptions {
model?: string;
lensModel?: string;
}
export interface GetCameraLensModelsOptions {
make?: string;
model?: string;
}
@Injectable()
@ -457,25 +464,40 @@ export class SearchRepository {
return res.map((row) => row.city!);
}
@GenerateSql({ params: [[DummyValue.UUID], DummyValue.STRING] })
async getCameraMakes(userIds: string[], { model }: GetCameraMakesOptions): Promise<string[]> {
@GenerateSql({ params: [[DummyValue.UUID], DummyValue.STRING, DummyValue.STRING] })
async getCameraMakes(userIds: string[], { model, lensModel }: GetCameraMakesOptions): Promise<string[]> {
const res = await this.getExifField('make', userIds)
.$if(!!model, (qb) => qb.where('model', '=', model!))
.$if(!!lensModel, (qb) => qb.where('lensModel', '=', lensModel!))
.execute();
return res.map((row) => row.make!);
}
@GenerateSql({ params: [[DummyValue.UUID], DummyValue.STRING] })
async getCameraModels(userIds: string[], { make }: GetCameraModelsOptions): Promise<string[]> {
@GenerateSql({ params: [[DummyValue.UUID], DummyValue.STRING, DummyValue.STRING] })
async getCameraModels(userIds: string[], { make, lensModel }: GetCameraModelsOptions): Promise<string[]> {
const res = await this.getExifField('model', userIds)
.$if(!!make, (qb) => qb.where('make', '=', make!))
.$if(!!lensModel, (qb) => qb.where('lensModel', '=', lensModel!))
.execute();
return res.map((row) => row.model!);
}
private getExifField<K extends 'city' | 'state' | 'country' | 'make' | 'model'>(field: K, userIds: string[]) {
@GenerateSql({ params: [[DummyValue.UUID], DummyValue.STRING] })
async getCameraLensModels(userIds: string[], { make, model }: GetCameraLensModelsOptions): Promise<string[]> {
const res = await this.getExifField('lensModel', userIds)
.$if(!!make, (qb) => qb.where('make', '=', make!))
.$if(!!model, (qb) => qb.where('model', '=', model!))
.execute();
return res.map((row) => row.lensModel!);
}
private getExifField<K extends 'city' | 'state' | 'country' | 'make' | 'model' | 'lensModel'>(
field: K,
userIds: string[],
) {
return this.db
.selectFrom('asset_exif')
.select(field)

View file

@ -179,6 +179,26 @@ describe(SearchService.name, () => {
).resolves.toEqual(['Fujifilm X100VI', null]);
expect(mocks.search.getCameraModels).toHaveBeenCalledWith([authStub.user1.user.id], expect.anything());
});
it('should return search suggestions for camera lens model', async () => {
mocks.search.getCameraLensModels.mockResolvedValue(['10-24mm']);
mocks.partner.getAll.mockResolvedValue([]);
await expect(
sut.getSearchSuggestions(authStub.user1, { includeNull: false, type: SearchSuggestionType.CAMERA_LENS_MODEL }),
).resolves.toEqual(['10-24mm']);
expect(mocks.search.getCameraLensModels).toHaveBeenCalledWith([authStub.user1.user.id], expect.anything());
});
it('should return search suggestions for camera lens model (including null)', async () => {
mocks.search.getCameraLensModels.mockResolvedValue(['10-24mm']);
mocks.partner.getAll.mockResolvedValue([]);
await expect(
sut.getSearchSuggestions(authStub.user1, { includeNull: true, type: SearchSuggestionType.CAMERA_LENS_MODEL }),
).resolves.toEqual(['10-24mm', null]);
expect(mocks.search.getCameraLensModels).toHaveBeenCalledWith([authStub.user1.user.id], expect.anything());
});
});
describe('searchSmart', () => {

View file

@ -177,6 +177,9 @@ export class SearchService extends BaseService {
case SearchSuggestionType.CAMERA_MODEL: {
return this.searchRepository.getCameraModels(userIds, dto);
}
case SearchSuggestionType.CAMERA_LENS_MODEL: {
return this.searchRepository.getCameraLensModels(userIds, dto);
}
default: {
return Promise.resolve([]);
}