Add asset repository and refactor asset service (#540)

* build endpoint to get asset count by month

* Added asset repository

* Added create asset

* get asset by device ID

* Added test for existing methods

* Refactor additional endpoint

* Refactor database api to get curated locations and curated objects

* Refactor get search properties

* Fixed cookies parsing for websocket

* Added API to get asset count by time group

* Remove unused code
This commit is contained in:
Alex 2022-08-26 22:53:37 -07:00 committed by GitHub
parent 6b7c97c02a
commit f980a2f27a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 587 additions and 126 deletions

View file

@ -8,6 +8,8 @@ doc/AdminSignupResponseDto.md
doc/AlbumApi.md doc/AlbumApi.md
doc/AlbumResponseDto.md doc/AlbumResponseDto.md
doc/AssetApi.md doc/AssetApi.md
doc/AssetCountByTimeGroupDto.md
doc/AssetCountByTimeGroupResponseDto.md
doc/AssetFileUploadResponseDto.md doc/AssetFileUploadResponseDto.md
doc/AssetResponseDto.md doc/AssetResponseDto.md
doc/AssetTypeEnum.md doc/AssetTypeEnum.md
@ -27,6 +29,7 @@ doc/DeviceInfoApi.md
doc/DeviceInfoResponseDto.md doc/DeviceInfoResponseDto.md
doc/DeviceTypeEnum.md doc/DeviceTypeEnum.md
doc/ExifResponseDto.md doc/ExifResponseDto.md
doc/GetAssetCountByTimeGroupDto.md
doc/LoginCredentialDto.md doc/LoginCredentialDto.md
doc/LoginResponseDto.md doc/LoginResponseDto.md
doc/LogoutResponseDto.md doc/LogoutResponseDto.md
@ -39,6 +42,7 @@ doc/ServerVersionReponseDto.md
doc/SignUpDto.md doc/SignUpDto.md
doc/SmartInfoResponseDto.md doc/SmartInfoResponseDto.md
doc/ThumbnailFormat.md doc/ThumbnailFormat.md
doc/TimeGroupEnum.md
doc/UpdateAlbumDto.md doc/UpdateAlbumDto.md
doc/UpdateDeviceInfoDto.md doc/UpdateDeviceInfoDto.md
doc/UpdateUserDto.md doc/UpdateUserDto.md
@ -66,6 +70,8 @@ lib/model/add_assets_dto.dart
lib/model/add_users_dto.dart lib/model/add_users_dto.dart
lib/model/admin_signup_response_dto.dart lib/model/admin_signup_response_dto.dart
lib/model/album_response_dto.dart lib/model/album_response_dto.dart
lib/model/asset_count_by_time_group_dto.dart
lib/model/asset_count_by_time_group_response_dto.dart
lib/model/asset_file_upload_response_dto.dart lib/model/asset_file_upload_response_dto.dart
lib/model/asset_response_dto.dart lib/model/asset_response_dto.dart
lib/model/asset_type_enum.dart lib/model/asset_type_enum.dart
@ -83,6 +89,7 @@ lib/model/delete_asset_status.dart
lib/model/device_info_response_dto.dart lib/model/device_info_response_dto.dart
lib/model/device_type_enum.dart lib/model/device_type_enum.dart
lib/model/exif_response_dto.dart lib/model/exif_response_dto.dart
lib/model/get_asset_count_by_time_group_dto.dart
lib/model/login_credential_dto.dart lib/model/login_credential_dto.dart
lib/model/login_response_dto.dart lib/model/login_response_dto.dart
lib/model/logout_response_dto.dart lib/model/logout_response_dto.dart
@ -94,6 +101,7 @@ lib/model/server_version_reponse_dto.dart
lib/model/sign_up_dto.dart lib/model/sign_up_dto.dart
lib/model/smart_info_response_dto.dart lib/model/smart_info_response_dto.dart
lib/model/thumbnail_format.dart lib/model/thumbnail_format.dart
lib/model/time_group_enum.dart
lib/model/update_album_dto.dart lib/model/update_album_dto.dart
lib/model/update_device_info_dto.dart lib/model/update_device_info_dto.dart
lib/model/update_user_dto.dart lib/model/update_user_dto.dart

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,187 @@
import { SearchPropertiesDto } from './dto/search-properties.dto';
import { CuratedLocationsResponseDto } from './response-dto/curated-locations-response.dto';
import { AssetEntity, AssetType } from '@app/database/entities/asset.entity';
import { BadRequestException, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm/repository/Repository';
import { CreateAssetDto } from './dto/create-asset.dto';
import { CuratedObjectsResponseDto } from './response-dto/curated-objects-response.dto';
import { AssetCountByTimeGroupDto } from './response-dto/asset-count-by-time-group-response.dto';
import { TimeGroupEnum } from './dto/get-asset-count-by-time-group.dto';
export interface IAssetRepository {
create(createAssetDto: CreateAssetDto, ownerId: string, originalPath: string, mimeType: string): Promise<AssetEntity>;
getAllByUserId(userId: string): Promise<AssetEntity[]>;
getAllByDeviceId(userId: string, deviceId: string): Promise<string[]>;
getById(assetId: string): Promise<AssetEntity>;
getLocationsByUserId(userId: string): Promise<CuratedLocationsResponseDto[]>;
getDetectedObjectsByUserId(userId: string): Promise<CuratedObjectsResponseDto[]>;
getSearchPropertiesByUserId(userId: string): Promise<SearchPropertiesDto[]>;
getAssetCountByTimeGroup(userId: string, timeGroup: TimeGroupEnum): Promise<AssetCountByTimeGroupDto[]>;
}
export const ASSET_REPOSITORY = 'ASSET_REPOSITORY';
@Injectable()
export class AssetRepository implements IAssetRepository {
constructor(
@InjectRepository(AssetEntity)
private assetRepository: Repository<AssetEntity>,
) {}
async getAssetCountByTimeGroup(userId: string, timeGroup: TimeGroupEnum) {
let result: AssetCountByTimeGroupDto[] = [];
if (timeGroup === TimeGroupEnum.Month) {
result = await this.assetRepository
.createQueryBuilder('asset')
.select(`COUNT(asset.id)::int`, 'count')
.addSelect(`to_char(date_trunc('month', "createdAt"::timestamptz), 'YYYY_MM')`, 'timeGroup')
.where('"userId" = :userId', { userId: userId })
.groupBy(`date_trunc('month', "createdAt"::timestamptz)`)
.orderBy(`date_trunc('month', "createdAt"::timestamptz)`, 'DESC')
.getRawMany();
} else if (timeGroup === TimeGroupEnum.Day) {
result = await this.assetRepository
.createQueryBuilder('asset')
.select(`COUNT(asset.id)::int`, 'count')
.addSelect(`to_char(date_trunc('day', "createdAt"::timestamptz), 'YYYY_MM_DD')`, 'timeGroup')
.where('"userId" = :userId', { userId: userId })
.groupBy(`date_trunc('day', "createdAt"::timestamptz)`)
.orderBy(`date_trunc('day', "createdAt"::timestamptz)`, 'DESC')
.getRawMany();
}
return result;
}
async getSearchPropertiesByUserId(userId: string): Promise<SearchPropertiesDto[]> {
return await this.assetRepository
.createQueryBuilder('asset')
.where('asset.userId = :userId', { userId: userId })
.leftJoin('asset.exifInfo', 'ei')
.leftJoin('asset.smartInfo', 'si')
.select('si.tags', 'tags')
.addSelect('si.objects', 'objects')
.addSelect('asset.type', 'assetType')
.addSelect('ei.orientation', 'orientation')
.addSelect('ei."lensModel"', 'lensModel')
.addSelect('ei.make', 'make')
.addSelect('ei.model', 'model')
.addSelect('ei.city', 'city')
.addSelect('ei.state', 'state')
.addSelect('ei.country', 'country')
.distinctOn(['si.tags'])
.getRawMany();
}
async getDetectedObjectsByUserId(userId: string): Promise<CuratedObjectsResponseDto[]> {
return await this.assetRepository.query(
`
SELECT DISTINCT ON (unnest(si.objects)) a.id, unnest(si.objects) as "object", a."resizePath", a."deviceAssetId", a."deviceId"
FROM assets a
LEFT JOIN smart_info si ON a.id = si."assetId"
WHERE a."userId" = $1
AND si.objects IS NOT NULL
`,
[userId],
);
}
async getLocationsByUserId(userId: string): Promise<CuratedLocationsResponseDto[]> {
return await this.assetRepository.query(
`
SELECT DISTINCT ON (e.city) a.id, e.city, a."resizePath", a."deviceAssetId", a."deviceId"
FROM assets a
LEFT JOIN exif e ON a.id = e."assetId"
WHERE a."userId" = $1
AND e.city IS NOT NULL
AND a.type = 'IMAGE';
`,
[userId],
);
}
/**
* Get a single asset information by its ID
* - include exif info
* @param assetId
*/
async getById(assetId: string): Promise<AssetEntity> {
return await this.assetRepository.findOneOrFail({
where: {
id: assetId,
},
relations: ['exifInfo'],
});
}
/**
* Get all assets belong to the user on the database
* @param userId
*/
async getAllByUserId(userId: string): Promise<AssetEntity[]> {
const query = this.assetRepository
.createQueryBuilder('asset')
.where('asset.userId = :userId', { userId: userId })
.andWhere('asset.resizePath is not NULL')
.leftJoinAndSelect('asset.exifInfo', 'exifInfo')
.orderBy('asset.createdAt', 'DESC');
return await query.getMany();
}
/**
* Create new asset information in database
* @param createAssetDto
* @param ownerId
* @param originalPath
* @param mimeType
* @returns Promise<AssetEntity>
*/
async create(
createAssetDto: CreateAssetDto,
ownerId: string,
originalPath: string,
mimeType: string,
): Promise<AssetEntity> {
const asset = new AssetEntity();
asset.deviceAssetId = createAssetDto.deviceAssetId;
asset.userId = ownerId;
asset.deviceId = createAssetDto.deviceId;
asset.type = createAssetDto.assetType || AssetType.OTHER;
asset.originalPath = originalPath;
asset.createdAt = createAssetDto.createdAt;
asset.modifiedAt = createAssetDto.modifiedAt;
asset.isFavorite = createAssetDto.isFavorite;
asset.mimeType = mimeType;
asset.duration = createAssetDto.duration || null;
const createdAsset = await this.assetRepository.save(asset);
if (!createdAsset) {
throw new BadRequestException('Asset not created');
}
return createdAsset;
}
/**
* Get assets by device's Id on the database
* @param userId
* @param deviceId
*
* @returns Promise<string[]> - Array of assetIds belong to the device
*/
async getAllByDeviceId(userId: string, deviceId: string): Promise<string[]> {
const rows = await this.assetRepository.find({
where: {
userId: userId,
deviceId: deviceId,
},
select: ['deviceAssetId'],
});
const res: string[] = [];
rows.forEach((v) => res.push(v.deviceAssetId));
return res;
}
}

View file

@ -2,7 +2,6 @@ import {
Controller, Controller,
Post, Post,
UseInterceptors, UseInterceptors,
UploadedFiles,
Body, Body,
UseGuards, UseGuards,
Get, Get,
@ -44,6 +43,8 @@ import { CreateAssetDto } from './dto/create-asset.dto';
import { AssetFileUploadResponseDto } from './response-dto/asset-file-upload-response.dto'; import { AssetFileUploadResponseDto } from './response-dto/asset-file-upload-response.dto';
import { DeleteAssetResponseDto, DeleteAssetStatusEnum } from './response-dto/delete-asset-response.dto'; import { DeleteAssetResponseDto, DeleteAssetStatusEnum } from './response-dto/delete-asset-response.dto';
import { GetAssetThumbnailDto } from './dto/get-asset-thumbnail.dto'; import { GetAssetThumbnailDto } from './dto/get-asset-thumbnail.dto';
import { AssetCountByTimeGroupResponseDto } from './response-dto/asset-count-by-time-group-response.dto';
import { GetAssetCountByTimeGroupDto } from './dto/get-asset-count-by-time-group.dto';
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@ApiBearerAuth() @ApiBearerAuth()
@ -117,17 +118,17 @@ export class AssetController {
return this.assetService.getAssetThumbnail(assetId, query); return this.assetService.getAssetThumbnail(assetId, query);
} }
@Get('/allObjects') @Get('/curated-objects')
async getCuratedObjects(@GetAuthUser() authUser: AuthUserDto): Promise<CuratedObjectsResponseDto[]> { async getCuratedObjects(@GetAuthUser() authUser: AuthUserDto): Promise<CuratedObjectsResponseDto[]> {
return this.assetService.getCuratedObject(authUser); return this.assetService.getCuratedObject(authUser);
} }
@Get('/allLocation') @Get('/curated-locations')
async getCuratedLocations(@GetAuthUser() authUser: AuthUserDto): Promise<CuratedLocationsResponseDto[]> { async getCuratedLocations(@GetAuthUser() authUser: AuthUserDto): Promise<CuratedLocationsResponseDto[]> {
return this.assetService.getCuratedLocation(authUser); return this.assetService.getCuratedLocation(authUser);
} }
@Get('/searchTerm') @Get('/search-terms')
async getAssetSearchTerms(@GetAuthUser() authUser: AuthUserDto): Promise<string[]> { async getAssetSearchTerms(@GetAuthUser() authUser: AuthUserDto): Promise<string[]> {
return this.assetService.getAssetSearchTerm(authUser); return this.assetService.getAssetSearchTerm(authUser);
} }
@ -140,6 +141,14 @@ export class AssetController {
return this.assetService.searchAsset(authUser, searchAssetDto); return this.assetService.searchAsset(authUser, searchAssetDto);
} }
@Get('/count-by-date')
async getAssetCountByTimeGroup(
@GetAuthUser() authUser: AuthUserDto,
@Body(ValidationPipe) getAssetCountByTimeGroupDto: GetAssetCountByTimeGroupDto,
): Promise<AssetCountByTimeGroupResponseDto> {
return this.assetService.getAssetCountByTimeGroup(authUser, getAssetCountByTimeGroupDto);
}
/** /**
* Get all AssetEntity belong to the user * Get all AssetEntity belong to the user
*/ */

View file

@ -8,6 +8,7 @@ import { BackgroundTaskModule } from '../../modules/background-task/background-t
import { BackgroundTaskService } from '../../modules/background-task/background-task.service'; import { BackgroundTaskService } from '../../modules/background-task/background-task.service';
import { CommunicationModule } from '../communication/communication.module'; import { CommunicationModule } from '../communication/communication.module';
import { assetUploadedQueueName } from '@app/job/constants/queue-name.constant'; import { assetUploadedQueueName } from '@app/job/constants/queue-name.constant';
import { AssetRepository, ASSET_REPOSITORY } from './asset-repository';
@Module({ @Module({
imports: [ imports: [
@ -24,7 +25,14 @@ import { assetUploadedQueueName } from '@app/job/constants/queue-name.constant';
}), }),
], ],
controllers: [AssetController], controllers: [AssetController],
providers: [AssetService, BackgroundTaskService], providers: [
AssetService,
BackgroundTaskService,
{
provide: ASSET_REPOSITORY,
useClass: AssetRepository,
},
],
exports: [AssetService], exports: [AssetService],
}) })
export class AssetModule {} export class AssetModule {}

View file

@ -0,0 +1,92 @@
import { AssetRepository, IAssetRepository } from './asset-repository';
import { AuthUserDto } from '../../decorators/auth-user.decorator';
import { AssetService } from './asset.service';
import { Repository } from 'typeorm';
import { AssetEntity, AssetType } from '@app/database/entities/asset.entity';
import { CreateAssetDto } from './dto/create-asset.dto';
describe('AssetService', () => {
let sui: AssetService;
let a: Repository<AssetEntity>; // TO BE DELETED AFTER FINISHED REFACTORING
let assetRepositoryMock: jest.Mocked<IAssetRepository>;
const authUser: AuthUserDto = Object.freeze({
id: '3ea54709-e168-42b7-90b0-a0dfe8a7ecbd',
email: 'auth@test.com',
});
const _getCreateAssetDto = (): CreateAssetDto => {
const createAssetDto = new CreateAssetDto();
createAssetDto.deviceAssetId = 'deviceAssetId';
createAssetDto.deviceId = 'deviceId';
createAssetDto.assetType = AssetType.OTHER;
createAssetDto.createdAt = '2022-06-19T23:41:36.910Z';
createAssetDto.modifiedAt = '2022-06-19T23:41:36.910Z';
createAssetDto.isFavorite = false;
createAssetDto.duration = '0:00:00.000000';
return createAssetDto;
};
const _getAsset = () => {
const assetEntity = new AssetEntity();
assetEntity.id = 'e8edabfd-7d8a-45d0-9d61-7c7ca60f2c67';
assetEntity.userId = '3ea54709-e168-42b7-90b0-a0dfe8a7ecbd';
assetEntity.deviceAssetId = '4967046344801';
assetEntity.deviceId = '116766fd-2ef2-52dc-a3ef-149988997291';
assetEntity.type = AssetType.VIDEO;
assetEntity.originalPath =
'upload/3ea54709-e168-42b7-90b0-a0dfe8a7ecbd/original/116766fd-2ef2-52dc-a3ef-149988997291/51c97f95-244f-462d-bdf0-e1dc19913516.jpg';
assetEntity.resizePath = '';
assetEntity.createdAt = '2022-06-19T23:41:36.910Z';
assetEntity.modifiedAt = '2022-06-19T23:41:36.910Z';
assetEntity.isFavorite = false;
assetEntity.mimeType = 'image/jpeg';
assetEntity.webpPath = '';
assetEntity.encodedVideoPath = '';
assetEntity.duration = '0:00:00.000000';
return assetEntity;
};
beforeAll(() => {
assetRepositoryMock = {
create: jest.fn(),
getAllByUserId: jest.fn(),
getAllByDeviceId: jest.fn(),
getAssetCountByTimeGroup: jest.fn(),
getById: jest.fn(),
getDetectedObjectsByUserId: jest.fn(),
getLocationsByUserId: jest.fn(),
getSearchPropertiesByUserId: jest.fn(),
};
sui = new AssetService(assetRepositoryMock, a);
});
it('create an asset', async () => {
const assetEntity = _getAsset();
assetRepositoryMock.create.mockImplementation(() => Promise.resolve<AssetEntity>(assetEntity));
const originalPath =
'upload/3ea54709-e168-42b7-90b0-a0dfe8a7ecbd/original/116766fd-2ef2-52dc-a3ef-149988997291/51c97f95-244f-462d-bdf0-e1dc19913516.jpg';
const mimeType = 'image/jpeg';
const createAssetDto = _getCreateAssetDto();
const result = await sui.createUserAsset(authUser, createAssetDto, originalPath, mimeType);
expect(result.userId).toEqual(authUser.id);
expect(result.resizePath).toEqual('');
expect(result.webpPath).toEqual('');
});
it('get assets by device id', async () => {
assetRepositoryMock.getAllByDeviceId.mockImplementation(() => Promise.resolve<string[]>(['4967046344801']));
const deviceId = '116766fd-2ef2-52dc-a3ef-149988997291';
const result = await sui.getUserAssetsByDeviceId(authUser, deviceId);
expect(result.length).toEqual(1);
expect(result[0]).toEqual('4967046344801');
});
});

View file

@ -1,5 +1,7 @@
import { CuratedLocationsResponseDto } from './response-dto/curated-locations-response.dto';
import { import {
BadRequestException, BadRequestException,
Inject,
Injectable, Injectable,
InternalServerErrorException, InternalServerErrorException,
Logger, Logger,
@ -7,7 +9,7 @@ import {
StreamableFile, StreamableFile,
} from '@nestjs/common'; } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { IsNull, Not, Repository } from 'typeorm'; import { Repository } from 'typeorm';
import { AuthUserDto } from '../../decorators/auth-user.decorator'; import { AuthUserDto } from '../../decorators/auth-user.decorator';
import { AssetEntity, AssetType } from '@app/database/entities/asset.entity'; import { AssetEntity, AssetType } from '@app/database/entities/asset.entity';
import { constants, createReadStream, ReadStream, stat } from 'fs'; import { constants, createReadStream, ReadStream, stat } from 'fs';
@ -25,83 +27,49 @@ import { CreateAssetDto } from './dto/create-asset.dto';
import { DeleteAssetResponseDto, DeleteAssetStatusEnum } from './response-dto/delete-asset-response.dto'; import { DeleteAssetResponseDto, DeleteAssetStatusEnum } from './response-dto/delete-asset-response.dto';
import { GetAssetThumbnailDto, GetAssetThumbnailFormatEnum } from './dto/get-asset-thumbnail.dto'; import { GetAssetThumbnailDto, GetAssetThumbnailFormatEnum } from './dto/get-asset-thumbnail.dto';
import { CheckDuplicateAssetResponseDto } from './response-dto/check-duplicate-asset-response.dto'; import { CheckDuplicateAssetResponseDto } from './response-dto/check-duplicate-asset-response.dto';
import { ASSET_REPOSITORY, IAssetRepository } from './asset-repository';
import { SearchPropertiesDto } from './dto/search-properties.dto';
import {
AssetCountByTimeGroupResponseDto,
mapAssetCountByTimeGroupResponse,
} from './response-dto/asset-count-by-time-group-response.dto';
import { GetAssetCountByTimeGroupDto } from './dto/get-asset-count-by-time-group.dto';
const fileInfo = promisify(stat); const fileInfo = promisify(stat);
@Injectable() @Injectable()
export class AssetService { export class AssetService {
constructor( constructor(
@Inject(ASSET_REPOSITORY)
private _assetRepository: IAssetRepository,
@InjectRepository(AssetEntity) @InjectRepository(AssetEntity)
private assetRepository: Repository<AssetEntity>, private assetRepository: Repository<AssetEntity>,
) {} ) {}
public async updateThumbnailInfo(asset: AssetEntity, thumbnailPath: string): Promise<AssetEntity> {
const updatedAsset = await this.assetRepository
.createQueryBuilder('assets')
.update<AssetEntity>(AssetEntity, { ...asset, resizePath: thumbnailPath })
.where('assets.id = :id', { id: asset.id })
.returning('*')
.updateEntity(true)
.execute();
return updatedAsset.raw[0];
}
public async createUserAsset( public async createUserAsset(
authUser: AuthUserDto, authUser: AuthUserDto,
assetInfo: CreateAssetDto, createAssetDto: CreateAssetDto,
path: string, originalPath: string,
mimeType: string, mimeType: string,
): Promise<AssetEntity | undefined> { ): Promise<AssetEntity> {
const asset = new AssetEntity(); const assetEntity = await this._assetRepository.create(createAssetDto, authUser.id, originalPath, mimeType);
asset.deviceAssetId = assetInfo.deviceAssetId;
asset.userId = authUser.id;
asset.deviceId = assetInfo.deviceId;
asset.type = assetInfo.assetType || AssetType.OTHER;
asset.originalPath = path;
asset.createdAt = assetInfo.createdAt;
asset.modifiedAt = assetInfo.modifiedAt;
asset.isFavorite = assetInfo.isFavorite;
asset.mimeType = mimeType;
asset.duration = assetInfo.duration || null;
const createdAsset = await this.assetRepository.save(asset); return assetEntity;
if (!createdAsset) {
throw new Error('Asset not created');
}
return createdAsset;
} }
public async getUserAssetsByDeviceId(authUser: AuthUserDto, deviceId: string) { public async getUserAssetsByDeviceId(authUser: AuthUserDto, deviceId: string) {
const rows = await this.assetRepository.find({ return this._assetRepository.getAllByDeviceId(authUser.id, deviceId);
where: {
userId: authUser.id,
deviceId: deviceId,
},
select: ['deviceAssetId'],
});
const res: string[] = [];
rows.forEach((v) => res.push(v.deviceAssetId));
return res;
} }
public async getAllAssets(authUser: AuthUserDto): Promise<AssetResponseDto[]> { public async getAllAssets(authUser: AuthUserDto): Promise<AssetResponseDto[]> {
const assets = await this.assetRepository.find({ const assets = await this._assetRepository.getAllByUserId(authUser.id);
where: {
userId: authUser.id,
resizePath: Not(IsNull()),
},
relations: ['exifInfo'],
order: {
createdAt: 'DESC',
},
});
return assets.map((asset) => mapAsset(asset)); return assets.map((asset) => mapAsset(asset));
} }
public async findAssetOfDevice(deviceId: string, assetId: string): Promise<AssetResponseDto> { // TODO - Refactor this to get asset by its own id
private async findAssetOfDevice(deviceId: string, assetId: string): Promise<AssetResponseDto> {
const rows = await this.assetRepository.query( const rows = await this.assetRepository.query(
'SELECT * FROM assets a WHERE a."deviceAssetId" = $1 AND a."deviceId" = $2', 'SELECT * FROM assets a WHERE a."deviceAssetId" = $1 AND a."deviceId" = $2',
[assetId, deviceId], [assetId, deviceId],
@ -117,16 +85,7 @@ export class AssetService {
} }
public async getAssetById(authUser: AuthUserDto, assetId: string): Promise<AssetResponseDto> { public async getAssetById(authUser: AuthUserDto, assetId: string): Promise<AssetResponseDto> {
const asset = await this.assetRepository.findOne({ const asset = await this._assetRepository.getById(assetId);
where: {
id: assetId,
},
relations: ['exifInfo'],
});
if (!asset) {
throw new NotFoundException('Asset not found');
}
return mapAsset(asset); return mapAsset(asset);
} }
@ -394,45 +353,35 @@ export class AssetService {
async getAssetSearchTerm(authUser: AuthUserDto): Promise<string[]> { async getAssetSearchTerm(authUser: AuthUserDto): Promise<string[]> {
const possibleSearchTerm = new Set<string>(); const possibleSearchTerm = new Set<string>();
// TODO: should use query builder
const rows = await this.assetRepository.query(
`
SELECT DISTINCT si.tags, si.objects, e.orientation, e."lensModel", e.make, e.model , a.type, e.city, e.state, e.country
FROM assets a
LEFT JOIN exif e ON a.id = e."assetId"
LEFT JOIN smart_info si ON a.id = si."assetId"
WHERE a."userId" = $1;
`,
[authUser.id],
);
rows.forEach((row: { [x: string]: any }) => { const rows = await this._assetRepository.getSearchPropertiesByUserId(authUser.id);
rows.forEach((row: SearchPropertiesDto) => {
// tags // tags
row['tags']?.map((tag: string) => possibleSearchTerm.add(tag?.toLowerCase())); row.tags?.map((tag: string) => possibleSearchTerm.add(tag?.toLowerCase()));
// objects // objects
row['objects']?.map((object: string) => possibleSearchTerm.add(object?.toLowerCase())); row.objects?.map((object: string) => possibleSearchTerm.add(object?.toLowerCase()));
// asset's tyoe // asset's tyoe
possibleSearchTerm.add(row['type']?.toLowerCase()); possibleSearchTerm.add(row.assetType?.toLowerCase() || '');
// image orientation // image orientation
possibleSearchTerm.add(row['orientation']?.toLowerCase()); possibleSearchTerm.add(row.orientation?.toLowerCase() || '');
// Lens model // Lens model
possibleSearchTerm.add(row['lensModel']?.toLowerCase()); possibleSearchTerm.add(row.lensModel?.toLowerCase() || '');
// Make and model // Make and model
possibleSearchTerm.add(row['make']?.toLowerCase()); possibleSearchTerm.add(row.make?.toLowerCase() || '');
possibleSearchTerm.add(row['model']?.toLowerCase()); possibleSearchTerm.add(row.model?.toLowerCase() || '');
// Location // Location
possibleSearchTerm.add(row['city']?.toLowerCase()); possibleSearchTerm.add(row.city?.toLowerCase() || '');
possibleSearchTerm.add(row['state']?.toLowerCase()); possibleSearchTerm.add(row.state?.toLowerCase() || '');
possibleSearchTerm.add(row['country']?.toLowerCase()); possibleSearchTerm.add(row.country?.toLowerCase() || '');
}); });
return Array.from(possibleSearchTerm).filter((x) => x != null); return Array.from(possibleSearchTerm).filter((x) => x != null && x != '');
} }
async searchAsset(authUser: AuthUserDto, searchAssetDto: SearchAssetDto): Promise<AssetResponseDto[]> { async searchAsset(authUser: AuthUserDto, searchAssetDto: SearchAssetDto): Promise<AssetResponseDto[]> {
@ -459,33 +408,12 @@ export class AssetService {
return searchResults.map((asset) => mapAsset(asset)); return searchResults.map((asset) => mapAsset(asset));
} }
async getCuratedLocation(authUser: AuthUserDto) { async getCuratedLocation(authUser: AuthUserDto): Promise<CuratedLocationsResponseDto[]> {
return await this.assetRepository.query( return this._assetRepository.getLocationsByUserId(authUser.id);
`
SELECT DISTINCT ON (e.city) a.id, e.city, a."resizePath", a."deviceAssetId", a."deviceId"
FROM assets a
LEFT JOIN exif e ON a.id = e."assetId"
WHERE a."userId" = $1
AND e.city IS NOT NULL
AND a.type = 'IMAGE';
`,
[authUser.id],
);
} }
async getCuratedObject(authUser: AuthUserDto): Promise<CuratedObjectsResponseDto[]> { async getCuratedObject(authUser: AuthUserDto): Promise<CuratedObjectsResponseDto[]> {
const curatedObjects: CuratedObjectsResponseDto[] = await this.assetRepository.query( return this._assetRepository.getDetectedObjectsByUserId(authUser.id);
`
SELECT DISTINCT ON (unnest(si.objects)) a.id, unnest(si.objects) as "object", a."resizePath", a."deviceAssetId", a."deviceId"
FROM assets a
LEFT JOIN smart_info si ON a.id = si."assetId"
WHERE a."userId" = $1
AND si.objects IS NOT NULL
`,
[authUser.id],
);
return curatedObjects;
} }
async checkDuplicatedAsset( async checkDuplicatedAsset(
@ -504,4 +432,16 @@ export class AssetService {
return new CheckDuplicateAssetResponseDto(isDuplicated, res?.id); return new CheckDuplicateAssetResponseDto(isDuplicated, res?.id);
} }
async getAssetCountByTimeGroup(
authUser: AuthUserDto,
getAssetCountByTimeGroupDto: GetAssetCountByTimeGroupDto,
): Promise<AssetCountByTimeGroupResponseDto> {
const result = await this._assetRepository.getAssetCountByTimeGroup(
authUser.id,
getAssetCountByTimeGroupDto.timeGroup,
);
return mapAssetCountByTimeGroupResponse(result);
}
} }

View file

@ -0,0 +1,16 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty } from 'class-validator';
export enum TimeGroupEnum {
Day = 'day',
Month = 'month',
}
export class GetAssetCountByTimeGroupDto {
@IsNotEmpty()
@ApiProperty({
type: String,
enum: TimeGroupEnum,
enumName: 'TimeGroupEnum',
})
timeGroup!: TimeGroupEnum;
}

View file

@ -0,0 +1,12 @@
export class SearchPropertiesDto {
tags?: string[];
objects?: string[];
assetType?: string;
orientation?: string;
lensModel?: string;
make?: string;
model?: string;
city?: string;
state?: string;
country?: string;
}

View file

@ -0,0 +1,23 @@
import { ApiProperty } from '@nestjs/swagger';
export class AssetCountByTimeGroupDto {
@ApiProperty({ type: 'string' })
timeGroup!: string;
@ApiProperty({ type: 'integer' })
count!: number;
}
export class AssetCountByTimeGroupResponseDto {
groups!: AssetCountByTimeGroupDto[];
@ApiProperty({ type: 'integer' })
totalAssets!: number;
}
export function mapAssetCountByTimeGroupResponse(result: AssetCountByTimeGroupDto[]): AssetCountByTimeGroupResponseDto {
return {
groups: result,
totalAssets: result.map((group) => group.count).reduce((a, b) => a + b, 0),
};
}

View file

@ -5,7 +5,7 @@ import { Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { UserEntity } from '@app/database/entities/user.entity'; import { UserEntity } from '@app/database/entities/user.entity';
import { Repository } from 'typeorm'; import { Repository } from 'typeorm';
import cookieParser from 'cookie';
@WebSocketGateway({ cors: true }) @WebSocketGateway({ cors: true })
export class CommunicationGateway implements OnGatewayConnection, OnGatewayDisconnect { export class CommunicationGateway implements OnGatewayConnection, OnGatewayDisconnect {
constructor( constructor(
@ -26,8 +26,24 @@ export class CommunicationGateway implements OnGatewayConnection, OnGatewayDisco
async handleConnection(client: Socket) { async handleConnection(client: Socket) {
try { try {
Logger.log(`New websocket connection: ${client.id}`, 'WebsocketConnectionEvent'); Logger.log(`New websocket connection: ${client.id}`, 'WebsocketConnectionEvent');
let accessToken = '';
const accessToken = client.handshake.headers.authorization?.split(' ')[1]; if (client.handshake.headers.cookie != undefined) {
const cookies = cookieParser.parse(client.handshake.headers.cookie);
if (cookies.immich_access_token) {
accessToken = cookies.immich_access_token;
} else {
client.emit('error', 'unauthorized');
client.disconnect();
return;
}
} else if (client.handshake.headers.authorization != undefined) {
accessToken = client.handshake.headers.authorization.split(' ')[1];
} else {
client.emit('error', 'unauthorized');
client.disconnect();
return;
}
const res: JwtValidationResult = accessToken const res: JwtValidationResult = accessToken
? await this.immichJwtService.validateToken(accessToken) ? await this.immichJwtService.validateToken(accessToken)

File diff suppressed because one or more lines are too long

View file

@ -145,6 +145,44 @@ export interface AlbumResponseDto {
*/ */
'assets': Array<AssetResponseDto>; 'assets': Array<AssetResponseDto>;
} }
/**
*
* @export
* @interface AssetCountByTimeGroupDto
*/
export interface AssetCountByTimeGroupDto {
/**
*
* @type {string}
* @memberof AssetCountByTimeGroupDto
*/
'timeGroup': string;
/**
*
* @type {number}
* @memberof AssetCountByTimeGroupDto
*/
'count': number;
}
/**
*
* @export
* @interface AssetCountByTimeGroupResponseDto
*/
export interface AssetCountByTimeGroupResponseDto {
/**
*
* @type {number}
* @memberof AssetCountByTimeGroupResponseDto
*/
'totalAssets': number;
/**
*
* @type {Array<AssetCountByTimeGroupDto>}
* @memberof AssetCountByTimeGroupResponseDto
*/
'groups': Array<AssetCountByTimeGroupDto>;
}
/** /**
* *
* @export * @export
@ -720,6 +758,19 @@ export interface ExifResponseDto {
*/ */
'country'?: string | null; 'country'?: string | null;
} }
/**
*
* @export
* @interface GetAssetCountByTimeGroupDto
*/
export interface GetAssetCountByTimeGroupDto {
/**
*
* @type {TimeGroupEnum}
* @memberof GetAssetCountByTimeGroupDto
*/
'timeGroup': TimeGroupEnum;
}
/** /**
* *
* @export * @export
@ -996,6 +1047,20 @@ export const ThumbnailFormat = {
export type ThumbnailFormat = typeof ThumbnailFormat[keyof typeof ThumbnailFormat]; export type ThumbnailFormat = typeof ThumbnailFormat[keyof typeof ThumbnailFormat];
/**
*
* @export
* @enum {string}
*/
export const TimeGroupEnum = {
Day: 'day',
Month: 'month'
} as const;
export type TimeGroupEnum = typeof TimeGroupEnum[keyof typeof TimeGroupEnum];
/** /**
* *
* @export * @export
@ -2072,13 +2137,52 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
options: localVarRequestOptions, options: localVarRequestOptions,
}; };
}, },
/**
*
* @param {GetAssetCountByTimeGroupDto} getAssetCountByTimeGroupDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getAssetCountByTimeGroup: async (getAssetCountByTimeGroupDto: GetAssetCountByTimeGroupDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'getAssetCountByTimeGroupDto' is not null or undefined
assertParamExists('getAssetCountByTimeGroup', 'getAssetCountByTimeGroupDto', getAssetCountByTimeGroupDto)
const localVarPath = `/asset/count-by-date`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication bearer required
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
localVarHeaderParameter['Content-Type'] = 'application/json';
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
localVarRequestOptions.data = serializeDataIfNeeded(getAssetCountByTimeGroupDto, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/** /**
* *
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
getAssetSearchTerms: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => { getAssetSearchTerms: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/asset/searchTerm`; const localVarPath = `/asset/search-terms`;
// use dummy base URL string because the URL constructor only accepts absolute URLs. // use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions; let baseOptions;
@ -2153,7 +2257,7 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
* @throws {RequiredError} * @throws {RequiredError}
*/ */
getCuratedLocations: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => { getCuratedLocations: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/asset/allLocation`; const localVarPath = `/asset/curated-locations`;
// use dummy base URL string because the URL constructor only accepts absolute URLs. // use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions; let baseOptions;
@ -2186,7 +2290,7 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
* @throws {RequiredError} * @throws {RequiredError}
*/ */
getCuratedObjects: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => { getCuratedObjects: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/asset/allObjects`; const localVarPath = `/asset/curated-objects`;
// use dummy base URL string because the URL constructor only accepts absolute URLs. // use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions; let baseOptions;
@ -2456,6 +2560,16 @@ export const AssetApiFp = function(configuration?: Configuration) {
const localVarAxiosArgs = await localVarAxiosParamCreator.getAssetById(assetId, options); const localVarAxiosArgs = await localVarAxiosParamCreator.getAssetById(assetId, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
}, },
/**
*
* @param {GetAssetCountByTimeGroupDto} getAssetCountByTimeGroupDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async getAssetCountByTimeGroup(getAssetCountByTimeGroupDto: GetAssetCountByTimeGroupDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AssetCountByTimeGroupResponseDto>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getAssetCountByTimeGroup(getAssetCountByTimeGroupDto, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/** /**
* *
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
@ -2598,6 +2712,15 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
getAssetById(assetId: string, options?: any): AxiosPromise<AssetResponseDto> { getAssetById(assetId: string, options?: any): AxiosPromise<AssetResponseDto> {
return localVarFp.getAssetById(assetId, options).then((request) => request(axios, basePath)); return localVarFp.getAssetById(assetId, options).then((request) => request(axios, basePath));
}, },
/**
*
* @param {GetAssetCountByTimeGroupDto} getAssetCountByTimeGroupDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getAssetCountByTimeGroup(getAssetCountByTimeGroupDto: GetAssetCountByTimeGroupDto, options?: any): AxiosPromise<AssetCountByTimeGroupResponseDto> {
return localVarFp.getAssetCountByTimeGroup(getAssetCountByTimeGroupDto, options).then((request) => request(axios, basePath));
},
/** /**
* *
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
@ -2742,6 +2865,17 @@ export class AssetApi extends BaseAPI {
return AssetApiFp(this.configuration).getAssetById(assetId, options).then((request) => request(this.axios, this.basePath)); return AssetApiFp(this.configuration).getAssetById(assetId, options).then((request) => request(this.axios, this.basePath));
} }
/**
*
* @param {GetAssetCountByTimeGroupDto} getAssetCountByTimeGroupDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof AssetApi
*/
public getAssetCountByTimeGroup(getAssetCountByTimeGroupDto: GetAssetCountByTimeGroupDto, options?: AxiosRequestConfig) {
return AssetApiFp(this.configuration).getAssetCountByTimeGroup(getAssetCountByTimeGroupDto, options).then((request) => request(this.axios, this.basePath));
}
/** /**
* *
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.

View file

@ -1,5 +1,4 @@
import { Socket, io } from 'socket.io-client'; import { Socket, io } from 'socket.io-client';
import { writable } from 'svelte/store';
let websocket: Socket; let websocket: Socket;

View file

@ -1,4 +1,4 @@
import { serverApi } from '@api'; import { serverApi, TimeGroupEnum } from '@api';
import * as cookieParser from 'cookie'; import * as cookieParser from 'cookie';
import type { LayoutServerLoad } from './$types'; import type { LayoutServerLoad } from './$types';
@ -21,6 +21,9 @@ export const load: LayoutServerLoad = async ({ request }) => {
user: userInfo user: userInfo
}; };
} catch (e) { } catch (e) {
console.log('[ERROR] layout.server.ts [LayoutServerLoad]: ', e); console.error('[ERROR] layout.server.ts [LayoutServerLoad]: ', e);
return {
user: undefined
};
} }
}; };

View file

@ -20,11 +20,13 @@
import Close from 'svelte-material-icons/Close.svelte'; import Close from 'svelte-material-icons/Close.svelte';
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte'; import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
import type { PageData } from './$types'; import type { PageData } from './$types';
import { onMount } from 'svelte';
import { onMount, onDestroy } from 'svelte';
import { import {
notificationController, notificationController,
NotificationType NotificationType
} from '$lib/components/shared-components/notification/notification'; } from '$lib/components/shared-components/notification/notification';
import { closeWebsocketConnection, openWebsocketConnection } from '$lib/stores/websocket';
export let data: PageData; export let data: PageData;
@ -193,6 +195,18 @@
console.error('Error deleteSelectedAssetHandler', e); console.error('Error deleteSelectedAssetHandler', e);
} }
}; };
onMount(async () => {
openWebsocketConnection();
const { data: assets } = await api.assetApi.getAllAssets();
setAssetInfo(assets);
});
onDestroy(() => {
closeWebsocketConnection();
});
</script> </script>
<svelte:head> <svelte:head>