refactor: migrate to native set operations (#32186)

This commit is contained in:
Daniel Dietzler
2026-10-07 19:33:21 +02:00
committed by GitHub
parent b620efba38
commit 7e7a1de3fd
4 changed files with 48 additions and 73 deletions
+2 -2
View File
@@ -5,7 +5,6 @@ import { chunk, flatten } from 'lodash-es';
import { ApiCustomExtension, ApiTag, ImmichWorker, JobName, MetadataKey, QueueName } from 'src/enum.js';
import { EmitEvent } from 'src/repositories/event.repository.js';
import { immich_uuid_v7, updated_at } from 'src/schema/functions.js';
import { setUnion } from 'src/utils/set.js';
const GeneratedUuidV7Column = (options: Omit<ColumnOptions, 'type' | 'default' | 'nullable'> = {}) =>
Column({ ...options, type: 'uuid', nullable: false, default: () => `${immich_uuid_v7.name}()` });
@@ -105,7 +104,8 @@ export function ChunkedArray(options?: { paramIndex?: number; chunkSize?: number
}
export function ChunkedSet(options?: { paramIndex?: number; chunkSize?: number }): MethodDecorator {
return Chunked({ ...options, mergeFn: (args: Set<any>[]) => setUnion(...args) });
// eslint-disable-next-line unicorn/no-array-reduce
return Chunked({ ...options, mergeFn: (args: Set<any>[]) => args.reduce((unionSet, set) => unionSet.union(set)) });
}
const UUID = '00000000-0000-4000-a000-000000000000';
+2 -3
View File
@@ -23,7 +23,6 @@ import {
LogLevel,
QueueName,
} from 'src/enum.js';
import { setDifference } from 'src/utils/set.js';
export interface EnvData {
host?: string;
@@ -181,7 +180,7 @@ const getEnv = (): EnvData => {
const includedWorkers = asSet(dto.IMMICH_WORKERS_INCLUDE, [ImmichWorker.Api, ImmichWorker.Microservices]);
const excludedWorkers = asSet(dto.IMMICH_WORKERS_EXCLUDE, []);
const workers = [...setDifference(includedWorkers, excludedWorkers)];
const workers = [...includedWorkers.difference(excludedWorkers)];
for (const worker of workers) {
if (!WORKER_TYPES.has(worker)) {
throw new Error(`Invalid worker(s) found: ${workers.join(',')}`);
@@ -220,7 +219,7 @@ const getEnv = (): EnvData => {
: asSet<ImmichTelemetry>(dto.IMMICH_TELEMETRY_INCLUDE, []);
const excludedTelemetries = asSet<ImmichTelemetry>(dto.IMMICH_TELEMETRY_EXCLUDE, []);
const telemetries = setDifference(includedTelemetries, excludedTelemetries);
const telemetries = includedTelemetries.difference(excludedTelemetries);
for (const telemetry of telemetries) {
if (!TELEMETRY_TYPES.has(telemetry)) {
throw new Error(`Invalid telemetry found: ${telemetry}`);
+44 -32
View File
@@ -5,7 +5,6 @@ import { PersonUserRole } from 'src/dtos/person.dto.js';
import { AlbumUserRole, Permission } from 'src/enum.js';
import { AccessRepository } from 'src/repositories/access.repository.js';
import { PersonId } from 'src/repositories/person.repository.js';
import { areSetsEqual, isSetSuperset, setDifference, setUnion } from 'src/utils/set.js';
export type GrantedRequest = {
requested: Permission[];
@@ -17,7 +16,11 @@ export const isGranted = ({ requested, current }: GrantedRequest) => {
return true;
}
return isSetSuperset(new Set(current), new Set(requested));
return new Set(current).isSupersetOf(new Set(requested));
};
export const areSetsEqual = <T>(setA: Set<T>, setB: Set<T>): boolean => {
return setA.size === setB.size && setA.isSupersetOf(setB);
};
export type AccessRequest = {
@@ -160,21 +163,24 @@ const checkOtherAccess = async (access: AccessRepository, request: OtherAccessRe
// uses activity id
case Permission.ActivityDelete: {
const isOwner = await access.activity.checkOwnerAccess(auth.user.id, ids);
const isAlbumOwner = await access.activity.checkAlbumOwnerAccess(auth.user.id, setDifference(ids, isOwner));
return setUnion(isOwner, isAlbumOwner);
const isAlbumOwner = await access.activity.checkAlbumOwnerAccess(auth.user.id, ids.difference(isOwner));
return isOwner.union(isAlbumOwner);
}
case Permission.AssetRead: {
const isOwner = await access.asset.checkOwnerAccess(auth.user.id, ids, auth.session?.hasElevatedPermission);
const isAlbum = await access.asset.checkAlbumAccess(auth.user.id, setDifference(ids, isOwner));
const isPartner = await access.asset.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner, isAlbum));
return setUnion(isOwner, isAlbum, isPartner);
const isAlbum = await access.asset.checkAlbumAccess(auth.user.id, ids.difference(isOwner));
const isPartner = await access.asset.checkPartnerAccess(
auth.user.id,
ids.difference(isOwner).difference(isAlbum),
);
return isOwner.union(isAlbum).union(isPartner);
}
case Permission.AssetShare: {
const isOwner = await access.asset.checkOwnerAccess(auth.user.id, ids, false);
const isPartner = await access.asset.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner));
return setUnion(isOwner, isPartner);
const isPartner = await access.asset.checkPartnerAccess(auth.user.id, ids.difference(isOwner));
return isOwner.union(isPartner);
}
case Permission.AssetFileDownload: {
@@ -183,16 +189,22 @@ const checkOtherAccess = async (access: AccessRepository, request: OtherAccessRe
case Permission.AssetView: {
const isOwner = await access.asset.checkOwnerAccess(auth.user.id, ids, auth.session?.hasElevatedPermission);
const isAlbum = await access.asset.checkAlbumAccess(auth.user.id, setDifference(ids, isOwner));
const isPartner = await access.asset.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner, isAlbum));
return setUnion(isOwner, isAlbum, isPartner);
const isAlbum = await access.asset.checkAlbumAccess(auth.user.id, ids.difference(isOwner));
const isPartner = await access.asset.checkPartnerAccess(
auth.user.id,
ids.difference(isOwner).difference(isAlbum),
);
return isOwner.union(isAlbum).union(isPartner);
}
case Permission.AssetDownload: {
const isOwner = await access.asset.checkOwnerAccess(auth.user.id, ids, auth.session?.hasElevatedPermission);
const isAlbum = await access.asset.checkAlbumAccess(auth.user.id, setDifference(ids, isOwner));
const isPartner = await access.asset.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner, isAlbum));
return setUnion(isOwner, isAlbum, isPartner);
const isAlbum = await access.asset.checkAlbumAccess(auth.user.id, ids.difference(isOwner));
const isPartner = await access.asset.checkPartnerAccess(
auth.user.id,
ids.difference(isOwner).difference(isAlbum),
);
return isOwner.union(isAlbum).union(isPartner);
}
case Permission.AssetUpdate: {
@@ -228,30 +240,30 @@ const checkOtherAccess = async (access: AccessRepository, request: OtherAccessRe
const isOwner = await access.album.checkOwnerAccess(auth.user.id, ids);
const isShared = await access.album.checkSharedAlbumAccess(
auth.user.id,
setDifference(ids, isOwner),
ids.difference(isOwner),
AlbumUserRole.Viewer,
);
return setUnion(isOwner, isShared);
return isOwner.union(isShared);
}
case Permission.AlbumAssetCreate: {
const isOwner = await access.album.checkOwnerAccess(auth.user.id, ids);
const isShared = await access.album.checkSharedAlbumAccess(
auth.user.id,
setDifference(ids, isOwner),
ids.difference(isOwner),
AlbumUserRole.Editor,
);
return setUnion(isOwner, isShared);
return isOwner.union(isShared);
}
case Permission.AlbumUpdate: {
const isOwner = await access.album.checkOwnerAccess(auth.user.id, ids);
const isShared = await access.album.checkSharedAlbumAccess(
auth.user.id,
setDifference(ids, isOwner),
ids.difference(isOwner),
AlbumUserRole.Editor,
);
return setUnion(isOwner, isShared);
return isOwner.union(isShared);
}
case Permission.AlbumDelete: {
@@ -262,30 +274,30 @@ const checkOtherAccess = async (access: AccessRepository, request: OtherAccessRe
const isOwner = await access.album.checkOwnerAccess(auth.user.id, ids);
const isShared = await access.album.checkSharedAlbumAccess(
auth.user.id,
setDifference(ids, isOwner),
ids.difference(isOwner),
AlbumUserRole.Editor,
);
return setUnion(isOwner, isShared);
return isOwner.union(isShared);
}
case Permission.AlbumDownload: {
const isOwner = await access.album.checkOwnerAccess(auth.user.id, ids);
const isShared = await access.album.checkSharedAlbumAccess(
auth.user.id,
setDifference(ids, isOwner),
ids.difference(isOwner),
AlbumUserRole.Viewer,
);
return setUnion(isOwner, isShared);
return isOwner.union(isShared);
}
case Permission.AlbumAssetDelete: {
const isOwner = await access.album.checkOwnerAccess(auth.user.id, ids);
const isShared = await access.album.checkSharedAlbumAccess(
auth.user.id,
setDifference(ids, isOwner),
ids.difference(isOwner),
AlbumUserRole.Editor,
);
return setUnion(isOwner, isShared);
return isOwner.union(isShared);
}
case Permission.AssetUpload: {
@@ -324,8 +336,8 @@ const checkOtherAccess = async (access: AccessRepository, request: OtherAccessRe
case Permission.TimelineRead: {
const isOwner = ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set<string>();
const isPartner = await access.timeline.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner));
return setUnion(isOwner, isPartner);
const isPartner = await access.timeline.checkPartnerAccess(auth.user.id, ids.difference(isOwner));
return isOwner.union(isPartner);
}
case Permission.TimelineDownload: {
@@ -354,8 +366,8 @@ const checkOtherAccess = async (access: AccessRepository, request: OtherAccessRe
case Permission.ClusterGroupRead: {
const isMember = await access.clusterGroup.checkOwnerAccess(auth.user.id, ids);
const isInvited = await access.clusterGroup.checkInviteAccess(auth.user.id, setDifference(ids, isMember));
return setUnion(isMember, isInvited);
const isInvited = await access.clusterGroup.checkInviteAccess(auth.user.id, ids.difference(isMember));
return isMember.union(isInvited);
}
case Permission.ClusterGroupLeave:
@@ -366,7 +378,7 @@ const checkOtherAccess = async (access: AccessRepository, request: OtherAccessRe
case Permission.ClusterGroupRequestDelete: {
const isOwner = await access.clusterGroupRequest.checkOwnerAccess(auth.user.id, ids);
const isGroupMember = await access.clusterGroupRequest.checkGroupAccess(auth.user.id, ids);
return setUnion(isOwner, isGroupMember);
return isOwner.union(isGroupMember);
}
case Permission.ClusterGroupRequestRead: {
-36
View File
@@ -1,36 +0,0 @@
// NOTE: The following Set utils have been added here, to easily determine where they are used.
// They should be replaced with native Set operations, when they are added to the language.
// Proposal reference: https://github.com/tc39/proposal-set-methods
export const setUnion = <T>(...sets: Set<T>[]): Set<T> => {
const union = new Set(sets[0]);
for (const set of sets.slice(1)) {
for (const element of set) {
union.add(element);
}
}
return union;
};
export const setDifference = <T>(setA: Set<T>, ...sets: Set<T>[]): Set<T> => {
const difference = new Set(setA);
for (const set of sets) {
for (const element of set) {
difference.delete(element);
}
}
return difference;
};
export const isSetSuperset = <T>(set: Set<T>, subset: Set<T>): boolean => {
for (const element of subset) {
if (!set.has(element)) {
return false;
}
}
return true;
};
export const areSetsEqual = <T>(setA: Set<T>, setB: Set<T>): boolean => {
return setA.size === setB.size && isSetSuperset(setA, setB);
};