Skip to content

Commit 6b48fe7

Browse files
committed
refactor: simplify FeaturedGalleriesService and remove TenantModule dependency
- Removed the TenantModule import from the FeaturedGalleriesModule, streamlining the module structure. - Refactored the FeaturedGalleriesService to eliminate the TenantService dependency, enhancing service independence. - Updated the logic in listFeaturedGalleries to calculate quality scores for tenants directly from the database, improving performance and clarity. Signed-off-by: Innei <[email protected]>
1 parent 59c647e commit 6b48fe7

File tree

2 files changed

+160
-95
lines changed

2 files changed

+160
-95
lines changed

be/apps/core/src/modules/platform/featured-galleries/featured-galleries.module.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import { Module } from '@afilmory/framework'
22
import { DatabaseModule } from 'core/database/database.module'
33

4-
import { TenantModule } from '../tenant/tenant.module'
54
import { FeaturedGalleriesController } from './featured-galleries.controller'
65
import { FeaturedGalleriesService } from './featured-galleries.service'
76

87
@Module({
9-
imports: [DatabaseModule, TenantModule],
8+
imports: [DatabaseModule],
109
controllers: [FeaturedGalleriesController],
1110
providers: [FeaturedGalleriesService],
1211
})

be/apps/core/src/modules/platform/featured-galleries/featured-galleries.service.ts

Lines changed: 159 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -1,81 +1,147 @@
1-
import { authUsers, photoAssets, settings, tenantDomains } from '@afilmory/db'
1+
import { authUsers, photoAssets, settings, tenantDomains, tenants } from '@afilmory/db'
22
import { DbAccessor } from 'core/database/database.provider'
33
import { normalizeDate } from 'core/helpers/normalize.helper'
44
import { and, asc, eq, inArray, sql } from 'drizzle-orm'
55
import { injectable } from 'tsyringe'
66

7-
import { TenantService } from '../tenant/tenant.service'
8-
97
@injectable()
108
export class FeaturedGalleriesService {
11-
constructor(
12-
private readonly tenantService: TenantService,
13-
private readonly dbAccessor: DbAccessor,
14-
) {}
9+
constructor(private readonly dbAccessor: DbAccessor) {}
1510

1611
async listFeaturedGalleries() {
17-
const aggregates = await this.tenantService.listTenants()
12+
const db = this.dbAccessor.get()
1813

19-
// Filter out banned, inactive, and suspended tenants
20-
const validTenants = aggregates
21-
.filter((aggregate) => {
22-
const { tenant } = aggregate
23-
return !tenant.banned && tenant.status === 'active' && tenant.slug !== 'root' && tenant.slug !== 'placeholder'
24-
})
25-
.slice(0, 20) // Limit to 20 most recent
14+
// Step 1: Calculate quality scores for all valid tenants with photos
15+
// Quality score formula:
16+
// - Photo count: 1 point per photo
17+
// - Total size: 0.1 points per MB (indicates high quality images)
18+
// - EXIF info: 2 points per photo with EXIF data (indicates professional shooting)
19+
// - Unique tags: 5 points per unique tag (indicates content diversity)
20+
// - GPS info: 1 point per photo with GPS (indicates complete metadata)
21+
const qualityScores = await db.execute<{
22+
tenant_id: string
23+
photo_count: number
24+
total_size_bytes: number
25+
exif_count: number
26+
unique_tag_count: number
27+
gps_count: number
28+
quality_score: number
29+
}>(sql`
30+
with tenant_quality as (
31+
select
32+
${photoAssets.tenantId} as tenant_id,
33+
count(*)::int as photo_count,
34+
coalesce(sum(${photoAssets.size}), 0)::bigint as total_size_bytes,
35+
count(case when ${photoAssets.manifest}->'data'->'exif'->>'Make' is not null
36+
and ${photoAssets.manifest}->'data'->'exif'->>'Make' != '' then 1 end)::int as exif_count,
37+
count(case when ${photoAssets.manifest}->'data'->'exif'->'GPSLatitude' is not null
38+
or ${photoAssets.manifest}->'data'->'exif'->'GPSLongitude' is not null then 1 end)::int as gps_count
39+
from ${photoAssets}
40+
where ${photoAssets.syncStatus} in ('synced', 'conflict')
41+
group by ${photoAssets.tenantId}
42+
),
43+
tenant_tags as (
44+
select
45+
${photoAssets.tenantId} as tenant_id,
46+
count(distinct tag)::int as unique_tag_count
47+
from ${photoAssets},
48+
lateral jsonb_array_elements_text(${photoAssets.manifest}->'data'->'tags') as tag
49+
where ${photoAssets.syncStatus} in ('synced', 'conflict')
50+
and nullif(trim(tag), '') is not null
51+
group by ${photoAssets.tenantId}
52+
),
53+
tenant_scores as (
54+
select
55+
tq.tenant_id,
56+
tq.photo_count,
57+
tq.total_size_bytes,
58+
tq.exif_count,
59+
coalesce(tt.unique_tag_count, 0) as unique_tag_count,
60+
tq.gps_count,
61+
-- Quality score calculation
62+
(tq.photo_count * 1.0 + -- Photo count: 1 point each
63+
(tq.total_size_bytes / 1024.0 / 1024.0) * 0.1 + -- Size: 0.1 points per MB
64+
tq.exif_count * 2.0 + -- EXIF: 2 points each
65+
coalesce(tt.unique_tag_count, 0) * 5.0 + -- Tags: 5 points each
66+
tq.gps_count * 1.0) as quality_score -- GPS: 1 point each
67+
from tenant_quality tq
68+
left join tenant_tags tt on tq.tenant_id = tt.tenant_id
69+
where tq.photo_count > 0
70+
)
71+
select * from tenant_scores
72+
order by quality_score desc
73+
limit 20
74+
`)
2675

27-
const tenantIds = validTenants.map((aggregate) => aggregate.tenant.id)
28-
if (tenantIds.length === 0) {
76+
if (qualityScores.rows.length === 0) {
2977
return { galleries: [] }
3078
}
3179

32-
const db = this.dbAccessor.get()
33-
34-
// Fetch site settings for all tenants
35-
const siteSettings = await db
80+
const topTenantIds = qualityScores.rows.map((row) => row.tenant_id)
81+
const scoreMap = new Map(
82+
qualityScores.rows.map((row) => [
83+
row.tenant_id,
84+
{
85+
photoCount: row.photo_count,
86+
totalSizeBytes: row.total_size_bytes,
87+
exifCount: row.exif_count,
88+
uniqueTagCount: row.unique_tag_count,
89+
gpsCount: row.gps_count,
90+
qualityScore: row.quality_score,
91+
},
92+
]),
93+
)
94+
95+
// Step 2: Fetch tenant basic info
96+
const tenantRecords = await db
3697
.select()
37-
.from(settings)
38-
.where(and(inArray(settings.tenantId, tenantIds), inArray(settings.key, ['site.name', 'site.description'])))
39-
40-
// Fetch primary author (admin) for each tenant
41-
const authors = await db
42-
.select({
43-
tenantId: authUsers.tenantId,
44-
name: authUsers.name,
45-
image: authUsers.image,
46-
})
47-
.from(authUsers)
48-
.where(inArray(authUsers.tenantId, tenantIds))
49-
.orderBy(
50-
sql`case when ${authUsers.role} = 'admin' then 0 when ${authUsers.role} = 'superadmin' then 1 else 2 end`,
51-
asc(authUsers.createdAt),
52-
)
98+
.from(tenants)
99+
.where(and(inArray(tenants.id, topTenantIds), eq(tenants.banned, false), eq(tenants.status, 'active')))
53100

54-
// Fetch verified domains for all tenants
55-
const domains = await db
56-
.select({
57-
tenantId: tenantDomains.tenantId,
58-
domain: tenantDomains.domain,
59-
})
60-
.from(tenantDomains)
61-
.where(and(inArray(tenantDomains.tenantId, tenantIds), eq(tenantDomains.status, 'verified')))
62-
63-
// Fetch photo counts for all tenants (only synced/conflict photos)
64-
const photoCounts = await db
65-
.select({
66-
tenantId: photoAssets.tenantId,
67-
count: sql<number>`count(*)::int`,
68-
})
69-
.from(photoAssets)
70-
.where(and(inArray(photoAssets.tenantId, tenantIds), inArray(photoAssets.syncStatus, ['synced', 'conflict'])))
71-
.groupBy(photoAssets.tenantId)
101+
// Filter out root and placeholder
102+
const validTenants = tenantRecords.filter((t) => t.slug !== 'root' && t.slug !== 'placeholder')
72103

73-
// Fetch popular tags for all tenants
74-
// This query extracts tags from manifest JSONB and counts them per tenant
75-
// Process tags per tenant to ensure proper SQL parameterization
76-
const tagMap = new Map<string, string[]>()
104+
if (validTenants.length === 0) {
105+
return { galleries: [] }
106+
}
107+
108+
const finalTenantIds = validTenants.map((t) => t.id)
109+
110+
// Step 3: Fetch all related data in parallel
111+
const [siteSettings, authors, domains] = await Promise.all([
112+
// Site settings
113+
db
114+
.select()
115+
.from(settings)
116+
.where(
117+
and(inArray(settings.tenantId, finalTenantIds), inArray(settings.key, ['site.name', 'site.description'])),
118+
),
119+
// Primary authors
120+
db
121+
.select({
122+
tenantId: authUsers.tenantId,
123+
name: authUsers.name,
124+
image: authUsers.image,
125+
})
126+
.from(authUsers)
127+
.where(inArray(authUsers.tenantId, finalTenantIds))
128+
.orderBy(
129+
sql`case when ${authUsers.role} = 'admin' then 0 when ${authUsers.role} = 'superadmin' then 1 else 2 end`,
130+
asc(authUsers.createdAt),
131+
),
132+
// Verified domains
133+
db
134+
.select({
135+
tenantId: tenantDomains.tenantId,
136+
domain: tenantDomains.domain,
137+
})
138+
.from(tenantDomains)
139+
.where(and(inArray(tenantDomains.tenantId, finalTenantIds), eq(tenantDomains.status, 'verified'))),
140+
])
77141

78-
for (const tenantId of tenantIds) {
142+
// Step 4: Fetch popular tags for top tenants (batch query)
143+
const tagMap = new Map<string, string[]>()
144+
for (const tenantId of finalTenantIds) {
79145
const tagsResult = await db.execute<{ tag: string | null; count: number | null }>(sql`
80146
select tag, count(*)::int as count
81147
from (
@@ -102,7 +168,7 @@ export class FeaturedGalleriesService {
102168
}
103169
}
104170

105-
// Build maps for quick lookup
171+
// Step 5: Build lookup maps
106172
const settingsMap = new Map<string, Map<string, string | null>>()
107173
for (const setting of siteSettings) {
108174
if (!settingsMap.has(setting.tenantId)) {
@@ -123,43 +189,43 @@ export class FeaturedGalleriesService {
123189

124190
const domainMap = new Map<string, string>()
125191
for (const domain of domains) {
126-
// Use the first verified domain for each tenant
127192
if (!domainMap.has(domain.tenantId)) {
128193
domainMap.set(domain.tenantId, domain.domain)
129194
}
130195
}
131196

132-
const photoCountMap = new Map<string, number>()
133-
for (const count of photoCounts) {
134-
photoCountMap.set(count.tenantId, Number(count.count ?? 0))
135-
}
136-
137-
// Build response
138-
const featuredGalleries = validTenants.map((aggregate) => {
139-
const { tenant } = aggregate
140-
const tenantSettings = settingsMap.get(tenant.id) ?? new Map()
141-
const author = authorMap.get(tenant.id)
142-
const domain = domainMap.get(tenant.id)
143-
const photoCount = photoCountMap.get(tenant.id) ?? 0
144-
const tags = tagMap.get(tenant.id) ?? []
145-
146-
return {
147-
id: tenant.id,
148-
name: tenantSettings.get('site.name') ?? tenant.name,
149-
slug: tenant.slug,
150-
domain: domain ?? null,
151-
description: tenantSettings.get('site.description') ?? null,
152-
author: author
153-
? {
154-
name: author.name,
155-
avatar: author.avatar,
156-
}
157-
: null,
158-
photoCount,
159-
tags,
160-
createdAt: normalizeDate(tenant.createdAt) ?? tenant.createdAt,
161-
}
162-
})
197+
// Step 6: Build response sorted by quality score
198+
const featuredGalleries = validTenants
199+
.map((tenant) => {
200+
const tenantSettings = settingsMap.get(tenant.id) ?? new Map()
201+
const author = authorMap.get(tenant.id)
202+
const domain = domainMap.get(tenant.id)
203+
const tags = tagMap.get(tenant.id) ?? []
204+
const score = scoreMap.get(tenant.id)
205+
206+
return {
207+
id: tenant.id,
208+
name: tenantSettings.get('site.name') ?? tenant.name,
209+
slug: tenant.slug,
210+
domain: domain ?? null,
211+
description: tenantSettings.get('site.description') ?? null,
212+
author: author
213+
? {
214+
name: author.name,
215+
avatar: author.avatar,
216+
}
217+
: null,
218+
photoCount: score?.photoCount ?? 0,
219+
tags,
220+
createdAt: normalizeDate(tenant.createdAt) ?? tenant.createdAt,
221+
}
222+
})
223+
.filter((gallery) => gallery.photoCount > 0)
224+
.sort((a, b) => {
225+
const scoreA = scoreMap.get(a.id)?.qualityScore ?? 0
226+
const scoreB = scoreMap.get(b.id)?.qualityScore ?? 0
227+
return scoreB - scoreA
228+
})
163229

164230
return {
165231
galleries: featuredGalleries,

0 commit comments

Comments
 (0)