Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
170 changes: 113 additions & 57 deletions backend/package.json
Original file line number Diff line number Diff line change
@@ -1,57 +1,113 @@
{
"name": "pulsartrack-backend",
"version": "1.0.0",
"description": "PulsarTrack backend API server with Stellar Horizon integration",
"main": "dist/index.js",
"scripts": {
"dev": "nodemon --exec ts-node src/index.ts",
"build": "tsc",
"start": "node dist/index.js",
"test": "vitest --run",
"test:coverage": "vitest --coverage",
"test:watch": "vitest",
"typecheck": "tsc --noEmit",
"audit": "npm audit --audit-level=high"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@prisma/client": "^5.22.0",
"@sentry/node": "^10.72.0",
"@stellar/stellar-sdk": "^14.5.0",
"cors": "^2.8.6",
"date-fns": "^4.1.0",
"dotenv": "^17.3.1",
"express": "^5.2.1",
"helmet": "^8.1.0",
"ioredis": "^5.9.3",
"morgan": "^1.10.1",
"pg": "^8.18.0",
"pino": "^10.3.1",
"pino-http": "^11.0.0",
"pino-pretty": "^13.1.3",
"rate-limiter-flexible": "^9.1.1",
"redis": "^5.11.0",
"uuid": "^13.0.0",
"ws": "^8.19.0"
},
"devDependencies": {
"@types/cors": "^2.8.19",
"@types/express": "^5.0.6",
"@types/morgan": "^1.9.10",
"@types/node": "^25.2.3",
"@types/pg": "^8.16.0",
"@types/pino-http": "^5.8.4",
"@types/supertest": "^6.0.3",
"@types/uuid": "^10.0.0",
"@types/ws": "^8.18.1",
"@vitest/coverage-v8": "^4.0.18",
"nodemon": "^3.1.11",
"prisma": "^5.22.0",
"supertest": "^7.2.2",
"ts-node": "^10.9.2",
"typescript": "^5.9.3",
"vitest": "^4.0.18"
}
}
{
"name": "pulsartrack-backend",
"version": "1.0.0",
"description": "PulsarTrack backend API server with Stellar Horizon integration",
"main": "dist/index.js",
"scripts": {
"dev": "nodemon --exec ts-node src/index.ts",
"build": "tsc",
"start": "node dist/index.js",
"test": "vitest --run",
"test:coverage": "vitest --coverage",
"test:watch": "vitest",
"typecheck": "tsc --noEmit",
"audit": "npm audit --audit-level=high"
},
"keywords": [],
"author": "",
"license": "Apache-2.0",
"dependencies": {
"@prisma/client": "^5.22.0",
"@stellar/stellar-sdk": "^14.5.0",
"cors": "^2.8.6",
"date-fns": "^4.1.0",
"dotenv": "^17.3.1",
"express": "^5.2.1",
"helmet": "^8.1.0",
"ioredis": "^5.9.3",
"morgan": "^1.10.1",
"pg": "^8.18.0",
"pino": "^10.3.1",
"pino-http": "^11.0.0",
"pino-pretty": "^13.1.3",
"rate-limiter-flexible": "^9.1.1",
"redis": "^5.11.0",
"uuid": "^13.0.0",
"ws": "^8.19.0"
},
"devDependencies": {
"@types/cors": "^2.8.19",
"@types/express": "^5.0.6",
"@types/morgan": "^1.9.10",
"@types/node": "^25.2.3",
"@types/pg": "^8.16.0",
"@types/pino-http": "^5.8.4",
"@types/supertest": "^6.0.3",
"@types/uuid": "^10.0.0",
"@types/ws": "^8.18.1",
"@vitest/coverage-v8": "^4.0.18",
"nodemon": "^3.1.11",
"prisma": "^5.22.0",
"supertest": "^7.2.2",
"ts-node": "^10.9.2",
"typescript": "^5.9.3",
"vitest": "^4.0.18"
}
}
{
"name": "pulsartrack-backend",
"version": "1.0.0",
"description": "PulsarTrack backend API server with Stellar Horizon integration",
"main": "dist/index.js",
"scripts": {
"dev": "nodemon --exec ts-node src/index.ts",
"build": "tsc",
"start": "node dist/index.js",
"test": "vitest --run",
"test:coverage": "vitest --coverage",
"test:watch": "vitest",
"typecheck": "tsc --noEmit",
"audit": "npm audit --audit-level=high"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@prisma/client": "^5.22.0",
"@sentry/node": "^10.72.0",
"@stellar/stellar-sdk": "^14.5.0",
"cors": "^2.8.6",
"date-fns": "^4.1.0",
"dotenv": "^17.3.1",
"express": "^5.2.1",
"helmet": "^8.1.0",
"ioredis": "^5.9.3",
"morgan": "^1.10.1",
"pg": "^8.18.0",
"pino": "^10.3.1",
"pino-http": "^11.0.0",
"pino-pretty": "^13.1.3",
"rate-limiter-flexible": "^9.1.1",
"redis": "^5.11.0",
"uuid": "^13.0.0",
"ws": "^8.19.0"
},
"devDependencies": {
"@types/cors": "^2.8.19",
"@types/express": "^5.0.6",
"@types/morgan": "^1.9.10",
"@types/node": "^25.2.3",
"@types/pg": "^8.16.0",
"@types/pino-http": "^5.8.4",
"@types/supertest": "^6.0.3",
"@types/uuid": "^10.0.0",
"@types/ws": "^8.18.1",
"@vitest/coverage-v8": "^4.0.18",
"nodemon": "^3.1.11",
"prisma": "^5.22.0",
"supertest": "^7.2.2",
"ts-node": "^10.9.2",
"typescript": "^5.9.3",
"vitest": "^4.0.18"
}
}
18 changes: 15 additions & 3 deletions backend/src/__tests__/repositories.subscriptions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,27 @@ describe('subscriptions repository', () => {
beforeEach(() => vi.clearAllMocks());

describe('findBySubscriber', () => {
it('returns every subscription for the subscriber, newest first', async () => {
it('returns subscriptions for the subscriber, newest first, with the default cap', async () => {
mockQuery.mockResolvedValue({ rows: [{ id: 1 }, { id: 2 }] });

const result = await subscriptions.findBySubscriber('GSUB');

expect(result).toHaveLength(2);
expect(mockQuery).toHaveBeenCalledWith(
expect.stringContaining('ORDER BY started_at DESC'),
['GSUB'],
['GSUB', subscriptions.DEFAULT_SUBSCRIPTION_HISTORY_LIMIT],
);
expect(mockQuery.mock.calls[0][0]).toContain('LIMIT $2');
});

it('accepts an explicit limit and caps oversized requests', async () => {
mockQuery.mockResolvedValue({ rows: [] });

await subscriptions.findBySubscriber('GSUB', 500);

expect(mockQuery).toHaveBeenCalledWith(
expect.stringContaining('LIMIT $2'),
['GSUB', subscriptions.MAX_SUBSCRIPTION_HISTORY_LIMIT],
);
});

Expand Down Expand Up @@ -78,4 +90,4 @@ describe('subscriptions repository', () => {
);
});
});
});
});
23 changes: 19 additions & 4 deletions backend/src/db/repositories/subscriptions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import pool from '../../config/database';

export const DEFAULT_SUBSCRIPTION_HISTORY_LIMIT = 50;
export const MAX_SUBSCRIPTION_HISTORY_LIMIT = 100;

interface SubscriptionRow {
id: string;
subscriber: string;
Expand All @@ -12,12 +15,24 @@ interface SubscriptionRow {
tx_hash: string | null;
}

export async function findBySubscriber(subscriber: string): Promise<SubscriptionRow[]> {
function normalizeSubscriptionLimit(take = DEFAULT_SUBSCRIPTION_HISTORY_LIMIT): number {
if (!Number.isFinite(take) || take <= 0) {
return DEFAULT_SUBSCRIPTION_HISTORY_LIMIT;
}
return Math.min(Math.floor(take), MAX_SUBSCRIPTION_HISTORY_LIMIT);
}

export async function findBySubscriber(
subscriber: string,
take = DEFAULT_SUBSCRIPTION_HISTORY_LIMIT,
): Promise<SubscriptionRow[]> {
const limit = normalizeSubscriptionLimit(take);
const { rows } = await pool.query(
`SELECT * FROM subscriptions
WHERE subscriber = $1
ORDER BY started_at DESC`,
[subscriber],
ORDER BY started_at DESC
LIMIT $2`,
[subscriber, limit],
);
return rows;
}
Expand Down Expand Up @@ -59,4 +74,4 @@ export async function create(data: {
],
);
return rows[0];
}
}
Loading