• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

bpatrik / pigallery2 / 17685697493

12 Sep 2025 08:48PM UTC coverage: 65.828% (+1.7%) from 64.102%
17685697493

push

github

web-flow
Merge pull request #1030 from bpatrik/db-projection

Implement projected (scoped/filtered) gallery. Fixes #1015

1305 of 2243 branches covered (58.18%)

Branch coverage included in aggregate %.

706 of 873 new or added lines in 54 files covered. (80.87%)

16 existing lines in 10 files now uncovered.

4794 of 7022 relevant lines covered (68.27%)

4355.01 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

85.37
/src/backend/model/database/AlbumManager.ts
1
import {SQLConnection} from './SQLConnection';
1✔
2
import {AlbumBaseEntity} from './enitites/album/AlbumBaseEntity';
1✔
3
import {AlbumBaseDTO} from '../../../common/entities/album/AlbumBaseDTO';
4
import {ObjectManagers} from '../ObjectManagers';
1✔
5
import {SearchQueryDTO} from '../../../common/entities/SearchQueryDTO';
6
import {SavedSearchEntity} from './enitites/album/SavedSearchEntity';
1✔
7
import {Logger} from '../../Logger';
1✔
8
import {SessionContext} from '../SessionContext';
9
import {ProjectedAlbumCacheEntity} from './enitites/album/ProjectedAlbumCacheEntity';
1✔
10
import {ProjectionAwareManager} from './ProjectionAwareManager';
1✔
11

12
const LOG_TAG = '[AlbumManager]';
1✔
13

14
export class AlbumManager extends ProjectionAwareManager<AlbumBaseEntity> {
1✔
15

16
  public async addIfNotExistSavedSearch(
17
    name: string,
18
    searchQuery: SearchQueryDTO,
19
    lockedAlbum: boolean
20
  ): Promise<void> {
21
    const connection = await SQLConnection.getConnection();
8✔
22
    const album = await connection
8✔
23
      .getRepository(SavedSearchEntity)
24
      .findOneBy({name, searchQuery});
25
    if (album) {
8!
26
      return;
×
27
    }
28
    await this.addSavedSearch(name, searchQuery, lockedAlbum);
8✔
29
  }
30

31
  public async addSavedSearch(
32
    name: string,
33
    searchQuery: SearchQueryDTO,
34
    lockedAlbum?: boolean
35
  ): Promise<void> {
36
    const connection = await SQLConnection.getConnection();
32✔
37
    await connection
32✔
38
      .getRepository(SavedSearchEntity)
39
      .save({name, searchQuery, locked: lockedAlbum});
40
  }
41

42
  public async deleteAlbum(id: number): Promise<void> {
43
    const connection = await SQLConnection.getConnection();
4✔
44

45
    if (
4✔
46
      (await connection
47
        .getRepository(AlbumBaseEntity)
48
        .countBy({id, locked: false})) !== 1
49
    ) {
50
      throw new Error('Could not delete album, id:' + id);
2✔
51
    }
52

53
    await connection
2✔
54
      .getRepository(AlbumBaseEntity)
55
      .delete({id, locked: false});
56
  }
57

58

59
  protected async loadEntities(session: SessionContext): Promise<AlbumBaseEntity[]> {
60
    Logger.debug(LOG_TAG, 'loadEntities called for projection key:', session.user.projectionKey);
22✔
61
    await this.updateAlbums(session);
22✔
62
    const connection = await SQLConnection.getConnection();
22✔
63

64
    // Return albums with projected cache data
65
    const result = await connection
22✔
66
      .getRepository(AlbumBaseEntity)
67
      .createQueryBuilder('album')
68
      .leftJoin('album.cache', 'cache', 'cache.projectionKey = :pk AND cache.valid = 1', {pk: session.user.projectionKey})
69
      .leftJoin('cache.cover', 'cover')
70
      .leftJoin('cover.directory', 'directory')
71
      .select(['album', 'cache', 'cover.name', 'directory.name', 'directory.path'])
72
      .getMany();
73

74
    Logger.debug(LOG_TAG, 'loadEntities returning', result.length, 'albums');
22✔
75
    return result;
22✔
76
  }
77

78
  protected async invalidateDBCache(): Promise<void> {
79
    // Invalidate all album cache entries
80
    const connection = await SQLConnection.getConnection();
100✔
81
    await connection.getRepository(ProjectedAlbumCacheEntity)
100✔
82
      .createQueryBuilder()
83
      .update()
84
      .set({valid: false})
85
      .execute();
86
  }
87

88
  async deleteAll() {
NEW
89
    const connection = await SQLConnection.getConnection();
×
NEW
90
    await connection
×
91
      .getRepository(AlbumBaseEntity)
92
      .createQueryBuilder('album')
93
      .delete()
94
      .execute();
95
  }
96

97
  private async updateAlbums(session: SessionContext): Promise<void> {
98
    Logger.debug(LOG_TAG, 'Updating derived album data');
22✔
99
    const connection = await SQLConnection.getConnection();
22✔
100
    const albums = await connection
22✔
101
      .getRepository(SavedSearchEntity)
102
      .createQueryBuilder('album')
103
      .leftJoinAndSelect('album.cache', 'cache', 'cache.projectionKey = :pk AND cache.valid = 1', {pk: session.user.projectionKey})
104
      .getMany();
105

106
    for (const a of albums) {
22✔
107
      if (a.cache?.valid === true) {
34!
NEW
108
        continue;
×
109
      }
110
      await ObjectManagers.getInstance().ProjectedCacheManager
34✔
111
        .setAndGetCacheForAlbum(connection, session, {
112
          id: a.id,
113
          searchQuery: a.searchQuery
114
        });
115
      // giving back the control to the main event loop (Macrotask queue)
116
      // https://blog.insiderattack.net/promises-next-ticks-and-immediates-nodejs-event-loop-part-3-9226cbe7a6aa
117
      await new Promise(setImmediate);
34✔
118
    }
119
  }
120

121
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc