feat: sync albums and album users (#18377)

This commit is contained in:
Jason Rasmussen 2025-05-21 15:35:32 -04:00 committed by GitHub
parent 58af574241
commit cd288533a1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
41 changed files with 2811 additions and 934 deletions

View file

@ -23,6 +23,19 @@ export const immich_uuid_v7 = registerFunction({
synchronize: false,
});
export const album_user_after_insert = registerFunction({
name: 'album_user_after_insert',
returnType: 'TRIGGER',
language: 'PLPGSQL',
body: `
BEGIN
UPDATE albums SET "updatedAt" = clock_timestamp(), "updateId" = immich_uuid_v7(clock_timestamp())
WHERE "id" IN (SELECT DISTINCT "albumsId" FROM inserted_rows);
RETURN NULL;
END`,
synchronize: false,
});
export const updated_at = registerFunction({
name: 'updated_at',
returnType: 'TRIGGER',
@ -114,3 +127,38 @@ export const assets_delete_audit = registerFunction({
END`,
synchronize: false,
});
export const albums_delete_audit = registerFunction({
name: 'albums_delete_audit',
returnType: 'TRIGGER',
language: 'PLPGSQL',
body: `
BEGIN
INSERT INTO albums_audit ("albumId", "userId")
SELECT "id", "ownerId"
FROM OLD;
RETURN NULL;
END`,
synchronize: false,
});
export const album_users_delete_audit = registerFunction({
name: 'album_users_delete_audit',
returnType: 'TRIGGER',
language: 'PLPGSQL',
body: `
BEGIN
INSERT INTO albums_audit ("albumId", "userId")
SELECT "albumsId", "usersId"
FROM OLD;
IF pg_trigger_depth() = 1 THEN
INSERT INTO album_users_audit ("albumId", "userId")
SELECT "albumsId", "usersId"
FROM OLD;
END IF;
RETURN NULL;
END`,
synchronize: false,
});

View file

@ -1,5 +1,8 @@
import { asset_face_source_type, asset_visibility_enum, assets_status_enum } from 'src/schema/enums';
import {
album_user_after_insert,
album_users_delete_audit,
albums_delete_audit,
assets_delete_audit,
f_concat_ws,
f_unaccent,
@ -11,6 +14,8 @@ import {
} from 'src/schema/functions';
import { ActivityTable } from 'src/schema/tables/activity.table';
import { AlbumAssetTable } from 'src/schema/tables/album-asset.table';
import { AlbumAuditTable } from 'src/schema/tables/album-audit.table';
import { AlbumUserAuditTable } from 'src/schema/tables/album-user-audit.table';
import { AlbumUserTable } from 'src/schema/tables/album-user.table';
import { AlbumTable } from 'src/schema/tables/album.table';
import { APIKeyTable } from 'src/schema/tables/api-key.table';
@ -45,15 +50,16 @@ import { UserAuditTable } from 'src/schema/tables/user-audit.table';
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 { ConfigurationParameter, Database, Extensions } from 'src/sql-tools';
import { Database, Extensions } from 'src/sql-tools';
@Extensions(['uuid-ossp', 'unaccent', 'cube', 'earthdistance', 'pg_trgm', 'plpgsql'])
@ConfigurationParameter({ name: 'search_path', value: () => '"$user", public, vectors', scope: 'database' })
@Database({ name: 'immich' })
export class ImmichDatabase {
tables = [
ActivityTable,
AlbumAssetTable,
AlbumAuditTable,
AlbumUserAuditTable,
AlbumUserTable,
AlbumTable,
APIKeyTable,
@ -99,6 +105,9 @@ export class ImmichDatabase {
users_delete_audit,
partners_delete_audit,
assets_delete_audit,
albums_delete_audit,
album_user_after_insert,
album_users_delete_audit,
];
enum = [assets_status_enum, asset_face_source_type, asset_visibility_enum];

View file

@ -0,0 +1,96 @@
import { Kysely, sql } from 'kysely';
export async function up(db: Kysely<any>): Promise<void> {
await sql`CREATE OR REPLACE FUNCTION album_user_after_insert()
RETURNS TRIGGER
LANGUAGE PLPGSQL
AS $$
BEGIN
UPDATE albums SET "updatedAt" = clock_timestamp(), "updateId" = immich_uuid_v7(clock_timestamp())
WHERE "id" IN (SELECT DISTINCT "albumsId" FROM inserted_rows);
RETURN NULL;
END
$$;`.execute(db);
await sql`CREATE OR REPLACE FUNCTION albums_delete_audit()
RETURNS TRIGGER
LANGUAGE PLPGSQL
AS $$
BEGIN
INSERT INTO albums_audit ("albumId", "userId")
SELECT "id", "ownerId"
FROM OLD;
RETURN NULL;
END
$$;`.execute(db);
await sql`CREATE OR REPLACE FUNCTION album_users_delete_audit()
RETURNS TRIGGER
LANGUAGE PLPGSQL
AS $$
BEGIN
INSERT INTO albums_audit ("albumId", "userId")
SELECT "albumsId", "usersId"
FROM OLD;
IF pg_trigger_depth() = 1 THEN
INSERT INTO album_users_audit ("albumId", "userId")
SELECT "albumsId", "usersId"
FROM OLD;
END IF;
RETURN NULL;
END
$$;`.execute(db);
await sql`CREATE TABLE "albums_audit" ("id" uuid NOT NULL DEFAULT immich_uuid_v7(), "albumId" uuid NOT NULL, "userId" uuid NOT NULL, "deletedAt" timestamp with time zone NOT NULL DEFAULT clock_timestamp());`.execute(db);
await sql`CREATE TABLE "album_users_audit" ("id" uuid NOT NULL DEFAULT immich_uuid_v7(), "albumId" uuid NOT NULL, "userId" uuid NOT NULL, "deletedAt" timestamp with time zone NOT NULL DEFAULT clock_timestamp());`.execute(db);
await sql`ALTER TABLE "albums_audit" ADD CONSTRAINT "PK_c75efea8d4dce316ad29b851a8b" PRIMARY KEY ("id");`.execute(db);
await sql`ALTER TABLE "album_users_audit" ADD CONSTRAINT "PK_f479a2e575b7ebc9698362c1688" PRIMARY KEY ("id");`.execute(db);
await sql`ALTER TABLE "albums_shared_users_users" ADD "updateId" uuid NOT NULL DEFAULT immich_uuid_v7();`.execute(db);
await sql`ALTER TABLE "albums_shared_users_users" ADD "updatedAt" timestamp with time zone NOT NULL DEFAULT now();`.execute(db);
await sql`CREATE INDEX "IDX_album_users_update_id" ON "albums_shared_users_users" ("updateId")`.execute(db);
await sql`CREATE INDEX "IDX_albums_audit_album_id" ON "albums_audit" ("albumId")`.execute(db);
await sql`CREATE INDEX "IDX_albums_audit_user_id" ON "albums_audit" ("userId")`.execute(db);
await sql`CREATE INDEX "IDX_albums_audit_deleted_at" ON "albums_audit" ("deletedAt")`.execute(db);
await sql`CREATE INDEX "IDX_album_users_audit_album_id" ON "album_users_audit" ("albumId")`.execute(db);
await sql`CREATE INDEX "IDX_album_users_audit_user_id" ON "album_users_audit" ("userId")`.execute(db);
await sql`CREATE INDEX "IDX_album_users_audit_deleted_at" ON "album_users_audit" ("deletedAt")`.execute(db);
await sql`CREATE OR REPLACE TRIGGER "albums_delete_audit"
AFTER DELETE ON "albums"
REFERENCING OLD TABLE AS "old"
FOR EACH STATEMENT
WHEN (pg_trigger_depth() = 0)
EXECUTE FUNCTION albums_delete_audit();`.execute(db);
await sql`CREATE OR REPLACE TRIGGER "album_users_delete_audit"
AFTER DELETE ON "albums_shared_users_users"
REFERENCING OLD TABLE AS "old"
FOR EACH STATEMENT
WHEN (pg_trigger_depth() <= 1)
EXECUTE FUNCTION album_users_delete_audit();`.execute(db);
await sql`CREATE OR REPLACE TRIGGER "album_user_after_insert"
AFTER INSERT ON "albums_shared_users_users"
REFERENCING NEW TABLE AS "inserted_rows"
FOR EACH STATEMENT
EXECUTE FUNCTION album_user_after_insert();`.execute(db);
await sql`CREATE OR REPLACE TRIGGER "album_users_updated_at"
BEFORE UPDATE ON "albums_shared_users_users"
FOR EACH ROW
EXECUTE FUNCTION updated_at();`.execute(db);
}
export async function down(db: Kysely<any>): Promise<void> {
await sql`DROP TRIGGER "albums_delete_audit" ON "albums";`.execute(db);
await sql`DROP TRIGGER "album_users_delete_audit" ON "albums_shared_users_users";`.execute(db);
await sql`DROP TRIGGER "album_user_after_insert" ON "albums_shared_users_users";`.execute(db);
await sql`DROP INDEX "IDX_albums_audit_album_id";`.execute(db);
await sql`DROP INDEX "IDX_albums_audit_user_id";`.execute(db);
await sql`DROP INDEX "IDX_albums_audit_deleted_at";`.execute(db);
await sql`DROP INDEX "IDX_album_users_audit_album_id";`.execute(db);
await sql`DROP INDEX "IDX_album_users_audit_user_id";`.execute(db);
await sql`DROP INDEX "IDX_album_users_audit_deleted_at";`.execute(db);
await sql`ALTER TABLE "albums_audit" DROP CONSTRAINT "PK_c75efea8d4dce316ad29b851a8b";`.execute(db);
await sql`ALTER TABLE "album_users_audit" DROP CONSTRAINT "PK_f479a2e575b7ebc9698362c1688";`.execute(db);
await sql`DROP TABLE "albums_audit";`.execute(db);
await sql`DROP TABLE "album_users_audit";`.execute(db);
await sql`DROP FUNCTION album_user_after_insert;`.execute(db);
await sql`DROP FUNCTION albums_delete_audit;`.execute(db);
await sql`DROP FUNCTION album_users_delete_audit;`.execute(db);
}

View file

@ -0,0 +1,17 @@
import { PrimaryGeneratedUuidV7Column } from 'src/decorators';
import { Column, CreateDateColumn, Table } from 'src/sql-tools';
@Table('albums_audit')
export class AlbumAuditTable {
@PrimaryGeneratedUuidV7Column()
id!: string;
@Column({ type: 'uuid', indexName: 'IDX_albums_audit_album_id' })
albumId!: string;
@Column({ type: 'uuid', indexName: 'IDX_albums_audit_user_id' })
userId!: string;
@CreateDateColumn({ default: () => 'clock_timestamp()', indexName: 'IDX_albums_audit_deleted_at' })
deletedAt!: Date;
}

View file

@ -0,0 +1,17 @@
import { PrimaryGeneratedUuidV7Column } from 'src/decorators';
import { Column, CreateDateColumn, Table } from 'src/sql-tools';
@Table('album_users_audit')
export class AlbumUserAuditTable {
@PrimaryGeneratedUuidV7Column()
id!: string;
@Column({ type: 'uuid', indexName: 'IDX_album_users_audit_album_id' })
albumId!: string;
@Column({ type: 'uuid', indexName: 'IDX_album_users_audit_user_id' })
userId!: string;
@CreateDateColumn({ default: () => 'clock_timestamp()', indexName: 'IDX_album_users_audit_deleted_at' })
deletedAt!: Date;
}

View file

@ -1,12 +1,36 @@
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
import { AlbumUserRole } from 'src/enum';
import { album_user_after_insert, album_users_delete_audit } from 'src/schema/functions';
import { AlbumTable } from 'src/schema/tables/album.table';
import { UserTable } from 'src/schema/tables/user.table';
import { Column, ForeignKeyColumn, Index, Table } from 'src/sql-tools';
import {
AfterDeleteTrigger,
AfterInsertTrigger,
Column,
ForeignKeyColumn,
Index,
Table,
UpdateDateColumn,
} from 'src/sql-tools';
@Table({ name: 'albums_shared_users_users', primaryConstraintName: 'PK_7df55657e0b2e8b626330a0ebc8' })
// Pre-existing indices from original album <--> user ManyToMany mapping
@Index({ name: 'IDX_427c350ad49bd3935a50baab73', columns: ['albumsId'] })
@Index({ name: 'IDX_f48513bf9bccefd6ff3ad30bd0', columns: ['usersId'] })
@UpdatedAtTrigger('album_users_updated_at')
@AfterInsertTrigger({
name: 'album_user_after_insert',
scope: 'statement',
referencingNewTableAs: 'inserted_rows',
function: album_user_after_insert,
})
@AfterDeleteTrigger({
name: 'album_users_delete_audit',
scope: 'statement',
function: album_users_delete_audit,
referencingOldTableAs: 'old',
when: 'pg_trigger_depth() <= 1',
})
export class AlbumUserTable {
@ForeignKeyColumn(() => AlbumTable, {
onDelete: 'CASCADE',
@ -26,4 +50,10 @@ export class AlbumUserTable {
@Column({ type: 'character varying', default: AlbumUserRole.EDITOR })
role!: AlbumUserRole;
@UpdateIdColumn({ indexName: 'IDX_album_users_update_id' })
updateId?: string;
@UpdateDateColumn()
updatedAt!: Date;
}

View file

@ -1,8 +1,10 @@
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
import { AssetOrder } from 'src/enum';
import { albums_delete_audit } from 'src/schema/functions';
import { AssetTable } from 'src/schema/tables/asset.table';
import { UserTable } from 'src/schema/tables/user.table';
import {
AfterDeleteTrigger,
Column,
CreateDateColumn,
DeleteDateColumn,
@ -14,6 +16,13 @@ import {
@Table({ name: 'albums', primaryConstraintName: 'PK_7f71c7b5bc7c87b8f94c9a93a00' })
@UpdatedAtTrigger('albums_updated_at')
@AfterDeleteTrigger({
name: 'albums_delete_audit',
scope: 'statement',
function: albums_delete_audit,
referencingOldTableAs: 'old',
when: 'pg_trigger_depth() = 0',
})
export class AlbumTable {
@PrimaryGeneratedColumn()
id!: string;