Skip to content

Commit 9def00a

Browse files
Merge pull request #77 from okekefrancis112/pagination
Pagination
2 parents 143119d + 1eab91c commit 9def00a

12 files changed

Lines changed: 156 additions & 11 deletions

File tree

jest.config.js renamed to jest.config.cjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export default {
22
preset: 'ts-jest/presets/default-esm',
33
testEnvironment: 'node',
44
extensionsToTreatAsEsm: ['.ts'],
5+
testMatch: ['**/?(*.)+(spec|test).ts'],
56
moduleNameMapper: {
67
'^(\\.{1,2}/.*)\\.js$': '$1',
78
},
@@ -26,5 +27,4 @@ export default {
2627
statements: 95,
2728
},
2829
},
29-
testMatch: ['**/tests/integration/**/*.test.ts'],
3030
};

src/app.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import { apiStatusEnum, type ApiStatus } from './db/schema.js';
1313
import { requireAuth, type AuthenticatedLocals } from './middleware/requireAuth.js';
1414
import { buildDeveloperAnalytics } from './services/developerAnalytics.js';
1515
import { errorHandler } from './middleware/errorHandler.js';
16+
import adminRouter from './routes/admin.js';
17+
import { parsePagination, paginatedResponse } from './lib/pagination.js';
1618
import { InMemoryVaultRepository, type VaultRepository } from './repositories/vaultRepository.js';
1719
import { DepositController } from './controllers/depositController.js';
1820
import { TransactionBuilderService } from './services/transactionBuilder.js';
@@ -83,6 +85,7 @@ export const createApp = (dependencies?: Partial<AppDependencies>) => {
8385
}
8486

8587
app.use(requestLogger);
88+
8689
const allowedOrigins = (process.env.CORS_ALLOWED_ORIGINS ?? 'http://localhost:5173')
8790
.split(',')
8891
.map((o) => o.trim());
@@ -109,8 +112,9 @@ export const createApp = (dependencies?: Partial<AppDependencies>) => {
109112
res.json({ status: 'ok', service: 'callora-backend' });
110113
});
111114

112-
app.get('/api/apis', (_req, res) => {
113-
res.json({ apis: [] });
115+
app.get('/api/apis', (req, res) => {
116+
const { limit, offset } = parsePagination(req.query as { limit?: string; offset?: string });
117+
res.json(paginatedResponse([], { limit, offset }));
114118
});
115119

116120
app.get('/api/apis/:id', async (req, res) => {
@@ -149,8 +153,9 @@ export const createApp = (dependencies?: Partial<AppDependencies>) => {
149153
});
150154
});
151155

152-
app.get('/api/usage', (_req, res) => {
153-
res.json({ calls: 0, period: 'current' });
156+
app.get('/api/usage', (req, res) => {
157+
const { limit, offset } = parsePagination(req.query as { limit?: string; offset?: string });
158+
res.json(paginatedResponse([], { limit, offset }));
154159
});
155160

156161
app.get('/api/developers/apis', requireAuth, async (req, res: express.Response<unknown, AuthenticatedLocals>) => {

src/controllers/depositController.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,11 @@ export class DepositController {
140140
network: unsignedTx.network,
141141
contractId: vault.contractId,
142142
amount: validation.normalizedAmount!,
143+
operation: {
144+
type: unsignedTx.operation.type,
145+
function: unsignedTx.operation.function as 'deposit',
146+
args: unsignedTx.operation.args,
147+
},
143148
operation: unsignedTx.operation as DepositPrepareResponse['operation'],
144149
metadata: {
145150
fee: unsignedTx.fee,

src/events/event.emitter.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { EventEmitter } from 'events';
22
import {
3+
WebhookConfig,
34
WebhookEventType,
45
WebhookPayload,
56
NewApiCallData,
@@ -24,7 +25,7 @@ async function handleEvent(
2425
};
2526

2627
const configs = WebhookStore.getByEvent(event).filter(
27-
(cfg) => cfg.developerId === developerId
28+
(cfg: WebhookConfig) => cfg.developerId === developerId
2829
);
2930

3031
if (configs.length > 0) {

src/index.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,8 @@ const PORT = process.env.PORT ?? 3000;
99

1010
// Inject the metrics middleware globally to track all incoming requests
1111
app.use(metricsMiddleware);
12-
13-
// Register the securely guarded metrics endpoint
1412
app.get('/api/metrics', metricsEndpoint);
1513

16-
// Execute the server only if this file is run directly
1714
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
1815
app.listen(PORT, () => {
1916
logger.info(`Callora backend listening on http://localhost:${PORT}`);
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import assert from 'node:assert/strict';
2+
import { describe, it } from 'node:test';
3+
import { parsePagination, paginatedResponse } from '../pagination.js';
4+
5+
describe('parsePagination', () => {
6+
it('returns defaults when no query params given', () => {
7+
assert.deepEqual(parsePagination({}), { limit: 20, offset: 0 });
8+
});
9+
10+
it('parses valid limit and offset', () => {
11+
assert.deepEqual(parsePagination({ limit: '10', offset: '30' }), { limit: 10, offset: 30 });
12+
});
13+
14+
it('clamps limit to max 100', () => {
15+
assert.deepEqual(parsePagination({ limit: '500' }), { limit: 100, offset: 0 });
16+
});
17+
18+
it('clamps limit to min 1', () => {
19+
assert.deepEqual(parsePagination({ limit: '0' }), { limit: 1, offset: 0 });
20+
assert.deepEqual(parsePagination({ limit: '-5' }), { limit: 1, offset: 0 });
21+
});
22+
23+
it('clamps offset to min 0', () => {
24+
assert.deepEqual(parsePagination({ offset: '-10' }), { limit: 20, offset: 0 });
25+
});
26+
27+
it('handles non-numeric strings gracefully', () => {
28+
assert.deepEqual(parsePagination({ limit: 'abc', offset: 'xyz' }), { limit: 20, offset: 0 });
29+
});
30+
});
31+
32+
describe('paginatedResponse', () => {
33+
it('wraps data and meta into the envelope', () => {
34+
const result = paginatedResponse([{ id: '1' }], { total: 1, limit: 20, offset: 0 });
35+
assert.deepEqual(result, {
36+
data: [{ id: '1' }],
37+
meta: { total: 1, limit: 20, offset: 0 },
38+
});
39+
});
40+
41+
it('works without total in meta', () => {
42+
const result = paginatedResponse([], { limit: 20, offset: 0 });
43+
assert.deepEqual(result, {
44+
data: [],
45+
meta: { limit: 20, offset: 0 },
46+
});
47+
assert.equal('total' in result.meta, false);
48+
});
49+
});

src/lib/pagination.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
export interface PaginationParams {
2+
limit: number;
3+
offset: number;
4+
}
5+
6+
export interface PaginationMeta {
7+
total?: number;
8+
limit: number;
9+
offset: number;
10+
}
11+
12+
export interface PaginatedResponse<T> {
13+
data: T[];
14+
meta: PaginationMeta;
15+
}
16+
17+
const DEFAULT_LIMIT = 20;
18+
const MAX_LIMIT = 100;
19+
20+
export function parsePagination(query: {
21+
limit?: string;
22+
offset?: string;
23+
}): PaginationParams {
24+
const parsedLimit = parseInt(query.limit ?? '', 10);
25+
const limit = Math.min(
26+
MAX_LIMIT,
27+
Math.max(1, Number.isNaN(parsedLimit) ? DEFAULT_LIMIT : parsedLimit),
28+
);
29+
30+
const parsedOffset = parseInt(query.offset ?? '', 10);
31+
const offset = Math.max(0, Number.isNaN(parsedOffset) ? 0 : parsedOffset);
32+
33+
return { limit, offset };
34+
}
35+
36+
export function paginatedResponse<T>(
37+
data: T[],
38+
meta: PaginationMeta,
39+
): PaginatedResponse<T> {
40+
return { data, meta };
41+
}

src/repositories/apiRepository.drizzle.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,23 @@
1-
import { eq, and } from 'drizzle-orm';
1+
import { eq, and, type SQL } from 'drizzle-orm';
22
import { db, schema } from '../db/index.js';
33
import type { Api } from '../db/schema.js';
44
import type { ApiDetails, ApiEndpointInfo, ApiListFilters, ApiRepository } from './apiRepository.js';
55

66
export class DrizzleApiRepository implements ApiRepository {
77
async listByDeveloper(developerId: number, filters: ApiListFilters = {}): Promise<Api[]> {
8+
const conditions: SQL[] = [eq(schema.apis.developer_id, developerId)];
9+
if (filters.status) {
10+
conditions.push(eq(schema.apis.status, filters.status));
11+
}
12+
const results = await db.select().from(schema.apis).where(and(...conditions));
13+
let rows = results as Api[];
14+
if (typeof filters.offset === 'number') {
15+
rows = rows.slice(filters.offset);
16+
}
17+
if (typeof filters.limit === 'number') {
18+
rows = rows.slice(0, filters.limit);
19+
}
20+
return rows;
821
const conditions = [eq(schema.apis.developer_id, developerId)];
922
if (filters.status) {
1023
conditions.push(eq(schema.apis.status, filters.status));

src/repositories/apiRepository.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { eq, and, type SQL } from 'drizzle-orm';
12
import { eq, and } from 'drizzle-orm';
23
import { db, schema } from '../db/index.js';
34
import type { Api, ApiStatus } from '../db/schema.js';
@@ -40,11 +41,25 @@ export interface ApiRepository {
4041

4142
export const defaultApiRepository: ApiRepository = {
4243
async listByDeveloper(developerId, filters = {}) {
44+
const conditions: SQL[] = [eq(schema.apis.developer_id, developerId)];
4345
const conditions = [eq(schema.apis.developer_id, developerId)];
4446
if (filters.status) {
4547
conditions.push(eq(schema.apis.status, filters.status));
4648
}
4749

50+
const results = await db
51+
.select()
52+
.from(schema.apis)
53+
.where(and(...conditions));
54+
55+
let rows = results as Api[];
56+
if (typeof filters.offset === 'number') {
57+
rows = rows.slice(filters.offset);
58+
}
59+
if (typeof filters.limit === 'number') {
60+
rows = rows.slice(0, filters.limit);
61+
}
62+
return rows;
4863
let query = db.select().from(schema.apis).where(and(...conditions));
4964

5065
if (typeof filters.limit === 'number') {

src/repositories/userRepository.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
import prisma from '../lib/prisma.js';
22
import type { User } from '../generated/prisma/client.js';
3+
import type { PaginationParams } from '../lib/pagination.js';
4+
5+
export type UserListItem = Pick<User, 'id' | 'stellar_address' | 'created_at'>;
6+
7+
interface FindUsersResult {
8+
users: UserListItem[];
9+
total: number;
10+
}
11+
12+
export async function findUsers(params: PaginationParams): Promise<FindUsersResult> {
313

414
interface PaginatedUsers {
515
users: Pick<User, 'id' | 'stellar_address' | 'created_at'>[];
@@ -20,12 +30,15 @@ export async function findUsers(page: number, limit: number): Promise<PaginatedU
2030
created_at: true,
2131
},
2232
orderBy: { created_at: 'desc' },
33+
skip: params.offset,
34+
take: params.limit,
2335
skip,
2436
take: limit,
2537
}),
2638
prisma.user.count(),
2739
]);
2840

41+
return { users, total };
2942
return {
3043
users,
3144
total,

0 commit comments

Comments
 (0)