feat: partner sync (#16424)

feat: partner CUD sync
This commit is contained in:
Zack Pollard 2025-03-03 11:05:30 +00:00 committed by GitHub
parent 869839f642
commit fe702ba6d7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 614 additions and 8 deletions

9
server/src/db.d.ts vendored
View file

@ -272,6 +272,13 @@ export interface NaturalearthCountries {
type: string;
}
export interface PartnersAudit {
deletedAt: Generated<Timestamp>;
id: Generated<string>;
sharedById: string;
sharedWithId: string;
}
export interface Partners {
createdAt: Generated<Timestamp>;
inTimeline: Generated<boolean>;
@ -316,7 +323,6 @@ export interface SessionSyncCheckpoints {
updateId: Generated<string>;
}
export interface SharedLinkAsset {
assetsId: string;
sharedLinksId: string;
@ -462,6 +468,7 @@ export interface DB {
migrations: Migrations;
move_history: MoveHistory;
naturalearth_countries: NaturalearthCountries;
partners_audit: PartnersAudit;
partners: Partners;
person: Person;
sessions: Sessions;

View file

@ -45,15 +45,30 @@ export class SyncUserDeleteV1 {
userId!: string;
}
export class SyncPartnerV1 {
sharedById!: string;
sharedWithId!: string;
inTimeline!: boolean;
}
export class SyncPartnerDeleteV1 {
sharedById!: string;
sharedWithId!: string;
}
export type SyncItem = {
[SyncEntityType.UserV1]: SyncUserV1;
[SyncEntityType.UserDeleteV1]: SyncUserDeleteV1;
[SyncEntityType.PartnerV1]: SyncPartnerV1;
[SyncEntityType.PartnerDeleteV1]: SyncPartnerDeleteV1;
};
const responseDtos = [
//
SyncUserV1,
SyncUserDeleteV1,
SyncPartnerV1,
SyncPartnerDeleteV1,
];
export const extraSyncModels = responseDtos;

View file

@ -0,0 +1,19 @@
import { Column, CreateDateColumn, Entity, Index, PrimaryColumn } from 'typeorm';
@Entity('partners_audit')
export class PartnerAuditEntity {
@PrimaryColumn({ type: 'uuid', nullable: false, default: () => 'immich_uuid_v7()' })
id!: string;
@Index('IDX_partners_audit_shared_by_id')
@Column({ type: 'uuid' })
sharedById!: string;
@Index('IDX_partners_audit_shared_with_id')
@Column({ type: 'uuid' })
sharedWithId!: string;
@Index('IDX_partners_audit_deleted_at')
@CreateDateColumn({ type: 'timestamptz', default: () => 'clock_timestamp()' })
deletedAt!: Date;
}

View file

@ -548,9 +548,12 @@ export enum DatabaseLock {
export enum SyncRequestType {
UsersV1 = 'UsersV1',
PartnersV1 = 'PartnersV1',
}
export enum SyncEntityType {
UserV1 = 'UserV1',
UserDeleteV1 = 'UserDeleteV1',
PartnerV1 = 'PartnerV1',
PartnerDeleteV1 = 'PartnerDeleteV1',
}

View file

@ -0,0 +1,38 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class CreatePartnersAuditTable1740739778549 implements MigrationInterface {
name = 'CreatePartnersAuditTable1740739778549'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE "partners_audit" ("id" uuid NOT NULL DEFAULT immich_uuid_v7(), "sharedById" uuid NOT NULL, "sharedWithId" uuid NOT NULL, "deletedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT clock_timestamp(), CONSTRAINT "PK_952b50217ff78198a7e380f0359" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE INDEX "IDX_partners_audit_shared_by_id" ON "partners_audit" ("sharedById") `);
await queryRunner.query(`CREATE INDEX "IDX_partners_audit_shared_with_id" ON "partners_audit" ("sharedWithId") `);
await queryRunner.query(`CREATE INDEX "IDX_partners_audit_deleted_at" ON "partners_audit" ("deletedAt") `);
await queryRunner.query(`CREATE OR REPLACE FUNCTION partners_delete_audit() RETURNS TRIGGER AS
$$
BEGIN
INSERT INTO partners_audit ("sharedById", "sharedWithId")
SELECT "sharedById", "sharedWithId"
FROM OLD;
RETURN NULL;
END;
$$ LANGUAGE plpgsql`
);
await queryRunner.query(`CREATE OR REPLACE TRIGGER partners_delete_audit
AFTER DELETE ON partners
REFERENCING OLD TABLE AS OLD
FOR EACH STATEMENT
EXECUTE FUNCTION partners_delete_audit();
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX "public"."IDX_partners_audit_deleted_at"`);
await queryRunner.query(`DROP INDEX "public"."IDX_partners_audit_shared_with_id"`);
await queryRunner.query(`DROP INDEX "public"."IDX_partners_audit_shared_by_id"`);
await queryRunner.query(`DROP TRIGGER partners_delete_audit`);
await queryRunner.query(`DROP FUNCTION partners_delete_audit`);
await queryRunner.query(`DROP TABLE "partners_audit"`);
}
}

View file

@ -56,4 +56,26 @@ export class SyncRepository {
.orderBy(['id asc'])
.stream();
}
getPartnerUpserts(userId: string, ack?: SyncAck) {
return this.db
.selectFrom('partners')
.select(['sharedById', 'sharedWithId', 'inTimeline', 'updateId'])
.$if(!!ack, (qb) => qb.where('updateId', '>', ack!.updateId))
.where((eb) => eb.or([eb('sharedById', '=', userId), eb('sharedWithId', '=', userId)]))
.where('updatedAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
.orderBy(['updateId asc'])
.stream();
}
getPartnerDeletes(userId: string, ack?: SyncAck) {
return this.db
.selectFrom('partners_audit')
.select(['id', 'sharedById', 'sharedWithId'])
.$if(!!ack, (qb) => qb.where('id', '>', ack!.updateId))
.where((eb) => eb.or([eb('sharedById', '=', userId), eb('sharedWithId', '=', userId)]))
.where('deletedAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
.orderBy(['id asc'])
.stream();
}
}

View file

@ -25,6 +25,7 @@ const FULL_SYNC = { needsFullSync: true, deleted: [], upserted: [] };
const SYNC_TYPES_ORDER = [
//
SyncRequestType.UsersV1,
SyncRequestType.PartnersV1,
];
const throwSessionRequired = () => {
@ -81,8 +82,6 @@ export class SyncService extends BaseService {
checkpoints.map(({ type, ack }) => [type, fromAck(ack)]),
);
// TODO pre-filter/sort list based on optimal sync order
for (const type of SYNC_TYPES_ORDER.filter((type) => dto.types.includes(type))) {
switch (type) {
case SyncRequestType.UsersV1: {
@ -99,6 +98,23 @@ export class SyncService extends BaseService {
break;
}
case SyncRequestType.PartnersV1: {
const deletes = this.syncRepository.getPartnerDeletes(
auth.user.id,
checkpointMap[SyncEntityType.PartnerDeleteV1],
);
for await (const { id, ...data } of deletes) {
response.write(serialize({ type: SyncEntityType.PartnerDeleteV1, updateId: id, data }));
}
const upserts = this.syncRepository.getPartnerUpserts(auth.user.id, checkpointMap[SyncEntityType.PartnerV1]);
for await (const { updateId, ...data } of upserts) {
response.write(serialize({ type: SyncEntityType.PartnerV1, updateId, data }));
}
break;
}
default: {
this.logger.warn(`Unsupported sync type: ${type}`);
break;