Skip to content

Commit e2aa496

Browse files
authored
Merge pull request #8 from LooseWireDev/fix/buffer-cached-responses
Optimize queries with slim hydration and denormalized tag counts
2 parents 234d24f + 47170d0 commit e2aa496

3 files changed

Lines changed: 146 additions & 77 deletions

File tree

db/queries.ts

Lines changed: 133 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type { SourceType, TagType } from "./schema";
1010
import {
1111
alternatives,
1212
appDownloads,
13+
appSources,
1314
apps,
1415
appTags,
1516
comparisonPairs,
@@ -41,6 +42,83 @@ function desktopOnlyAppIds(db: DrizzleDB) {
4142
);
4243
}
4344

45+
// ─── Slim List Helper ────────────────────────────────────────────────
46+
// List pages only need card-relevant fields. Loading full sources
47+
// (with metadata/packageName) and all tags wastes rows.
48+
49+
const appCardColumns = {
50+
id: apps.id,
51+
name: apps.name,
52+
slug: apps.slug,
53+
description: apps.description,
54+
iconUrl: apps.iconUrl,
55+
};
56+
57+
async function withSlimRelations(
58+
db: DrizzleDB,
59+
appRows: { id: string }[],
60+
): Promise<
61+
{
62+
id: string;
63+
name: string;
64+
slug: string;
65+
description: string | null;
66+
iconUrl: string | null;
67+
sources: { source: string; url: string }[];
68+
tags: { name: string; slug: string; type: string }[];
69+
}[]
70+
> {
71+
if (appRows.length === 0) return [];
72+
73+
const appIds = appRows.map((a) => a.id);
74+
75+
const [sources, platformTags] = await Promise.all([
76+
db
77+
.select({
78+
appId: appSources.appId,
79+
source: appSources.source,
80+
url: appSources.url,
81+
})
82+
.from(appSources)
83+
.where(inArray(appSources.appId, appIds)),
84+
db
85+
.select({
86+
appId: appTags.appId,
87+
name: tags.name,
88+
slug: tags.slug,
89+
type: tags.type,
90+
})
91+
.from(appTags)
92+
.innerJoin(tags, eq(appTags.tagId, tags.id))
93+
.where(and(inArray(appTags.appId, appIds), eq(tags.type, "platform"))),
94+
]);
95+
96+
const sourcesByApp = new Map<string, { source: string; url: string }[]>();
97+
for (const s of sources) {
98+
const arr = sourcesByApp.get(s.appId) ?? [];
99+
arr.push({ source: s.source, url: s.url });
100+
sourcesByApp.set(s.appId, arr);
101+
}
102+
103+
const tagsByApp = new Map<
104+
string,
105+
{ name: string; slug: string; type: string }[]
106+
>();
107+
for (const t of platformTags) {
108+
const arr = tagsByApp.get(t.appId) ?? [];
109+
arr.push({ name: t.name, slug: t.slug, type: t.type });
110+
tagsByApp.set(t.appId, arr);
111+
}
112+
113+
return (
114+
appRows as ((typeof appRows)[number] & Record<string, unknown>)[]
115+
).map((app) => ({
116+
...(app as any),
117+
sources: sourcesByApp.get(app.id) ?? [],
118+
tags: tagsByApp.get(app.id) ?? [],
119+
}));
120+
}
121+
44122
// ─── Types ──────────────────────────────────────────────────────────
45123

46124
export type AppWithDetails = Awaited<ReturnType<typeof getAppBySlug>>;
@@ -79,18 +157,15 @@ export async function listApps(
79157
conditions.push(inArray(apps.id, appIdsWithAllTags));
80158
}
81159

82-
const results = await db.query.apps.findMany({
83-
where: and(...conditions),
84-
with: { sources: true, tags: { with: { tag: true } } },
85-
limit,
86-
offset,
87-
orderBy: apps.name,
88-
});
160+
const rows = await db
161+
.select(appCardColumns)
162+
.from(apps)
163+
.where(and(...conditions))
164+
.limit(limit)
165+
.offset(offset)
166+
.orderBy(apps.name);
89167

90-
return results.map((app) => ({
91-
...app,
92-
tags: app.tags.map((at) => at.tag),
93-
}));
168+
return withSlimRelations(db, rows);
94169
}
95170

96171
export async function getAppBySlug(db: DrizzleDB, slug: string) {
@@ -200,20 +275,16 @@ export async function listTagsByType(db: DrizzleDB, type: TagType) {
200275
}
201276

202277
export async function listCategoriesWithApps(db: DrizzleDB) {
203-
const rows = await db
278+
return db
204279
.select({
205280
id: tags.id,
206281
name: tags.name,
207282
slug: tags.slug,
208283
type: tags.type,
209284
})
210285
.from(tags)
211-
.innerJoin(appTags, eq(tags.id, appTags.tagId))
212-
.where(eq(tags.type, "category"))
213-
.groupBy(tags.id)
286+
.where(and(eq(tags.type, "category"), sql`${tags.appCount} > 0`))
214287
.orderBy(tags.name);
215-
216-
return rows;
217288
}
218289

219290
// ─── Tag / Category Page Queries ────────────────────────────────────
@@ -265,18 +336,15 @@ export async function listAppsByTag(
265336
.from(appTags)
266337
.where(eq(appTags.tagId, tagRow[0].id));
267338

268-
const results = await db.query.apps.findMany({
269-
where: inArray(apps.id, appIdsWithTag),
270-
with: { sources: true, tags: { with: { tag: true } } },
271-
limit,
272-
offset,
273-
orderBy: apps.name,
274-
});
339+
const rows = await db
340+
.select(appCardColumns)
341+
.from(apps)
342+
.where(inArray(apps.id, appIdsWithTag))
343+
.limit(limit)
344+
.offset(offset)
345+
.orderBy(apps.name);
275346

276-
return results.map((app) => ({
277-
...app,
278-
tags: app.tags.map((at) => at.tag),
279-
}));
347+
return withSlimRelations(db, rows);
280348
}
281349

282350
export async function listTagsWithCounts(db: DrizzleDB, type?: TagType) {
@@ -288,12 +356,10 @@ export async function listTagsWithCounts(db: DrizzleDB, type?: TagType) {
288356
name: tags.name,
289357
slug: tags.slug,
290358
type: tags.type,
291-
appCount: sql<number>`count(${appTags.appId})`,
359+
appCount: tags.appCount,
292360
})
293361
.from(tags)
294-
.leftJoin(appTags, eq(tags.id, appTags.tagId))
295362
.where(conditions.length ? and(...conditions) : undefined)
296-
.groupBy(tags.id)
297363
.orderBy(tags.name);
298364
}
299365

@@ -302,16 +368,18 @@ export async function listTagsWithCounts(db: DrizzleDB, type?: TagType) {
302368
export async function searchApps(db: DrizzleDB, query: string) {
303369
const pattern = `%${query}%`;
304370

305-
const [appResults, propResults] = await Promise.all([
306-
db.query.apps.findMany({
307-
where: and(
308-
like(apps.name, pattern),
309-
notInArray(apps.id, desktopOnlyAppIds(db)),
310-
),
311-
with: { sources: true, tags: { with: { tag: true } } },
312-
limit: 20,
313-
orderBy: apps.name,
314-
}),
371+
const [appRows, propResults] = await Promise.all([
372+
db
373+
.select(appCardColumns)
374+
.from(apps)
375+
.where(
376+
and(
377+
like(apps.name, pattern),
378+
notInArray(apps.id, desktopOnlyAppIds(db)),
379+
),
380+
)
381+
.limit(20)
382+
.orderBy(apps.name),
315383
db
316384
.select()
317385
.from(proprietaryApps)
@@ -321,28 +389,22 @@ export async function searchApps(db: DrizzleDB, query: string) {
321389
]);
322390

323391
return {
324-
apps: appResults.map((app) => ({
325-
...app,
326-
tags: app.tags.map((at) => at.tag),
327-
})),
392+
apps: await withSlimRelations(db, appRows),
328393
proprietaryApps: propResults,
329394
};
330395
}
331396

332397
// ─── Discovery Queries ──────────────────────────────────────────────
333398

334399
export async function getRecentApps(db: DrizzleDB) {
335-
const results = await db.query.apps.findMany({
336-
where: notInArray(apps.id, desktopOnlyAppIds(db)),
337-
with: { sources: true, tags: { with: { tag: true } } },
338-
orderBy: sql`${apps.createdAt} desc`,
339-
limit: 20,
340-
});
400+
const rows = await db
401+
.select(appCardColumns)
402+
.from(apps)
403+
.where(notInArray(apps.id, desktopOnlyAppIds(db)))
404+
.orderBy(sql`${apps.createdAt} desc`)
405+
.limit(20);
341406

342-
return results.map((app) => ({
343-
...app,
344-
tags: app.tags.map((at) => at.tag),
345-
}));
407+
return withSlimRelations(db, rows);
346408
}
347409

348410
// ─── Desktop App Queries ────────────────────────────────────────────
@@ -366,18 +428,15 @@ export async function listDesktopApps(
366428
.where(inArray(appTags.tagId, desktopTagIds))
367429
.groupBy(appTags.appId);
368430

369-
const results = await db.query.apps.findMany({
370-
where: inArray(apps.id, appsWithDesktopTag),
371-
with: { sources: true, tags: { with: { tag: true } } },
372-
limit,
373-
offset,
374-
orderBy: apps.name,
375-
});
431+
const rows = await db
432+
.select(appCardColumns)
433+
.from(apps)
434+
.where(inArray(apps.id, appsWithDesktopTag))
435+
.limit(limit)
436+
.offset(offset)
437+
.orderBy(apps.name);
376438

377-
return results.map((app) => ({
378-
...app,
379-
tags: app.tags.map((at) => at.tag),
380-
}));
439+
return withSlimRelations(db, rows);
381440
}
382441

383442
// ─── Scan Query ─────────────────────────────────────────────────────
@@ -569,18 +628,15 @@ export async function listAppsByLicense(
569628
const limit = Math.min(rawLimit, 100);
570629
const offset = (page - 1) * limit;
571630

572-
const results = await db.query.apps.findMany({
573-
where: eq(apps.license, license),
574-
with: { sources: true, tags: { with: { tag: true } } },
575-
limit,
576-
offset,
577-
orderBy: apps.name,
578-
});
631+
const rows = await db
632+
.select(appCardColumns)
633+
.from(apps)
634+
.where(eq(apps.license, license))
635+
.limit(limit)
636+
.offset(offset)
637+
.orderBy(apps.name);
579638

580-
return results.map((app) => ({
581-
...app,
582-
tags: app.tags.map((at) => at.tag),
583-
}));
639+
return withSlimRelations(db, rows);
584640
}
585641

586642
// ─── Sitemap Queries ────────────────────────────────────────────────

db/schema.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ export const tags = sqliteTable(
170170
name: text("name").notNull(),
171171
slug: text("slug").notNull(),
172172
type: text("type").$type<TagType>().notNull(),
173+
appCount: integer("app_count").notNull().default(0),
173174
},
174175
(table) => ({
175176
uniqueTag: uniqueIndex("tag_unique").on(table.slug, table.type),

db/seed/import.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,17 @@ async function upsertAlternatives() {
464464
console.log(` ${stats.alternativesCreated} alternative mappings upserted`);
465465
}
466466

467+
async function updateTagCounts() {
468+
console.log("Updating tag counts...");
469+
await client.execute(
470+
"UPDATE tags SET app_count = (SELECT COUNT(*) FROM app_tags WHERE tag_id = tags.id)",
471+
);
472+
const result = await client.execute(
473+
"SELECT COUNT(*) as total FROM tags WHERE app_count > 0",
474+
);
475+
console.log(` ${result.rows[0].total} tags with apps`);
476+
}
477+
467478
async function main() {
468479
console.log("\nSeed import");
469480
console.log("═".repeat(50));
@@ -483,6 +494,7 @@ async function main() {
483494
await upsertWebApps();
484495
await upsertProprietaryApps();
485496
await upsertAlternatives();
497+
await updateTagCounts();
486498

487499
console.log(`\n${"═".repeat(50)}`);
488500
console.log("Import complete:");

0 commit comments

Comments
 (0)