1
0
mirror of https://github.com/immich-app/immich.git synced 2024-12-25 10:43:13 +02:00

feat(server): sql access checks (#6635)

This commit is contained in:
Jason Rasmussen 2024-01-25 10:14:38 -05:00 committed by GitHub
parent bd87eb309c
commit 7fc4abba72
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 760 additions and 418 deletions

View File

@ -4,14 +4,17 @@ export const GENERATE_SQL_KEY = 'generate-sql-key';
export interface GenerateSqlQueries {
name?: string;
params?: any[];
params: unknown[];
}
/** Decorator to enable versioning/tracking of generated Sql */
export const GenerateSql = (...options: GenerateSqlQueries[]) => SetMetadata(GENERATE_SQL_KEY, options);
const UUID = '00000000-0000-4000-a000-000000000000';
export const DummyValue = {
UUID: '00000000-0000-4000-a000-000000000000',
UUID,
UUID_SET: new Set([UUID]),
PAGINATION: { take: 10, skip: 0 },
EMAIL: 'user@immich.app',
STRING: 'abcdefghi',

View File

@ -1,7 +1,6 @@
import { IAccessRepository } from '@app/domain';
import { IAccessRepository, chunks, setUnion } from '@app/domain';
import { InjectRepository } from '@nestjs/typeorm';
import { Brackets, In, Repository } from 'typeorm';
import { chunks, setUnion } from '../../domain/domain.util';
import {
ActivityEntity,
AlbumEntity,
@ -13,426 +12,503 @@ import {
SharedLinkEntity,
UserTokenEntity,
} from '../entities';
import { DATABASE_PARAMETER_CHUNK_SIZE } from '../infra.util';
import { DATABASE_PARAMETER_CHUNK_SIZE, DummyValue, GenerateSql } from '../infra.util';
export class AccessRepository implements IAccessRepository {
type IActivityAccess = IAccessRepository['activity'];
type IAlbumAccess = IAccessRepository['album'];
type IAssetAccess = IAccessRepository['asset'];
type IAuthDeviceAccess = IAccessRepository['authDevice'];
type ILibraryAccess = IAccessRepository['library'];
type ITimelineAccess = IAccessRepository['timeline'];
type IPersonAccess = IAccessRepository['person'];
type IPartnerAccess = IAccessRepository['partner'];
class ActivityAccess implements IActivityAccess {
constructor(
@InjectRepository(ActivityEntity) private activityRepository: Repository<ActivityEntity>,
@InjectRepository(AssetEntity) private assetRepository: Repository<AssetEntity>,
@InjectRepository(AlbumEntity) private albumRepository: Repository<AlbumEntity>,
@InjectRepository(LibraryEntity) private libraryRepository: Repository<LibraryEntity>,
@InjectRepository(PartnerEntity) private partnerRepository: Repository<PartnerEntity>,
@InjectRepository(PersonEntity) private personRepository: Repository<PersonEntity>,
@InjectRepository(AssetFaceEntity) private assetFaceRepository: Repository<AssetFaceEntity>,
@InjectRepository(SharedLinkEntity) private sharedLinkRepository: Repository<SharedLinkEntity>,
@InjectRepository(UserTokenEntity) private tokenRepository: Repository<UserTokenEntity>,
private activityRepository: Repository<ActivityEntity>,
private albumRepository: Repository<AlbumEntity>,
) {}
activity = {
checkOwnerAccess: async (userId: string, activityIds: Set<string>): Promise<Set<string>> => {
if (activityIds.size === 0) {
return new Set();
}
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
async checkOwnerAccess(userId: string, activityIds: Set<string>): Promise<Set<string>> {
if (activityIds.size === 0) {
return new Set();
}
return Promise.all(
chunks(activityIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) =>
this.activityRepository
.find({
select: { id: true },
where: {
id: In(idChunk),
userId,
},
})
.then((activities) => new Set(activities.map((activity) => activity.id))),
),
).then((results) => setUnion(...results));
},
return Promise.all(
chunks(activityIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) =>
this.activityRepository
.find({
select: { id: true },
where: {
id: In(idChunk),
userId,
},
})
.then((activities) => new Set(activities.map((activity) => activity.id))),
),
).then((results) => setUnion(...results));
}
checkAlbumOwnerAccess: async (userId: string, activityIds: Set<string>): Promise<Set<string>> => {
if (activityIds.size === 0) {
return new Set();
}
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
async checkAlbumOwnerAccess(userId: string, activityIds: Set<string>): Promise<Set<string>> {
if (activityIds.size === 0) {
return new Set();
}
return Promise.all(
chunks(activityIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) =>
this.activityRepository
.find({
select: { id: true },
where: {
id: In(idChunk),
album: {
ownerId: userId,
},
},
})
.then((activities) => new Set(activities.map((activity) => activity.id))),
),
).then((results) => setUnion(...results));
},
checkCreateAccess: async (userId: string, albumIds: Set<string>): Promise<Set<string>> => {
if (albumIds.size === 0) {
return new Set();
}
return Promise.all(
chunks(albumIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) =>
this.albumRepository
.createQueryBuilder('album')
.select('album.id')
.leftJoin('album.sharedUsers', 'sharedUsers')
.where('album.id IN (:...albumIds)', { albumIds: idChunk })
.andWhere('album.isActivityEnabled = true')
.andWhere(
new Brackets((qb) => {
qb.where('album.ownerId = :userId', { userId }).orWhere('sharedUsers.id = :userId', { userId });
}),
)
.getMany()
.then((albums) => new Set(albums.map((album) => album.id))),
),
).then((results) => setUnion(...results));
},
};
library = {
checkOwnerAccess: async (userId: string, libraryIds: Set<string>): Promise<Set<string>> => {
if (libraryIds.size === 0) {
return new Set();
}
return Promise.all(
chunks(libraryIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) =>
this.libraryRepository
.find({
select: { id: true },
where: {
id: In(idChunk),
return Promise.all(
chunks(activityIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) =>
this.activityRepository
.find({
select: { id: true },
where: {
id: In(idChunk),
album: {
ownerId: userId,
},
})
.then((libraries) => new Set(libraries.map((library) => library.id))),
),
).then((results) => setUnion(...results));
},
},
})
.then((activities) => new Set(activities.map((activity) => activity.id))),
),
).then((results) => setUnion(...results));
}
checkPartnerAccess: async (userId: string, partnerIds: Set<string>): Promise<Set<string>> => {
if (partnerIds.size === 0) {
return new Set();
}
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
async checkCreateAccess(userId: string, albumIds: Set<string>): Promise<Set<string>> {
if (albumIds.size === 0) {
return new Set();
}
return Promise.all(
chunks(partnerIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) =>
this.partnerRepository
.createQueryBuilder('partner')
.select('partner.sharedById')
.where('partner.sharedById IN (:...partnerIds)', { partnerIds: idChunk })
.andWhere('partner.sharedWithId = :userId', { userId })
.getMany()
.then((partners) => new Set(partners.map((partner) => partner.sharedById))),
),
).then((results) => setUnion(...results));
},
};
timeline = {
checkPartnerAccess: async (userId: string, partnerIds: Set<string>): Promise<Set<string>> => {
if (partnerIds.size === 0) {
return new Set();
}
return Promise.all(
chunks(partnerIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) =>
this.partnerRepository
.createQueryBuilder('partner')
.select('partner.sharedById')
.where('partner.sharedById IN (:...partnerIds)', { partnerIds: idChunk })
.andWhere('partner.sharedWithId = :userId', { userId })
.getMany()
.then((partners) => new Set(partners.map((partner) => partner.sharedById))),
),
).then((results) => setUnion(...results));
},
};
asset = {
checkAlbumAccess: async (userId: string, assetIds: Set<string>): Promise<Set<string>> => {
if (assetIds.size === 0) {
return new Set();
}
return Promise.all(
chunks(assetIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) =>
this.albumRepository
.createQueryBuilder('album')
.innerJoin('album.assets', 'asset')
.leftJoin('album.sharedUsers', 'sharedUsers')
.select('asset.id', 'assetId')
.addSelect('asset.livePhotoVideoId', 'livePhotoVideoId')
.where('array["asset"."id", "asset"."livePhotoVideoId"] && array[:...assetIds]::uuid[]', {
assetIds: idChunk,
})
.andWhere(
new Brackets((qb) => {
qb.where('album.ownerId = :userId', { userId }).orWhere('sharedUsers.id = :userId', { userId });
}),
)
.getRawMany()
.then((rows) => {
const allowedIds = new Set<string>();
for (const row of rows) {
if (row.assetId && assetIds.has(row.assetId)) {
allowedIds.add(row.assetId);
}
if (row.livePhotoVideoId && assetIds.has(row.livePhotoVideoId)) {
allowedIds.add(row.livePhotoVideoId);
}
}
return allowedIds;
return Promise.all(
chunks(albumIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) =>
this.albumRepository
.createQueryBuilder('album')
.select('album.id')
.leftJoin('album.sharedUsers', 'sharedUsers')
.where('album.id IN (:...albumIds)', { albumIds: idChunk })
.andWhere('album.isActivityEnabled = true')
.andWhere(
new Brackets((qb) => {
qb.where('album.ownerId = :userId', { userId }).orWhere('sharedUsers.id = :userId', { userId });
}),
),
).then((results) => setUnion(...results));
},
checkOwnerAccess: async (userId: string, assetIds: Set<string>): Promise<Set<string>> => {
if (assetIds.size === 0) {
return new Set();
}
return Promise.all(
chunks(assetIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) =>
this.assetRepository
.find({
select: { id: true },
where: {
id: In(idChunk),
ownerId: userId,
},
withDeleted: true,
})
.then((assets) => new Set(assets.map((asset) => asset.id))),
),
).then((results) => setUnion(...results));
},
checkPartnerAccess: async (userId: string, assetIds: Set<string>): Promise<Set<string>> => {
if (assetIds.size === 0) {
return new Set();
}
return Promise.all(
chunks(assetIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) =>
this.partnerRepository
.createQueryBuilder('partner')
.innerJoin('partner.sharedBy', 'sharedBy')
.innerJoin('sharedBy.assets', 'asset')
.select('asset.id', 'assetId')
.where('partner.sharedWithId = :userId', { userId })
.andWhere('asset.id IN (:...assetIds)', { assetIds: idChunk })
.getRawMany()
.then((rows) => new Set(rows.map((row) => row.assetId))),
),
).then((results) => setUnion(...results));
},
checkSharedLinkAccess: async (sharedLinkId: string, assetIds: Set<string>): Promise<Set<string>> => {
if (assetIds.size === 0) {
return new Set();
}
return Promise.all(
chunks(assetIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) =>
this.sharedLinkRepository
.createQueryBuilder('sharedLink')
.leftJoin('sharedLink.album', 'album')
.leftJoin('sharedLink.assets', 'assets')
.leftJoin('album.assets', 'albumAssets')
.select('assets.id', 'assetId')
.addSelect('albumAssets.id', 'albumAssetId')
.addSelect('assets.livePhotoVideoId', 'assetLivePhotoVideoId')
.addSelect('albumAssets.livePhotoVideoId', 'albumAssetLivePhotoVideoId')
.where('sharedLink.id = :sharedLinkId', { sharedLinkId })
.andWhere(
'array["assets"."id", "assets"."livePhotoVideoId", "albumAssets"."id", "albumAssets"."livePhotoVideoId"] && array[:...assetIds]::uuid[]',
{
assetIds: idChunk,
},
)
.getRawMany()
.then((rows) => {
const allowedIds = new Set<string>();
for (const row of rows) {
if (row.assetId && assetIds.has(row.assetId)) {
allowedIds.add(row.assetId);
}
if (row.assetLivePhotoVideoId && assetIds.has(row.assetLivePhotoVideoId)) {
allowedIds.add(row.assetLivePhotoVideoId);
}
if (row.albumAssetId && assetIds.has(row.albumAssetId)) {
allowedIds.add(row.albumAssetId);
}
if (row.albumAssetLivePhotoVideoId && assetIds.has(row.albumAssetLivePhotoVideoId)) {
allowedIds.add(row.albumAssetLivePhotoVideoId);
}
}
return allowedIds;
}),
),
).then((results) => setUnion(...results));
},
};
authDevice = {
checkOwnerAccess: async (userId: string, deviceIds: Set<string>): Promise<Set<string>> => {
if (deviceIds.size === 0) {
return new Set();
}
return Promise.all(
chunks(deviceIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) =>
this.tokenRepository
.find({
select: { id: true },
where: {
userId,
id: In(idChunk),
},
})
.then((tokens) => new Set(tokens.map((token) => token.id))),
),
).then((results) => setUnion(...results));
},
};
album = {
checkOwnerAccess: async (userId: string, albumIds: Set<string>): Promise<Set<string>> => {
if (albumIds.size === 0) {
return new Set();
}
return Promise.all(
chunks(albumIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) =>
this.albumRepository
.find({
select: { id: true },
where: {
id: In(idChunk),
ownerId: userId,
},
})
.then((albums) => new Set(albums.map((album) => album.id))),
),
).then((results) => setUnion(...results));
},
checkSharedAlbumAccess: async (userId: string, albumIds: Set<string>): Promise<Set<string>> => {
if (albumIds.size === 0) {
return new Set();
}
return Promise.all(
chunks(albumIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) =>
this.albumRepository
.find({
select: { id: true },
where: {
id: In(idChunk),
sharedUsers: {
id: userId,
},
},
})
.then((albums) => new Set(albums.map((album) => album.id))),
),
).then((results) => setUnion(...results));
},
checkSharedLinkAccess: async (sharedLinkId: string, albumIds: Set<string>): Promise<Set<string>> => {
if (albumIds.size === 0) {
return new Set();
}
return Promise.all(
chunks(albumIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) =>
this.sharedLinkRepository
.find({
select: { albumId: true },
where: {
id: sharedLinkId,
albumId: In(idChunk),
},
})
.then(
(sharedLinks) =>
new Set(sharedLinks.flatMap((sharedLink) => (!!sharedLink.albumId ? [sharedLink.albumId] : []))),
),
),
).then((results) => setUnion(...results));
},
};
person = {
checkOwnerAccess: async (userId: string, personIds: Set<string>): Promise<Set<string>> => {
if (personIds.size === 0) {
return new Set();
}
return Promise.all(
chunks(personIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) =>
this.personRepository
.find({
select: { id: true },
where: {
id: In(idChunk),
ownerId: userId,
},
})
.then((persons) => new Set(persons.map((person) => person.id))),
),
).then((results) => setUnion(...results));
},
checkFaceOwnerAccess: async (userId: string, assetFaceIds: Set<string>): Promise<Set<string>> => {
if (assetFaceIds.size === 0) {
return new Set();
}
return Promise.all(
chunks(assetFaceIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) =>
this.assetFaceRepository
.find({
select: { id: true },
where: {
id: In(idChunk),
asset: {
ownerId: userId,
},
},
})
.then((faces) => new Set(faces.map((face) => face.id))),
),
).then((results) => setUnion(...results));
},
};
partner = {
checkUpdateAccess: async (userId: string, partnerIds: Set<string>): Promise<Set<string>> => {
if (partnerIds.size === 0) {
return new Set();
}
return Promise.all(
chunks(partnerIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) =>
this.partnerRepository
.createQueryBuilder('partner')
.select('partner.sharedById')
.where('partner.sharedById IN (:...partnerIds)', { partnerIds: idChunk })
.andWhere('partner.sharedWithId = :userId', { userId })
.getMany()
.then((partners) => new Set(partners.map((partner) => partner.sharedById))),
),
).then((results) => setUnion(...results));
},
};
)
.getMany()
.then((albums) => new Set(albums.map((album) => album.id))),
),
).then((results) => setUnion(...results));
}
}
class AlbumAccess implements IAlbumAccess {
constructor(
private albumRepository: Repository<AlbumEntity>,
private sharedLinkRepository: Repository<SharedLinkEntity>,
) {}
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
async checkOwnerAccess(userId: string, albumIds: Set<string>): Promise<Set<string>> {
if (albumIds.size === 0) {
return new Set();
}
return Promise.all(
chunks(albumIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) =>
this.albumRepository
.find({
select: { id: true },
where: {
id: In(idChunk),
ownerId: userId,
},
})
.then((albums) => new Set(albums.map((album) => album.id))),
),
).then((results) => setUnion(...results));
}
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
async checkSharedAlbumAccess(userId: string, albumIds: Set<string>): Promise<Set<string>> {
if (albumIds.size === 0) {
return new Set();
}
return Promise.all(
chunks(albumIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) =>
this.albumRepository
.find({
select: { id: true },
where: {
id: In(idChunk),
sharedUsers: {
id: userId,
},
},
})
.then((albums) => new Set(albums.map((album) => album.id))),
),
).then((results) => setUnion(...results));
}
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
async checkSharedLinkAccess(sharedLinkId: string, albumIds: Set<string>): Promise<Set<string>> {
if (albumIds.size === 0) {
return new Set();
}
return Promise.all(
chunks(albumIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) =>
this.sharedLinkRepository
.find({
select: { albumId: true },
where: {
id: sharedLinkId,
albumId: In(idChunk),
},
})
.then(
(sharedLinks) =>
new Set(sharedLinks.flatMap((sharedLink) => (!!sharedLink.albumId ? [sharedLink.albumId] : []))),
),
),
).then((results) => setUnion(...results));
}
}
class AssetAccess implements IAssetAccess {
constructor(
private albumRepository: Repository<AlbumEntity>,
private assetRepository: Repository<AssetEntity>,
private partnerRepository: Repository<PartnerEntity>,
private sharedLinkRepository: Repository<SharedLinkEntity>,
) {}
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
async checkAlbumAccess(userId: string, assetIds: Set<string>): Promise<Set<string>> {
if (assetIds.size === 0) {
return new Set();
}
return Promise.all(
chunks(assetIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) =>
this.albumRepository
.createQueryBuilder('album')
.innerJoin('album.assets', 'asset')
.leftJoin('album.sharedUsers', 'sharedUsers')
.select('asset.id', 'assetId')
.addSelect('asset.livePhotoVideoId', 'livePhotoVideoId')
.where('array["asset"."id", "asset"."livePhotoVideoId"] && array[:...assetIds]::uuid[]', {
assetIds: idChunk,
})
.andWhere(
new Brackets((qb) => {
qb.where('album.ownerId = :userId', { userId }).orWhere('sharedUsers.id = :userId', { userId });
}),
)
.getRawMany()
.then((rows) => {
const allowedIds = new Set<string>();
for (const row of rows) {
if (row.assetId && assetIds.has(row.assetId)) {
allowedIds.add(row.assetId);
}
if (row.livePhotoVideoId && assetIds.has(row.livePhotoVideoId)) {
allowedIds.add(row.livePhotoVideoId);
}
}
return allowedIds;
}),
),
).then((results) => setUnion(...results));
}
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
async checkOwnerAccess(userId: string, assetIds: Set<string>): Promise<Set<string>> {
if (assetIds.size === 0) {
return new Set();
}
return Promise.all(
chunks(assetIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) =>
this.assetRepository
.find({
select: { id: true },
where: {
id: In(idChunk),
ownerId: userId,
},
withDeleted: true,
})
.then((assets) => new Set(assets.map((asset) => asset.id))),
),
).then((results) => setUnion(...results));
}
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
async checkPartnerAccess(userId: string, assetIds: Set<string>): Promise<Set<string>> {
if (assetIds.size === 0) {
return new Set();
}
return Promise.all(
chunks(assetIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) =>
this.partnerRepository
.createQueryBuilder('partner')
.innerJoin('partner.sharedBy', 'sharedBy')
.innerJoin('sharedBy.assets', 'asset')
.select('asset.id', 'assetId')
.where('partner.sharedWithId = :userId', { userId })
.andWhere('asset.id IN (:...assetIds)', { assetIds: idChunk })
.getRawMany()
.then((rows) => new Set(rows.map((row) => row.assetId))),
),
).then((results) => setUnion(...results));
}
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
async checkSharedLinkAccess(sharedLinkId: string, assetIds: Set<string>): Promise<Set<string>> {
if (assetIds.size === 0) {
return new Set();
}
return Promise.all(
chunks(assetIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) =>
this.sharedLinkRepository
.createQueryBuilder('sharedLink')
.leftJoin('sharedLink.album', 'album')
.leftJoin('sharedLink.assets', 'assets')
.leftJoin('album.assets', 'albumAssets')
.select('assets.id', 'assetId')
.addSelect('albumAssets.id', 'albumAssetId')
.addSelect('assets.livePhotoVideoId', 'assetLivePhotoVideoId')
.addSelect('albumAssets.livePhotoVideoId', 'albumAssetLivePhotoVideoId')
.where('sharedLink.id = :sharedLinkId', { sharedLinkId })
.andWhere(
'array["assets"."id", "assets"."livePhotoVideoId", "albumAssets"."id", "albumAssets"."livePhotoVideoId"] && array[:...assetIds]::uuid[]',
{
assetIds: idChunk,
},
)
.getRawMany()
.then((rows) => {
const allowedIds = new Set<string>();
for (const row of rows) {
if (row.assetId && assetIds.has(row.assetId)) {
allowedIds.add(row.assetId);
}
if (row.assetLivePhotoVideoId && assetIds.has(row.assetLivePhotoVideoId)) {
allowedIds.add(row.assetLivePhotoVideoId);
}
if (row.albumAssetId && assetIds.has(row.albumAssetId)) {
allowedIds.add(row.albumAssetId);
}
if (row.albumAssetLivePhotoVideoId && assetIds.has(row.albumAssetLivePhotoVideoId)) {
allowedIds.add(row.albumAssetLivePhotoVideoId);
}
}
return allowedIds;
}),
),
).then((results) => setUnion(...results));
}
}
class AuthDeviceAccess implements IAuthDeviceAccess {
constructor(private tokenRepository: Repository<UserTokenEntity>) {}
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
async checkOwnerAccess(userId: string, deviceIds: Set<string>): Promise<Set<string>> {
if (deviceIds.size === 0) {
return new Set();
}
return Promise.all(
chunks(deviceIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) =>
this.tokenRepository
.find({
select: { id: true },
where: {
userId,
id: In(idChunk),
},
})
.then((tokens) => new Set(tokens.map((token) => token.id))),
),
).then((results) => setUnion(...results));
}
}
class LibraryAccess implements ILibraryAccess {
constructor(
private libraryRepository: Repository<LibraryEntity>,
private partnerRepository: Repository<PartnerEntity>,
) {}
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
async checkOwnerAccess(userId: string, libraryIds: Set<string>): Promise<Set<string>> {
if (libraryIds.size === 0) {
return new Set();
}
return Promise.all(
chunks(libraryIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) =>
this.libraryRepository
.find({
select: { id: true },
where: {
id: In(idChunk),
ownerId: userId,
},
})
.then((libraries) => new Set(libraries.map((library) => library.id))),
),
).then((results) => setUnion(...results));
}
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
async checkPartnerAccess(userId: string, partnerIds: Set<string>): Promise<Set<string>> {
if (partnerIds.size === 0) {
return new Set();
}
return Promise.all(
chunks(partnerIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) =>
this.partnerRepository
.createQueryBuilder('partner')
.select('partner.sharedById')
.where('partner.sharedById IN (:...partnerIds)', { partnerIds: idChunk })
.andWhere('partner.sharedWithId = :userId', { userId })
.getMany()
.then((partners) => new Set(partners.map((partner) => partner.sharedById))),
),
).then((results) => setUnion(...results));
}
}
class TimelineAccess implements ITimelineAccess {
constructor(private partnerRepository: Repository<PartnerEntity>) {}
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
async checkPartnerAccess(userId: string, partnerIds: Set<string>): Promise<Set<string>> {
if (partnerIds.size === 0) {
return new Set();
}
return Promise.all(
chunks(partnerIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) =>
this.partnerRepository
.createQueryBuilder('partner')
.select('partner.sharedById')
.where('partner.sharedById IN (:...partnerIds)', { partnerIds: idChunk })
.andWhere('partner.sharedWithId = :userId', { userId })
.getMany()
.then((partners) => new Set(partners.map((partner) => partner.sharedById))),
),
).then((results) => setUnion(...results));
}
}
class PersonAccess implements IPersonAccess {
constructor(
private assetFaceRepository: Repository<AssetFaceEntity>,
private personRepository: Repository<PersonEntity>,
) {}
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
async checkOwnerAccess(userId: string, personIds: Set<string>): Promise<Set<string>> {
if (personIds.size === 0) {
return new Set();
}
return Promise.all(
chunks(personIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) =>
this.personRepository
.find({
select: { id: true },
where: {
id: In(idChunk),
ownerId: userId,
},
})
.then((persons) => new Set(persons.map((person) => person.id))),
),
).then((results) => setUnion(...results));
}
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
async checkFaceOwnerAccess(userId: string, assetFaceIds: Set<string>): Promise<Set<string>> {
if (assetFaceIds.size === 0) {
return new Set();
}
return Promise.all(
chunks(assetFaceIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) =>
this.assetFaceRepository
.find({
select: { id: true },
where: {
id: In(idChunk),
asset: {
ownerId: userId,
},
},
})
.then((faces) => new Set(faces.map((face) => face.id))),
),
).then((results) => setUnion(...results));
}
}
class PartnerAccess implements IPartnerAccess {
constructor(private partnerRepository: Repository<PartnerEntity>) {}
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
async checkUpdateAccess(userId: string, partnerIds: Set<string>): Promise<Set<string>> {
if (partnerIds.size === 0) {
return new Set();
}
return Promise.all(
chunks(partnerIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) =>
this.partnerRepository
.createQueryBuilder('partner')
.select('partner.sharedById')
.where('partner.sharedById IN (:...partnerIds)', { partnerIds: idChunk })
.andWhere('partner.sharedWithId = :userId', { userId })
.getMany()
.then((partners) => new Set(partners.map((partner) => partner.sharedById))),
),
).then((results) => setUnion(...results));
}
}
export class AccessRepository implements IAccessRepository {
activity: IActivityAccess;
album: IAlbumAccess;
asset: IAssetAccess;
authDevice: IAuthDeviceAccess;
library: ILibraryAccess;
person: IPersonAccess;
partner: IPartnerAccess;
timeline: ITimelineAccess;
constructor(
@InjectRepository(ActivityEntity) activityRepository: Repository<ActivityEntity>,
@InjectRepository(AssetEntity) assetRepository: Repository<AssetEntity>,
@InjectRepository(AlbumEntity) albumRepository: Repository<AlbumEntity>,
@InjectRepository(LibraryEntity) libraryRepository: Repository<LibraryEntity>,
@InjectRepository(PartnerEntity) partnerRepository: Repository<PartnerEntity>,
@InjectRepository(PersonEntity) personRepository: Repository<PersonEntity>,
@InjectRepository(AssetFaceEntity) assetFaceRepository: Repository<AssetFaceEntity>,
@InjectRepository(SharedLinkEntity) sharedLinkRepository: Repository<SharedLinkEntity>,
@InjectRepository(UserTokenEntity) tokenRepository: Repository<UserTokenEntity>,
) {
this.activity = new ActivityAccess(activityRepository, albumRepository);
this.album = new AlbumAccess(albumRepository, sharedLinkRepository);
this.asset = new AssetAccess(albumRepository, assetRepository, partnerRepository, sharedLinkRepository);
this.authDevice = new AuthDeviceAccess(tokenRepository);
this.library = new LibraryAccess(libraryRepository, partnerRepository);
this.person = new PersonAccess(assetFaceRepository, personRepository);
this.partner = new PartnerAccess(partnerRepository);
this.timeline = new TimelineAccess(partnerRepository);
}
}

View File

@ -98,8 +98,25 @@ class SqlGenerator {
const data: string[] = [`-- NOTE: This file is auto generated by ./sql-generator`];
const instance = this.app.get<Repository>(Repository);
const properties = Object.getOwnPropertyNames(Repository.prototype) as Array<keyof typeof Repository>;
for (const key of properties) {
// normal repositories
data.push(...(await this.runTargets(instance, `${Repository.name}`)));
// nested repositories
if (Repository.name === AccessRepository.name) {
for (const key of Object.keys(instance)) {
const subInstance = (instance as any)[key];
data.push(...(await this.runTargets(subInstance, `${Repository.name}.${key}`)));
}
}
this.results[Repository.name] = data;
}
private async runTargets(instance: any, label: string) {
const data: string[] = [];
for (const key of this.getPropertyNames(instance)) {
const target = instance[key];
if (!(target instanceof Function)) {
continue;
@ -116,7 +133,7 @@ class SqlGenerator {
}
for (const { name, params } of queries) {
let queryLabel = `${Repository.name}.${key}`;
let queryLabel = `${label}.${key}`;
if (name) {
queryLabel += ` (${name})`;
}
@ -135,7 +152,7 @@ class SqlGenerator {
}
}
this.results[Repository.name] = data;
return data;
}
private async write() {
@ -156,6 +173,10 @@ class SqlGenerator {
await this.app.close();
}
}
private getPropertyNames(instance: any): string[] {
return Object.getOwnPropertyNames(Object.getPrototypeOf(instance)) as any[];
}
}
new SqlGenerator({ targetDir: './src/infra/sql' })

View File

@ -1 +1,243 @@
-- NOTE: This file is auto generated by ./sql-generator
-- AccessRepository.activity.checkOwnerAccess
SELECT
"ActivityEntity"."id" AS "ActivityEntity_id"
FROM
"activity" "ActivityEntity"
WHERE
(
"ActivityEntity"."id" IN ($1)
AND "ActivityEntity"."userId" = $2
)
-- AccessRepository.activity.checkAlbumOwnerAccess
SELECT
"ActivityEntity"."id" AS "ActivityEntity_id"
FROM
"activity" "ActivityEntity"
LEFT JOIN "albums" "ActivityEntity__ActivityEntity_album" ON "ActivityEntity__ActivityEntity_album"."id" = "ActivityEntity"."albumId"
AND (
"ActivityEntity__ActivityEntity_album"."deletedAt" IS NULL
)
WHERE
(
"ActivityEntity"."id" IN ($1)
AND "ActivityEntity__ActivityEntity_album"."ownerId" = $2
)
-- AccessRepository.activity.checkCreateAccess
SELECT
"album"."id" AS "album_id"
FROM
"albums" "album"
LEFT JOIN "albums_shared_users_users" "album_sharedUsers" ON "album_sharedUsers"."albumsId" = "album"."id"
LEFT JOIN "users" "sharedUsers" ON "sharedUsers"."id" = "album_sharedUsers"."usersId"
AND ("sharedUsers"."deletedAt" IS NULL)
WHERE
(
"album"."id" IN ($1)
AND "album"."isActivityEnabled" = true
AND (
"album"."ownerId" = $2
OR "sharedUsers"."id" = $2
)
)
AND ("album"."deletedAt" IS NULL)
-- AccessRepository.album.checkOwnerAccess
SELECT
"AlbumEntity"."id" AS "AlbumEntity_id"
FROM
"albums" "AlbumEntity"
WHERE
(
(
"AlbumEntity"."id" IN ($1)
AND "AlbumEntity"."ownerId" = $2
)
)
AND ("AlbumEntity"."deletedAt" IS NULL)
-- AccessRepository.album.checkSharedAlbumAccess
SELECT
"AlbumEntity"."id" AS "AlbumEntity_id"
FROM
"albums" "AlbumEntity"
LEFT JOIN "albums_shared_users_users" "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers" ON "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers"."albumsId" = "AlbumEntity"."id"
LEFT JOIN "users" "AlbumEntity__AlbumEntity_sharedUsers" ON "AlbumEntity__AlbumEntity_sharedUsers"."id" = "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers"."usersId"
AND (
"AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" IS NULL
)
WHERE
(
(
"AlbumEntity"."id" IN ($1)
AND "AlbumEntity__AlbumEntity_sharedUsers"."id" = $2
)
)
AND ("AlbumEntity"."deletedAt" IS NULL)
-- AccessRepository.album.checkSharedLinkAccess
SELECT
"SharedLinkEntity"."albumId" AS "SharedLinkEntity_albumId",
"SharedLinkEntity"."id" AS "SharedLinkEntity_id"
FROM
"shared_links" "SharedLinkEntity"
WHERE
(
"SharedLinkEntity"."id" = $1
AND "SharedLinkEntity"."albumId" IN ($2)
)
-- AccessRepository.asset.checkAlbumAccess
SELECT
"asset"."id" AS "assetId",
"asset"."livePhotoVideoId" AS "livePhotoVideoId"
FROM
"albums" "album"
INNER JOIN "albums_assets_assets" "album_asset" ON "album_asset"."albumsId" = "album"."id"
INNER JOIN "assets" "asset" ON "asset"."id" = "album_asset"."assetsId"
AND ("asset"."deletedAt" IS NULL)
LEFT JOIN "albums_shared_users_users" "album_sharedUsers" ON "album_sharedUsers"."albumsId" = "album"."id"
LEFT JOIN "users" "sharedUsers" ON "sharedUsers"."id" = "album_sharedUsers"."usersId"
AND ("sharedUsers"."deletedAt" IS NULL)
WHERE
(
array["asset"."id", "asset"."livePhotoVideoId"] && array[$1]::uuid []
AND (
"album"."ownerId" = $2
OR "sharedUsers"."id" = $2
)
)
AND ("album"."deletedAt" IS NULL)
-- AccessRepository.asset.checkOwnerAccess
SELECT
"AssetEntity"."id" AS "AssetEntity_id"
FROM
"assets" "AssetEntity"
WHERE
(
"AssetEntity"."id" IN ($1)
AND "AssetEntity"."ownerId" = $2
)
-- AccessRepository.asset.checkPartnerAccess
SELECT
"asset"."id" AS "assetId"
FROM
"partners" "partner"
INNER JOIN "users" "sharedBy" ON "sharedBy"."id" = "partner"."sharedById"
AND ("sharedBy"."deletedAt" IS NULL)
INNER JOIN "assets" "asset" ON "asset"."ownerId" = "sharedBy"."id"
AND ("asset"."deletedAt" IS NULL)
WHERE
"partner"."sharedWithId" = $1
AND "asset"."id" IN ($2)
-- AccessRepository.asset.checkSharedLinkAccess
SELECT
"assets"."id" AS "assetId",
"assets"."livePhotoVideoId" AS "assetLivePhotoVideoId",
"albumAssets"."id" AS "albumAssetId",
"albumAssets"."livePhotoVideoId" AS "albumAssetLivePhotoVideoId"
FROM
"shared_links" "sharedLink"
LEFT JOIN "albums" "album" ON "album"."id" = "sharedLink"."albumId"
AND ("album"."deletedAt" IS NULL)
LEFT JOIN "shared_link__asset" "assets_sharedLink" ON "assets_sharedLink"."sharedLinksId" = "sharedLink"."id"
LEFT JOIN "assets" "assets" ON "assets"."id" = "assets_sharedLink"."assetsId"
AND ("assets"."deletedAt" IS NULL)
LEFT JOIN "albums_assets_assets" "album_albumAssets" ON "album_albumAssets"."albumsId" = "album"."id"
LEFT JOIN "assets" "albumAssets" ON "albumAssets"."id" = "album_albumAssets"."assetsId"
AND ("albumAssets"."deletedAt" IS NULL)
WHERE
"sharedLink"."id" = $1
AND array[
"assets"."id",
"assets"."livePhotoVideoId",
"albumAssets"."id",
"albumAssets"."livePhotoVideoId"
] && array[$2]::uuid []
-- AccessRepository.authDevice.checkOwnerAccess
SELECT
"UserTokenEntity"."id" AS "UserTokenEntity_id"
FROM
"user_token" "UserTokenEntity"
WHERE
(
"UserTokenEntity"."userId" = $1
AND "UserTokenEntity"."id" IN ($2)
)
-- AccessRepository.library.checkOwnerAccess
SELECT
"LibraryEntity"."id" AS "LibraryEntity_id"
FROM
"libraries" "LibraryEntity"
WHERE
(
(
"LibraryEntity"."id" IN ($1)
AND "LibraryEntity"."ownerId" = $2
)
)
AND ("LibraryEntity"."deletedAt" IS NULL)
-- AccessRepository.library.checkPartnerAccess
SELECT
"partner"."sharedById" AS "partner_sharedById",
"partner"."sharedWithId" AS "partner_sharedWithId"
FROM
"partners" "partner"
WHERE
"partner"."sharedById" IN ($1)
AND "partner"."sharedWithId" = $2
-- AccessRepository.person.checkOwnerAccess
SELECT
"PersonEntity"."id" AS "PersonEntity_id"
FROM
"person" "PersonEntity"
WHERE
(
"PersonEntity"."id" IN ($1)
AND "PersonEntity"."ownerId" = $2
)
-- AccessRepository.person.checkFaceOwnerAccess
SELECT
"AssetFaceEntity"."id" AS "AssetFaceEntity_id"
FROM
"asset_faces" "AssetFaceEntity"
LEFT JOIN "assets" "AssetFaceEntity__AssetFaceEntity_asset" ON "AssetFaceEntity__AssetFaceEntity_asset"."id" = "AssetFaceEntity"."assetId"
AND (
"AssetFaceEntity__AssetFaceEntity_asset"."deletedAt" IS NULL
)
WHERE
(
"AssetFaceEntity"."id" IN ($1)
AND "AssetFaceEntity__AssetFaceEntity_asset"."ownerId" = $2
)
-- AccessRepository.partner.checkUpdateAccess
SELECT
"partner"."sharedById" AS "partner_sharedById",
"partner"."sharedWithId" AS "partner_sharedWithId"
FROM
"partners" "partner"
WHERE
"partner"."sharedById" IN ($1)
AND "partner"."sharedWithId" = $2
-- AccessRepository.timeline.checkPartnerAccess
SELECT
"partner"."sharedById" AS "partner_sharedById",
"partner"."sharedWithId" AS "partner_sharedWithId"
FROM
"partners" "partner"
WHERE
"partner"."sharedById" IN ($1)
AND "partner"."sharedWithId" = $2