mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
refactor!: disallow star rating of -1
This commit is contained in:
@@ -214,23 +214,16 @@ describe(AssetController.name, () => {
|
||||
});
|
||||
|
||||
it('should reject invalid rating', async () => {
|
||||
for (const test of [{ rating: 7 }, { rating: 3.5 }, { rating: -2 }]) {
|
||||
for (const test of [{ rating: 0 }, { rating: 7 }, { rating: 3.5 }, { rating: -2 }]) {
|
||||
const { status, body } = await request(ctx.getHttpServer()).put(`/assets/${factory.uuid()}`).send(test);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(factory.responses.badRequest());
|
||||
}
|
||||
});
|
||||
|
||||
it('should convert rating 0 to null', async () => {
|
||||
const assetId = factory.uuid();
|
||||
const { status } = await request(ctx.getHttpServer()).put(`/assets/${assetId}`).send({ rating: 0 });
|
||||
expect(service.update).toHaveBeenCalledWith(undefined, assetId, { rating: null });
|
||||
expect(status).toBe(200);
|
||||
});
|
||||
|
||||
it('should leave correct ratings as-is', async () => {
|
||||
const assetId = factory.uuid();
|
||||
for (const test of [{ rating: -1 }, { rating: 1 }, { rating: 5 }]) {
|
||||
for (const test of [{ rating: 1 }, { rating: 5 }]) {
|
||||
const { status } = await request(ctx.getHttpServer()).put(`/assets/${assetId}`).send(test);
|
||||
expect(service.update).toHaveBeenCalledWith(undefined, assetId, test);
|
||||
expect(status).toBe(200);
|
||||
|
||||
@@ -14,11 +14,9 @@ const UpdateAssetBaseSchema = z
|
||||
latitude: latitudeSchema.optional().describe('Latitude coordinate'),
|
||||
longitude: longitudeSchema.optional().describe('Longitude coordinate'),
|
||||
rating: z
|
||||
.number()
|
||||
.int()
|
||||
.min(-1)
|
||||
.min(1)
|
||||
.max(5)
|
||||
.transform((value) => (value === 0 ? null : value))
|
||||
.nullish()
|
||||
.describe('Rating in range [1-5], or null for unrated')
|
||||
.meta({
|
||||
@@ -26,6 +24,7 @@ const UpdateAssetBaseSchema = z
|
||||
.added('v1')
|
||||
.stable('v2')
|
||||
.updated('v2.6.0', 'Using -1 as a rating is deprecated and will be removed in the next major version.')
|
||||
.updated('v3', 'Using -1 as a rating is no longer valid.')
|
||||
.getExtensions(),
|
||||
}),
|
||||
description: z.string().optional().describe('Asset description'),
|
||||
|
||||
@@ -29,7 +29,7 @@ export const ExifResponseSchema = z
|
||||
country: z.string().nullish().default(null).describe('Country name'),
|
||||
description: z.string().nullish().default(null).describe('Image description'),
|
||||
projectionType: z.string().nullish().default(null).describe('Projection type'),
|
||||
rating: z.number().nullish().default(null).describe('Rating'),
|
||||
rating: z.int().min(1).max(5).nullish().default(null).describe('Rating'),
|
||||
})
|
||||
.describe('EXIF response')
|
||||
.meta({ id: 'ExifResponseDto' });
|
||||
|
||||
@@ -34,8 +34,8 @@ const BaseSearchSchema = z.object({
|
||||
tagIds: z.array(z.uuidv4()).nullish().describe('Filter by tag IDs'),
|
||||
albumIds: z.array(z.uuidv4()).optional().describe('Filter by album IDs'),
|
||||
rating: z
|
||||
.number()
|
||||
.min(-1)
|
||||
.int()
|
||||
.min(1)
|
||||
.max(5)
|
||||
.nullish()
|
||||
.describe('Filter by rating [1-5], or null for unrated')
|
||||
@@ -44,6 +44,7 @@ const BaseSearchSchema = z.object({
|
||||
.added('v1')
|
||||
.stable('v2')
|
||||
.updated('v2.6.0', 'Using -1 as a rating is deprecated and will be removed in the next major version.')
|
||||
.updated('v3', 'Using -1 as a rating is no longer valid.')
|
||||
.getExtensions(),
|
||||
}),
|
||||
ocr: z.string().optional().describe('Filter by OCR text content'),
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Kysely, sql } from 'kysely';
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await sql`UPDATE "asset_exif" SET "rating" = NULL WHERE "rating" = -1;`.execute(db);
|
||||
}
|
||||
|
||||
export async function down(): Promise<void> {
|
||||
// not supported
|
||||
}
|
||||
@@ -1436,20 +1436,6 @@ describe(MetadataService.name, () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle valid negative rating value', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||
mockReadTags({ Rating: -1 });
|
||||
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
expect(mocks.asset.upsertExif).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
rating: -1,
|
||||
}),
|
||||
{ lockedPropertiesBehavior: 'skip' },
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle livePhotoCID not set', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset));
|
||||
|
||||
@@ -304,7 +304,7 @@ export class MetadataService extends BaseService {
|
||||
// comments
|
||||
description: String(exifTags.ImageDescription || exifTags.Description || '').trim(),
|
||||
profileDescription: exifTags.ProfileDescription || null,
|
||||
rating: exifTags.Rating === 0 ? null : validateRange(exifTags.Rating, -1, 5),
|
||||
rating: exifTags.Rating === 0 ? null : validateRange(exifTags.Rating, 1, 5),
|
||||
|
||||
// grouping
|
||||
livePhotoCID: (exifTags.ContentIdentifier || exifTags.MediaGroupUUID) ?? null,
|
||||
|
||||
@@ -78,7 +78,7 @@ describe('duplicate utils', () => {
|
||||
model: null,
|
||||
latitude: undefined,
|
||||
city: '',
|
||||
rating: 0,
|
||||
rating: null,
|
||||
});
|
||||
// fileSizeInByte (1000) + make ('Canon') = 2 truthy values
|
||||
// model (null), latitude (undefined), city (''), rating (0) are all falsy
|
||||
|
||||
Reference in New Issue
Block a user