mirror of
https://github.com/xuthus83/pigallery2.git
synced 2024-11-03 21:04:03 +08:00
Implementing AlbumBase and SavedSearch Entities and Manager #45
This commit is contained in:
parent
e63a7cae98
commit
512f5c18d6
39
src/backend/model/database/sql/AlbumManager.ts
Normal file
39
src/backend/model/database/sql/AlbumManager.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import {SQLConnection} from './SQLConnection';
|
||||
import {AlbumBaseEntity} from './enitites/album/AlbumBaseEntity';
|
||||
import {AlbumBaseDTO} from '../../../../common/entities/album/AlbumBaseDTO';
|
||||
import {SavedSearchDTO} from '../../../../common/entities/album/SavedSearchDTO';
|
||||
import {ObjectManagers} from '../../ObjectManagers';
|
||||
import {ISQLSearchManager} from './ISearchManager';
|
||||
import {SearchQueryDTO} from '../../../../common/entities/SearchQueryDTO';
|
||||
import {SavedSearchEntity} from './enitites/album/SavedSearchEntity';
|
||||
|
||||
export class AlbumManager {
|
||||
private static async fillPreviewToAlbum(album: AlbumBaseDTO): Promise<void> {
|
||||
if (!(album as SavedSearchDTO).searchQuery) {
|
||||
throw new Error('no search query present');
|
||||
}
|
||||
album.preview = await (ObjectManagers.getInstance().SearchManager as ISQLSearchManager)
|
||||
.getPreview((album as SavedSearchDTO).searchQuery);
|
||||
}
|
||||
|
||||
public async addSavedSearch(name: string, searchQuery: SearchQueryDTO): Promise<void> {
|
||||
const connection = await SQLConnection.getConnection();
|
||||
await connection.getRepository(SavedSearchEntity).insert({name, searchQuery});
|
||||
|
||||
}
|
||||
|
||||
public async deleteAlbum(id: number): Promise<void> {
|
||||
const connection = await SQLConnection.getConnection();
|
||||
await connection.getRepository(AlbumBaseEntity).delete({id});
|
||||
}
|
||||
|
||||
public async getAlbums(): Promise<AlbumBaseDTO[]> {
|
||||
const connection = await SQLConnection.getConnection();
|
||||
const albums = await connection.getRepository(AlbumBaseEntity).find();
|
||||
for (const a of albums) {
|
||||
await AlbumManager.fillPreviewToAlbum(a);
|
||||
}
|
||||
|
||||
return albums;
|
||||
}
|
||||
}
|
17
src/backend/model/database/sql/ISearchManager.ts
Normal file
17
src/backend/model/database/sql/ISearchManager.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import {SearchQueryDTO, SearchQueryTypes} from '../../../../common/entities/SearchQueryDTO';
|
||||
import {MediaDTO} from '../../../../common/entities/MediaDTO';
|
||||
import {ISearchManager} from '../interfaces/ISearchManager';
|
||||
import {AutoCompleteItem} from '../../../../common/entities/AutoCompleteItem';
|
||||
import {SearchResultDTO} from '../../../../common/entities/SearchResultDTO';
|
||||
import {PhotoDTO} from '../../../../common/entities/PhotoDTO';
|
||||
|
||||
export interface ISQLSearchManager extends ISearchManager {
|
||||
autocomplete(text: string, type: SearchQueryTypes): Promise<AutoCompleteItem[]>;
|
||||
|
||||
search(query: SearchQueryDTO): Promise<SearchResultDTO>;
|
||||
|
||||
getRandomPhoto(queryFilter: SearchQueryDTO): Promise<PhotoDTO>;
|
||||
|
||||
// "Protected" functions. only called from other Managers, not from middlewares
|
||||
getPreview(query: SearchQueryDTO): Promise<MediaDTO>;
|
||||
}
|
@ -19,6 +19,8 @@ import {PersonEntry} from './enitites/PersonEntry';
|
||||
import {Utils} from '../../../../common/Utils';
|
||||
import * as path from 'path';
|
||||
import {DatabaseType, ServerDataBaseConfig, SQLLogLevel} from '../../../../common/config/private/PrivateConfig';
|
||||
import {AlbumBaseEntity} from './enitites/album/AlbumBaseEntity';
|
||||
import {SavedSearchEntity} from './enitites/album/SavedSearchEntity';
|
||||
|
||||
|
||||
export class SQLConnection {
|
||||
@ -43,6 +45,8 @@ export class SQLConnection {
|
||||
VideoEntity,
|
||||
DirectoryEntity,
|
||||
SharingEntity,
|
||||
AlbumBaseEntity,
|
||||
SavedSearchEntity,
|
||||
VersionEntity
|
||||
];
|
||||
options.synchronize = false;
|
||||
@ -73,6 +77,8 @@ export class SQLConnection {
|
||||
VideoEntity,
|
||||
DirectoryEntity,
|
||||
SharingEntity,
|
||||
AlbumBaseEntity,
|
||||
SavedSearchEntity,
|
||||
VersionEntity
|
||||
];
|
||||
options.synchronize = false;
|
||||
|
@ -1,5 +1,4 @@
|
||||
import {AutoCompleteItem} from '../../../../common/entities/AutoCompleteItem';
|
||||
import {ISearchManager} from '../interfaces/ISearchManager';
|
||||
import {SearchResultDTO} from '../../../../common/entities/SearchResultDTO';
|
||||
import {SQLConnection} from './SQLConnection';
|
||||
import {PhotoEntity} from './enitites/PhotoEntity';
|
||||
@ -32,8 +31,10 @@ import {Utils} from '../../../../common/Utils';
|
||||
import {PhotoDTO} from '../../../../common/entities/PhotoDTO';
|
||||
import {DatabaseType} from '../../../../common/config/private/PrivateConfig';
|
||||
import {ISQLGalleryManager} from './IGalleryManager';
|
||||
import {ISQLSearchManager} from './ISearchManager';
|
||||
import {MediaDTO} from '../../../../common/entities/MediaDTO';
|
||||
|
||||
export class SearchManager implements ISearchManager {
|
||||
export class SearchManager implements ISQLSearchManager {
|
||||
|
||||
private static autoCompleteItemsUnique(array: Array<AutoCompleteItem>): Array<AutoCompleteItem> {
|
||||
const a = array.concat();
|
||||
@ -223,6 +224,21 @@ export class SearchManager implements ISearchManager {
|
||||
|
||||
}
|
||||
|
||||
public async getPreview(queryIN: SearchQueryDTO): Promise<MediaDTO> {
|
||||
let query = this.flattenSameOfQueries(queryIN);
|
||||
query = await this.getGPSData(query);
|
||||
const connection = await SQLConnection.getConnection();
|
||||
|
||||
return await connection
|
||||
.getRepository(MediaEntity)
|
||||
.createQueryBuilder('media')
|
||||
.innerJoinAndSelect('media.directory', 'directory')
|
||||
.where(this.buildWhereQuery(query))
|
||||
.orderBy('media.metadata.creationDate', 'DESC')
|
||||
.limit(1)
|
||||
.getOne();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns only those part of a query tree that only contains directory related search queries
|
||||
*/
|
||||
@ -632,4 +648,5 @@ export class SearchManager implements ISearchManager {
|
||||
return res;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -82,7 +82,7 @@ export class MediaMetadataEntity implements MediaMetadata {
|
||||
// TODO: fix inheritance once its working in typeorm
|
||||
@Entity()
|
||||
@Unique(['name', 'directory'])
|
||||
@TableInheritance({column: {type: 'varchar', name: 'type', length: 32}})
|
||||
@TableInheritance({column: {type: 'varchar', name: 'type', length: 16}})
|
||||
export abstract class MediaEntity implements MediaDTO {
|
||||
|
||||
@Index()
|
||||
|
@ -0,0 +1,21 @@
|
||||
import {Column, Entity, Index, PrimaryGeneratedColumn, TableInheritance} from 'typeorm';
|
||||
import {MediaEntity} from '../MediaEntity';
|
||||
import {columnCharsetCS} from '../EntityUtils';
|
||||
import {AlbumBaseDTO} from '../../../../../../common/entities/album/AlbumBaseDTO';
|
||||
|
||||
@Entity()
|
||||
@TableInheritance({column: {type: 'varchar', name: 'type', length: 16}})
|
||||
export class AlbumBaseEntity implements AlbumBaseDTO {
|
||||
|
||||
@Index()
|
||||
@PrimaryGeneratedColumn({unsigned: true})
|
||||
id: number;
|
||||
|
||||
@Index()
|
||||
@Column(columnCharsetCS)
|
||||
name: string;
|
||||
|
||||
// not saving to database, it is only assigned when querying the DB
|
||||
public preview: MediaEntity;
|
||||
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
import {ChildEntity, Column} from 'typeorm';
|
||||
import {AlbumBaseEntity} from './AlbumBaseEntity';
|
||||
import {SavedSearchDTO} from '../../../../../../common/entities/album/SavedSearchDTO';
|
||||
import {SearchQueryDTO} from '../../../../../../common/entities/SearchQueryDTO';
|
||||
|
||||
@ChildEntity()
|
||||
export class SavedSearchEntity extends AlbumBaseEntity implements SavedSearchDTO {
|
||||
@Column({
|
||||
type: 'text',
|
||||
nullable: false,
|
||||
transformer: {
|
||||
// used to deserialize your data from db field value
|
||||
from: (val: string) => {
|
||||
return JSON.parse(val);
|
||||
},
|
||||
// used to serialize your data to db field
|
||||
to: (val: object) => {
|
||||
return JSON.stringify(val);
|
||||
}
|
||||
}
|
||||
})
|
||||
searchQuery: SearchQueryDTO;
|
||||
}
|
@ -158,7 +158,7 @@ export interface TextSearch extends NegatableSearchQuery {
|
||||
SearchQueryTypes.caption |
|
||||
SearchQueryTypes.file_name |
|
||||
SearchQueryTypes.directory;
|
||||
matchType: TextSearchQueryMatchTypes;
|
||||
matchType?: TextSearchQueryMatchTypes;
|
||||
text: string;
|
||||
}
|
||||
|
||||
|
7
src/common/entities/album/AlbumBaseDTO.ts
Normal file
7
src/common/entities/album/AlbumBaseDTO.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import {PreviewPhotoDTO} from '../PhotoDTO';
|
||||
|
||||
export interface AlbumBaseDTO {
|
||||
id: number;
|
||||
name: string;
|
||||
preview: PreviewPhotoDTO;
|
||||
}
|
11
src/common/entities/album/SavedSearchDTO.ts
Normal file
11
src/common/entities/album/SavedSearchDTO.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import {AlbumBaseDTO} from './AlbumBaseDTO';
|
||||
import {PreviewPhotoDTO} from '../PhotoDTO';
|
||||
import {SearchQueryDTO} from '../SearchQueryDTO';
|
||||
|
||||
export interface SavedSearchDTO extends AlbumBaseDTO {
|
||||
id: number;
|
||||
name: string;
|
||||
preview: PreviewPhotoDTO;
|
||||
|
||||
searchQuery: SearchQueryDTO;
|
||||
}
|
176
test/backend/unit/model/sql/AlbumManager.ts
Normal file
176
test/backend/unit/model/sql/AlbumManager.ts
Normal file
@ -0,0 +1,176 @@
|
||||
import {DBTestHelper} from '../../../DBTestHelper';
|
||||
import {DirectoryDTO} from '../../../../../src/common/entities/DirectoryDTO';
|
||||
import {TestHelper} from './TestHelper';
|
||||
import {ObjectManagers} from '../../../../../src/backend/model/ObjectManagers';
|
||||
import {PhotoDTO, PhotoMetadata} from '../../../../../src/common/entities/PhotoDTO';
|
||||
import {VideoDTO} from '../../../../../src/common/entities/VideoDTO';
|
||||
import {AlbumManager} from '../../../../../src/backend/model/database/sql/AlbumManager';
|
||||
import {SearchQueryTypes, TextSearch} from '../../../../../src/common/entities/SearchQueryDTO';
|
||||
import {SQLConnection} from '../../../../../src/backend/model/database/sql/SQLConnection';
|
||||
import {AlbumBaseEntity} from '../../../../../src/backend/model/database/sql/enitites/album/AlbumBaseEntity';
|
||||
import {Utils} from '../../../../../src/common/Utils';
|
||||
import {MediaDTO} from '../../../../../src/common/entities/MediaDTO';
|
||||
|
||||
|
||||
const deepEqualInAnyOrder = require('deep-equal-in-any-order');
|
||||
const chai = require('chai');
|
||||
|
||||
chai.use(deepEqualInAnyOrder);
|
||||
const {expect} = chai;
|
||||
|
||||
// to help WebStorm to handle the test cases
|
||||
declare let describe: any;
|
||||
declare const after: any;
|
||||
declare const before: any;
|
||||
const tmpDescribe = describe;
|
||||
describe = DBTestHelper.describe(); // fake it os IDE plays nicely (recognize the test)
|
||||
|
||||
|
||||
describe('AlbumManager', (sqlHelper: DBTestHelper) => {
|
||||
describe = tmpDescribe;
|
||||
/**
|
||||
* dir
|
||||
* |- v
|
||||
* |- p
|
||||
* |- p2
|
||||
* |-> subDir
|
||||
* |- p3
|
||||
* |-> subDir2
|
||||
* |- p4
|
||||
*/
|
||||
|
||||
let dir: DirectoryDTO;
|
||||
let subDir: DirectoryDTO;
|
||||
let subDir2: DirectoryDTO;
|
||||
let v: VideoDTO;
|
||||
let p: PhotoDTO;
|
||||
let p2: PhotoDTO;
|
||||
let p3: PhotoDTO;
|
||||
let p4: PhotoDTO;
|
||||
|
||||
|
||||
const setUpTestGallery = async (): Promise<void> => {
|
||||
const directory: DirectoryDTO = TestHelper.getDirectoryEntry();
|
||||
subDir = TestHelper.getDirectoryEntry(directory, 'The Phantom Menace');
|
||||
subDir2 = TestHelper.getDirectoryEntry(directory, 'Return of the Jedi');
|
||||
p = TestHelper.getRandomizedPhotoEntry(directory, 'Photo1');
|
||||
p2 = TestHelper.getRandomizedPhotoEntry(directory, 'Photo2');
|
||||
p3 = TestHelper.getRandomizedPhotoEntry(subDir, 'Photo3');
|
||||
p4 = TestHelper.getRandomizedPhotoEntry(subDir2, 'Photo4');
|
||||
v = TestHelper.getVideoEntry1(directory);
|
||||
|
||||
dir = await DBTestHelper.persistTestDir(directory);
|
||||
subDir = dir.directories[0];
|
||||
subDir2 = dir.directories[1];
|
||||
p = (dir.media.filter(m => m.name === p.name)[0] as any);
|
||||
p2 = (dir.media.filter(m => m.name === p2.name)[0] as any);
|
||||
v = (dir.media.filter(m => m.name === v.name)[0] as any);
|
||||
p3 = (dir.directories[0].media[0] as any);
|
||||
p4 = (dir.directories[1].media[0] as any);
|
||||
};
|
||||
|
||||
const setUpSqlDB = async () => {
|
||||
await sqlHelper.initDB();
|
||||
await setUpTestGallery();
|
||||
await ObjectManagers.InitSQLManagers();
|
||||
};
|
||||
|
||||
|
||||
const toAlbumPreview = (m: MediaDTO): MediaDTO => {
|
||||
const tmpM = m.directory.media;
|
||||
const tmpD = m.directory.directories;
|
||||
const tmpP = m.directory.preview;
|
||||
const tmpMT = m.directory.metaFile;
|
||||
delete m.directory.directories;
|
||||
delete m.directory.media;
|
||||
delete m.directory.preview;
|
||||
delete m.directory.metaFile;
|
||||
const ret = Utils.clone(m);
|
||||
delete (ret.metadata as PhotoMetadata).faces;
|
||||
m.directory.directories = tmpD;
|
||||
m.directory.media = tmpM;
|
||||
m.directory.preview = tmpP;
|
||||
m.directory.metaFile = tmpMT;
|
||||
return ret;
|
||||
};
|
||||
|
||||
|
||||
before(setUpSqlDB);
|
||||
after(sqlHelper.clearDB);
|
||||
|
||||
describe('Saved search', () => {
|
||||
|
||||
|
||||
beforeEach(setUpSqlDB);
|
||||
afterEach(sqlHelper.clearDB);
|
||||
|
||||
it('should add album', async () => {
|
||||
const am = new AlbumManager();
|
||||
const connection = await SQLConnection.getConnection();
|
||||
|
||||
const query: TextSearch = {text: 'test', type: SearchQueryTypes.any_text};
|
||||
|
||||
expect(await connection.getRepository(AlbumBaseEntity).find()).to.deep.equalInAnyOrder([]);
|
||||
|
||||
await am.addSavedSearch('Test Album', Utils.clone(query));
|
||||
|
||||
expect(await connection.getRepository(AlbumBaseEntity).find()).to.deep.equalInAnyOrder([{
|
||||
id: 1,
|
||||
name: 'Test Album',
|
||||
searchQuery: query
|
||||
}]);
|
||||
});
|
||||
|
||||
it('should delete album', async () => {
|
||||
const am = new AlbumManager();
|
||||
const connection = await SQLConnection.getConnection();
|
||||
|
||||
const query: TextSearch = {text: 'test', type: SearchQueryTypes.any_text};
|
||||
|
||||
|
||||
await am.addSavedSearch('Test Album', Utils.clone(query));
|
||||
await am.addSavedSearch('Test Album2', Utils.clone(query));
|
||||
|
||||
expect(await connection.getRepository(AlbumBaseEntity).find()).to.deep.equalInAnyOrder([
|
||||
{
|
||||
id: 1,
|
||||
name: 'Test Album',
|
||||
searchQuery: query
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Test Album2',
|
||||
searchQuery: query
|
||||
}]);
|
||||
|
||||
await am.deleteAlbum(1);
|
||||
expect(await connection.getRepository(AlbumBaseEntity).find()).to.deep.equalInAnyOrder([{
|
||||
id: 2,
|
||||
name: 'Test Album2',
|
||||
searchQuery: query
|
||||
}]);
|
||||
|
||||
await am.deleteAlbum(2);
|
||||
expect(await connection.getRepository(AlbumBaseEntity).find()).to.deep.equalInAnyOrder([]);
|
||||
});
|
||||
});
|
||||
|
||||
it('should list album', async () => {
|
||||
const am = new AlbumManager();
|
||||
|
||||
const query: TextSearch = {text: 'photo1', type: SearchQueryTypes.any_text};
|
||||
|
||||
await am.addSavedSearch('Test Album', Utils.clone(query));
|
||||
|
||||
expect(await am.getAlbums()).to.deep.equalInAnyOrder(([{
|
||||
id: 1,
|
||||
name: 'Test Album',
|
||||
searchQuery: query,
|
||||
preview: toAlbumPreview(p)
|
||||
}]));
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
||||
});
|
Loading…
Reference in New Issue
Block a user