chore!: remove getRandom api endpoint (#27780)

* chore!: remove getRandom api endpoint

* chore: sync openapi

* fix: test

* chore: more cleanup
This commit is contained in:
Brandon Wees 2026-04-14 20:32:12 -05:00 committed by GitHub
parent 41d2d84b21
commit 6da2d3d587
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 2 additions and 357 deletions

View File

@ -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)

View File

@ -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

View File

@ -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.

View File

@ -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.

View File

@ -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.",

View File

@ -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
*/

View File

@ -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`);

View File

@ -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',

View File

@ -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) {}

View File

@ -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

View File

@ -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();

View File

@ -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);
}

View File

@ -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(),