90 lines
3.0 KiB
TypeScript
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 };
|