mirror of
https://github.com/immich-app/immich
synced 2026-08-15 13:03:57 +00:00
Merge deb9bb7555 into f9c05af45f
This commit is contained in:
commit
bbaed6d3e0
6 changed files with 99 additions and 1 deletions
|
|
@ -0,0 +1,16 @@
|
|||
import { Kysely, sql } from 'kysely';
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await sql`ALTER TABLE "asset_ocr" ADD "updatedAt" timestamp with time zone NOT NULL DEFAULT now();`.execute(db);
|
||||
await sql`CREATE OR REPLACE TRIGGER "asset_ocr_updatedAt"
|
||||
BEFORE UPDATE ON "asset_ocr"
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION updated_at();`.execute(db);
|
||||
await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_asset_ocr_updatedAt', '{"type":"trigger","name":"asset_ocr_updatedAt","sql":"CREATE OR REPLACE TRIGGER \\"asset_ocr_updatedAt\\"\\n BEFORE UPDATE ON \\"asset_ocr\\"\\n FOR EACH ROW\\n EXECUTE FUNCTION updated_at();"}'::jsonb);`.execute(db);
|
||||
}
|
||||
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await sql`DROP TRIGGER "asset_ocr_updatedAt" ON "asset_ocr";`.execute(db);
|
||||
await sql`ALTER TABLE "asset_ocr" DROP COLUMN "updatedAt";`.execute(db);
|
||||
await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_asset_ocr_updatedAt';`.execute(db);
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import { Kysely, sql } from 'kysely';
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
// OCR visibility updates did not bump updateId before the asset_ocr_updatedAt trigger was added,
|
||||
// so clients never received those changes. There is no way to know which rows were missed, so
|
||||
// reset the checkpoint to backfill all OCR rows on the next sync.
|
||||
await sql`DELETE FROM session_sync_checkpoint WHERE type = 'AssetOcrV1'`.execute(db);
|
||||
}
|
||||
|
||||
export async function down(): Promise<void> {
|
||||
// Not implemented
|
||||
}
|
||||
|
|
@ -5,12 +5,15 @@ import {
|
|||
Generated,
|
||||
PrimaryGeneratedColumn,
|
||||
Table,
|
||||
Timestamp,
|
||||
UpdateDateColumn,
|
||||
} from '@immich/sql-tools';
|
||||
import { UpdateIdColumn } from 'src/decorators';
|
||||
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
||||
import { asset_ocr_delete_audit } from 'src/schema/functions';
|
||||
import { AssetTable } from 'src/schema/tables/asset.table';
|
||||
|
||||
@Table('asset_ocr')
|
||||
@UpdatedAtTrigger('asset_ocr_updatedAt')
|
||||
@AfterDeleteTrigger({
|
||||
scope: 'statement',
|
||||
function: asset_ocr_delete_audit,
|
||||
|
|
@ -61,6 +64,9 @@ export class AssetOcrTable {
|
|||
@Column({ type: 'boolean', default: true })
|
||||
isVisible!: Generated<boolean>;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt!: Generated<Timestamp>;
|
||||
|
||||
@UpdateIdColumn({ index: true })
|
||||
updateId!: Generated<string>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ describe(OcrService.name, () => {
|
|||
boxScore: 0.99,
|
||||
id: expect.any(String),
|
||||
updateId: expect.any(String),
|
||||
updatedAt: expect.any(Date),
|
||||
text: 'Test OCR',
|
||||
textScore: 0.95,
|
||||
isVisible: true,
|
||||
|
|
@ -107,6 +108,7 @@ describe(OcrService.name, () => {
|
|||
boxScore: 0.7,
|
||||
id: expect.any(String),
|
||||
updateId: expect.any(String),
|
||||
updatedAt: expect.any(Date),
|
||||
text: 'One',
|
||||
textScore: 0.9,
|
||||
isVisible: true,
|
||||
|
|
@ -124,6 +126,7 @@ describe(OcrService.name, () => {
|
|||
boxScore: 0.67,
|
||||
id: expect.any(String),
|
||||
updateId: expect.any(String),
|
||||
updatedAt: expect.any(Date),
|
||||
text: 'Two',
|
||||
textScore: 0.89,
|
||||
isVisible: true,
|
||||
|
|
@ -141,6 +144,7 @@ describe(OcrService.name, () => {
|
|||
boxScore: 0.65,
|
||||
id: expect.any(String),
|
||||
updateId: expect.any(String),
|
||||
updatedAt: expect.any(Date),
|
||||
text: 'Three',
|
||||
textScore: 0.88,
|
||||
isVisible: true,
|
||||
|
|
@ -158,6 +162,7 @@ describe(OcrService.name, () => {
|
|||
boxScore: 0.62,
|
||||
id: expect.any(String),
|
||||
updateId: expect.any(String),
|
||||
updatedAt: expect.any(Date),
|
||||
text: 'Four',
|
||||
textScore: 0.87,
|
||||
isVisible: true,
|
||||
|
|
@ -175,6 +180,7 @@ describe(OcrService.name, () => {
|
|||
boxScore: 0.6,
|
||||
id: expect.any(String),
|
||||
updateId: expect.any(String),
|
||||
updatedAt: expect.any(Date),
|
||||
text: 'Five',
|
||||
textScore: 0.86,
|
||||
isVisible: true,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { SyncEntityType, SyncRequestType } from 'src/enum';
|
|||
import { OcrRepository } from 'src/repositories/ocr.repository';
|
||||
import { DB } from 'src/schema';
|
||||
import { SyncTestContext } from 'test/medium.factory';
|
||||
import { factory } from 'test/small.factory';
|
||||
import { getKyselyDB } from 'test/utils';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
|
@ -303,6 +304,61 @@ describe(SyncEntityType.AssetOcrV1, () => {
|
|||
await ctx.syncAckAll(auth, updatedResponse);
|
||||
await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetOcrV1]);
|
||||
});
|
||||
|
||||
it('should sync in-place OCR visibility toggles (e.g. after an asset edit)', async () => {
|
||||
const { auth, user, ctx } = await setup();
|
||||
|
||||
const ocrRepo = ctx.get(OcrRepository);
|
||||
const { asset } = await ctx.newAsset({ ownerId: user.id });
|
||||
await ocrRepo.upsert(
|
||||
asset.id,
|
||||
[
|
||||
{
|
||||
assetId: asset.id,
|
||||
x1: 0.1,
|
||||
y1: 0.2,
|
||||
x2: 0.9,
|
||||
y2: 0.2,
|
||||
x3: 0.9,
|
||||
y3: 0.8,
|
||||
x4: 0.1,
|
||||
y4: 0.8,
|
||||
boxScore: 0.95,
|
||||
textScore: 0.92,
|
||||
text: 'Hello World',
|
||||
isVisible: true,
|
||||
},
|
||||
],
|
||||
'Hello World',
|
||||
);
|
||||
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.AssetOcrV1]);
|
||||
const ocrId = (response[0] as { data: { id: string } }).data.id;
|
||||
await ctx.syncAckAll(auth, response);
|
||||
|
||||
await ocrRepo.updateOcrVisibilities(
|
||||
asset.id,
|
||||
[],
|
||||
[factory.assetOcr({ id: ocrId, assetId: asset.id, text: 'Hello World' })],
|
||||
);
|
||||
|
||||
const updatedResponse = await ctx.syncStream(auth, [SyncRequestType.AssetOcrV1]);
|
||||
expect(updatedResponse).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({
|
||||
id: ocrId,
|
||||
assetId: asset.id,
|
||||
isVisible: false,
|
||||
}),
|
||||
type: 'AssetOcrV1',
|
||||
},
|
||||
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
|
||||
]);
|
||||
|
||||
await ctx.syncAckAll(auth, updatedResponse);
|
||||
await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetOcrV1]);
|
||||
});
|
||||
});
|
||||
|
||||
describe(SyncEntityType.AssetOcrDeleteV1, () => {
|
||||
|
|
|
|||
|
|
@ -185,10 +185,12 @@ const assetOcrFactory = (
|
|||
textScore?: number;
|
||||
text?: string;
|
||||
isVisible?: boolean;
|
||||
updatedAt?: Date;
|
||||
} = {},
|
||||
) => ({
|
||||
id: newUuid(),
|
||||
updateId: newUuidV7(),
|
||||
updatedAt: newDate(),
|
||||
assetId: newUuid(),
|
||||
x1: 0.1,
|
||||
y1: 0.2,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue