Tonne of debugging - getting ready for the production builds
This commit is contained in:
39
api/dist/modules/influence/representatives/represent-api.client.d.ts
vendored
Normal file
39
api/dist/modules/influence/representatives/represent-api.client.d.ts
vendored
Normal 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
|
||||
1
api/dist/modules/influence/representatives/represent-api.client.d.ts.map
vendored
Normal file
1
api/dist/modules/influence/representatives/represent-api.client.d.ts.map
vendored
Normal 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"}
|
||||
77
api/dist/modules/influence/representatives/represent-api.client.js
vendored
Normal file
77
api/dist/modules/influence/representatives/represent-api.client.js
vendored
Normal 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
|
||||
1
api/dist/modules/influence/representatives/represent-api.client.js.map
vendored
Normal file
1
api/dist/modules/influence/representatives/represent-api.client.js.map
vendored
Normal 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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"}
|
||||
3
api/dist/modules/influence/representatives/representatives.routes.d.ts
vendored
Normal file
3
api/dist/modules/influence/representatives/representatives.routes.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
declare const router: import("express-serve-static-core").Router;
|
||||
export { router as representativesRouter };
|
||||
//# sourceMappingURL=representatives.routes.d.ts.map
|
||||
1
api/dist/modules/influence/representatives/representatives.routes.d.ts.map
vendored
Normal file
1
api/dist/modules/influence/representatives/representatives.routes.d.ts.map
vendored
Normal 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"}
|
||||
98
api/dist/modules/influence/representatives/representatives.routes.js
vendored
Normal file
98
api/dist/modules/influence/representatives/representatives.routes.js
vendored
Normal 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
|
||||
1
api/dist/modules/influence/representatives/representatives.routes.js.map
vendored
Normal file
1
api/dist/modules/influence/representatives/representatives.routes.js.map
vendored
Normal 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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"}
|
||||
19
api/dist/modules/influence/representatives/representatives.schemas.d.ts
vendored
Normal file
19
api/dist/modules/influence/representatives/representatives.schemas.d.ts
vendored
Normal 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
|
||||
1
api/dist/modules/influence/representatives/representatives.schemas.d.ts.map
vendored
Normal file
1
api/dist/modules/influence/representatives/representatives.schemas.d.ts.map
vendored
Normal 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"}
|
||||
11
api/dist/modules/influence/representatives/representatives.schemas.js
vendored
Normal file
11
api/dist/modules/influence/representatives/representatives.schemas.js
vendored
Normal 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
|
||||
1
api/dist/modules/influence/representatives/representatives.schemas.js.map
vendored
Normal file
1
api/dist/modules/influence/representatives/representatives.schemas.js.map
vendored
Normal 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"}
|
||||
98
api/dist/modules/influence/representatives/representatives.service.d.ts
vendored
Normal file
98
api/dist/modules/influence/representatives/representatives.service.d.ts
vendored
Normal 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
|
||||
1
api/dist/modules/influence/representatives/representatives.service.d.ts.map
vendored
Normal file
1
api/dist/modules/influence/representatives/representatives.service.d.ts.map
vendored
Normal 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"}
|
||||
175
api/dist/modules/influence/representatives/representatives.service.js
vendored
Normal file
175
api/dist/modules/influence/representatives/representatives.service.js
vendored
Normal 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
|
||||
1
api/dist/modules/influence/representatives/representatives.service.js.map
vendored
Normal file
1
api/dist/modules/influence/representatives/representatives.service.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user