Tonne of debugging - getting ready for the production builds

This commit is contained in:
2026-02-16 10:44:18 -07:00
parent a77306fac2
commit 7895ce683e
1367 changed files with 404191 additions and 2005 deletions

View File

@@ -0,0 +1,39 @@
export interface RepresentOffice {
type?: string;
tel?: string;
fax?: string;
postal?: string;
}
export interface RepresentRepresentative {
name: string;
email: string | null;
elected_office: string;
district_name: string;
party_name: string | null;
representative_set_name: string;
url: string;
photo_url: string | null;
offices: RepresentOffice[];
}
export interface RepresentPostalCodeResponse {
city: string | null;
province: string | null;
centroid: {
type: string;
coordinates: [number, number];
} | null;
representatives_centroid: RepresentRepresentative[];
representatives_concordance: RepresentRepresentative[];
}
declare class RepresentApiClient {
private baseUrl;
constructor();
getByPostalCode(code: string): Promise<RepresentPostalCodeResponse>;
testConnection(): Promise<{
ok: boolean;
message: string;
}>;
}
export declare const representApiClient: RepresentApiClient;
export {};
//# sourceMappingURL=represent-api.client.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"represent-api.client.d.ts","sourceRoot":"","sources":["../../../../src/modules/influence/representatives/represent-api.client.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,eAAe;IAC9B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,uBAAuB,EAAE,MAAM,CAAC;IAChC,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,OAAO,EAAE,eAAe,EAAE,CAAC;CAC5B;AAED,MAAM,WAAW,2BAA2B;IAC1C,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,QAAQ,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,GAAG,IAAI,CAAC;IACjE,wBAAwB,EAAE,uBAAuB,EAAE,CAAC;IACpD,2BAA2B,EAAE,uBAAuB,EAAE,CAAC;CACxD;AAuBD,cAAM,kBAAkB;IACtB,OAAO,CAAC,OAAO,CAAS;;IAMlB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,2BAA2B,CAAC;IA6BnE,cAAc,IAAI,OAAO,CAAC;QAAE,EAAE,EAAE,OAAO,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;CAyBlE;AAED,eAAO,MAAM,kBAAkB,oBAA2B,CAAC"}

View File

@@ -0,0 +1,77 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.representApiClient = void 0;
const env_1 = require("../../../config/env");
const logger_1 = require("../../../utils/logger");
// --- In-memory sliding window rate limiter (55/min, under 60/min API limit) ---
const RATE_LIMIT = 55;
const RATE_WINDOW_MS = 60_000;
const requestTimestamps = [];
function checkRateLimit() {
const now = Date.now();
// Remove timestamps outside the window
while (requestTimestamps.length > 0 && requestTimestamps[0] < now - RATE_WINDOW_MS) {
requestTimestamps.shift();
}
return requestTimestamps.length < RATE_LIMIT;
}
function recordRequest() {
requestTimestamps.push(Date.now());
}
// --- API Client ---
class RepresentApiClient {
baseUrl;
constructor() {
this.baseUrl = env_1.env.REPRESENT_API_URL;
}
async getByPostalCode(code) {
if (!checkRateLimit()) {
throw new Error('Represent API rate limit reached. Please try again in a minute.');
}
const url = `${this.baseUrl}/postcodes/${code}/`;
logger_1.logger.debug(`Represent API request: ${url}`);
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 10_000);
try {
recordRequest();
const response = await fetch(url, {
signal: controller.signal,
headers: { Accept: 'application/json' },
});
if (!response.ok) {
const text = await response.text().catch(() => '');
throw new Error(`Represent API error ${response.status}: ${text}`);
}
return (await response.json());
}
finally {
clearTimeout(timeout);
}
}
async testConnection() {
try {
const url = `${this.baseUrl}/boundary-sets/?limit=1`;
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 10_000);
try {
const response = await fetch(url, {
signal: controller.signal,
headers: { Accept: 'application/json' },
});
if (!response.ok) {
return { ok: false, message: `HTTP ${response.status}` };
}
return { ok: true, message: 'Represent API is reachable' };
}
finally {
clearTimeout(timeout);
}
}
catch (err) {
const message = err instanceof Error ? err.message : 'Unknown error';
return { ok: false, message };
}
}
}
exports.representApiClient = new RepresentApiClient();
//# sourceMappingURL=represent-api.client.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"represent-api.client.js","sourceRoot":"","sources":["../../../../src/modules/influence/representatives/represent-api.client.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,3 @@
declare const router: import("express-serve-static-core").Router;
export { router as representativesRouter };
//# sourceMappingURL=representatives.routes.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"representatives.routes.d.ts","sourceRoot":"","sources":["../../../../src/modules/influence/representatives/representatives.routes.ts"],"names":[],"mappings":"AAWA,QAAA,MAAM,MAAM,4CAAW,CAAC;AAiHxB,OAAO,EAAE,MAAM,IAAI,qBAAqB,EAAE,CAAC"}

View File

@@ -0,0 +1,98 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.representativesRouter = void 0;
const express_1 = require("express");
const client_1 = require("@prisma/client");
const representatives_service_1 = require("./representatives.service");
const representatives_schemas_1 = require("./representatives.schemas");
const postal_codes_schemas_1 = require("../postal-codes/postal-codes.schemas");
const validate_1 = require("../../../middleware/validate");
const auth_middleware_1 = require("../../../middleware/auth.middleware");
const rbac_middleware_1 = require("../../../middleware/rbac.middleware");
const ADMIN_ROLES = [client_1.UserRole.SUPER_ADMIN, client_1.UserRole.INFLUENCE_ADMIN, client_1.UserRole.MAP_ADMIN];
const router = (0, express_1.Router)();
exports.representativesRouter = router;
// =============================================
// PUBLIC ROUTES (no auth required)
// =============================================
// GET /api/representatives/by-postal/:postalCode — cache-first lookup
router.get('/by-postal/:postalCode', (0, validate_1.validate)(postal_codes_schemas_1.postalCodeParamSchema, 'params'), (0, validate_1.validate)(postal_codes_schemas_1.postalCodeQuerySchema, 'query'), async (req, res, next) => {
try {
const code = req.params.postalCode;
const refresh = req.query.refresh === 'true';
const result = await representatives_service_1.representativesService.lookupByPostalCode(code, refresh);
res.json(result);
}
catch (err) {
next(err);
}
});
// GET /api/representatives/test-connection — Represent API health check
router.get('/test-connection', async (_req, res, next) => {
try {
const result = await representatives_service_1.representativesService.testApiConnection();
res.json(result);
}
catch (err) {
next(err);
}
});
// =============================================
// ADMIN ROUTES (auth + role required)
// =============================================
router.use(auth_middleware_1.authenticate);
router.use((0, rbac_middleware_1.requireRole)(...ADMIN_ROLES));
// GET /api/representatives/cache-stats — cache statistics
router.get('/cache-stats', async (_req, res, next) => {
try {
const stats = await representatives_service_1.representativesService.getCacheStats();
res.json(stats);
}
catch (err) {
next(err);
}
});
// GET /api/representatives — list all cached reps (paginated)
router.get('/', (0, validate_1.validate)(representatives_schemas_1.listRepresentativesSchema, 'query'), async (req, res, next) => {
try {
const result = await representatives_service_1.representativesService.findAll(req.query);
res.json(result);
}
catch (err) {
next(err);
}
});
// GET /api/representatives/:id — single cached rep
router.get('/:id', async (req, res, next) => {
try {
const id = req.params.id;
const rep = await representatives_service_1.representativesService.findById(id);
res.json(rep);
}
catch (err) {
next(err);
}
});
// DELETE /api/representatives/by-postal/:postalCode — clear cache for postal code
router.delete('/by-postal/:postalCode', (0, validate_1.validate)(postal_codes_schemas_1.postalCodeParamSchema, 'params'), async (req, res, next) => {
try {
const code = req.params.postalCode;
const result = await representatives_service_1.representativesService.clearByPostalCode(code);
res.json(result);
}
catch (err) {
next(err);
}
});
// DELETE /api/representatives/:id — delete single cached rep
router.delete('/:id', async (req, res, next) => {
try {
const id = req.params.id;
await representatives_service_1.representativesService.deleteById(id);
res.status(204).send();
}
catch (err) {
next(err);
}
});
//# sourceMappingURL=representatives.routes.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"representatives.routes.js","sourceRoot":"","sources":["../../../../src/modules/influence/representatives/representatives.routes.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,19 @@
import { z } from 'zod';
export declare const listRepresentativesSchema: z.ZodObject<{
page: z.ZodDefault<z.ZodNumber>;
limit: z.ZodDefault<z.ZodNumber>;
search: z.ZodOptional<z.ZodString>;
postalCode: z.ZodOptional<z.ZodString>;
}, "strip", z.ZodTypeAny, {
limit: number;
page: number;
search?: string | undefined;
postalCode?: string | undefined;
}, {
search?: string | undefined;
limit?: number | undefined;
page?: number | undefined;
postalCode?: string | undefined;
}>;
export type ListRepresentativesInput = z.infer<typeof listRepresentativesSchema>;
//# sourceMappingURL=representatives.schemas.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"representatives.schemas.d.ts","sourceRoot":"","sources":["../../../../src/modules/influence/representatives/representatives.schemas.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;EAKpC,CAAC;AAEH,MAAM,MAAM,wBAAwB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC"}

View File

@@ -0,0 +1,11 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.listRepresentativesSchema = void 0;
const zod_1 = require("zod");
exports.listRepresentativesSchema = zod_1.z.object({
page: zod_1.z.coerce.number().int().positive().default(1),
limit: zod_1.z.coerce.number().int().positive().max(100).default(20),
search: zod_1.z.string().optional(),
postalCode: zod_1.z.string().optional(),
});
//# sourceMappingURL=representatives.schemas.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"representatives.schemas.js","sourceRoot":"","sources":["../../../../src/modules/influence/representatives/representatives.schemas.ts"],"names":[],"mappings":";;;AAAA,6BAAwB;AAEX,QAAA,yBAAyB,GAAG,OAAC,CAAC,MAAM,CAAC;IAChD,IAAI,EAAE,OAAC,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC;IACnD,KAAK,EAAE,OAAC,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;IAC9D,MAAM,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC7B,UAAU,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CAClC,CAAC,CAAC"}

View File

@@ -0,0 +1,98 @@
import { Prisma } from '@prisma/client';
import type { ListRepresentativesInput } from './representatives.schemas';
export declare const representativesService: {
lookupByPostalCode(code: string, forceRefresh?: boolean): Promise<{
source: "cache";
postalCode: string;
location: {
city: string | null;
province: string | null;
};
representatives: {
id: string;
email: string | null;
name: string | null;
url: string | null;
postalCode: string;
districtName: string | null;
electedOffice: string | null;
partyName: string | null;
representativeSetName: string | null;
photoUrl: string | null;
offices: Prisma.JsonValue | null;
cachedAt: Date;
}[];
} | {
source: "api";
postalCode: string;
location: {
city: string | null;
province: string | null;
};
representatives: {
id: null;
postalCode: string;
name: string | null;
email: string | null;
districtName: string | null;
electedOffice: string | null;
partyName: string | null;
representativeSetName: string | null;
url: string | null;
photoUrl: string | null;
offices: import("./represent-api.client").RepresentOffice[];
cachedAt: string;
}[];
}>;
findAll(filters: ListRepresentativesInput): Promise<{
representatives: {
id: string;
email: string | null;
name: string | null;
url: string | null;
postalCode: string;
districtName: string | null;
electedOffice: string | null;
partyName: string | null;
representativeSetName: string | null;
photoUrl: string | null;
offices: Prisma.JsonValue | null;
cachedAt: Date;
}[];
pagination: {
page: number;
limit: number;
total: number;
totalPages: number;
};
}>;
findById(id: string): Promise<{
id: string;
email: string | null;
name: string | null;
url: string | null;
postalCode: string;
districtName: string | null;
electedOffice: string | null;
partyName: string | null;
representativeSetName: string | null;
photoUrl: string | null;
offices: Prisma.JsonValue | null;
cachedAt: Date;
}>;
clearByPostalCode(code: string): Promise<{
deleted: number;
postalCode: string;
}>;
deleteById(id: string): Promise<void>;
testApiConnection(): Promise<{
ok: boolean;
message: string;
}>;
getCacheStats(): Promise<{
totalRepresentatives: number;
postalCodesWithRepresentatives: number;
totalPostalCodes: number;
}>;
};
//# sourceMappingURL=representatives.service.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"representatives.service.d.ts","sourceRoot":"","sources":["../../../../src/modules/influence/representatives/representatives.service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAMxC,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAC;AAY1E,eAAO,MAAM,sBAAsB;6BACF,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAiGd,wBAAwB;;;;;;;;;;;;;;;;;;;;;;iBAmC5B,MAAM;;;;;;;;;;;;;;4BAQK,MAAM;;;;mBAOf,MAAM;;;;;;;;;;CAyB5B,CAAC"}

View File

@@ -0,0 +1,175 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.representativesService = void 0;
const client_1 = require("@prisma/client");
const database_1 = require("../../../config/database");
const logger_1 = require("../../../utils/logger");
const error_handler_1 = require("../../../middleware/error-handler");
const represent_api_client_1 = require("./represent-api.client");
const postal_codes_service_1 = require("../postal-codes/postal-codes.service");
function deduplicateReps(reps) {
const seen = new Set();
return reps.filter((rep) => {
const key = `${rep.name}|${rep.elected_office}`;
if (seen.has(key))
return false;
seen.add(key);
return true;
});
}
exports.representativesService = {
async lookupByPostalCode(code, forceRefresh = false) {
// 1. Check cache unless forcing refresh
if (!forceRefresh) {
const cached = await database_1.prisma.representative.findMany({
where: { postalCode: code },
});
if (cached.length > 0) {
const postalInfo = await postal_codes_service_1.postalCodesService.findByPostalCode(code);
return {
source: 'cache',
postalCode: code,
location: {
city: postalInfo?.city ?? null,
province: postalInfo?.province ?? null,
},
representatives: cached,
};
}
}
// 2. Call Represent API
const apiResponse = await represent_api_client_1.representApiClient.getByPostalCode(code);
// Merge centroid + concordance reps and deduplicate
const allReps = [
...(apiResponse.representatives_centroid || []),
...(apiResponse.representatives_concordance || []),
];
const uniqueReps = deduplicateReps(allReps);
// 3. Fire-and-forget cache write
const cacheWrite = async () => {
try {
// Delete old cached reps for this postal code
await database_1.prisma.representative.deleteMany({ where: { postalCode: code } });
// Cache new reps
if (uniqueReps.length > 0) {
await database_1.prisma.representative.createMany({
data: uniqueReps.map((rep) => ({
postalCode: code,
name: rep.name || null,
email: rep.email || null,
districtName: rep.district_name || null,
electedOffice: rep.elected_office || null,
partyName: rep.party_name || null,
representativeSetName: rep.representative_set_name || null,
url: rep.url || null,
photoUrl: rep.photo_url || null,
offices: rep.offices ? rep.offices : client_1.Prisma.JsonNull,
})),
});
}
// Upsert postal code cache
const coords = apiResponse.centroid?.coordinates;
await postal_codes_service_1.postalCodesService.upsert({
postalCode: code,
city: apiResponse.city,
province: apiResponse.province,
centroidLat: coords ? coords[1] : null,
centroidLng: coords ? coords[0] : null,
});
}
catch (err) {
logger_1.logger.error('Failed to cache representatives', { postalCode: code, error: err });
}
};
// Don't await — fire and forget
cacheWrite();
// 4. Build response from API data
return {
source: 'api',
postalCode: code,
location: {
city: apiResponse.city ?? null,
province: apiResponse.province ?? null,
},
representatives: uniqueReps.map((rep) => ({
id: null,
postalCode: code,
name: rep.name || null,
email: rep.email || null,
districtName: rep.district_name || null,
electedOffice: rep.elected_office || null,
partyName: rep.party_name || null,
representativeSetName: rep.representative_set_name || null,
url: rep.url || null,
photoUrl: rep.photo_url || null,
offices: rep.offices ?? null,
cachedAt: new Date().toISOString(),
})),
};
},
async findAll(filters) {
const { page, limit, search, postalCode } = filters;
const skip = (page - 1) * limit;
const where = {};
if (postalCode) {
where.postalCode = postalCode;
}
if (search) {
where.OR = [
{ name: { contains: search, mode: 'insensitive' } },
{ email: { contains: search, mode: 'insensitive' } },
{ districtName: { contains: search, mode: 'insensitive' } },
{ electedOffice: { contains: search, mode: 'insensitive' } },
];
}
const [representatives, total] = await Promise.all([
database_1.prisma.representative.findMany({
where,
skip,
take: limit,
orderBy: { cachedAt: 'desc' },
}),
database_1.prisma.representative.count({ where }),
]);
return {
representatives,
pagination: { page, limit, total, totalPages: Math.ceil(total / limit) },
};
},
async findById(id) {
const rep = await database_1.prisma.representative.findUnique({ where: { id } });
if (!rep) {
throw new error_handler_1.AppError(404, 'Representative not found', 'REPRESENTATIVE_NOT_FOUND');
}
return rep;
},
async clearByPostalCode(code) {
const result = await database_1.prisma.representative.deleteMany({
where: { postalCode: code },
});
return { deleted: result.count, postalCode: code };
},
async deleteById(id) {
const existing = await database_1.prisma.representative.findUnique({ where: { id } });
if (!existing) {
throw new error_handler_1.AppError(404, 'Representative not found', 'REPRESENTATIVE_NOT_FOUND');
}
await database_1.prisma.representative.delete({ where: { id } });
},
async testApiConnection() {
return represent_api_client_1.representApiClient.testConnection();
},
async getCacheStats() {
const [totalReps, postalCodesWithReps, totalPostalCodes] = await Promise.all([
database_1.prisma.representative.count(),
database_1.prisma.representative.groupBy({ by: ['postalCode'] }).then((g) => g.length),
database_1.prisma.postalCodeCache.count(),
]);
return {
totalRepresentatives: totalReps,
postalCodesWithRepresentatives: postalCodesWithReps,
totalPostalCodes,
};
},
};
//# sourceMappingURL=representatives.service.js.map

File diff suppressed because one or more lines are too long