mirror of
https://github.com/immich-app/immich.git
synced 2026-04-27 19:49:50 -04:00
chore!: remove getRandom api endpoint (#27780)
* chore!: remove getRandom api endpoint * chore: sync openapi * fix: test * chore: more cleanup
This commit is contained in:
parent
41d2d84b21
commit
6da2d3d587
@ -1,7 +1,6 @@
|
||||
import {
|
||||
AssetMediaResponseDto,
|
||||
AssetMediaStatus,
|
||||
AssetResponseDto,
|
||||
AssetTypeEnum,
|
||||
AssetVisibility,
|
||||
getAssetInfo,
|
||||
@ -19,7 +18,7 @@ import { Socket } from 'socket.io-client';
|
||||
import { createUserDto, uuidDto } from 'src/fixtures';
|
||||
import { makeRandomImage } from 'src/generators';
|
||||
import { errorDto } from 'src/responses';
|
||||
import { app, asBearerAuth, tempDir, TEN_TIMES, testAssetDir, utils } from 'src/utils';
|
||||
import { app, asBearerAuth, tempDir, testAssetDir, utils } from 'src/utils';
|
||||
import request from 'supertest';
|
||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
||||
|
||||
@ -380,57 +379,6 @@ describe('/asset', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /assets/random', () => {
|
||||
beforeAll(async () => {
|
||||
await Promise.all([
|
||||
utils.createAsset(user1.accessToken),
|
||||
utils.createAsset(user1.accessToken),
|
||||
utils.createAsset(user1.accessToken),
|
||||
utils.createAsset(user1.accessToken),
|
||||
utils.createAsset(user1.accessToken),
|
||||
utils.createAsset(user1.accessToken),
|
||||
]);
|
||||
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'thumbnailGeneration');
|
||||
});
|
||||
|
||||
it.each(TEN_TIMES)('should return 1 random assets', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.get('/assets/random')
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||
|
||||
expect(status).toBe(200);
|
||||
|
||||
const assets: AssetResponseDto[] = body;
|
||||
expect(assets.length).toBe(1);
|
||||
expect(assets[0].ownerId).toBe(user1.userId);
|
||||
});
|
||||
|
||||
it.each(TEN_TIMES)('should return 2 random assets', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.get('/assets/random?count=2')
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||
|
||||
expect(status).toBe(200);
|
||||
|
||||
const assets: AssetResponseDto[] = body;
|
||||
expect(assets.length).toBe(2);
|
||||
|
||||
for (const asset of assets) {
|
||||
expect(asset.ownerId).toBe(user1.userId);
|
||||
}
|
||||
});
|
||||
|
||||
it.skip('should return 1 asset if there are 10 assets in the database but user 2 only has 1', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.get('/assets/random')
|
||||
.set('Authorization', `Bearer ${user2.accessToken}`);
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual([expect.objectContaining({ id: user2Assets[0].id })]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('PUT /assets/:id', () => {
|
||||
it('should require access', async () => {
|
||||
const { status, body } = await request(app)
|
||||
|
||||
2
mobile/openapi/README.md
generated
2
mobile/openapi/README.md
generated
@ -110,7 +110,6 @@ Class | Method | HTTP request | Description
|
||||
*AssetsApi* | [**getAssetMetadataByKey**](doc//AssetsApi.md#getassetmetadatabykey) | **GET** /assets/{id}/metadata/{key} | Retrieve asset metadata by key
|
||||
*AssetsApi* | [**getAssetOcr**](doc//AssetsApi.md#getassetocr) | **GET** /assets/{id}/ocr | Retrieve asset OCR data
|
||||
*AssetsApi* | [**getAssetStatistics**](doc//AssetsApi.md#getassetstatistics) | **GET** /assets/statistics | Get asset statistics
|
||||
*AssetsApi* | [**getRandom**](doc//AssetsApi.md#getrandom) | **GET** /assets/random | Get random assets
|
||||
*AssetsApi* | [**playAssetVideo**](doc//AssetsApi.md#playassetvideo) | **GET** /assets/{id}/video/playback | Play asset video
|
||||
*AssetsApi* | [**removeAssetEdits**](doc//AssetsApi.md#removeassetedits) | **DELETE** /assets/{id}/edits | Remove edits from an existing asset
|
||||
*AssetsApi* | [**runAssetJobs**](doc//AssetsApi.md#runassetjobs) | **POST** /assets/jobs | Run an asset job
|
||||
@ -147,7 +146,6 @@ Class | Method | HTTP request | Description
|
||||
*DeprecatedApi* | [**getDeltaSync**](doc//DeprecatedApi.md#getdeltasync) | **POST** /sync/delta-sync | Get delta sync for user
|
||||
*DeprecatedApi* | [**getFullSyncForUser**](doc//DeprecatedApi.md#getfullsyncforuser) | **POST** /sync/full-sync | Get full sync for user
|
||||
*DeprecatedApi* | [**getQueuesLegacy**](doc//DeprecatedApi.md#getqueueslegacy) | **GET** /jobs | Retrieve queue counts and status
|
||||
*DeprecatedApi* | [**getRandom**](doc//DeprecatedApi.md#getrandom) | **GET** /assets/random | Get random assets
|
||||
*DeprecatedApi* | [**runQueueCommandLegacy**](doc//DeprecatedApi.md#runqueuecommandlegacy) | **PUT** /jobs/{name} | Run jobs
|
||||
*DownloadApi* | [**downloadArchive**](doc//DownloadApi.md#downloadarchive) | **POST** /download/archive | Download asset archive
|
||||
*DownloadApi* | [**getDownloadInfo**](doc//DownloadApi.md#getdownloadinfo) | **POST** /download/info | Retrieve download information
|
||||
|
||||
65
mobile/openapi/lib/api/assets_api.dart
generated
65
mobile/openapi/lib/api/assets_api.dart
generated
@ -927,71 +927,6 @@ class AssetsApi {
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Get random assets
|
||||
///
|
||||
/// Retrieve a specified number of random assets for the authenticated user.
|
||||
///
|
||||
/// Note: This method returns the HTTP [Response].
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [num] count:
|
||||
/// Number of random assets to return
|
||||
Future<Response> getRandomWithHttpInfo({ num? count, }) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/assets/random';
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
if (count != null) {
|
||||
queryParams.addAll(_queryParams('', 'count', count));
|
||||
}
|
||||
|
||||
const contentTypes = <String>[];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
apiPath,
|
||||
'GET',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
/// Get random assets
|
||||
///
|
||||
/// Retrieve a specified number of random assets for the authenticated user.
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [num] count:
|
||||
/// Number of random assets to return
|
||||
Future<List<AssetResponseDto>?> getRandom({ num? count, }) async {
|
||||
final response = await getRandomWithHttpInfo( count: count, );
|
||||
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) {
|
||||
final responseBody = await _decodeBodyBytes(response);
|
||||
return (await apiClient.deserializeAsync(responseBody, 'List<AssetResponseDto>') as List)
|
||||
.cast<AssetResponseDto>()
|
||||
.toList(growable: false);
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Play asset video
|
||||
///
|
||||
/// Streams the video file for the specified asset. This endpoint also supports byte range requests.
|
||||
|
||||
65
mobile/openapi/lib/api/deprecated_api.dart
generated
65
mobile/openapi/lib/api/deprecated_api.dart
generated
@ -298,71 +298,6 @@ class DeprecatedApi {
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Get random assets
|
||||
///
|
||||
/// Retrieve a specified number of random assets for the authenticated user.
|
||||
///
|
||||
/// Note: This method returns the HTTP [Response].
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [num] count:
|
||||
/// Number of random assets to return
|
||||
Future<Response> getRandomWithHttpInfo({ num? count, }) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/assets/random';
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
if (count != null) {
|
||||
queryParams.addAll(_queryParams('', 'count', count));
|
||||
}
|
||||
|
||||
const contentTypes = <String>[];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
apiPath,
|
||||
'GET',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
/// Get random assets
|
||||
///
|
||||
/// Retrieve a specified number of random assets for the authenticated user.
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [num] count:
|
||||
/// Number of random assets to return
|
||||
Future<List<AssetResponseDto>?> getRandom({ num? count, }) async {
|
||||
final response = await getRandomWithHttpInfo( count: count, );
|
||||
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) {
|
||||
final responseBody = await _decodeBodyBytes(response);
|
||||
return (await apiClient.deserializeAsync(responseBody, 'List<AssetResponseDto>') as List)
|
||||
.cast<AssetResponseDto>()
|
||||
.toList(growable: false);
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Run jobs
|
||||
///
|
||||
/// Queue all assets for a specific job type. Defaults to only queueing assets that have not yet been processed, but the force command can be used to re-process all assets.
|
||||
|
||||
@ -3363,69 +3363,6 @@
|
||||
"x-immich-state": "Beta"
|
||||
}
|
||||
},
|
||||
"/assets/random": {
|
||||
"get": {
|
||||
"deprecated": true,
|
||||
"description": "Retrieve a specified number of random assets for the authenticated user.",
|
||||
"operationId": "getRandom",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "count",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Number of random assets to return",
|
||||
"schema": {
|
||||
"minimum": 1,
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/AssetResponseDto"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"summary": "Get random assets",
|
||||
"tags": [
|
||||
"Assets",
|
||||
"Deprecated"
|
||||
],
|
||||
"x-immich-history": [
|
||||
{
|
||||
"version": "v1",
|
||||
"state": "Added"
|
||||
},
|
||||
{
|
||||
"version": "v1",
|
||||
"state": "Deprecated",
|
||||
"replacementId": "searchAssets"
|
||||
}
|
||||
],
|
||||
"x-immich-permission": "asset.read",
|
||||
"x-immich-state": "Deprecated"
|
||||
}
|
||||
},
|
||||
"/assets/statistics": {
|
||||
"get": {
|
||||
"description": "Retrieve various statistics about the assets owned by the authenticated user.",
|
||||
|
||||
@ -4050,21 +4050,6 @@ export function updateBulkAssetMetadata({ assetMetadataBulkUpsertDto }: {
|
||||
body: assetMetadataBulkUpsertDto
|
||||
})));
|
||||
}
|
||||
/**
|
||||
* Get random assets
|
||||
*/
|
||||
export function getRandom({ count }: {
|
||||
count?: number;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: AssetResponseDto[];
|
||||
}>(`/assets/random${QS.query(QS.explode({
|
||||
count
|
||||
}))}`, {
|
||||
...opts
|
||||
}));
|
||||
}
|
||||
/**
|
||||
* Get asset statistics
|
||||
*/
|
||||
|
||||
@ -245,19 +245,6 @@ describe(AssetController.name, () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /assets/random', () => {
|
||||
it('should be an authenticated route', async () => {
|
||||
await request(ctx.getHttpServer()).get(`/assets/random`);
|
||||
expect(ctx.authenticate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not allow count to be a string', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer()).get('/assets/random?count=ABC');
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(factory.responses.badRequest(['[count] Invalid input: expected number, received NaN']));
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /assets/:id/metadata', () => {
|
||||
it('should be an authenticated route', async () => {
|
||||
await request(ctx.getHttpServer()).get(`/assets/${factory.uuid()}/metadata`);
|
||||
|
||||
@ -16,7 +16,6 @@ import {
|
||||
AssetStatsDto,
|
||||
AssetStatsResponseDto,
|
||||
DeviceIdDto,
|
||||
RandomAssetsDto,
|
||||
UpdateAssetDto,
|
||||
} from 'src/dtos/asset.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
@ -32,17 +31,6 @@ import { UUIDParamDto } from 'src/validation';
|
||||
export class AssetController {
|
||||
constructor(private service: AssetService) {}
|
||||
|
||||
@Get('random')
|
||||
@Authenticated({ permission: Permission.AssetRead })
|
||||
@Endpoint({
|
||||
summary: 'Get random assets',
|
||||
description: 'Retrieve a specified number of random assets for the authenticated user.',
|
||||
history: new HistoryBuilder().added('v1').deprecated('v1', { replacementId: 'searchAssets' }),
|
||||
})
|
||||
getRandom(@Auth() auth: AuthDto, @Query() dto: RandomAssetsDto): Promise<AssetResponseDto[]> {
|
||||
return this.service.getRandom(auth, dto.count ?? 1);
|
||||
}
|
||||
|
||||
@Get('/device/:deviceId')
|
||||
@Endpoint({
|
||||
summary: 'Retrieve assets by device ID',
|
||||
|
||||
@ -58,12 +58,6 @@ const UpdateAssetSchema = UpdateAssetBaseSchema.extend({
|
||||
livePhotoVideoId: z.uuidv4().nullish().describe('Live photo video ID'),
|
||||
}).meta({ id: 'UpdateAssetDto' });
|
||||
|
||||
const RandomAssetsSchema = z
|
||||
.object({
|
||||
count: z.coerce.number().min(1).optional().describe('Number of random assets to return'),
|
||||
})
|
||||
.meta({ id: 'RandomAssetsDto' });
|
||||
|
||||
const AssetBulkDeleteSchema = BulkIdsSchema.extend({
|
||||
force: z.boolean().optional().describe('Force delete even if in use'),
|
||||
}).meta({ id: 'AssetBulkDeleteDto' });
|
||||
@ -191,7 +185,6 @@ export const mapStats = (stats: AssetStats): AssetStatsResponseDto => {
|
||||
export class DeviceIdDto extends createZodDto(DeviceIdSchema) {}
|
||||
export class AssetBulkUpdateDto extends createZodDto(AssetBulkUpdateSchema) {}
|
||||
export class UpdateAssetDto extends createZodDto(UpdateAssetSchema) {}
|
||||
export class RandomAssetsDto extends createZodDto(RandomAssetsSchema) {}
|
||||
export class AssetBulkDeleteDto extends createZodDto(AssetBulkDeleteSchema) {}
|
||||
export class AssetIdsDto extends createZodDto(AssetIdsSchema) {}
|
||||
export class AssetJobsDto extends createZodDto(AssetJobsSchema) {}
|
||||
|
||||
@ -681,19 +681,6 @@ export class AssetRepository {
|
||||
.executeTakeFirstOrThrow();
|
||||
}
|
||||
|
||||
getRandom(userIds: string[], take: number) {
|
||||
return this.db
|
||||
.selectFrom('asset')
|
||||
.selectAll('asset')
|
||||
.$call(withExif)
|
||||
.$call(withDefaultVisibility)
|
||||
.where('ownerId', '=', anyUuid(userIds))
|
||||
.where('deletedAt', 'is', null)
|
||||
.orderBy((eb) => eb.fn('random'))
|
||||
.limit(take)
|
||||
.execute();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [{}] })
|
||||
async getTimeBuckets(options: TimeBucketOptions): Promise<TimeBucketItem[]> {
|
||||
return this.db
|
||||
|
||||
@ -7,9 +7,8 @@ import { AssetStats } from 'src/repositories/asset.repository';
|
||||
import { AssetService } from 'src/services/asset.service';
|
||||
import { AssetFactory } from 'test/factories/asset.factory';
|
||||
import { AuthFactory } from 'test/factories/auth.factory';
|
||||
import { PartnerFactory } from 'test/factories/partner.factory';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { getForAsset, getForAssetDeletion, getForPartner } from 'test/mappers';
|
||||
import { getForAsset, getForAssetDeletion } from 'test/mappers';
|
||||
import { factory, newUuid } from 'test/small.factory';
|
||||
import { makeStream, newTestService, ServiceMocks } from 'test/utils';
|
||||
|
||||
@ -70,41 +69,6 @@ describe(AssetService.name, () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRandom', () => {
|
||||
it('should get own random assets', async () => {
|
||||
mocks.partner.getAll.mockResolvedValue([]);
|
||||
mocks.asset.getRandom.mockResolvedValue([getForAsset(AssetFactory.create())]);
|
||||
|
||||
await sut.getRandom(authStub.admin, 1);
|
||||
|
||||
expect(mocks.asset.getRandom).toHaveBeenCalledWith([authStub.admin.user.id], 1);
|
||||
});
|
||||
|
||||
it('should not include partner assets if not in timeline', async () => {
|
||||
const partner = PartnerFactory.create({ inTimeline: false });
|
||||
const auth = AuthFactory.create({ id: partner.sharedWithId });
|
||||
|
||||
mocks.asset.getRandom.mockResolvedValue([getForAsset(AssetFactory.create())]);
|
||||
mocks.partner.getAll.mockResolvedValue([getForPartner(partner)]);
|
||||
|
||||
await sut.getRandom(auth, 1);
|
||||
|
||||
expect(mocks.asset.getRandom).toHaveBeenCalledWith([auth.user.id], 1);
|
||||
});
|
||||
|
||||
it('should include partner assets if in timeline', async () => {
|
||||
const partner = PartnerFactory.create({ inTimeline: true });
|
||||
const auth = AuthFactory.create({ id: partner.sharedWithId });
|
||||
|
||||
mocks.asset.getRandom.mockResolvedValue([getForAsset(AssetFactory.create())]);
|
||||
mocks.partner.getAll.mockResolvedValue([getForPartner(partner)]);
|
||||
|
||||
await sut.getRandom(auth, 1);
|
||||
|
||||
expect(mocks.asset.getRandom).toHaveBeenCalledWith([auth.user.id, partner.sharedById], 1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('get', () => {
|
||||
it('should allow owner access', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
|
||||
@ -39,7 +39,6 @@ import { requireElevatedPermission } from 'src/utils/access';
|
||||
import {
|
||||
getAssetFiles,
|
||||
getDimensions,
|
||||
getMyPartnerIds,
|
||||
isPanorama,
|
||||
onAfterUnlink,
|
||||
onBeforeLink,
|
||||
@ -60,16 +59,6 @@ export class AssetService extends BaseService {
|
||||
return mapStats(stats);
|
||||
}
|
||||
|
||||
async getRandom(auth: AuthDto, count: number): Promise<AssetResponseDto[]> {
|
||||
const partnerIds = await getMyPartnerIds({
|
||||
userId: auth.user.id,
|
||||
repository: this.partnerRepository,
|
||||
timelineEnabled: true,
|
||||
});
|
||||
const assets = await this.assetRepository.getRandom([auth.user.id, ...partnerIds], count);
|
||||
return assets.map((a) => mapAsset(a, { auth }));
|
||||
}
|
||||
|
||||
async getUserAssetsByDeviceId(auth: AuthDto, deviceId: string) {
|
||||
return this.assetRepository.getAllByDeviceId(auth.user.id, deviceId);
|
||||
}
|
||||
|
||||
@ -20,7 +20,6 @@ export const newAssetRepositoryMock = (): Mocked<RepositoryInterface<AssetReposi
|
||||
getByChecksum: vitest.fn(),
|
||||
getByChecksums: vitest.fn(),
|
||||
getUploadAssetIdByChecksum: vitest.fn(),
|
||||
getRandom: vitest.fn(),
|
||||
getAllByDeviceId: vitest.fn(),
|
||||
getLivePhotoCount: vitest.fn(),
|
||||
getLibraryAssetCount: vitest.fn(),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user