From f706738f936ca6a7fabfcab689758082decc4557 Mon Sep 17 00:00:00 2001 From: Yaros Date: Wed, 25 Feb 2026 12:45:56 +0100 Subject: [PATCH] feat(server): ocr audit table --- mobile/openapi/README.md | 1 + mobile/openapi/lib/api.dart | 1 + mobile/openapi/lib/api_client.dart | 2 + .../lib/model/sync_asset_ocr_delete_v1.dart | 118 ++++++++++++++++++ .../openapi/lib/model/sync_entity_type.dart | 3 + open-api/immich-openapi-specs.json | 24 ++++ open-api/typescript-sdk/src/fetch-client.ts | 9 ++ server/src/dtos/sync.dto.ts | 13 ++ server/src/enum.ts | 1 + server/src/repositories/sync.repository.ts | 13 ++ server/src/schema/functions.ts | 15 ++- server/src/schema/index.ts | 4 + .../1772014962747-AssetOcrUpdateId.ts | 11 -- .../migrations/1772019080456-AssetOcrSync.ts | 63 ++++++++++ .../schema/tables/asset-ocr-audit.table.ts | 14 +++ server/src/schema/tables/asset-ocr.table.ts | 16 ++- server/src/services/sync.service.ts | 11 ++ 17 files changed, 306 insertions(+), 13 deletions(-) create mode 100644 mobile/openapi/lib/model/sync_asset_ocr_delete_v1.dart delete mode 100644 server/src/schema/migrations/1772014962747-AssetOcrUpdateId.ts create mode 100644 server/src/schema/migrations/1772019080456-AssetOcrSync.ts create mode 100644 server/src/schema/tables/asset-ocr-audit.table.ts diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 52e88c3f93..19e51f6fee 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -582,6 +582,7 @@ Class | Method | HTTP request | Description - [SyncAssetFaceV2](doc//SyncAssetFaceV2.md) - [SyncAssetMetadataDeleteV1](doc//SyncAssetMetadataDeleteV1.md) - [SyncAssetMetadataV1](doc//SyncAssetMetadataV1.md) + - [SyncAssetOcrDeleteV1](doc//SyncAssetOcrDeleteV1.md) - [SyncAssetOcrV1](doc//SyncAssetOcrV1.md) - [SyncAssetV1](doc//SyncAssetV1.md) - [SyncAuthUserV1](doc//SyncAuthUserV1.md) diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index d13994a3bd..632d6fbdc7 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -321,6 +321,7 @@ part 'model/sync_asset_face_v1.dart'; part 'model/sync_asset_face_v2.dart'; part 'model/sync_asset_metadata_delete_v1.dart'; part 'model/sync_asset_metadata_v1.dart'; +part 'model/sync_asset_ocr_delete_v1.dart'; part 'model/sync_asset_ocr_v1.dart'; part 'model/sync_asset_v1.dart'; part 'model/sync_auth_user_v1.dart'; diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index a7699b4288..843a3e3fa7 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -688,6 +688,8 @@ class ApiClient { return SyncAssetMetadataDeleteV1.fromJson(value); case 'SyncAssetMetadataV1': return SyncAssetMetadataV1.fromJson(value); + case 'SyncAssetOcrDeleteV1': + return SyncAssetOcrDeleteV1.fromJson(value); case 'SyncAssetOcrV1': return SyncAssetOcrV1.fromJson(value); case 'SyncAssetV1': diff --git a/mobile/openapi/lib/model/sync_asset_ocr_delete_v1.dart b/mobile/openapi/lib/model/sync_asset_ocr_delete_v1.dart new file mode 100644 index 0000000000..5e1830c360 --- /dev/null +++ b/mobile/openapi/lib/model/sync_asset_ocr_delete_v1.dart @@ -0,0 +1,118 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class SyncAssetOcrDeleteV1 { + /// Returns a new [SyncAssetOcrDeleteV1] instance. + SyncAssetOcrDeleteV1({ + required this.assetId, + required this.deletedAt, + required this.id, + }); + + /// Original asset ID of the deleted OCR entry + String assetId; + + /// Timestamp when the OCR entry was deleted + DateTime deletedAt; + + /// Audit row ID of the deleted OCR entry + String id; + + @override + bool operator ==(Object other) => identical(this, other) || other is SyncAssetOcrDeleteV1 && + other.assetId == assetId && + other.deletedAt == deletedAt && + other.id == id; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (assetId.hashCode) + + (deletedAt.hashCode) + + (id.hashCode); + + @override + String toString() => 'SyncAssetOcrDeleteV1[assetId=$assetId, deletedAt=$deletedAt, id=$id]'; + + Map toJson() { + final json = {}; + json[r'assetId'] = this.assetId; + json[r'deletedAt'] = this.deletedAt.toUtc().toIso8601String(); + json[r'id'] = this.id; + return json; + } + + /// Returns a new [SyncAssetOcrDeleteV1] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static SyncAssetOcrDeleteV1? fromJson(dynamic value) { + upgradeDto(value, "SyncAssetOcrDeleteV1"); + if (value is Map) { + final json = value.cast(); + + return SyncAssetOcrDeleteV1( + assetId: mapValueOfType(json, r'assetId')!, + deletedAt: mapDateTime(json, r'deletedAt', r'')!, + id: mapValueOfType(json, r'id')!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = SyncAssetOcrDeleteV1.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = SyncAssetOcrDeleteV1.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of SyncAssetOcrDeleteV1-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = SyncAssetOcrDeleteV1.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'assetId', + 'deletedAt', + 'id', + }; +} + diff --git a/mobile/openapi/lib/model/sync_entity_type.dart b/mobile/openapi/lib/model/sync_entity_type.dart index e05d027c16..c89439408e 100644 --- a/mobile/openapi/lib/model/sync_entity_type.dart +++ b/mobile/openapi/lib/model/sync_entity_type.dart @@ -32,6 +32,7 @@ class SyncEntityType { static const assetMetadataV1 = SyncEntityType._(r'AssetMetadataV1'); static const assetMetadataDeleteV1 = SyncEntityType._(r'AssetMetadataDeleteV1'); static const assetOcrV1 = SyncEntityType._(r'AssetOcrV1'); + static const assetOcrDeleteV1 = SyncEntityType._(r'AssetOcrDeleteV1'); static const partnerV1 = SyncEntityType._(r'PartnerV1'); static const partnerDeleteV1 = SyncEntityType._(r'PartnerDeleteV1'); static const partnerAssetV1 = SyncEntityType._(r'PartnerAssetV1'); @@ -84,6 +85,7 @@ class SyncEntityType { assetMetadataV1, assetMetadataDeleteV1, assetOcrV1, + assetOcrDeleteV1, partnerV1, partnerDeleteV1, partnerAssetV1, @@ -171,6 +173,7 @@ class SyncEntityTypeTypeTransformer { case r'AssetMetadataV1': return SyncEntityType.assetMetadataV1; case r'AssetMetadataDeleteV1': return SyncEntityType.assetMetadataDeleteV1; case r'AssetOcrV1': return SyncEntityType.assetOcrV1; + case r'AssetOcrDeleteV1': return SyncEntityType.assetOcrDeleteV1; case r'PartnerV1': return SyncEntityType.partnerV1; case r'PartnerDeleteV1': return SyncEntityType.partnerDeleteV1; case r'PartnerAssetV1': return SyncEntityType.partnerAssetV1; diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 4b96cd3a80..c2b33256a6 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -22944,6 +22944,29 @@ ], "type": "object" }, + "SyncAssetOcrDeleteV1": { + "properties": { + "assetId": { + "description": "Original asset ID of the deleted OCR entry", + "type": "string" + }, + "deletedAt": { + "description": "Timestamp when the OCR entry was deleted", + "format": "date-time", + "type": "string" + }, + "id": { + "description": "Audit row ID of the deleted OCR entry", + "type": "string" + } + }, + "required": [ + "assetId", + "deletedAt", + "id" + ], + "type": "object" + }, "SyncAssetOcrV1": { "properties": { "assetId": { @@ -23243,6 +23266,7 @@ "AssetMetadataV1", "AssetMetadataDeleteV1", "AssetOcrV1", + "AssetOcrDeleteV1", "PartnerV1", "PartnerDeleteV1", "PartnerAssetV1", diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 63d3e32ed4..fa0e9bad1a 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -3073,6 +3073,14 @@ export type SyncAssetMetadataV1 = { /** Value */ value: object; }; +export type SyncAssetOcrDeleteV1 = { + /** Original asset ID of the deleted OCR entry */ + assetId: string; + /** Timestamp when the OCR entry was deleted */ + deletedAt: string; + /** Audit row ID of the deleted OCR entry */ + id: string; +}; export type SyncAssetOcrV1 = { /** Asset ID */ assetId: string; @@ -7263,6 +7271,7 @@ export enum SyncEntityType { AssetMetadataV1 = "AssetMetadataV1", AssetMetadataDeleteV1 = "AssetMetadataDeleteV1", AssetOcrV1 = "AssetOcrV1", + AssetOcrDeleteV1 = "AssetOcrDeleteV1", PartnerV1 = "PartnerV1", PartnerDeleteV1 = "PartnerDeleteV1", PartnerAssetV1 = "PartnerAssetV1", diff --git a/server/src/dtos/sync.dto.ts b/server/src/dtos/sync.dto.ts index 35023a45d7..ca7a64adc1 100644 --- a/server/src/dtos/sync.dto.ts +++ b/server/src/dtos/sync.dto.ts @@ -263,6 +263,18 @@ export class SyncAssetOcrV1 { isVisible!: boolean; } +@ExtraModel() +export class SyncAssetOcrDeleteV1 { + @ApiProperty({ description: 'Audit row ID of the deleted OCR entry' }) + id!: string; + + @ApiProperty({ description: 'Original asset ID of the deleted OCR entry' }) + assetId!: string; + + @ApiProperty({ description: 'Timestamp when the OCR entry was deleted' }) + deletedAt!: Date; +} + @ExtraModel() export class SyncAssetMetadataV1 { @ApiProperty({ description: 'Asset ID' }) @@ -526,6 +538,7 @@ export type SyncItem = { [SyncEntityType.AssetMetadataDeleteV1]: SyncAssetMetadataDeleteV1; [SyncEntityType.AssetExifV1]: SyncAssetExifV1; [SyncEntityType.AssetOcrV1]: SyncAssetOcrV1; + [SyncEntityType.AssetOcrDeleteV1]: SyncAssetOcrDeleteV1; [SyncEntityType.PartnerAssetV1]: SyncAssetV1; [SyncEntityType.PartnerAssetBackfillV1]: SyncAssetV1; [SyncEntityType.PartnerAssetDeleteV1]: SyncAssetDeleteV1; diff --git a/server/src/enum.ts b/server/src/enum.ts index ca5d22cbeb..8b48e8303a 100644 --- a/server/src/enum.ts +++ b/server/src/enum.ts @@ -749,6 +749,7 @@ export enum SyncEntityType { AssetMetadataV1 = 'AssetMetadataV1', AssetMetadataDeleteV1 = 'AssetMetadataDeleteV1', AssetOcrV1 = 'AssetOcrV1', + AssetOcrDeleteV1 = 'AssetOcrDeleteV1', PartnerV1 = 'PartnerV1', PartnerDeleteV1 = 'PartnerDeleteV1', diff --git a/server/src/repositories/sync.repository.ts b/server/src/repositories/sync.repository.ts index a2c5f19193..3d9de4081a 100644 --- a/server/src/repositories/sync.repository.ts +++ b/server/src/repositories/sync.repository.ts @@ -776,6 +776,19 @@ class AssetMetadataSync extends BaseSync { } class AssetOcrSync extends BaseSync { + @GenerateSql({ params: [dummyQueryOptions, DummyValue.UUID], stream: true }) + getDeletes(options: SyncQueryOptions, userId: string) { + return this.auditQuery('asset_ocr_audit', options) + .select(['asset_ocr_audit.id', 'assetId', 'deletedAt']) + .leftJoin('asset', 'asset.id', 'asset_ocr_audit.assetId') + .where('asset.ownerId', '=', userId) + .stream(); + } + + cleanupAuditTable(daysAgo: number) { + return this.auditCleanup('asset_ocr_audit', daysAgo); + } + @GenerateSql({ params: [dummyQueryOptions, DummyValue.UUID], stream: true }) getUpserts(options: SyncQueryOptions, userId: string) { return this.upsertQuery('asset_ocr', options) diff --git a/server/src/schema/functions.ts b/server/src/schema/functions.ts index 6acfc45750..331ab6adba 100644 --- a/server/src/schema/functions.ts +++ b/server/src/schema/functions.ts @@ -280,9 +280,22 @@ export const asset_edit_delete = registerFunction({ UPDATE asset SET "isEdited" = false FROM deleted_edit - WHERE asset.id = deleted_edit."assetId" AND asset."isEdited" + WHERE asset.id = deleted_edit."assetId" AND asset."isEdited" AND NOT EXISTS (SELECT FROM asset_edit edit WHERE edit."assetId" = asset.id); RETURN NULL; END `, }); + +export const asset_ocr_delete_audit = registerFunction({ + name: 'asset_ocr_delete_audit', + returnType: 'TRIGGER', + language: 'PLPGSQL', + body: ` + BEGIN + INSERT INTO asset_ocr_audit ("assetId") + SELECT "assetId" + FROM OLD; + RETURN NULL; + END`, +}); diff --git a/server/src/schema/index.ts b/server/src/schema/index.ts index 790973785f..bc2aff6415 100644 --- a/server/src/schema/index.ts +++ b/server/src/schema/index.ts @@ -7,6 +7,7 @@ import { asset_delete_audit, asset_face_audit, asset_metadata_audit, + asset_ocr_delete_audit, f_concat_ws, f_unaccent, immich_uuid_v7, @@ -73,6 +74,7 @@ import { UserMetadataTable } from 'src/schema/tables/user-metadata.table'; import { UserTable } from 'src/schema/tables/user.table'; import { VersionHistoryTable } from 'src/schema/tables/version-history.table'; import { WorkflowActionTable, WorkflowFilterTable, WorkflowTable } from 'src/schema/tables/workflow.table'; +import { AssetOcrAuditTable } from './tables/asset-ocr-audit.table'; @Extensions(['uuid-ossp', 'unaccent', 'cube', 'earthdistance', 'pg_trgm', 'plpgsql']) @Database({ name: 'immich' }) @@ -156,6 +158,7 @@ export class ImmichDatabase { user_metadata_audit, asset_metadata_audit, asset_face_audit, + asset_ocr_delete_audit, ]; enum = [assets_status_enum, asset_face_source_type, asset_visibility_enum]; @@ -192,6 +195,7 @@ export interface DB { asset_metadata_audit: AssetMetadataAuditTable; asset_job_status: AssetJobStatusTable; asset_ocr: AssetOcrTable; + asset_ocr_audit: AssetOcrAuditTable; ocr_search: OcrSearchTable; audit: AuditTable; diff --git a/server/src/schema/migrations/1772014962747-AssetOcrUpdateId.ts b/server/src/schema/migrations/1772014962747-AssetOcrUpdateId.ts deleted file mode 100644 index b17f93363a..0000000000 --- a/server/src/schema/migrations/1772014962747-AssetOcrUpdateId.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Kysely, sql } from 'kysely'; - -export async function up(db: Kysely): Promise { - await sql`ALTER TABLE "asset_ocr" ADD "updateId" uuid NOT NULL DEFAULT immich_uuid_v7();`.execute(db); - await sql`CREATE INDEX "asset_ocr_updateId_idx" ON "asset_ocr" ("updateId");`.execute(db); -} - -export async function down(db: Kysely): Promise { - await sql`DROP INDEX "asset_ocr_updateId_idx";`.execute(db); - await sql`ALTER TABLE "asset_ocr" DROP COLUMN "updateId";`.execute(db); -} diff --git a/server/src/schema/migrations/1772019080456-AssetOcrSync.ts b/server/src/schema/migrations/1772019080456-AssetOcrSync.ts new file mode 100644 index 0000000000..3b457ba5b4 --- /dev/null +++ b/server/src/schema/migrations/1772019080456-AssetOcrSync.ts @@ -0,0 +1,63 @@ +import { Kysely, sql } from 'kysely'; + +export async function up(db: Kysely): Promise { + await sql`CREATE OR REPLACE FUNCTION asset_edit_delete() + RETURNS TRIGGER + LANGUAGE PLPGSQL + AS $$ + BEGIN + UPDATE asset + SET "isEdited" = false + FROM deleted_edit + WHERE asset.id = deleted_edit."assetId" AND asset."isEdited" + AND NOT EXISTS (SELECT FROM asset_edit edit WHERE edit."assetId" = asset.id); + RETURN NULL; + END + $$;`.execute(db); + await sql`CREATE OR REPLACE FUNCTION asset_ocr_delete_audit() + RETURNS TRIGGER + LANGUAGE PLPGSQL + AS $$ + BEGIN + INSERT INTO asset_ocr_audit ("assetId") + SELECT "assetId" + FROM OLD; + RETURN NULL; + END + $$;`.execute(db); + await sql`ALTER TABLE "asset_ocr" ADD "updateId" uuid NOT NULL DEFAULT immich_uuid_v7();`.execute(db); + await sql`CREATE INDEX "asset_ocr_updateId_idx" ON "asset_ocr" ("updateId");`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "asset_ocr_delete_audit" + AFTER DELETE ON "asset_ocr" + REFERENCING OLD TABLE AS "old" + FOR EACH STATEMENT + WHEN (pg_trigger_depth() = 0) + EXECUTE FUNCTION asset_ocr_delete_audit();`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('function_asset_ocr_delete_audit', '{"type":"function","name":"asset_ocr_delete_audit","sql":"CREATE OR REPLACE FUNCTION asset_ocr_delete_audit()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n INSERT INTO asset_ocr_audit (\\"assetId\\")\\n SELECT \\"assetId\\"\\n FROM OLD;\\n RETURN NULL;\\n END\\n $$;"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_asset_ocr_delete_audit', '{"type":"trigger","name":"asset_ocr_delete_audit","sql":"CREATE OR REPLACE TRIGGER \\"asset_ocr_delete_audit\\"\\n AFTER DELETE ON \\"asset_ocr\\"\\n REFERENCING OLD TABLE AS \\"old\\"\\n FOR EACH STATEMENT\\n WHEN (pg_trigger_depth() = 0)\\n EXECUTE FUNCTION asset_ocr_delete_audit();"}'::jsonb);`.execute(db); + await sql`UPDATE "migration_overrides" SET "value" = '{"type":"function","name":"asset_edit_delete","sql":"CREATE OR REPLACE FUNCTION asset_edit_delete()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n UPDATE asset\\n SET \\"isEdited\\" = false\\n FROM deleted_edit\\n WHERE asset.id = deleted_edit.\\"assetId\\" AND asset.\\"isEdited\\"\\n AND NOT EXISTS (SELECT FROM asset_edit edit WHERE edit.\\"assetId\\" = asset.id);\\n RETURN NULL;\\n END\\n $$;"}'::jsonb WHERE "name" = 'function_asset_edit_delete';`.execute(db); +} + +export async function down(db: Kysely): Promise { + await sql`CREATE OR REPLACE FUNCTION public.asset_edit_delete() + RETURNS trigger + LANGUAGE plpgsql +AS $function$ + BEGIN + UPDATE asset + SET "isEdited" = false + FROM deleted_edit + WHERE asset.id = deleted_edit."assetId" AND asset."isEdited" + AND NOT EXISTS (SELECT FROM asset_edit edit WHERE edit."assetId" = asset.id); + RETURN NULL; + END + $function$ +`.execute(db); + await sql`DROP TRIGGER "asset_ocr_delete_audit" ON "asset_ocr";`.execute(db); + await sql`DROP INDEX "asset_ocr_updateId_idx";`.execute(db); + await sql`ALTER TABLE "asset_ocr" DROP COLUMN "updateId";`.execute(db); + await sql`DROP FUNCTION asset_ocr_delete_audit;`.execute(db); + await sql`UPDATE "migration_overrides" SET "value" = '{"sql":"CREATE OR REPLACE FUNCTION asset_edit_delete()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n UPDATE asset\\n SET \\"isEdited\\" = false\\n FROM deleted_edit\\n WHERE asset.id = deleted_edit.\\"assetId\\" AND asset.\\"isEdited\\" \\n AND NOT EXISTS (SELECT FROM asset_edit edit WHERE edit.\\"assetId\\" = asset.id);\\n RETURN NULL;\\n END\\n $$;","name":"asset_edit_delete","type":"function"}'::jsonb WHERE "name" = 'function_asset_edit_delete';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'function_asset_ocr_delete_audit';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_asset_ocr_delete_audit';`.execute(db); +} diff --git a/server/src/schema/tables/asset-ocr-audit.table.ts b/server/src/schema/tables/asset-ocr-audit.table.ts new file mode 100644 index 0000000000..8c7efe1d66 --- /dev/null +++ b/server/src/schema/tables/asset-ocr-audit.table.ts @@ -0,0 +1,14 @@ +import { Column, CreateDateColumn, Generated, Table } from '@immich/sql-tools'; +import { PrimaryGeneratedUuidV7Column } from 'src/decorators'; + +@Table('asset_ocr_audit') +export class AssetOcrAuditTable { + @PrimaryGeneratedUuidV7Column() + id!: Generated; + + @Column({ type: 'uuid', index: true }) + assetId!: string; + + @CreateDateColumn({ default: () => 'clock_timestamp()', index: true }) + deletedAt!: Date; +} diff --git a/server/src/schema/tables/asset-ocr.table.ts b/server/src/schema/tables/asset-ocr.table.ts index 54946f037a..42c9cedc52 100644 --- a/server/src/schema/tables/asset-ocr.table.ts +++ b/server/src/schema/tables/asset-ocr.table.ts @@ -1,8 +1,22 @@ -import { Column, ForeignKeyColumn, Generated, PrimaryGeneratedColumn, Table } from '@immich/sql-tools'; +import { + AfterDeleteTrigger, + Column, + ForeignKeyColumn, + Generated, + PrimaryGeneratedColumn, + Table, +} from '@immich/sql-tools'; import { UpdateIdColumn } from 'src/decorators'; import { AssetTable } from 'src/schema/tables/asset.table'; +import { asset_ocr_delete_audit } from '../functions'; @Table('asset_ocr') +@AfterDeleteTrigger({ + scope: 'statement', + function: asset_ocr_delete_audit, + referencingOldTableAs: 'old', + when: 'pg_trigger_depth() = 0', +}) export class AssetOcrTable { @PrimaryGeneratedColumn() id!: Generated; diff --git a/server/src/services/sync.service.ts b/server/src/services/sync.service.ts index dfe4826457..9acd1caf41 100644 --- a/server/src/services/sync.service.ts +++ b/server/src/services/sync.service.ts @@ -223,6 +223,7 @@ export class SyncService extends BaseService { await this.syncRepository.stack.cleanupAuditTable(pruneThreshold); await this.syncRepository.user.cleanupAuditTable(pruneThreshold); await this.syncRepository.userMetadata.cleanupAuditTable(pruneThreshold); + await this.syncRepository.assetOcr.cleanupAuditTable(pruneThreshold); } private needsFullSync(checkpointMap: CheckpointMap) { @@ -862,6 +863,16 @@ export class SyncService extends BaseService { checkpointMap: CheckpointMap, auth: AuthDto, ) { + const deleteType = SyncEntityType.AssetOcrDeleteV1; + const deletes = this.syncRepository.assetOcr.getDeletes( + { ...options, ack: checkpointMap[deleteType] }, + auth.user.id, + ); + + for await (const row of deletes) { + send(response, { type: deleteType, ids: [row.id], data: row }); + } + const upsertType = SyncEntityType.AssetOcrV1; const upserts = this.syncRepository.assetOcr.getUpserts( { ...options, ack: checkpointMap[upsertType] },