feat: sync memories (#19579)

This commit is contained in:
Jason Rasmussen 2025-06-27 12:20:13 -04:00 committed by GitHub
parent 97aabe466e
commit 6feca56da8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 1482 additions and 203 deletions

View file

@ -176,3 +176,31 @@ export const album_users_delete_audit = registerFunction({
END`,
synchronize: false,
});
export const memories_delete_audit = registerFunction({
name: 'memories_delete_audit',
returnType: 'TRIGGER',
language: 'PLPGSQL',
body: `
BEGIN
INSERT INTO memories_audit ("memoryId", "userId")
SELECT "id", "ownerId"
FROM OLD;
RETURN NULL;
END`,
synchronize: false,
});
export const memory_assets_delete_audit = registerFunction({
name: 'memory_assets_delete_audit',
returnType: 'TRIGGER',
language: 'PLPGSQL',
body: `
BEGIN
INSERT INTO memory_assets_audit ("memoryId", "assetId")
SELECT "memoriesId", "assetsId" FROM OLD
WHERE "memoriesId" IN (SELECT "id" FROM memories WHERE "id" IN (SELECT "memoriesId" FROM OLD));
RETURN NULL;
END`,
synchronize: false,
});

View file

@ -8,6 +8,8 @@ import {
f_unaccent,
immich_uuid_v7,
ll_to_earth_public,
memories_delete_audit,
memory_assets_delete_audit,
partners_delete_audit,
updated_at,
users_delete_audit,
@ -30,8 +32,10 @@ import { ExifTable } from 'src/schema/tables/exif.table';
import { FaceSearchTable } from 'src/schema/tables/face-search.table';
import { GeodataPlacesTable } from 'src/schema/tables/geodata-places.table';
import { LibraryTable } from 'src/schema/tables/library.table';
import { MemoryAssetAuditTable } from 'src/schema/tables/memory-asset-audit.table';
import { MemoryAssetTable } from 'src/schema/tables/memory-asset.table';
import { MemoryAuditTable } from 'src/schema/tables/memory-audit.table';
import { MemoryTable } from 'src/schema/tables/memory.table';
import { MemoryAssetTable } from 'src/schema/tables/memory_asset.table';
import { MoveTable } from 'src/schema/tables/move.table';
import { NaturalEarthCountriesTable } from 'src/schema/tables/natural-earth-countries.table';
import { NotificationTable } from 'src/schema/tables/notification.table';
@ -75,8 +79,10 @@ export class ImmichDatabase {
FaceSearchTable,
GeodataPlacesTable,
LibraryTable,
MemoryAssetTable,
MemoryTable,
MemoryAuditTable,
MemoryAssetTable,
MemoryAssetAuditTable,
MoveTable,
NaturalEarthCountriesTable,
NotificationTable,
@ -110,6 +116,8 @@ export class ImmichDatabase {
albums_delete_audit,
album_user_after_insert,
album_users_delete_audit,
memories_delete_audit,
memory_assets_delete_audit,
];
enum = [assets_status_enum, asset_face_source_type, asset_visibility_enum];

View file

@ -0,0 +1,80 @@
import { Kysely, sql } from 'kysely';
export async function up(db: Kysely<any>): Promise<void> {
await sql`CREATE TABLE "memory_assets_audit" ("id" uuid NOT NULL DEFAULT immich_uuid_v7(), "memoryId" uuid NOT NULL, "assetId" uuid NOT NULL, "deletedAt" timestamp with time zone NOT NULL DEFAULT clock_timestamp());`.execute(db);
await sql`CREATE TABLE "memories_audit" ("id" uuid NOT NULL DEFAULT immich_uuid_v7(), "memoryId" uuid NOT NULL, "userId" uuid NOT NULL, "deletedAt" timestamp with time zone NOT NULL DEFAULT clock_timestamp());`.execute(db);
await sql`ALTER TABLE "memories_assets_assets" ADD "createdAt" timestamp with time zone NOT NULL DEFAULT now();`.execute(db);
await sql`ALTER TABLE "memories_assets_assets" ADD "updatedAt" timestamp with time zone NOT NULL DEFAULT now();`.execute(db);
await sql`ALTER TABLE "memories_assets_assets" ADD "updateId" uuid NOT NULL DEFAULT immich_uuid_v7();`.execute(db);
await sql`ALTER TABLE "memory_assets_audit" ADD CONSTRAINT "PK_35ef16910228f980e0766dcc59b" PRIMARY KEY ("id");`.execute(db);
await sql`ALTER TABLE "memories_audit" ADD CONSTRAINT "PK_19de798c033a710dcfa5c72f81b" PRIMARY KEY ("id");`.execute(db);
await sql`ALTER TABLE "memory_assets_audit" ADD CONSTRAINT "FK_225a204afcb0bd6de015080fb03" FOREIGN KEY ("memoryId") REFERENCES "memories" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db);
await sql`CREATE INDEX "IDX_memory_assets_audit_memory_id" ON "memory_assets_audit" ("memoryId")`.execute(db);
await sql`CREATE INDEX "IDX_memory_assets_audit_asset_id" ON "memory_assets_audit" ("assetId")`.execute(db);
await sql`CREATE INDEX "IDX_memory_assets_audit_deleted_at" ON "memory_assets_audit" ("deletedAt")`.execute(db);
await sql`CREATE INDEX "IDX_memory_assets_update_id" ON "memories_assets_assets" ("updateId")`.execute(db);
await sql`CREATE INDEX "IDX_memories_audit_memory_id" ON "memories_audit" ("memoryId")`.execute(db);
await sql`CREATE INDEX "IDX_memories_audit_user_id" ON "memories_audit" ("userId")`.execute(db);
await sql`CREATE INDEX "IDX_memories_audit_deleted_at" ON "memories_audit" ("deletedAt")`.execute(db);
await sql`CREATE OR REPLACE FUNCTION memories_delete_audit()
RETURNS TRIGGER
LANGUAGE PLPGSQL
AS $$
BEGIN
INSERT INTO memories_audit ("memoryId", "userId")
SELECT "id", "ownerId"
FROM OLD;
RETURN NULL;
END
$$;`.execute(db);
await sql`CREATE OR REPLACE FUNCTION memory_assets_delete_audit()
RETURNS TRIGGER
LANGUAGE PLPGSQL
AS $$
BEGIN
INSERT INTO memory_assets_audit ("memoryId", "assetId")
SELECT "memoriesId", "assetsId" FROM OLD
WHERE "memoriesId" IN (SELECT "id" FROM memories WHERE "id" IN (SELECT "memoriesId" FROM OLD));
RETURN NULL;
END
$$;`.execute(db);
await sql`CREATE OR REPLACE TRIGGER "memories_delete_audit"
AFTER DELETE ON "memories"
REFERENCING OLD TABLE AS "old"
FOR EACH STATEMENT
WHEN (pg_trigger_depth() = 0)
EXECUTE FUNCTION memories_delete_audit();`.execute(db);
await sql`CREATE OR REPLACE TRIGGER "memory_assets_delete_audit"
AFTER DELETE ON "memories_assets_assets"
REFERENCING OLD TABLE AS "old"
FOR EACH STATEMENT
WHEN (pg_trigger_depth() <= 1)
EXECUTE FUNCTION memory_assets_delete_audit();`.execute(db);
await sql`CREATE OR REPLACE TRIGGER "memory_assets_updated_at"
BEFORE UPDATE ON "memories_assets_assets"
FOR EACH ROW
EXECUTE FUNCTION updated_at();`.execute(db);
}
export async function down(db: Kysely<any>): Promise<void> {
await sql`DROP TRIGGER "memories_delete_audit" ON "memories";`.execute(db);
await sql`DROP TRIGGER "memory_assets_delete_audit" ON "memories_assets_assets";`.execute(db);
await sql`DROP TRIGGER "memory_assets_updated_at" ON "memories_assets_assets";`.execute(db);
await sql`DROP INDEX "IDX_memory_assets_update_id";`.execute(db);
await sql`DROP INDEX "IDX_memory_assets_audit_memory_id";`.execute(db);
await sql`DROP INDEX "IDX_memory_assets_audit_asset_id";`.execute(db);
await sql`DROP INDEX "IDX_memory_assets_audit_deleted_at";`.execute(db);
await sql`DROP INDEX "IDX_memories_audit_memory_id";`.execute(db);
await sql`DROP INDEX "IDX_memories_audit_user_id";`.execute(db);
await sql`DROP INDEX "IDX_memories_audit_deleted_at";`.execute(db);
await sql`ALTER TABLE "memory_assets_audit" DROP CONSTRAINT "FK_225a204afcb0bd6de015080fb03";`.execute(db);
await sql`ALTER TABLE "memory_assets_audit" DROP CONSTRAINT "PK_35ef16910228f980e0766dcc59b";`.execute(db);
await sql`ALTER TABLE "memories_audit" DROP CONSTRAINT "PK_19de798c033a710dcfa5c72f81b";`.execute(db);
await sql`ALTER TABLE "memories_assets_assets" DROP COLUMN "createdAt";`.execute(db);
await sql`ALTER TABLE "memories_assets_assets" DROP COLUMN "updatedAt";`.execute(db);
await sql`ALTER TABLE "memories_assets_assets" DROP COLUMN "updateId";`.execute(db);
await sql`DROP TABLE "memory_assets_audit";`.execute(db);
await sql`DROP TABLE "memories_audit";`.execute(db);
await sql`DROP FUNCTION memories_delete_audit;`.execute(db);
await sql`DROP FUNCTION memory_assets_delete_audit;`.execute(db);
}

View file

@ -13,8 +13,8 @@ import 'src/schema/tables/exif.table';
import 'src/schema/tables/face-search.table';
import 'src/schema/tables/geodata-places.table';
import 'src/schema/tables/library.table';
import 'src/schema/tables/memory-asset.table';
import 'src/schema/tables/memory.table';
import 'src/schema/tables/memory_asset.table';
import 'src/schema/tables/move.table';
import 'src/schema/tables/natural-earth-countries.table';
import 'src/schema/tables/partner-audit.table';

View file

@ -0,0 +1,23 @@
import { PrimaryGeneratedUuidV7Column } from 'src/decorators';
import { MemoryTable } from 'src/schema/tables/memory.table';
import { Column, CreateDateColumn, ForeignKeyColumn, Table } from 'src/sql-tools';
@Table('memory_assets_audit')
export class MemoryAssetAuditTable {
@PrimaryGeneratedUuidV7Column()
id!: string;
@ForeignKeyColumn(() => MemoryTable, {
type: 'uuid',
indexName: 'IDX_memory_assets_audit_memory_id',
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
})
memoryId!: string;
@Column({ type: 'uuid', indexName: 'IDX_memory_assets_audit_asset_id' })
assetId!: string;
@CreateDateColumn({ default: () => 'clock_timestamp()', indexName: 'IDX_memory_assets_audit_deleted_at' })
deletedAt!: Date;
}

View file

@ -0,0 +1,36 @@
import { ColumnType } from 'kysely';
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
import { memory_assets_delete_audit } from 'src/schema/functions';
import { AssetTable } from 'src/schema/tables/asset.table';
import { MemoryTable } from 'src/schema/tables/memory.table';
import { AfterDeleteTrigger, CreateDateColumn, ForeignKeyColumn, Table, UpdateDateColumn } from 'src/sql-tools';
type Timestamp = ColumnType<Date, Date | string, Date | string>;
type Generated<T> =
T extends ColumnType<infer S, infer I, infer U> ? ColumnType<S, I | undefined, U> : ColumnType<T, T | undefined, T>;
@Table('memories_assets_assets')
@UpdatedAtTrigger('memory_assets_updated_at')
@AfterDeleteTrigger({
name: 'memory_assets_delete_audit',
scope: 'statement',
function: memory_assets_delete_audit,
referencingOldTableAs: 'old',
when: 'pg_trigger_depth() <= 1',
})
export class MemoryAssetTable {
@ForeignKeyColumn(() => MemoryTable, { onUpdate: 'CASCADE', onDelete: 'CASCADE', primary: true })
memoriesId!: string;
@ForeignKeyColumn(() => AssetTable, { onUpdate: 'CASCADE', onDelete: 'CASCADE', primary: true })
assetsId!: string;
@CreateDateColumn()
createdAt!: Generated<Timestamp>;
@UpdateDateColumn()
updatedAt!: Generated<Timestamp>;
@UpdateIdColumn({ indexName: 'IDX_memory_assets_update_id' })
updateId!: Generated<string>;
}

View file

@ -0,0 +1,22 @@
import { ColumnType } from 'kysely';
import { PrimaryGeneratedUuidV7Column } from 'src/decorators';
import { Column, CreateDateColumn, Table } from 'src/sql-tools';
type Timestamp = ColumnType<Date, Date | string, Date | string>;
type Generated<T> =
T extends ColumnType<infer S, infer I, infer U> ? ColumnType<S, I | undefined, U> : ColumnType<T, T | undefined, T>;
@Table('memories_audit')
export class MemoryAuditTable {
@PrimaryGeneratedUuidV7Column()
id!: Generated<string>;
@Column({ type: 'uuid', indexName: 'IDX_memories_audit_memory_id' })
memoryId!: string;
@Column({ type: 'uuid', indexName: 'IDX_memories_audit_user_id' })
userId!: string;
@CreateDateColumn({ default: () => 'clock_timestamp()', indexName: 'IDX_memories_audit_deleted_at' })
deletedAt!: Timestamp;
}

View file

@ -1,7 +1,9 @@
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
import { MemoryType } from 'src/enum';
import { memories_delete_audit } from 'src/schema/functions';
import { UserTable } from 'src/schema/tables/user.table';
import {
AfterDeleteTrigger,
Column,
CreateDateColumn,
DeleteDateColumn,
@ -10,11 +12,17 @@ import {
Table,
UpdateDateColumn,
} from 'src/sql-tools';
import { MemoryData } from 'src/types';
@Table('memories')
@UpdatedAtTrigger('memories_updated_at')
export class MemoryTable<T extends MemoryType = MemoryType> {
@AfterDeleteTrigger({
name: 'memories_delete_audit',
scope: 'statement',
function: memories_delete_audit,
referencingOldTableAs: 'old',
when: 'pg_trigger_depth() = 0',
})
export class MemoryTable {
@PrimaryGeneratedColumn()
id!: string;
@ -31,10 +39,10 @@ export class MemoryTable<T extends MemoryType = MemoryType> {
ownerId!: string;
@Column()
type!: T;
type!: MemoryType;
@Column({ type: 'jsonb' })
data!: MemoryData[T];
data!: object;
/** unless set to true, will be automatically deleted in the future */
@Column({ type: 'boolean', default: false })

View file

@ -1,12 +0,0 @@
import { AssetTable } from 'src/schema/tables/asset.table';
import { MemoryTable } from 'src/schema/tables/memory.table';
import { ForeignKeyColumn, Table } from 'src/sql-tools';
@Table('memories_assets_assets')
export class MemoryAssetTable {
@ForeignKeyColumn(() => MemoryTable, { onUpdate: 'CASCADE', onDelete: 'CASCADE', primary: true })
memoriesId!: string;
@ForeignKeyColumn(() => AssetTable, { onUpdate: 'CASCADE', onDelete: 'CASCADE', primary: true })
assetsId!: string;
}