You've already forked pigallery2
							
							
				mirror of
				https://github.com/bpatrik/pigallery2.git
				synced 2025-10-30 23:57:43 +02:00 
			
		
		
		
	Implementing AlbumBase and SavedSearch Entities and Manager #45
This commit is contained in:
		
							
								
								
									
										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) | ||||
|     }])); | ||||
|  | ||||
|  | ||||
|   }); | ||||
|  | ||||
|  | ||||
| }); | ||||
		Reference in New Issue
	
	Block a user