mirror of
https://github.com/immich-app/immich.git
synced 2026-10-16 21:05:52 +08:00
refactor: migrate to native set operations (#32186)
This commit is contained in:
@@ -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';
|
||||
|
||||
@@ -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
@@ -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: {
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
Reference in New Issue
Block a user