changemaker.lite/api/src/modules/map/locations/area-import.routes.ts

90 lines
3.0 KiB
TypeScript

import { Router, Request, Response, NextFunction } from 'express';
import { UserRole } from '@prisma/client';
import { randomUUID } from 'crypto';
import { authenticate } from '../../../middleware/auth.middleware';
import { requireRole } from '../../../middleware/rbac.middleware';
import { validate } from '../../../middleware/validate';
import { areaImportPreviewSchema, areaImportStartSchema } from './area-import.schemas';
import { areaImportService, type AreaImportProgress } from './area-import.service';
import { redis } from '../../../config/redis';
import { logger } from '../../../utils/logger';
const MAP_ADMIN_ROLES: UserRole[] = [UserRole.SUPER_ADMIN, UserRole.MAP_ADMIN];
const areaImportRouter = Router();
areaImportRouter.use(authenticate);
areaImportRouter.use(requireRole(...MAP_ADMIN_ROLES));
// POST /api/map/area-import/preview — get bounds, estimates, and existing count
areaImportRouter.post(
'/preview',
validate(areaImportPreviewSchema),
async (req: Request, res: Response, next: NextFunction) => {
try {
const result = await areaImportService.previewAreaImport(req.body);
res.json(result);
} catch (err) {
next(err);
}
},
);
// POST /api/map/area-import — start import (fire-and-forget, returns importId)
areaImportRouter.post(
'/',
validate(areaImportStartSchema),
async (req: Request, res: Response, next: NextFunction) => {
try {
const importId = randomUUID();
const userId = req.user!.id;
// Write initial progress so status endpoint works immediately
const initialProgress: AreaImportProgress = {
status: 'initializing',
bounds: null,
areaSqKm: 0,
sources: {
osm: { status: 'pending', candidatesFound: 0 },
nar: { status: 'pending', candidatesFound: 0 },
reverseGeocode: { status: 'pending', candidatesFound: 0 },
},
locationsCreated: 0,
addressesCreated: 0,
skippedDuplicate: 0,
totalCandidates: 0,
};
await redis.set(`area-import:${importId}`, JSON.stringify(initialProgress), 'EX', 3600);
// Fire and forget
areaImportService.runAreaImport(userId, req.body, importId).catch((err) => {
const errorMsg = err instanceof Error ? err.message : 'Unknown error';
logger.error(`Area import ${importId} failed: ${errorMsg}`);
});
res.json({ importId });
} catch (err) {
next(err);
}
},
);
// GET /api/map/area-import/status/:importId — poll import progress
areaImportRouter.get(
'/status/:importId',
async (req: Request, res: Response, next: NextFunction) => {
try {
const importId = req.params.importId as string;
const progress = await areaImportService.getProgress(importId);
if (!progress) {
res.status(404).json({ error: { message: 'Import not found or expired', code: 'NOT_FOUND' } });
return;
}
res.json(progress);
} catch (err) {
next(err);
}
},
);
export { areaImportRouter };