diff --git a/services/api/src/store/db.test.ts b/services/api/src/store/db.test.ts new file mode 100644 index 0000000..5c1205d --- /dev/null +++ b/services/api/src/store/db.test.ts @@ -0,0 +1,17 @@ +import { describe, expect, it } from 'vitest'; +import { boundedInteger } from './db.js'; + +describe('boundedInteger', () => { + it('falls back for non-finite pagination values', () => { + expect(boundedInteger(Number.NaN, 50, 1, 100)).toBe(50); + expect(boundedInteger(Number.POSITIVE_INFINITY, 50, 1, 100)).toBe(50); + expect(boundedInteger(Number.NEGATIVE_INFINITY, 0, 0)).toBe(0); + }); + + it('clamps and truncates finite pagination values', () => { + expect(boundedInteger(-1, 50, 1, 100)).toBe(1); + expect(boundedInteger(200, 50, 1, 100)).toBe(100); + expect(boundedInteger(12.9, 50, 1, 100)).toBe(12); + expect(boundedInteger(2.7, 0, 0)).toBe(2); + }); +}); diff --git a/services/api/src/store/db.ts b/services/api/src/store/db.ts index 0848276..fdea9a7 100644 --- a/services/api/src/store/db.ts +++ b/services/api/src/store/db.ts @@ -80,9 +80,16 @@ export async function slugTaken(slug: string): Promise { return r.rows.length > 0; } +export function boundedInteger(value: number | undefined, fallback: number, min: number, max?: number): number { + if (!Number.isFinite(value)) return fallback; + const integer = Math.trunc(value as number); + const lowerBounded = Math.max(integer, min); + return typeof max === 'number' ? Math.min(lowerBounded, max) : lowerBounded; +} + export async function listLiveExtensions(opts: { q?: string | undefined; limit?: number; offset?: number } = {}): Promise { - const limit = Math.min(Math.max(opts.limit ?? 50, 1), 100); - const offset = Math.max(opts.offset ?? 0, 0); + const limit = boundedInteger(opts.limit, 50, 1, 100); + const offset = boundedInteger(opts.offset, 0, 0); if (opts.q) { const like = `%${opts.q}%`; const r = await db().execute({