mirror of
https://github.com/immich-app/immich.git
synced 2025-05-31 04:05:39 -04:00
refactor: search e2e (#7732)
This commit is contained in:
parent
ffdd504008
commit
89f6190fb0
224
e2e/src/api/specs/search.e2e-spec.ts
Normal file
224
e2e/src/api/specs/search.e2e-spec.ts
Normal file
@ -0,0 +1,224 @@
|
|||||||
|
import { AssetFileUploadResponseDto, LoginResponseDto } from '@immich/sdk';
|
||||||
|
import { readFile } from 'node:fs/promises';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import { Socket } from 'socket.io-client';
|
||||||
|
import { errorDto } from 'src/responses';
|
||||||
|
import { app, testAssetDir, utils } from 'src/utils';
|
||||||
|
import request from 'supertest';
|
||||||
|
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
const albums = { total: 0, count: 0, items: [], facets: [] };
|
||||||
|
|
||||||
|
describe('/search', () => {
|
||||||
|
let admin: LoginResponseDto;
|
||||||
|
let assetFalcon: AssetFileUploadResponseDto;
|
||||||
|
let assetDenali: AssetFileUploadResponseDto;
|
||||||
|
let websocket: Socket;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await utils.resetDatabase();
|
||||||
|
admin = await utils.adminSetup();
|
||||||
|
websocket = await utils.connectWebsocket(admin.accessToken);
|
||||||
|
|
||||||
|
const files: string[] = [
|
||||||
|
'/albums/nature/prairie_falcon.jpg',
|
||||||
|
'/formats/webp/denali.webp',
|
||||||
|
'/formats/raw/Nikon/D700/philadelphia.nef',
|
||||||
|
'/albums/nature/orychophragmus_violaceus.jpg',
|
||||||
|
'/albums/nature/notocactus_minimus.jpg',
|
||||||
|
'/albums/nature/silver_fir.jpg',
|
||||||
|
'/albums/nature/tanners_ridge.jpg',
|
||||||
|
'/albums/nature/cyclamen_persicum.jpg',
|
||||||
|
'/albums/nature/polemonium_reptans.jpg',
|
||||||
|
'/albums/nature/wood_anemones.jpg',
|
||||||
|
'/formats/heic/IMG_2682.heic',
|
||||||
|
'/formats/jpg/el_torcal_rocks.jpg',
|
||||||
|
'/formats/png/density_plot.png',
|
||||||
|
'/formats/motionphoto/Samsung One UI 6.jpg',
|
||||||
|
'/formats/motionphoto/Samsung One UI 6.heic',
|
||||||
|
'/formats/motionphoto/Samsung One UI 5.jpg',
|
||||||
|
'/formats/raw/Nikon/D80/glarus.nef',
|
||||||
|
'/metadata/gps-position/thompson-springs.jpg',
|
||||||
|
];
|
||||||
|
const assets: AssetFileUploadResponseDto[] = [];
|
||||||
|
for (const filename of files) {
|
||||||
|
const bytes = await readFile(join(testAssetDir, filename));
|
||||||
|
assets.push(
|
||||||
|
await utils.createAsset(admin.accessToken, {
|
||||||
|
deviceAssetId: `test-${filename}`,
|
||||||
|
assetData: { bytes, filename },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const asset of assets) {
|
||||||
|
await utils.waitForWebsocketEvent({ event: 'upload', assetId: asset.id });
|
||||||
|
}
|
||||||
|
|
||||||
|
[assetFalcon, assetDenali] = assets;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await utils.disconnectWebsocket(websocket);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('POST /search/metadata', () => {
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(app).post('/search/metadata');
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should search by camera make', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.post('/search/metadata')
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
|
.send({ make: 'Canon' });
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body).toEqual({
|
||||||
|
albums,
|
||||||
|
assets: {
|
||||||
|
count: 2,
|
||||||
|
items: expect.arrayContaining([
|
||||||
|
expect.objectContaining({ id: assetDenali.id }),
|
||||||
|
expect.objectContaining({ id: assetFalcon.id }),
|
||||||
|
]),
|
||||||
|
facets: [],
|
||||||
|
nextPage: null,
|
||||||
|
total: 2,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should search by camera model', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.post('/search/metadata')
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
|
.send({ model: 'Canon EOS 7D' });
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body).toEqual({
|
||||||
|
albums,
|
||||||
|
assets: {
|
||||||
|
count: 1,
|
||||||
|
items: [expect.objectContaining({ id: assetDenali.id })],
|
||||||
|
facets: [],
|
||||||
|
nextPage: null,
|
||||||
|
total: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('POST /search/smart', () => {
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(app).post('/search/smart');
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /search/explore', () => {
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(app).get('/search/explore');
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get explore data', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.get('/search/explore')
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body).toEqual([
|
||||||
|
{ fieldName: 'exifInfo.city', items: [] },
|
||||||
|
{ fieldName: 'smartInfo.tags', items: [] },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /search/places', () => {
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(app).get('/search/places');
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get places', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.get('/search/places?name=Paris')
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(Array.isArray(body)).toBe(true);
|
||||||
|
expect(body.length).toBeGreaterThan(10);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /search/suggestions', () => {
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(app).get('/search/suggestions');
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get suggestions for country', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.get('/search/suggestions?type=country')
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
expect(body).toEqual(['United States of America']);
|
||||||
|
expect(status).toBe(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get suggestions for state', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.get('/search/suggestions?type=state')
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
expect(body).toEqual(['Douglas County, Nebraska', 'Mesa County, Colorado']);
|
||||||
|
expect(status).toBe(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get suggestions for city', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.get('/search/suggestions?type=city')
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
expect(body).toEqual(['Palisade', 'Ralston']);
|
||||||
|
expect(status).toBe(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get suggestions for camera make', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.get('/search/suggestions?type=camera-make')
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
expect(body).toEqual([
|
||||||
|
'Apple',
|
||||||
|
'Canon',
|
||||||
|
'FUJIFILM',
|
||||||
|
'NIKON CORPORATION',
|
||||||
|
'PENTAX Corporation',
|
||||||
|
'samsung',
|
||||||
|
'SONY',
|
||||||
|
]);
|
||||||
|
expect(status).toBe(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get suggestions for camera model', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.get('/search/suggestions?type=camera-model')
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
expect(body).toEqual([
|
||||||
|
'Canon EOS 7D',
|
||||||
|
'Canon EOS R5',
|
||||||
|
'DSLR-A550',
|
||||||
|
'FinePix S3Pro',
|
||||||
|
'iPhone 7',
|
||||||
|
'NIKON D700',
|
||||||
|
'NIKON D750',
|
||||||
|
'NIKON D80',
|
||||||
|
'PENTAX K10D',
|
||||||
|
'SM-F711N',
|
||||||
|
'SM-S906U',
|
||||||
|
'SM-T970',
|
||||||
|
]);
|
||||||
|
expect(status).toBe(200);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -173,6 +173,7 @@ export const utils = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
waitForWebsocketEvent: async ({ event, assetId, timeout: ms }: WaitOptions): Promise<void> => {
|
waitForWebsocketEvent: async ({ event, assetId, timeout: ms }: WaitOptions): Promise<void> => {
|
||||||
|
console.log(`Waiting for ${event} [${assetId}]`);
|
||||||
const set = events[event];
|
const set = events[event];
|
||||||
if (set.has(assetId)) {
|
if (set.has(assetId)) {
|
||||||
return;
|
return;
|
||||||
@ -232,6 +233,10 @@ export const utils = {
|
|||||||
const assetData = dto?.assetData?.bytes || makeRandomImage();
|
const assetData = dto?.assetData?.bytes || makeRandomImage();
|
||||||
const filename = dto?.assetData?.filename || 'example.png';
|
const filename = dto?.assetData?.filename || 'example.png';
|
||||||
|
|
||||||
|
if (dto?.assetData?.bytes) {
|
||||||
|
console.log(`Uploading ${filename}`);
|
||||||
|
}
|
||||||
|
|
||||||
const builder = request(app)
|
const builder = request(app)
|
||||||
.post(`/asset/upload`)
|
.post(`/asset/upload`)
|
||||||
.attach('assetData', assetData, filename)
|
.attach('assetData', assetData, filename)
|
||||||
|
@ -1,292 +0,0 @@
|
|||||||
import {
|
|
||||||
AssetResponseDto,
|
|
||||||
IAssetRepository,
|
|
||||||
ISearchRepository,
|
|
||||||
LibraryResponseDto,
|
|
||||||
LoginResponseDto,
|
|
||||||
mapAsset,
|
|
||||||
} from '@app/domain';
|
|
||||||
import { SearchController } from '@app/immich';
|
|
||||||
import { INestApplication } from '@nestjs/common';
|
|
||||||
import { errorStub, searchStub } from '@test/fixtures';
|
|
||||||
import request from 'supertest';
|
|
||||||
import { api } from '../../client';
|
|
||||||
import { generateAsset, testApp } from '../utils';
|
|
||||||
|
|
||||||
describe(`${SearchController.name}`, () => {
|
|
||||||
let app: INestApplication;
|
|
||||||
let server: any;
|
|
||||||
let loginResponse: LoginResponseDto;
|
|
||||||
let accessToken: string;
|
|
||||||
let libraries: LibraryResponseDto[];
|
|
||||||
let assetRepository: IAssetRepository;
|
|
||||||
let smartInfoRepository: ISearchRepository;
|
|
||||||
let asset1: AssetResponseDto;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
app = await testApp.create();
|
|
||||||
server = app.getHttpServer();
|
|
||||||
assetRepository = app.get<IAssetRepository>(IAssetRepository);
|
|
||||||
smartInfoRepository = app.get<ISearchRepository>(ISearchRepository);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterAll(async () => {
|
|
||||||
await testApp.teardown();
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
await testApp.reset();
|
|
||||||
await api.authApi.adminSignUp(server);
|
|
||||||
loginResponse = await api.authApi.adminLogin(server);
|
|
||||||
accessToken = loginResponse.accessToken;
|
|
||||||
libraries = await api.libraryApi.getAll(server, accessToken);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('GET /search (exif)', () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
const { id: assetId } = await assetRepository.create(generateAsset(loginResponse.userId, libraries));
|
|
||||||
await assetRepository.upsertExif({ assetId, ...searchStub.exif });
|
|
||||||
|
|
||||||
const assetWithMetadata = await assetRepository.getById(assetId, { exifInfo: true });
|
|
||||||
if (!assetWithMetadata) {
|
|
||||||
throw new Error('Asset not found');
|
|
||||||
}
|
|
||||||
asset1 = mapAsset(assetWithMetadata);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should require authentication', async () => {
|
|
||||||
const { status, body } = await request(server).get('/search');
|
|
||||||
|
|
||||||
expect(status).toBe(401);
|
|
||||||
expect(body).toEqual(errorStub.unauthorized);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return assets when searching by exif', async () => {
|
|
||||||
if (!asset1?.exifInfo?.make) {
|
|
||||||
throw new Error('Asset 1 does not have exif info');
|
|
||||||
}
|
|
||||||
|
|
||||||
const { status, body } = await request(server)
|
|
||||||
.get('/search')
|
|
||||||
.set('Authorization', `Bearer ${accessToken}`)
|
|
||||||
.query({ q: asset1.exifInfo.make });
|
|
||||||
|
|
||||||
expect(status).toBe(200);
|
|
||||||
expect(body).toMatchObject({
|
|
||||||
albums: {
|
|
||||||
total: 0,
|
|
||||||
count: 0,
|
|
||||||
items: [],
|
|
||||||
facets: [],
|
|
||||||
},
|
|
||||||
assets: {
|
|
||||||
total: 1,
|
|
||||||
count: 1,
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
id: asset1.id,
|
|
||||||
exifInfo: {
|
|
||||||
make: asset1.exifInfo.make,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
facets: [],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be case-insensitive for metadata search', async () => {
|
|
||||||
if (!asset1?.exifInfo?.make) {
|
|
||||||
throw new Error('Asset 1 does not have exif info');
|
|
||||||
}
|
|
||||||
|
|
||||||
const { status, body } = await request(server)
|
|
||||||
.get('/search')
|
|
||||||
.set('Authorization', `Bearer ${accessToken}`)
|
|
||||||
.query({ q: asset1.exifInfo.make.toLowerCase() });
|
|
||||||
|
|
||||||
expect(status).toBe(200);
|
|
||||||
expect(body).toMatchObject({
|
|
||||||
albums: {
|
|
||||||
total: 0,
|
|
||||||
count: 0,
|
|
||||||
items: [],
|
|
||||||
facets: [],
|
|
||||||
},
|
|
||||||
assets: {
|
|
||||||
total: 1,
|
|
||||||
count: 1,
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
id: asset1.id,
|
|
||||||
exifInfo: {
|
|
||||||
make: asset1.exifInfo.make,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
facets: [],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be whitespace-insensitive for metadata search', async () => {
|
|
||||||
if (!asset1?.exifInfo?.make) {
|
|
||||||
throw new Error('Asset 1 does not have exif info');
|
|
||||||
}
|
|
||||||
|
|
||||||
const { status, body } = await request(server)
|
|
||||||
.get('/search')
|
|
||||||
.set('Authorization', `Bearer ${accessToken}`)
|
|
||||||
.query({ q: ` ${asset1.exifInfo.make} ` });
|
|
||||||
|
|
||||||
expect(status).toBe(200);
|
|
||||||
expect(body).toMatchObject({
|
|
||||||
albums: {
|
|
||||||
total: 0,
|
|
||||||
count: 0,
|
|
||||||
items: [],
|
|
||||||
facets: [],
|
|
||||||
},
|
|
||||||
assets: {
|
|
||||||
total: 1,
|
|
||||||
count: 1,
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
id: asset1.id,
|
|
||||||
exifInfo: {
|
|
||||||
make: asset1.exifInfo.make,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
facets: [],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('GET /search (smart info)', () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
const { id: assetId } = await assetRepository.create(generateAsset(loginResponse.userId, libraries));
|
|
||||||
await assetRepository.upsertExif({ assetId, ...searchStub.exif });
|
|
||||||
await smartInfoRepository.upsert({ assetId, ...searchStub.smartInfo }, Array.from({ length: 512 }, Math.random));
|
|
||||||
|
|
||||||
const assetWithMetadata = await assetRepository.getById(assetId, { exifInfo: true, smartInfo: true });
|
|
||||||
if (!assetWithMetadata) {
|
|
||||||
throw new Error('Asset not found');
|
|
||||||
}
|
|
||||||
asset1 = mapAsset(assetWithMetadata);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return assets when searching by object', async () => {
|
|
||||||
if (!asset1?.smartInfo?.objects) {
|
|
||||||
throw new Error('Asset 1 does not have smart info');
|
|
||||||
}
|
|
||||||
|
|
||||||
const { status, body } = await request(server)
|
|
||||||
.get('/search')
|
|
||||||
.set('Authorization', `Bearer ${accessToken}`)
|
|
||||||
.query({ q: asset1.smartInfo.objects[0] });
|
|
||||||
|
|
||||||
expect(status).toBe(200);
|
|
||||||
expect(body).toMatchObject({
|
|
||||||
albums: {
|
|
||||||
total: 0,
|
|
||||||
count: 0,
|
|
||||||
items: [],
|
|
||||||
facets: [],
|
|
||||||
},
|
|
||||||
assets: {
|
|
||||||
total: 1,
|
|
||||||
count: 1,
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
id: asset1.id,
|
|
||||||
smartInfo: {
|
|
||||||
objects: asset1.smartInfo.objects,
|
|
||||||
tags: asset1.smartInfo.tags,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
facets: [],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('GET /search (file name)', () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
const { id: assetId } = await assetRepository.create(generateAsset(loginResponse.userId, libraries));
|
|
||||||
await assetRepository.upsertExif({ assetId, ...searchStub.exif });
|
|
||||||
|
|
||||||
const assetWithMetadata = await assetRepository.getById(assetId, { exifInfo: true });
|
|
||||||
if (!assetWithMetadata) {
|
|
||||||
throw new Error('Asset not found');
|
|
||||||
}
|
|
||||||
asset1 = mapAsset(assetWithMetadata);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return assets when searching by file name', async () => {
|
|
||||||
if (asset1?.originalFileName.length === 0) {
|
|
||||||
throw new Error('Asset 1 does not have an original file name');
|
|
||||||
}
|
|
||||||
|
|
||||||
const { status, body } = await request(server)
|
|
||||||
.get('/search')
|
|
||||||
.set('Authorization', `Bearer ${accessToken}`)
|
|
||||||
.query({ q: asset1.originalFileName });
|
|
||||||
|
|
||||||
expect(status).toBe(200);
|
|
||||||
expect(body).toMatchObject({
|
|
||||||
albums: {
|
|
||||||
total: 0,
|
|
||||||
count: 0,
|
|
||||||
items: [],
|
|
||||||
facets: [],
|
|
||||||
},
|
|
||||||
assets: {
|
|
||||||
total: 1,
|
|
||||||
count: 1,
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
id: asset1.id,
|
|
||||||
originalFileName: asset1.originalFileName,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
facets: [],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return assets when searching by file name with extension', async () => {
|
|
||||||
if (asset1?.originalFileName.length === 0) {
|
|
||||||
throw new Error('Asset 1 does not have an original file name');
|
|
||||||
}
|
|
||||||
|
|
||||||
const { status, body } = await request(server)
|
|
||||||
.get('/search')
|
|
||||||
.set('Authorization', `Bearer ${accessToken}`)
|
|
||||||
.query({ q: asset1.originalFileName + '.jpg' });
|
|
||||||
|
|
||||||
expect(status).toBe(200);
|
|
||||||
expect(body).toMatchObject({
|
|
||||||
albums: {
|
|
||||||
total: 0,
|
|
||||||
count: 0,
|
|
||||||
items: [],
|
|
||||||
facets: [],
|
|
||||||
},
|
|
||||||
assets: {
|
|
||||||
total: 1,
|
|
||||||
count: 1,
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
id: asset1.id,
|
|
||||||
originalFileName: asset1.originalFileName,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
facets: [],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
@ -123,7 +123,7 @@ class BaseSearchDto {
|
|||||||
@ValidateBoolean({ optional: true })
|
@ValidateBoolean({ optional: true })
|
||||||
isNotInAlbum?: boolean;
|
isNotInAlbum?: boolean;
|
||||||
|
|
||||||
@Optional()
|
@ValidateUUID({ each: true, optional: true })
|
||||||
personIds?: string[];
|
personIds?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { AssetEntity } from '@app/infra/entities';
|
import { AssetEntity } from '@app/infra/entities';
|
||||||
import { ImmichLogger } from '@app/infra/logger';
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { AssetOrder, AssetResponseDto, mapAsset } from '../asset';
|
import { AssetOrder, AssetResponseDto, mapAsset } from '../asset';
|
||||||
import { AuthDto } from '../auth';
|
import { AuthDto } from '../auth';
|
||||||
@ -30,7 +29,6 @@ import { SearchResponseDto } from './response-dto';
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SearchService {
|
export class SearchService {
|
||||||
private logger = new ImmichLogger(SearchService.name);
|
|
||||||
private configCore: SystemConfigCore;
|
private configCore: SystemConfigCore;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -12,7 +12,7 @@ import {
|
|||||||
SmartSearchDto,
|
SmartSearchDto,
|
||||||
} from '@app/domain';
|
} from '@app/domain';
|
||||||
import { SearchSuggestionRequestDto } from '@app/domain/search/dto/search-suggestion.dto';
|
import { SearchSuggestionRequestDto } from '@app/domain/search/dto/search-suggestion.dto';
|
||||||
import { Body, Controller, Get, Post, Query } from '@nestjs/common';
|
import { Body, Controller, Get, HttpCode, HttpStatus, Post, Query } from '@nestjs/common';
|
||||||
import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||||
import { Auth, Authenticated } from '../app.guard';
|
import { Auth, Authenticated } from '../app.guard';
|
||||||
import { UseValidation } from '../app.utils';
|
import { UseValidation } from '../app.utils';
|
||||||
@ -24,22 +24,24 @@ import { UseValidation } from '../app.utils';
|
|||||||
export class SearchController {
|
export class SearchController {
|
||||||
constructor(private service: SearchService) {}
|
constructor(private service: SearchService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@ApiOperation({ deprecated: true })
|
||||||
|
search(@Auth() auth: AuthDto, @Query() dto: SearchDto): Promise<SearchResponseDto> {
|
||||||
|
return this.service.search(auth, dto);
|
||||||
|
}
|
||||||
|
|
||||||
@Post('metadata')
|
@Post('metadata')
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
searchMetadata(@Auth() auth: AuthDto, @Body() dto: MetadataSearchDto): Promise<SearchResponseDto> {
|
searchMetadata(@Auth() auth: AuthDto, @Body() dto: MetadataSearchDto): Promise<SearchResponseDto> {
|
||||||
return this.service.searchMetadata(auth, dto);
|
return this.service.searchMetadata(auth, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('smart')
|
@Post('smart')
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
searchSmart(@Auth() auth: AuthDto, @Body() dto: SmartSearchDto): Promise<SearchResponseDto> {
|
searchSmart(@Auth() auth: AuthDto, @Body() dto: SmartSearchDto): Promise<SearchResponseDto> {
|
||||||
return this.service.searchSmart(auth, dto);
|
return this.service.searchSmart(auth, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get()
|
|
||||||
@ApiOperation({ deprecated: true })
|
|
||||||
search(@Auth() auth: AuthDto, @Query() dto: SearchDto): Promise<SearchResponseDto> {
|
|
||||||
return this.service.search(auth, dto);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get('explore')
|
@Get('explore')
|
||||||
getExploreData(@Auth() auth: AuthDto): Promise<SearchExploreResponseDto[]> {
|
getExploreData(@Auth() auth: AuthDto): Promise<SearchExploreResponseDto[]> {
|
||||||
return this.service.getExploreData(auth) as Promise<SearchExploreResponseDto[]>;
|
return this.service.getExploreData(auth) as Promise<SearchExploreResponseDto[]>;
|
||||||
|
1
server/test/fixtures/index.ts
vendored
1
server/test/fixtures/index.ts
vendored
@ -10,7 +10,6 @@ export * from './library.stub';
|
|||||||
export * from './media.stub';
|
export * from './media.stub';
|
||||||
export * from './partner.stub';
|
export * from './partner.stub';
|
||||||
export * from './person.stub';
|
export * from './person.stub';
|
||||||
export * from './search.stub';
|
|
||||||
export * from './shared-link.stub';
|
export * from './shared-link.stub';
|
||||||
export * from './system-config.stub';
|
export * from './system-config.stub';
|
||||||
export * from './tag.stub';
|
export * from './tag.stub';
|
||||||
|
36
server/test/fixtures/search.stub.ts
vendored
36
server/test/fixtures/search.stub.ts
vendored
@ -1,36 +0,0 @@
|
|||||||
import { SearchResult } from '@app/domain';
|
|
||||||
import { AssetEntity, ExifEntity, SmartInfoEntity } from '@app/infra/entities';
|
|
||||||
import { assetStub } from '.';
|
|
||||||
|
|
||||||
export const searchStub = {
|
|
||||||
emptyResults: Object.freeze<SearchResult<any>>({
|
|
||||||
total: 0,
|
|
||||||
count: 0,
|
|
||||||
page: 1,
|
|
||||||
items: [],
|
|
||||||
facets: [],
|
|
||||||
distances: [],
|
|
||||||
}),
|
|
||||||
|
|
||||||
withImage: Object.freeze<SearchResult<AssetEntity>>({
|
|
||||||
total: 1,
|
|
||||||
count: 1,
|
|
||||||
page: 1,
|
|
||||||
items: [assetStub.image],
|
|
||||||
facets: [],
|
|
||||||
distances: [],
|
|
||||||
}),
|
|
||||||
|
|
||||||
exif: Object.freeze<Partial<ExifEntity>>({
|
|
||||||
latitude: 90,
|
|
||||||
longitude: 90,
|
|
||||||
city: 'Immich',
|
|
||||||
state: 'Nebraska',
|
|
||||||
country: 'United States',
|
|
||||||
make: 'Canon',
|
|
||||||
model: 'EOS Rebel T7',
|
|
||||||
lensModel: 'Fancy lens',
|
|
||||||
}),
|
|
||||||
|
|
||||||
smartInfo: Object.freeze<Partial<SmartInfoEntity>>({ objects: ['car', 'tree'], tags: ['accident'] }),
|
|
||||||
};
|
|
Loading…
x
Reference in New Issue
Block a user