page defaults

This commit is contained in:
timonrieger
2026-07-25 16:11:42 +02:00
parent fe0f4e2140
commit 72e2a18f03
4 changed files with 56 additions and 38 deletions
+3 -2
View File
@@ -70,12 +70,12 @@ const BaseSearchSchema = z.object({
const BaseSearchWithResultsSchema = BaseSearchSchema.extend({
withDeleted: z.boolean().optional().describe('Include deleted assets').meta(DEPRECATED_FLAT_FIELD),
withExif: z.boolean().optional().describe('Include EXIF data in response'),
size: z.int().min(1).max(1000).optional().describe('Number of results to return'),
size: z.int().min(1).max(1000).default(250).describe('Number of results to return'),
});
const LargeAssetSearchSchema = BaseSearchWithResultsSchema.extend({
minFileSize: z.coerce.number().int().min(0).optional().describe('Minimum file size in bytes'),
size: z.coerce.number().int().min(1).max(1000).optional().describe('Number of results to return'),
size: z.coerce.number().int().min(1).max(1000).default(250).describe('Number of results to return'),
}).meta({ id: 'LargeAssetSearchDto' });
const SearchPlacesSchema = z
@@ -372,6 +372,7 @@ const StatisticsSearchSchema = withShapeExclusivity(
const SmartSearchSchema = withShapeExclusivity(
BaseSearchWithResultsSchema.extend({
size: z.int().min(1).max(1000).default(100).describe('Number of results to return'),
query: z.string().trim().optional().describe('Natural language search query'),
queryAssetId: z.uuidv4().optional().describe('Asset ID to use as search reference'),
language: z.string().optional().describe('Search language code'),
+22 -14
View File
@@ -222,12 +222,12 @@ describe(SearchService.name, () => {
const auth = AuthFactory.create();
mocks.search.searchMetadataV3.mockResolvedValue({ hasNextPage: false, items: [] });
await sut.searchMetadata(auth, { filter: {} });
await sut.searchMetadata(auth, { size: 250, filter: {} });
expect(mocks.search.searchMetadataV3).toHaveBeenCalled();
expect(mocks.search.searchMetadata).not.toHaveBeenCalled();
mocks.search.searchMetadata.mockResolvedValue({ hasNextPage: false, items: [] });
await sut.searchMetadata(auth, { city: 'Oslo' });
await sut.searchMetadata(auth, { size: 250, city: 'Oslo' });
expect(mocks.search.searchMetadata).toHaveBeenCalled();
});
@@ -238,11 +238,11 @@ describe(SearchService.name, () => {
await expect(sut.searchStatistics(auth, { filter: {} })).resolves.toEqual({ total: 0 });
mocks.search.searchRandomV3.mockResolvedValue([]);
await expect(sut.searchRandom(auth, { filter: {} })).resolves.toEqual([]);
await expect(sut.searchRandom(auth, { size: 250, filter: {} })).resolves.toEqual([]);
mocks.search.searchSmartV3.mockResolvedValue({ hasNextPage: false, items: [] });
mocks.machineLearning.encodeText.mockResolvedValue('[1, 2, 3]');
await sut.searchSmart(auth, { filter: {}, query: 'test' });
await sut.searchSmart(auth, { size: 100, filter: {}, query: 'test' });
expect(mocks.search.searchSmartV3).toHaveBeenCalledWith(
{ size: 100, offset: 0 },
expect.objectContaining({ embedding: '[1, 2, 3]' }),
@@ -250,27 +250,29 @@ describe(SearchService.name, () => {
});
it('should reject an invalid cursor', async () => {
await expect(sut.searchMetadata(AuthFactory.create(), { cursor: '???' })).rejects.toThrowError(
await expect(sut.searchMetadata(AuthFactory.create(), { size: 250, cursor: '???' })).rejects.toThrowError(
new BadRequestException('Invalid cursor'),
);
});
it('should reject an unelevated session whose filter could match locked assets', async () => {
const filter = { visibility: { in: [AssetVisibility.Locked, AssetVisibility.Timeline] } };
await expect(sut.searchMetadata(AuthFactory.create(), { filter })).rejects.toBeInstanceOf(UnauthorizedException);
await expect(sut.searchMetadata(AuthFactory.create(), { size: 250, filter })).rejects.toBeInstanceOf(
UnauthorizedException,
);
});
it('should reject a shared link without a top-level album constraint', async () => {
const auth = AuthFactory.from().sharedLink().build();
await expect(sut.searchMetadata(auth, { filter: {} })).rejects.toThrowError(
await expect(sut.searchMetadata(auth, { size: 250, filter: {} })).rejects.toThrowError(
new BadRequestException('Shared link access is only allowed in combination with an albumIds filter'),
);
const albumId = newUuid();
mocks.access.album.checkSharedLinkAccess.mockResolvedValue(new Set([albumId]));
await expect(
sut.searchMetadata(auth, { filter: { or: [{ albumIds: { any: [albumId] } }] } }),
sut.searchMetadata(auth, { size: 250, filter: { or: [{ albumIds: { any: [albumId] } }] } }),
).rejects.toThrowError(
new BadRequestException('Shared link access is only allowed in combination with an albumIds filter'),
);
@@ -288,7 +290,7 @@ describe(SearchService.name, () => {
machineLearning: { enabled: false },
});
await expect(sut.searchSmart(authStub.user1, { query: 'test' })).rejects.toThrowError(
await expect(sut.searchSmart(authStub.user1, { size: 100, query: 'test' })).rejects.toThrowError(
new BadRequestException('Smart search is not enabled'),
);
});
@@ -298,13 +300,13 @@ describe(SearchService.name, () => {
machineLearning: { clip: { enabled: false } },
});
await expect(sut.searchSmart(authStub.user1, { query: 'test' })).rejects.toThrowError(
await expect(sut.searchSmart(authStub.user1, { size: 100, query: 'test' })).rejects.toThrowError(
new BadRequestException('Smart search is not enabled'),
);
});
it('should work', async () => {
await sut.searchSmart(authStub.user1, { query: 'test' });
await sut.searchSmart(authStub.user1, { size: 100, query: 'test' });
expect(mocks.machineLearning.encodeText).toHaveBeenCalledWith(
'test',
@@ -312,7 +314,13 @@ describe(SearchService.name, () => {
);
expect(mocks.search.searchSmart).toHaveBeenCalledWith(
{ page: 1, size: 100 },
{ query: 'test', embedding: '[1, 2, 3]', userIds: [authStub.user1.user.id], visibility: 'not-locked' },
{
query: 'test',
size: 100,
embedding: '[1, 2, 3]',
userIds: [authStub.user1.user.id],
visibility: 'not-locked',
},
);
});
@@ -334,7 +342,7 @@ describe(SearchService.name, () => {
machineLearning: { clip: { modelName: 'ViT-B-16-SigLIP__webli' } },
});
await sut.searchSmart(authStub.user1, { query: 'test' });
await sut.searchSmart(authStub.user1, { size: 100, query: 'test' });
expect(mocks.machineLearning.encodeText).toHaveBeenCalledWith(
'test',
@@ -343,7 +351,7 @@ describe(SearchService.name, () => {
});
it('should use language specified in request', async () => {
await sut.searchSmart(authStub.user1, { query: 'test', language: 'de' });
await sut.searchSmart(authStub.user1, { size: 100, query: 'test', language: 'de' });
expect(mocks.machineLearning.encodeText).toHaveBeenCalledWith(
'test',
+7 -7
View File
@@ -98,7 +98,7 @@ export class SearchService extends BaseService {
}
const page = dto.page ?? 1;
const size = dto.size || 250;
const size = dto.size;
const { hasNextPage, items } = await this.searchRepository.searchMetadata(
{ page, size },
{
@@ -140,7 +140,7 @@ export class SearchService extends BaseService {
}
const userIds = await this.getUserIdsToSearch(auth, dto.visibility);
const items = await this.searchRepository.searchRandom(dto.size || 250, {
const items = await this.searchRepository.searchRandom(dto.size, {
...dto,
visibility: dto.visibility ?? (auth.session?.hasElevatedPermission ? undefined : 'not-locked'),
userIds,
@@ -154,7 +154,7 @@ export class SearchService extends BaseService {
}
const userIds = await this.getUserIdsToSearch(auth, dto.visibility);
const items = await this.searchRepository.searchLargeAssets(dto.size || 250, {
const items = await this.searchRepository.searchLargeAssets(dto.size, {
...dto,
visibility: dto.visibility ?? (auth.session?.hasElevatedPermission ? undefined : 'not-locked'),
userIds,
@@ -179,7 +179,7 @@ export class SearchService extends BaseService {
const userIds = this.getUserIdsToSearch(auth, dto.visibility);
const embedding = await this.resolveEmbedding(auth, dto, machineLearning);
const page = dto.page ?? 1;
const size = dto.size || 100;
const size = dto.size;
const { hasNextPage, items } = await this.searchRepository.searchSmart(
{ page, size },
{
@@ -252,7 +252,7 @@ export class SearchService extends BaseService {
]);
const { offset } = decodeSearchCursor(dto.cursor);
const size = dto.size || 250;
const size = dto.size;
const { hasNextPage, items } = await this.searchRepository.searchMetadataV3(
{ size, offset },
{
@@ -275,7 +275,7 @@ export class SearchService extends BaseService {
private async searchRandomV3(auth: AuthDto, dto: RandomSearchDto): Promise<AssetResponseDto[]> {
const { effectiveFilter, userIds } = await this.resolveSearchScopeV3(auth, dto);
const items = await this.searchRepository.searchRandomV3(dto.size || 250, {
const items = await this.searchRepository.searchRandomV3(dto.size, {
filter: effectiveFilter,
userIds,
withExif: dto.withExif,
@@ -297,7 +297,7 @@ export class SearchService extends BaseService {
]);
const { offset } = decodeSearchCursor(dto.cursor);
const size = dto.size || 100;
const size = dto.size;
const { hasNextPage, items } = await this.searchRepository.searchSmartV3(
{ size, offset },
{ filter: effectiveFilter, userIds, withExif: dto.withExif, embedding },
@@ -58,7 +58,7 @@ describe(SearchService.name, () => {
const auth = factory.auth({ user: { id: user.id } });
await expect(sut.searchLargeAssets(auth, {})).resolves.toEqual([
await expect(sut.searchLargeAssets(auth, { size: 250 })).resolves.toEqual([
expect.objectContaining({ id: assets[2].id }),
expect.objectContaining({ id: assets[0].id }),
expect.objectContaining({ id: assets[1].id }),
@@ -122,7 +122,7 @@ describe(SearchService.name, () => {
const auth = factory.auth({ user: { id: user.id } });
const response = await sut.searchMetadata(auth, { withStacked: false });
const response = await sut.searchMetadata(auth, { size: 250, withStacked: false });
expect(response.assets.items.length).toBe(1);
expect(response.assets.items[0].id).toBe(unstackedAsset.id);
@@ -137,7 +137,7 @@ describe(SearchService.name, () => {
const auth = factory.auth({ user: { id: user.id } });
const response = await sut.searchMetadata(auth, { withStacked: false });
const response = await sut.searchMetadata(auth, { size: 250, withStacked: false });
expect(response.assets.items.length).toBe(0);
});
@@ -150,7 +150,7 @@ describe(SearchService.name, () => {
const auth = factory.auth({ user: { id: user.id }, session: { hasElevatedPermission: true } });
const response = await sut.searchMetadata(auth, { withStacked: false });
const response = await sut.searchMetadata(auth, { size: 250, withStacked: false });
expect(response.assets.items.length).toBe(1);
});
@@ -170,7 +170,7 @@ describe(SearchService.name, () => {
const auth = factory.auth({ user: { id: user.id } });
const response = await sut.searchMetadata(auth, { albumIds: [album.id] });
const response = await sut.searchMetadata(auth, { size: 250, albumIds: [album.id] });
expect(response.assets.items.length).toBe(1);
});
@@ -188,7 +188,7 @@ describe(SearchService.name, () => {
const auth = factory.auth({ user: { id: user.id } });
await expect(sut.searchMetadata(auth, { albumIds: [album.id] })).rejects.toThrow(
await expect(sut.searchMetadata(auth, { size: 250, albumIds: [album.id] })).rejects.toThrow(
'Not found or no album.read access',
);
});
@@ -224,7 +224,7 @@ describe(SearchService.name, () => {
const auth = factory.auth({ user: { id: user.id } });
const response = await sut.searchRandom(auth, {});
const response = await sut.searchRandom(auth, { size: 250 });
expect(response.length).toBe(0);
});
@@ -240,7 +240,7 @@ describe(SearchService.name, () => {
await ctx.newExif({ assetId: other.id, city: 'Bergen' });
const auth = factory.auth({ user: { id: user.id } });
const response = await sut.searchMetadata(auth, { filter: { city: { eq: 'Oslo' } } });
const response = await sut.searchMetadata(auth, { size: 250, filter: { city: { eq: 'Oslo' } } });
expect(response.assets.items).toEqual([expect.objectContaining({ id: asset.id })]);
expect(response.assets.nextPage).toBeNull();
@@ -257,6 +257,7 @@ describe(SearchService.name, () => {
const auth = factory.auth({ user: { id: user.id } });
const response = await sut.searchMetadata(auth, {
size: 250,
filter: { or: [{ city: { eq: 'Oslo' } }, { isFavorite: { eq: true } }] },
});
@@ -273,10 +274,13 @@ describe(SearchService.name, () => {
const auth = factory.auth({ user: { id: user.id } });
const topLevel = await sut.searchMetadata(auth, { filter: { albumIds: { any: [album.id] } } });
const topLevel = await sut.searchMetadata(auth, { size: 250, filter: { albumIds: { any: [album.id] } } });
expect(topLevel.assets.items).toEqual([expect.objectContaining({ id: asset.id })]);
const branchOnly = await sut.searchMetadata(auth, { filter: { or: [{ albumIds: { any: [album.id] } }] } });
const branchOnly = await sut.searchMetadata(auth, {
size: 250,
filter: { or: [{ albumIds: { any: [album.id] } }] },
});
expect(branchOnly.assets.items).toEqual([]);
});
@@ -288,9 +292,9 @@ describe(SearchService.name, () => {
const auth = factory.auth({ user: { id: user.id } });
await expect(sut.searchMetadata(auth, { filter: { or: [{ albumIds: { none: [album.id] } }] } })).rejects.toThrow(
'Not found or no album.read access',
);
await expect(
sut.searchMetadata(auth, { size: 250, filter: { or: [{ albumIds: { none: [album.id] } }] } }),
).rejects.toThrow('Not found or no album.read access');
});
it('should return locked assets only to an elevated session that asks for them', async () => {
@@ -299,11 +303,12 @@ describe(SearchService.name, () => {
const { asset: timeline } = await ctx.newAsset({ ownerId: user.id });
const { asset: locked } = await ctx.newAsset({ ownerId: user.id, visibility: AssetVisibility.Locked });
const unelevated = await sut.searchMetadata(factory.auth({ user: { id: user.id } }), { filter: {} });
const unelevated = await sut.searchMetadata(factory.auth({ user: { id: user.id } }), { size: 250, filter: {} });
expect(unelevated.assets.items).toEqual([expect.objectContaining({ id: timeline.id })]);
const elevatedAuth = factory.auth({ user: { id: user.id }, session: { hasElevatedPermission: true } });
const elevated = await sut.searchMetadata(elevatedAuth, {
size: 250,
filter: { visibility: { eq: AssetVisibility.Locked } },
});
expect(elevated.assets.items).toEqual([expect.objectContaining({ id: locked.id })]);
@@ -318,7 +323,10 @@ describe(SearchService.name, () => {
await ctx.newAsset({ ownerId: partner.id, visibility: AssetVisibility.Locked });
const auth = factory.auth({ user: { id: user.id }, session: { hasElevatedPermission: true } });
const response = await sut.searchMetadata(auth, { filter: { visibility: { eq: AssetVisibility.Locked } } });
const response = await sut.searchMetadata(auth, {
size: 250,
filter: { visibility: { eq: AssetVisibility.Locked } },
});
expect(response.assets.items).toEqual([expect.objectContaining({ id: ownLocked.id })]);
});
@@ -358,6 +366,7 @@ describe(SearchService.name, () => {
const auth = factory.auth({ user: { id: user.id } });
const response = await sut.searchMetadata(auth, {
size: 250,
orderBy: { field: SearchOrderField.FileSizeInBytes, direction: AssetOrder.Asc },
});