Skip to content

Commit f210cbb

Browse files
committed
adds pruning for old briefs
1 parent b3ac4bb commit f210cbb

File tree

6 files changed

+72
-9
lines changed

6 files changed

+72
-9
lines changed

README.md

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
This is a demo project showcasing a simple, well-structured backend API using Node.js, Fastify, and TypeScript. It aggregates mock data from multiple crypto-related sources — such as Farcaster posts, CoinDesk articles, and Decrypt news — and returns topic-based summaries with sentiment analysis. While the data is static for now, the architecture is built to support real external APIs.
66

7-
The API also supports persistent, database-backed briefs: snapshots of topic-based narratives and sentiment, linked to their underlying summaries via a NarrativeRecord relation. These are stored in a PostgreSQL database using Prisma. For demo purposes, daily brief creation is triggered both internally (`node-cron`) and externally (GitHub Actions) — the external trigger is a workaround for free-tier hosting limitations where in-app schedulers can’t run reliably.
7+
The API also supports persistent, database-backed briefs: snapshots of topic-based narratives and sentiment, linked to their underlying summaries via a NarrativeRecord relation. These are stored in a PostgreSQL database using Prisma. Briefs are created daily and old ones are automatically pruned (keeping only the most recent 7 for demo purposes). For demo purposes, daily brief creation is triggered both internally (`node-cron`) and externally (GitHub Actions) — the external trigger is a workaround for free-tier hosting limitations where in-app schedulers can’t run reliably.
88

99
In addition, a full CRUD Notes feature was added for demonstration purposes, showing additional DB interactions and endpoint patterns.
1010

@@ -129,14 +129,17 @@ Then run an initial migration:
129129
yarn testdb:reset
130130
```
131131

132-
## Scheduled Jobs
132+
### Scheduled Jobs
133133

134-
This project includes support for automated daily brief creation.
134+
This project includes support for automated daily brief creation and cleanup.
135135

136136
### Local/Production Setup (node-cron)
137137

138138
The codebase includes a `createBriefJob` scheduled task (via `node-cron`) that runs daily at midnight.
139-
In a normal production environment (e.g. dedicated server, container with uptime), this would automatically generate a new daily brief without external triggers.
139+
It will:
140+
141+
- Generate a new daily brief (if one doesn’t already exist).
142+
- Prune old briefs, keeping only the most recent 7 for demo purposes.
140143

141144
### Free-tier Hosting Limitation
142145

@@ -145,7 +148,7 @@ On free-tier Render, apps sleep when idle. This prevents in-app schedulers like
145148
### Workaround (External Trigger)
146149

147150
To make daily jobs run reliably even on free-tier hosting, we use GitHub Actions to call the **`/narratives`** endpoint on a schedule.
148-
This wakes the service and triggers daily brief creation externally.
151+
This wakes the service, creates a new daily brief, and prunes old ones externally.
149152

150153
---
151154

src/jobs/createBriefJob.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import cron from 'node-cron';
22

33
import { getNarratives } from '../api/index.js';
4-
import { createDailyBriefIfNotExists } from '../utils/brief.js';
4+
import { createDailyBriefIfNotExists, pruneOldBriefs } from '../utils/brief.js';
55

66
// Run every day at midnight
77
cron.schedule('0 0 * * *', async () => {
@@ -17,6 +17,11 @@ cron.schedule('0 0 * * *', async () => {
1717
console.log('Brief already exists, skipping creation');
1818
}
1919

20+
const deletedCount = await pruneOldBriefs();
21+
if (deletedCount > 0) {
22+
console.log(`Pruned ${deletedCount} old brief(s)`);
23+
}
24+
2025
console.log('Brief creation job finished');
2126
} catch (err) {
2227
console.log('Brief creation job failed', err);

src/routes/narratives.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { getNarratives } from '../api/index.js';
55
import { NarrativeSchema } from '../schemas/narrative.js';
66
import { QuerySchema } from '../schemas/query.js';
77
import { Query } from '../types/query.js';
8-
import { createDailyBriefIfNotExists } from '../utils/brief.js';
8+
import { createDailyBriefIfNotExists, pruneOldBriefs } from '../utils/brief.js';
99

1010
export default async function (fastify: FastifyInstance) {
1111
fastify.get<{ Querystring: Query }>(
@@ -38,6 +38,11 @@ export default async function (fastify: FastifyInstance) {
3838
request.log.info('Brief already exists, skipping generation');
3939
}
4040

41+
const deletedCount = await pruneOldBriefs();
42+
if (deletedCount > 0) {
43+
request.log.info({ deletedCount }, 'Old briefs pruned');
44+
}
45+
4146
return narratives;
4247
}
4348
);

src/utils/brief.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,18 @@ export async function createDailyBriefIfNotExists(
5454
throw err;
5555
}
5656
}
57+
58+
export async function pruneOldBriefs(limit: number = 7): Promise<number> {
59+
const briefs = await prisma.brief.findMany({
60+
orderBy: { createdAt: 'desc' },
61+
skip: limit,
62+
});
63+
64+
if (briefs.length > 0) {
65+
await prisma.brief.deleteMany({
66+
where: { id: { in: briefs.map(b => b.id) } },
67+
});
68+
}
69+
70+
return briefs.length;
71+
}

tests/briefUtils.test.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import { describe, it, expect, beforeEach, afterAll } from 'vitest';
1+
import { describe, it, expect } from 'vitest';
22

33
import { prisma } from '../src/lib/prisma.js';
44
import {
55
getBriefCreatedAtDate,
66
createBrief,
77
createDailyBriefIfNotExists,
8+
pruneOldBriefs,
89
} from '../src/utils/brief.js';
910
import type { Narrative } from '../src/types/narrative.js';
1011

@@ -83,4 +84,37 @@ describe('brief utils', () => {
8384
const briefs = await prisma.brief.findMany();
8485
expect(briefs.length).toBe(1);
8586
});
87+
88+
it('prunes old briefs, keeping the latest limit', async () => {
89+
// Create 10 briefs on different days
90+
const today = new Date();
91+
for (let i = 0; i < 10; i++) {
92+
const briefDate = new Date(today);
93+
briefDate.setUTCDate(today.getUTCDate() - i);
94+
await prisma.brief.create({
95+
data: {
96+
createdAt: briefDate,
97+
narratives: {
98+
create: [{ topic: `topic${i}`, sentiment: 'neutral' }],
99+
},
100+
},
101+
});
102+
}
103+
104+
// Limit to 7 latest briefs
105+
const deletedCount = await pruneOldBriefs(7);
106+
107+
expect(deletedCount).toBe(3); // 10 - 7 = 3 should be deleted
108+
109+
const remainingBriefs = await prisma.brief.findMany({
110+
orderBy: { createdAt: 'desc' },
111+
});
112+
expect(remainingBriefs.length).toBe(7);
113+
114+
// Ensure the newest dates remain
115+
const newestDate = remainingBriefs[0].createdAt;
116+
const oldestDate = remainingBriefs[6].createdAt;
117+
expect(newestDate.getUTCDate()).toBe(today.getUTCDate());
118+
expect(oldestDate.getUTCDate()).toBe(today.getUTCDate() - 6);
119+
});
86120
});

tests/narratives.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { TypeBoxTypeProvider } from '@fastify/type-provider-typebox';
55
import envSetup from '../src/plugins/env.js';
66
import corsSetup from '../src/plugins/cors.js';
77
import narrativesRoutes from '../src/routes/narratives.js';
8+
import { Narrative } from '../src/types/narrative.js';
89

910
describe('GET /narratives', () => {
1011
let app: ReturnType<typeof Fastify>;
@@ -42,7 +43,7 @@ describe('GET /narratives', () => {
4243
});
4344

4445
expect(response.statusCode).toBe(200);
45-
const result = await response.json();
46+
const result: Narrative[] = await response.json();
4647

4748
expect(Array.isArray(result)).toBe(true);
4849
result.forEach(item => {

0 commit comments

Comments
 (0)