mirror of
https://github.com/xtr-dev/rondevu-server.git
synced 2025-12-10 02:43:24 +00:00
Add startsWith filter to topics endpoint
Added optional startsWith parameter to GET /topics endpoint: - Filters topics by prefix using SQL LIKE operator - Updated storage interface and implementations (SQLite & D1) - Added query param documentation - Returns startsWith in response when used Version bumped to 0.1.1 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@xtr-dev/rondevu-server",
|
"name": "@xtr-dev/rondevu-server",
|
||||||
"version": "0.1.0",
|
"version": "0.1.1",
|
||||||
"description": "Topic-based peer discovery and signaling server for distributed P2P applications",
|
"description": "Topic-based peer discovery and signaling server for distributed P2P applications",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
11
src/app.ts
11
src/app.ts
@@ -231,22 +231,29 @@ export function createApp(storage: Storage, config: Config) {
|
|||||||
* GET /topics
|
* GET /topics
|
||||||
* List all topics with active peer counts (paginated)
|
* List all topics with active peer counts (paginated)
|
||||||
* Public endpoint (no auth required)
|
* Public endpoint (no auth required)
|
||||||
|
* Query params:
|
||||||
|
* - limit: Max topics to return (default 50, max 200)
|
||||||
|
* - offset: Number of topics to skip (default 0)
|
||||||
|
* - startsWith: Filter topics starting with this prefix (optional)
|
||||||
*/
|
*/
|
||||||
app.get('/topics', async (c) => {
|
app.get('/topics', async (c) => {
|
||||||
try {
|
try {
|
||||||
const limitParam = c.req.query('limit');
|
const limitParam = c.req.query('limit');
|
||||||
const offsetParam = c.req.query('offset');
|
const offsetParam = c.req.query('offset');
|
||||||
|
const startsWithParam = c.req.query('startsWith');
|
||||||
|
|
||||||
const limit = limitParam ? Math.min(parseInt(limitParam, 10), 200) : 50;
|
const limit = limitParam ? Math.min(parseInt(limitParam, 10), 200) : 50;
|
||||||
const offset = offsetParam ? parseInt(offsetParam, 10) : 0;
|
const offset = offsetParam ? parseInt(offsetParam, 10) : 0;
|
||||||
|
const startsWith = startsWithParam || undefined;
|
||||||
|
|
||||||
const result = await storage.getTopics(limit, offset);
|
const result = await storage.getTopics(limit, offset, startsWith);
|
||||||
|
|
||||||
return c.json({
|
return c.json({
|
||||||
topics: result.topics,
|
topics: result.topics,
|
||||||
total: result.total,
|
total: result.total,
|
||||||
limit,
|
limit,
|
||||||
offset
|
offset,
|
||||||
|
...(startsWith && { startsWith })
|
||||||
}, 200);
|
}, 200);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error fetching topics:', err);
|
console.error('Error fetching topics:', err);
|
||||||
|
|||||||
@@ -296,32 +296,51 @@ export class D1Storage implements Storage {
|
|||||||
return candidates;
|
return candidates;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getTopics(limit: number, offset: number): Promise<{
|
async getTopics(limit: number, offset: number, startsWith?: string): Promise<{
|
||||||
topics: TopicInfo[];
|
topics: TopicInfo[];
|
||||||
total: number;
|
total: number;
|
||||||
}> {
|
}> {
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
// Build WHERE clause for startsWith filter
|
||||||
|
const whereClause = startsWith
|
||||||
|
? 'o.expires_at > ? AND ot.topic LIKE ?'
|
||||||
|
: 'o.expires_at > ?';
|
||||||
|
|
||||||
|
const startsWithPattern = startsWith ? `${startsWith}%` : null;
|
||||||
|
|
||||||
// Get total count of topics with active offers
|
// Get total count of topics with active offers
|
||||||
const countResult = await this.db.prepare(`
|
const countQuery = `
|
||||||
SELECT COUNT(DISTINCT ot.topic) as count
|
SELECT COUNT(DISTINCT ot.topic) as count
|
||||||
FROM offer_topics ot
|
FROM offer_topics ot
|
||||||
INNER JOIN offers o ON ot.offer_id = o.id
|
INNER JOIN offers o ON ot.offer_id = o.id
|
||||||
WHERE o.expires_at > ?
|
WHERE ${whereClause}
|
||||||
`).bind(Date.now()).first();
|
`;
|
||||||
|
|
||||||
|
const countStmt = this.db.prepare(countQuery);
|
||||||
|
const countResult = startsWith
|
||||||
|
? await countStmt.bind(now, startsWithPattern).first()
|
||||||
|
: await countStmt.bind(now).first();
|
||||||
|
|
||||||
const total = (countResult as any)?.count || 0;
|
const total = (countResult as any)?.count || 0;
|
||||||
|
|
||||||
// Get topics with peer counts (paginated)
|
// Get topics with peer counts (paginated)
|
||||||
const topicsResult = await this.db.prepare(`
|
const topicsQuery = `
|
||||||
SELECT
|
SELECT
|
||||||
ot.topic,
|
ot.topic,
|
||||||
COUNT(DISTINCT o.peer_id) as active_peers
|
COUNT(DISTINCT o.peer_id) as active_peers
|
||||||
FROM offer_topics ot
|
FROM offer_topics ot
|
||||||
INNER JOIN offers o ON ot.offer_id = o.id
|
INNER JOIN offers o ON ot.offer_id = o.id
|
||||||
WHERE o.expires_at > ?
|
WHERE ${whereClause}
|
||||||
GROUP BY ot.topic
|
GROUP BY ot.topic
|
||||||
ORDER BY active_peers DESC, ot.topic ASC
|
ORDER BY active_peers DESC, ot.topic ASC
|
||||||
LIMIT ? OFFSET ?
|
LIMIT ? OFFSET ?
|
||||||
`).bind(Date.now(), limit, offset).all();
|
`;
|
||||||
|
|
||||||
|
const topicsStmt = this.db.prepare(topicsQuery);
|
||||||
|
const topicsResult = startsWith
|
||||||
|
? await topicsStmt.bind(now, startsWithPattern, limit, offset).all()
|
||||||
|
: await topicsStmt.bind(now, limit, offset).all();
|
||||||
|
|
||||||
const topics = (topicsResult.results || []).map((row: any) => ({
|
const topics = (topicsResult.results || []).map((row: any) => ({
|
||||||
topic: row.topic,
|
topic: row.topic,
|
||||||
|
|||||||
@@ -305,35 +305,50 @@ export class SQLiteStorage implements Storage {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
async getTopics(limit: number, offset: number): Promise<{
|
async getTopics(limit: number, offset: number, startsWith?: string): Promise<{
|
||||||
topics: TopicInfo[];
|
topics: TopicInfo[];
|
||||||
total: number;
|
total: number;
|
||||||
}> {
|
}> {
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
// Build WHERE clause for startsWith filter
|
||||||
|
const whereClause = startsWith
|
||||||
|
? 'o.expires_at > ? AND ot.topic LIKE ?'
|
||||||
|
: 'o.expires_at > ?';
|
||||||
|
|
||||||
|
const startsWithPattern = startsWith ? `${startsWith}%` : null;
|
||||||
|
|
||||||
// Get total count of topics with active offers
|
// Get total count of topics with active offers
|
||||||
const countStmt = this.db.prepare(`
|
const countQuery = `
|
||||||
SELECT COUNT(DISTINCT ot.topic) as count
|
SELECT COUNT(DISTINCT ot.topic) as count
|
||||||
FROM offer_topics ot
|
FROM offer_topics ot
|
||||||
INNER JOIN offers o ON ot.offer_id = o.id
|
INNER JOIN offers o ON ot.offer_id = o.id
|
||||||
WHERE o.expires_at > ?
|
WHERE ${whereClause}
|
||||||
`);
|
`;
|
||||||
|
|
||||||
const countRow = countStmt.get(Date.now()) as any;
|
const countStmt = this.db.prepare(countQuery);
|
||||||
|
const countParams = startsWith ? [now, startsWithPattern] : [now];
|
||||||
|
const countRow = countStmt.get(...countParams) as any;
|
||||||
const total = countRow.count;
|
const total = countRow.count;
|
||||||
|
|
||||||
// Get topics with peer counts (paginated)
|
// Get topics with peer counts (paginated)
|
||||||
const topicsStmt = this.db.prepare(`
|
const topicsQuery = `
|
||||||
SELECT
|
SELECT
|
||||||
ot.topic,
|
ot.topic,
|
||||||
COUNT(DISTINCT o.peer_id) as active_peers
|
COUNT(DISTINCT o.peer_id) as active_peers
|
||||||
FROM offer_topics ot
|
FROM offer_topics ot
|
||||||
INNER JOIN offers o ON ot.offer_id = o.id
|
INNER JOIN offers o ON ot.offer_id = o.id
|
||||||
WHERE o.expires_at > ?
|
WHERE ${whereClause}
|
||||||
GROUP BY ot.topic
|
GROUP BY ot.topic
|
||||||
ORDER BY active_peers DESC, ot.topic ASC
|
ORDER BY active_peers DESC, ot.topic ASC
|
||||||
LIMIT ? OFFSET ?
|
LIMIT ? OFFSET ?
|
||||||
`);
|
`;
|
||||||
|
|
||||||
const rows = topicsStmt.all(Date.now(), limit, offset) as any[];
|
const topicsStmt = this.db.prepare(topicsQuery);
|
||||||
|
const topicsParams = startsWith
|
||||||
|
? [now, startsWithPattern, limit, offset]
|
||||||
|
: [now, limit, offset];
|
||||||
|
const rows = topicsStmt.all(...topicsParams) as any[];
|
||||||
|
|
||||||
const topics = rows.map(row => ({
|
const topics = rows.map(row => ({
|
||||||
topic: row.topic,
|
topic: row.topic,
|
||||||
|
|||||||
@@ -146,9 +146,10 @@ export interface Storage {
|
|||||||
* Retrieves topics with active peer counts (paginated)
|
* Retrieves topics with active peer counts (paginated)
|
||||||
* @param limit Maximum number of topics to return
|
* @param limit Maximum number of topics to return
|
||||||
* @param offset Number of topics to skip
|
* @param offset Number of topics to skip
|
||||||
|
* @param startsWith Optional prefix filter - only return topics starting with this string
|
||||||
* @returns Object with topics array and total count
|
* @returns Object with topics array and total count
|
||||||
*/
|
*/
|
||||||
getTopics(limit: number, offset: number): Promise<{
|
getTopics(limit: number, offset: number, startsWith?: string): Promise<{
|
||||||
topics: TopicInfo[];
|
topics: TopicInfo[];
|
||||||
total: number;
|
total: number;
|
||||||
}>;
|
}>;
|
||||||
|
|||||||
Reference in New Issue
Block a user