refactor(server)!: move markers and style to dedicated map endpoint/controller (#9832)
* move markers and style to dedicated map endpoint * chore: open api * chore: clean up repos --------- Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
This commit is contained in:
@@ -2,7 +2,6 @@ import { BadRequestException, UnauthorizedException } from '@nestjs/common';
|
||||
import { mapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { AssetJobName, AssetStatsResponseDto, UploadFieldName } from 'src/dtos/asset.dto';
|
||||
import { AssetEntity, AssetType } from 'src/entities/asset.entity';
|
||||
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
||||
import { IAssetStackRepository } from 'src/interfaces/asset-stack.interface';
|
||||
import { AssetStats, IAssetRepository } from 'src/interfaces/asset.interface';
|
||||
import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface';
|
||||
@@ -19,7 +18,6 @@ import { faceStub } from 'test/fixtures/face.stub';
|
||||
import { partnerStub } from 'test/fixtures/partner.stub';
|
||||
import { userStub } from 'test/fixtures/user.stub';
|
||||
import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock';
|
||||
import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock';
|
||||
import { newAssetStackRepositoryMock } from 'test/repositories/asset-stack.repository.mock';
|
||||
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
|
||||
import { newEventRepositoryMock } from 'test/repositories/event.repository.mock';
|
||||
@@ -162,7 +160,6 @@ describe(AssetService.name, () => {
|
||||
let systemMock: Mocked<ISystemMetadataRepository>;
|
||||
let partnerMock: Mocked<IPartnerRepository>;
|
||||
let assetStackMock: Mocked<IAssetStackRepository>;
|
||||
let albumMock: Mocked<IAlbumRepository>;
|
||||
let loggerMock: Mocked<ILoggerRepository>;
|
||||
|
||||
it('should work', () => {
|
||||
@@ -185,7 +182,6 @@ describe(AssetService.name, () => {
|
||||
systemMock = newSystemMetadataRepositoryMock();
|
||||
partnerMock = newPartnerRepositoryMock();
|
||||
assetStackMock = newAssetStackRepositoryMock();
|
||||
albumMock = newAlbumRepositoryMock();
|
||||
loggerMock = newLoggerRepositoryMock();
|
||||
|
||||
sut = new AssetService(
|
||||
@@ -198,7 +194,6 @@ describe(AssetService.name, () => {
|
||||
eventMock,
|
||||
partnerMock,
|
||||
assetStackMock,
|
||||
albumMock,
|
||||
loggerMock,
|
||||
);
|
||||
|
||||
@@ -314,27 +309,6 @@ describe(AssetService.name, () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMapMarkers', () => {
|
||||
it('should get geo information of assets', async () => {
|
||||
const asset = assetStub.withLocation;
|
||||
const marker = {
|
||||
id: asset.id,
|
||||
lat: asset.exifInfo!.latitude!,
|
||||
lon: asset.exifInfo!.longitude!,
|
||||
city: asset.exifInfo!.city,
|
||||
state: asset.exifInfo!.state,
|
||||
country: asset.exifInfo!.country,
|
||||
};
|
||||
partnerMock.getAll.mockResolvedValue([]);
|
||||
assetMock.getMapMarkers.mockResolvedValue([marker]);
|
||||
|
||||
const markers = await sut.getMapMarkers(authStub.user1, {});
|
||||
|
||||
expect(markers).toHaveLength(1);
|
||||
expect(markers[0]).toEqual(marker);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMemoryLane', () => {
|
||||
beforeAll(() => {
|
||||
vitest.useFakeTimers();
|
||||
|
||||
@@ -24,11 +24,10 @@ import {
|
||||
mapStats,
|
||||
} from 'src/dtos/asset.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { MapMarkerDto, MapMarkerResponseDto, MemoryLaneDto } from 'src/dtos/search.dto';
|
||||
import { MemoryLaneDto } from 'src/dtos/search.dto';
|
||||
import { UpdateStackParentDto } from 'src/dtos/stack.dto';
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { IAccessRepository } from 'src/interfaces/access.interface';
|
||||
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
||||
import { IAssetStackRepository } from 'src/interfaces/asset-stack.interface';
|
||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||
import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface';
|
||||
@@ -65,7 +64,6 @@ export class AssetService {
|
||||
@Inject(IEventRepository) private eventRepository: IEventRepository,
|
||||
@Inject(IPartnerRepository) private partnerRepository: IPartnerRepository,
|
||||
@Inject(IAssetStackRepository) private assetStackRepository: IAssetStackRepository,
|
||||
@Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
|
||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
||||
) {
|
||||
this.logger.setContext(AssetService.name);
|
||||
@@ -153,30 +151,6 @@ export class AssetService {
|
||||
return folder;
|
||||
}
|
||||
|
||||
async getMapMarkers(auth: AuthDto, options: MapMarkerDto): Promise<MapMarkerResponseDto[]> {
|
||||
const userIds: string[] = [auth.user.id];
|
||||
// TODO convert to SQL join
|
||||
if (options.withPartners) {
|
||||
const partners = await this.partnerRepository.getAll(auth.user.id);
|
||||
const partnersIds = partners
|
||||
.filter((partner) => partner.sharedBy && partner.sharedWith && partner.sharedById != auth.user.id)
|
||||
.map((partner) => partner.sharedById);
|
||||
userIds.push(...partnersIds);
|
||||
}
|
||||
|
||||
// TODO convert to SQL join
|
||||
const albumIds: string[] = [];
|
||||
if (options.withSharedAlbums) {
|
||||
const [ownedAlbums, sharedAlbums] = await Promise.all([
|
||||
this.albumRepository.getOwned(auth.user.id),
|
||||
this.albumRepository.getShared(auth.user.id),
|
||||
]);
|
||||
albumIds.push(...ownedAlbums.map((album) => album.id), ...sharedAlbums.map((album) => album.id));
|
||||
}
|
||||
|
||||
return this.assetRepository.getMapMarkers(userIds, albumIds, options);
|
||||
}
|
||||
|
||||
async getMemoryLane(auth: AuthDto, dto: MemoryLaneDto): Promise<MemoryLaneResponseDto[]> {
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import { DownloadService } from 'src/services/download.service';
|
||||
import { DuplicateService } from 'src/services/duplicate.service';
|
||||
import { JobService } from 'src/services/job.service';
|
||||
import { LibraryService } from 'src/services/library.service';
|
||||
import { MapService } from 'src/services/map.service';
|
||||
import { MediaService } from 'src/services/media.service';
|
||||
import { MemoryService } from 'src/services/memory.service';
|
||||
import { MetadataService } from 'src/services/metadata.service';
|
||||
@@ -38,11 +39,10 @@ import { UserService } from 'src/services/user.service';
|
||||
import { VersionService } from 'src/services/version.service';
|
||||
|
||||
export const services = [
|
||||
ApiService,
|
||||
MicroservicesService,
|
||||
APIKeyService,
|
||||
ActivityService,
|
||||
AlbumService,
|
||||
ApiService,
|
||||
AssetMediaService,
|
||||
AssetService,
|
||||
AssetServiceV1,
|
||||
@@ -54,9 +54,11 @@ export const services = [
|
||||
DuplicateService,
|
||||
JobService,
|
||||
LibraryService,
|
||||
MapService,
|
||||
MediaService,
|
||||
MemoryService,
|
||||
MetadataService,
|
||||
MicroservicesService,
|
||||
NotificationService,
|
||||
PartnerService,
|
||||
PersonService,
|
||||
@@ -73,7 +75,7 @@ export const services = [
|
||||
TagService,
|
||||
TimelineService,
|
||||
TrashService,
|
||||
UserService,
|
||||
UserAdminService,
|
||||
UserService,
|
||||
VersionService,
|
||||
];
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { IMapRepository } from 'src/interfaces/map.interface';
|
||||
import { IPartnerRepository } from 'src/interfaces/partner.interface';
|
||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||
import { MapService } from 'src/services/map.service';
|
||||
import { assetStub } from 'test/fixtures/asset.stub';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock';
|
||||
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
||||
import { newMapRepositoryMock } from 'test/repositories/map.repository.mock';
|
||||
import { newPartnerRepositoryMock } from 'test/repositories/partner.repository.mock';
|
||||
import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock';
|
||||
import { Mocked } from 'vitest';
|
||||
|
||||
describe(MapService.name, () => {
|
||||
let sut: MapService;
|
||||
let albumMock: Mocked<IAlbumRepository>;
|
||||
let loggerMock: Mocked<ILoggerRepository>;
|
||||
let partnerMock: Mocked<IPartnerRepository>;
|
||||
let mapMock: Mocked<IMapRepository>;
|
||||
let systemMetadataMock: Mocked<ISystemMetadataRepository>;
|
||||
|
||||
beforeEach(() => {
|
||||
albumMock = newAlbumRepositoryMock();
|
||||
loggerMock = newLoggerRepositoryMock();
|
||||
partnerMock = newPartnerRepositoryMock();
|
||||
mapMock = newMapRepositoryMock();
|
||||
systemMetadataMock = newSystemMetadataRepositoryMock();
|
||||
|
||||
sut = new MapService(albumMock, loggerMock, partnerMock, mapMock, systemMetadataMock);
|
||||
});
|
||||
|
||||
describe('getMapMarkers', () => {
|
||||
it('should get geo information of assets', async () => {
|
||||
const asset = assetStub.withLocation;
|
||||
const marker = {
|
||||
id: asset.id,
|
||||
lat: asset.exifInfo!.latitude!,
|
||||
lon: asset.exifInfo!.longitude!,
|
||||
city: asset.exifInfo!.city,
|
||||
state: asset.exifInfo!.state,
|
||||
country: asset.exifInfo!.country,
|
||||
};
|
||||
partnerMock.getAll.mockResolvedValue([]);
|
||||
mapMock.getMapMarkers.mockResolvedValue([marker]);
|
||||
|
||||
const markers = await sut.getMapMarkers(authStub.user1, {});
|
||||
|
||||
expect(markers).toHaveLength(1);
|
||||
expect(markers[0]).toEqual(marker);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
import { Inject } from '@nestjs/common';
|
||||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { MapMarkerDto, MapMarkerResponseDto } from 'src/dtos/search.dto';
|
||||
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { IMapRepository } from 'src/interfaces/map.interface';
|
||||
import { IPartnerRepository } from 'src/interfaces/partner.interface';
|
||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||
|
||||
export class MapService {
|
||||
private configCore: SystemConfigCore;
|
||||
|
||||
constructor(
|
||||
@Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
|
||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
||||
@Inject(IPartnerRepository) private partnerRepository: IPartnerRepository,
|
||||
@Inject(IMapRepository) private mapRepository: IMapRepository,
|
||||
@Inject(ISystemMetadataRepository) private systemMetadataRepository: ISystemMetadataRepository,
|
||||
) {
|
||||
this.logger.setContext(MapService.name);
|
||||
this.configCore = SystemConfigCore.create(systemMetadataRepository, logger);
|
||||
}
|
||||
|
||||
async getMapMarkers(auth: AuthDto, options: MapMarkerDto): Promise<MapMarkerResponseDto[]> {
|
||||
const userIds: string[] = [auth.user.id];
|
||||
// TODO convert to SQL join
|
||||
if (options.withPartners) {
|
||||
const partners = await this.partnerRepository.getAll(auth.user.id);
|
||||
const partnersIds = partners
|
||||
.filter((partner) => partner.sharedBy && partner.sharedWith && partner.sharedById != auth.user.id)
|
||||
.map((partner) => partner.sharedById);
|
||||
userIds.push(...partnersIds);
|
||||
}
|
||||
|
||||
// TODO convert to SQL join
|
||||
const albumIds: string[] = [];
|
||||
if (options.withSharedAlbums) {
|
||||
const [ownedAlbums, sharedAlbums] = await Promise.all([
|
||||
this.albumRepository.getOwned(auth.user.id),
|
||||
this.albumRepository.getShared(auth.user.id),
|
||||
]);
|
||||
albumIds.push(...ownedAlbums.map((album) => album.id), ...sharedAlbums.map((album) => album.id));
|
||||
}
|
||||
|
||||
return this.mapRepository.getMapMarkers(userIds, albumIds, options);
|
||||
}
|
||||
|
||||
async getMapStyle(theme: 'light' | 'dark') {
|
||||
const { map } = await this.configCore.getConfig();
|
||||
const styleUrl = theme === 'dark' ? map.darkStyle : map.lightStyle;
|
||||
|
||||
if (styleUrl) {
|
||||
return this.mapRepository.fetchStyle(styleUrl);
|
||||
}
|
||||
|
||||
return JSON.parse(await this.systemMetadataRepository.readFile(`./resources/style-${theme}.json`));
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import { IDatabaseRepository } from 'src/interfaces/database.interface';
|
||||
import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface';
|
||||
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { IMapRepository } from 'src/interfaces/map.interface';
|
||||
import { IMediaRepository } from 'src/interfaces/media.interface';
|
||||
import { IMetadataRepository, ImmichTags } from 'src/interfaces/metadata.interface';
|
||||
import { IMoveRepository } from 'src/interfaces/move.interface';
|
||||
@@ -29,6 +30,7 @@ import { newDatabaseRepositoryMock } from 'test/repositories/database.repository
|
||||
import { newEventRepositoryMock } from 'test/repositories/event.repository.mock';
|
||||
import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
|
||||
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
||||
import { newMapRepositoryMock } from 'test/repositories/map.repository.mock';
|
||||
import { newMediaRepositoryMock } from 'test/repositories/media.repository.mock';
|
||||
import { newMetadataRepositoryMock } from 'test/repositories/metadata.repository.mock';
|
||||
import { newMoveRepositoryMock } from 'test/repositories/move.repository.mock';
|
||||
@@ -44,6 +46,7 @@ describe(MetadataService.name, () => {
|
||||
let systemMock: Mocked<ISystemMetadataRepository>;
|
||||
let cryptoRepository: Mocked<ICryptoRepository>;
|
||||
let jobMock: Mocked<IJobRepository>;
|
||||
let mapMock: Mocked<IMapRepository>;
|
||||
let metadataMock: Mocked<IMetadataRepository>;
|
||||
let moveMock: Mocked<IMoveRepository>;
|
||||
let mediaMock: Mocked<IMediaRepository>;
|
||||
@@ -60,6 +63,7 @@ describe(MetadataService.name, () => {
|
||||
assetMock = newAssetRepositoryMock();
|
||||
cryptoRepository = newCryptoRepositoryMock();
|
||||
jobMock = newJobRepositoryMock();
|
||||
mapMock = newMapRepositoryMock();
|
||||
metadataMock = newMetadataRepositoryMock();
|
||||
moveMock = newMoveRepositoryMock();
|
||||
personMock = newPersonRepositoryMock();
|
||||
@@ -78,6 +82,7 @@ describe(MetadataService.name, () => {
|
||||
cryptoRepository,
|
||||
databaseMock,
|
||||
jobMock,
|
||||
mapMock,
|
||||
mediaMock,
|
||||
metadataMock,
|
||||
moveMock,
|
||||
@@ -102,7 +107,7 @@ describe(MetadataService.name, () => {
|
||||
await sut.init();
|
||||
|
||||
expect(jobMock.pause).toHaveBeenCalledTimes(1);
|
||||
expect(metadataMock.init).toHaveBeenCalledTimes(1);
|
||||
expect(mapMock.init).toHaveBeenCalledTimes(1);
|
||||
expect(jobMock.resume).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
@@ -112,7 +117,7 @@ describe(MetadataService.name, () => {
|
||||
await sut.init();
|
||||
|
||||
expect(jobMock.pause).not.toHaveBeenCalled();
|
||||
expect(metadataMock.init).not.toHaveBeenCalled();
|
||||
expect(mapMock.init).not.toHaveBeenCalled();
|
||||
expect(jobMock.resume).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -297,7 +302,7 @@ describe(MetadataService.name, () => {
|
||||
it('should apply reverse geocoding', async () => {
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.withLocation]);
|
||||
systemMock.get.mockResolvedValue({ reverseGeocoding: { enabled: true } });
|
||||
metadataMock.reverseGeocode.mockResolvedValue({ city: 'City', state: 'State', country: 'Country' });
|
||||
mapMock.reverseGeocode.mockResolvedValue({ city: 'City', state: 'State', country: 'Country' });
|
||||
metadataMock.readTags.mockResolvedValue({
|
||||
GPSLatitude: assetStub.withLocation.exifInfo!.latitude!,
|
||||
GPSLongitude: assetStub.withLocation.exifInfo!.longitude!,
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
QueueName,
|
||||
} from 'src/interfaces/job.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { IMapRepository } from 'src/interfaces/map.interface';
|
||||
import { IMediaRepository } from 'src/interfaces/media.interface';
|
||||
import { IMetadataRepository, ImmichTags } from 'src/interfaces/metadata.interface';
|
||||
import { IMoveRepository } from 'src/interfaces/move.interface';
|
||||
@@ -108,6 +109,7 @@ export class MetadataService {
|
||||
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
|
||||
@Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository,
|
||||
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
||||
@Inject(IMapRepository) private mapRepository: IMapRepository,
|
||||
@Inject(IMediaRepository) private mediaRepository: IMediaRepository,
|
||||
@Inject(IMetadataRepository) private repository: IMetadataRepository,
|
||||
@Inject(IMoveRepository) moveRepository: IMoveRepository,
|
||||
@@ -144,7 +146,7 @@ export class MetadataService {
|
||||
|
||||
try {
|
||||
await this.jobRepository.pause(QueueName.METADATA_EXTRACTION);
|
||||
await this.databaseRepository.withLock(DatabaseLock.GeodataImport, () => this.repository.init());
|
||||
await this.databaseRepository.withLock(DatabaseLock.GeodataImport, () => this.mapRepository.init());
|
||||
await this.jobRepository.resume(QueueName.METADATA_EXTRACTION);
|
||||
|
||||
this.logger.log(`Initialized local reverse geocoder`);
|
||||
@@ -337,7 +339,7 @@ export class MetadataService {
|
||||
}
|
||||
|
||||
try {
|
||||
const reverseGeocode = await this.repository.reverseGeocode({ latitude, longitude });
|
||||
const reverseGeocode = await this.mapRepository.reverseGeocode({ latitude, longitude });
|
||||
if (!reverseGeocode) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ export class SystemConfigService {
|
||||
private core: SystemConfigCore;
|
||||
|
||||
constructor(
|
||||
@Inject(ISystemMetadataRepository) private repository: ISystemMetadataRepository,
|
||||
@Inject(ISystemMetadataRepository) repository: ISystemMetadataRepository,
|
||||
@Inject(IEventRepository) private eventRepository: IEventRepository,
|
||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
||||
@Inject(ISearchRepository) private smartInfoRepository: ISearchRepository,
|
||||
@@ -109,17 +109,6 @@ export class SystemConfigService {
|
||||
return options;
|
||||
}
|
||||
|
||||
async getMapStyle(theme: 'light' | 'dark') {
|
||||
const { map } = await this.getConfig();
|
||||
const styleUrl = theme === 'dark' ? map.darkStyle : map.lightStyle;
|
||||
|
||||
if (styleUrl) {
|
||||
return this.repository.fetchStyle(styleUrl);
|
||||
}
|
||||
|
||||
return JSON.parse(await this.repository.readFile(`./resources/style-${theme}.json`));
|
||||
}
|
||||
|
||||
async getCustomCss(): Promise<string> {
|
||||
const { theme } = await this.core.getConfig();
|
||||
return theme.customCss;
|
||||
|
||||
Reference in New Issue
Block a user