411 lines
23 KiB
JavaScript
411 lines
23 KiB
JavaScript
'use strict';
|
||
|
||
/**
|
||
* Public Data Export API routes — mounted at /api/v1/
|
||
* All routes authenticated via checkApiKey (X-API-Key header).
|
||
*
|
||
* ─── Integration guide ───────────────────────────────────────────────────────
|
||
*
|
||
* Session summary:
|
||
* GET /api/v1/jobs/:jobId/sessions
|
||
* → Returns one record per uploaded file for the job.
|
||
* reportConfirmed: false when applicator has not yet confirmed values in Report Settings.
|
||
* Re-fetch when your data warehouse detects this field changed.
|
||
*
|
||
* Raw GPS trace (paginated):
|
||
* GET /api/v1/jobs/:jobId/sessions/:fileId/records
|
||
* Query: startingAfter=<cursor>, limit=<n>, interval=<seconds>
|
||
* → Use interval=1 or interval=5 for lighter Power BI queries.
|
||
* → Use the /export endpoint instead for full bulk loads.
|
||
*
|
||
* Spray-area polygons:
|
||
* GET /api/v1/jobs/:jobId/areas
|
||
* → GeoJSON FeatureCollection of planned spray-area polygons.
|
||
*
|
||
* Async bulk export:
|
||
* POST /api/v1/jobs/:jobId/export body: { format: 'csv'|'json', interval?: number }
|
||
* GET /api/v1/exports/:exportId poll for { status, downloadUrl }
|
||
* GET /api/v1/exports/:exportId stream file
|
||
*
|
||
* ─────────────────────────────────────────────────────────────────────────────
|
||
*
|
||
* FE integration notes:
|
||
* - The key management UI (create/list/revoke keys) lives at /api/keys — see routes/api_keys.js.
|
||
* - The API key is supplied as the X-API-Key request header, NOT Authorization Bearer.
|
||
* - For Power BI: use paginated records endpoint with startingAfter cursor for incremental refresh.
|
||
* - For ArcGIS / daily batch: use the export endpoint — POST once, poll, then download CSV/JSON.
|
||
*/
|
||
module.exports = function (app) {
|
||
const router = require('express').Router();
|
||
const rateLimit = require('express-rate-limit');
|
||
const { checkApiKey } = require('../middlewares/app_validator');
|
||
const pubCtl = require('../controllers/api_pub');
|
||
const exportCtl = require('../controllers/api_export');
|
||
const env = require('../helpers/env');
|
||
|
||
// Apply API key auth to all /api/v1/ routes
|
||
router.use(checkApiKey);
|
||
|
||
/**
|
||
* Per-account rate limiter — applied after checkApiKey so req.uid is available.
|
||
*
|
||
* ── Configuration ──────────────────────────────────────────────────────
|
||
* Keyed on account ID (not IP) to prevent one API key from flooding the export pipeline.
|
||
*
|
||
* Environment variables (see helpers/env.js):
|
||
* EXPORT_RATE_LIMIT_MAX: 20 — Max export triggers per account per window
|
||
* EXPORT_RATE_LIMIT_WINDOW_MINS: 60 — Time window in minutes
|
||
*
|
||
* ── Behavior ────────────────────────────────────────────────────────────
|
||
* Default: 20 exports per 60 minutes = 1 export every 3 minutes
|
||
*
|
||
* When exceeded:
|
||
* HTTP 429 Too Many Requests
|
||
* Response headers: RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset, Retry-After
|
||
*
|
||
* ── Deduplication ───────────────────────────────────────────────────────
|
||
* Rate limit is NOT consumed if the request is deduplicated:
|
||
* - Existing ready export (same job/format/units) → return cached
|
||
* - Existing in-progress export (within EXPORT_DEDUP_MINS) → return existing
|
||
*
|
||
* ── Documentation ───────────────────────────────────────────────────────
|
||
* See docs/DATA_EXPORT_API_RATE_LIMITING.md for:
|
||
* - Detailed examples and scenarios
|
||
* - Best practices for batch workflows
|
||
* - Handling 429 responses
|
||
* - Integration guide for customers
|
||
*
|
||
* See docs/DATA_EXPORT_CUSTOMER_INTEGRATION_GUIDE.md for:
|
||
* - Full API documentation (all 6 endpoints)
|
||
* - Use cases and code examples
|
||
* - Error handling
|
||
*/
|
||
const exportAccountLimiter = rateLimit({
|
||
windowMs: env.EXPORT_RATE_LIMIT_WINDOW_MINS * 60 * 1000,
|
||
max: env.EXPORT_RATE_LIMIT_MAX,
|
||
keyGenerator: req => String(req.uid),
|
||
skipFailedRequests: true,
|
||
standardHeaders: true,
|
||
legacyHeaders: false,
|
||
message: { error: 'Export rate limit exceeded. Please wait before requesting another export.' }
|
||
});
|
||
|
||
// ── Session summary ──────────────────────────────────────────────────────
|
||
/**
|
||
* @api {get} /api/v1/jobs/:jobId/sessions Get Session Summary
|
||
* @apiVersion 1.0.0
|
||
* @apiName GetSessions
|
||
* @apiGroup Sessions
|
||
* @apiDescription Returns aggregated spray application data (coverage, timing, pilot, aircraft)
|
||
* from one or more flight files. Each session represents one uploaded log file.
|
||
*
|
||
* @apiParam {Number} jobId Job ID
|
||
*
|
||
* @apiHeader {String} X-API-Key API key (e.g., ak_test_xxx)
|
||
*
|
||
* @apiSuccess (200) {Number} jobId Job identifier
|
||
* @apiSuccess (200) {String} [clientId] Client account ObjectId
|
||
* @apiSuccess (200) {String} [clientName] Client account name
|
||
* @apiSuccess (200) {Boolean} reportConfirmed True if applicator confirmed values in Report Settings
|
||
* @apiSuccess (200) {Number} mappedArea_ha Job mapped area in hectares (`rptOp.areaSize` fallback `ttSprArea`)
|
||
* @apiSuccess (200) {Number} areaSize_ha Planned spray area (hectares)
|
||
* @apiSuccess (200) {Number} coverage_ha Actual coverage (hectares)
|
||
* @apiSuccess (200) {Number} overSprayed_pct Over-spray percentage `(coverage - areaSize) / areaSize * 100` (2 dp)
|
||
* @apiSuccess (200) {Number} appRate Application rate (material per area)
|
||
* @apiSuccess (200) {String} appRateUnit Rate unit string (e.g., 'lit/ha', 'oz/ac')
|
||
* @apiSuccess (200) {Number} [appRateConfirmed] Confirmed app rate only; null when not confirmed
|
||
* @apiSuccess (200) {String} volumeUnit Material unit string (`lit` or `gal`) derived from `job.measureUnit`
|
||
* @apiSuccess (200) {Number} sprayVolume Planned/estimated spray volume (`coverage_ha × appRate`) converted by `job.measureUnit`
|
||
* @apiSuccess (200) {Boolean} useConfirmedVolume True when confirmed actual volume override is active
|
||
* @apiSuccess (200) {Number} [actualSprayVolume] Actual spray volume calculated from applications (`SUM(App.totalSprayMat)` normalized and converted)
|
||
* @apiSuccess (200) {Number} [confirmedActualVolume] Confirmed actual spray volume from report settings (`rptOp.actualVol`) converted by `job.measureUnit`
|
||
* @apiSuccess (200) {Number} [effectiveVolume] Authoritative volume (`confirmedActualVolume` when `useConfirmedVolume=true`, otherwise `actualSprayVolume`)
|
||
* @apiSuccess (200) {Boolean} useCustomWeather True when custom weather was entered on the job
|
||
* @apiSuccess (200) {Object} [weather] Custom weather block when present
|
||
* @apiSuccess (200) {Number} [weather.windSpeed_kt] Wind speed in knots
|
||
* @apiSuccess (200) {String} [weather.windDir] Wind direction value stored on the job
|
||
* @apiSuccess (200) {Number} [weather.temp_c] Temperature in Celsius
|
||
* @apiSuccess (200) {Number} [weather.humidity_pct] Humidity percent
|
||
* @apiSuccess (200) {Object[]} data Array of session records (one per file)
|
||
* @apiSuccess (200) {String} data.sessionId Session/file ID
|
||
* @apiSuccess (200) {String} data.fileName Log file name
|
||
* @apiSuccess (200) {String} data.startDateTime ISO 8601 start time
|
||
* @apiSuccess (200) {String} data.endDateTime ISO 8601 end time
|
||
* @apiSuccess (200) {Number} data.totalFlightTime_s Total flight time (seconds)
|
||
* @apiSuccess (200) {Number} data.totalSprayTime_s Total spray time (seconds)
|
||
* @apiSuccess (200) {Number} data.totalTurnTime_s Total turn time (seconds)
|
||
* @apiSuccess (200) {Number} data.totalSprayed_ha Area sprayed (hectares)
|
||
* @apiSuccess (200) {Number} data.totalSprayMat Total material sprayed
|
||
* @apiSuccess (200) {String} data.totalSprayMatUnit Material unit (e.g., 'lit', 'gal', 'kg')
|
||
* @apiSuccess (200) {Number} data.avgSpraySpeed_ms Average spray speed (m/s)
|
||
* @apiSuccess (200) {String} [data.sprayZoneName] Spray zone/area name from file metadata
|
||
* @apiSuccess (200) {Number} [data.sprayZoneArea_ha] Spray zone area in hectares from file metadata
|
||
* @apiSuccess (200) {Number} data.appRate Application rate
|
||
* @apiSuccess (200) {String} data.appRateUnit Rate unit (e.g., 'lit/ha')
|
||
* @apiSuccess (200) {String} [data.flowController] Flow controller display name
|
||
* @apiSuccess (200) {Number} [data.sprayOnLag_s] Spray-on lag in seconds
|
||
* @apiSuccess (200) {Number} [data.sprayOffLag_s] Spray-off lag in seconds
|
||
* @apiSuccess (200) {Number} [data.pulsesPerLiter] Pulses per liter from file metadata
|
||
* @apiSuccess (200) {Object[]} data.files File list for the session
|
||
* @apiSuccess (200) {String} data.files.fileId File ObjectId
|
||
* @apiSuccess (200) {String} [data.files.name] File name
|
||
* @apiSuccess (200) {String} [assignedPilotId] Assigned pilot ObjectId (from job record)
|
||
* @apiSuccess (200) {String} [assignedPilotName] Assigned pilot name (from job record)
|
||
* @apiSuccess (200) {String} [assignedAircraftId] Assigned aircraft ObjectId from latest live JobAssign (null when not live-assigned)
|
||
* @apiSuccess (200) {String} [assignedAircraftName] Assigned aircraft name from latest live JobAssign (null when not live-assigned)
|
||
* @apiSuccess (200) {String} [assignedAircraftTailNumber] Assigned aircraft tail number from latest live JobAssign (null when not live-assigned)
|
||
* @apiSuccess (200) {String} [planAircraftName] Planned aircraft name (from job record)
|
||
* @apiSuccess (200) {String} [planAircraftTailNumber] Planned aircraft tail number (from job record)
|
||
* @apiSuccess (200) {String} [assignedDate] Latest job assignment timestamp (ISO 8601 UTC)
|
||
*
|
||
* @apiError (401) {Object} error Not authorized (missing/invalid X-API-Key)
|
||
* @apiError (404) {Object} error Job not found
|
||
*
|
||
* @apiExample {curl} Example Usage:
|
||
* curl -X GET https://api.agmission.com/api/v1/jobs/12345/sessions \
|
||
* -H "X-API-Key: ak_test_..."
|
||
*
|
||
* @apiSeeAlso GET /api/v1/jobs/:jobId/sessions/:fileId/records, GET /api/v1/jobs/:jobId/areas, POST /api/v1/jobs/:jobId/export
|
||
*/
|
||
router.get('/jobs/:jobId/sessions', pubCtl.getSessions);
|
||
|
||
// ── Raw GPS trace records ────────────────────────────────────────────────
|
||
/**
|
||
* @api {get} /api/v1/jobs/:jobId/sessions/:fileId/records Get Session Records (Paginated)
|
||
* @apiVersion 1.0.0
|
||
* @apiName GetSessionRecords
|
||
* @apiGroup Sessions
|
||
* @apiDescription Returns raw GPS trace points with cursor-based pagination.
|
||
* Use `interval` parameter for GPS thinning (e.g., every 5 seconds).
|
||
* Recommended for incremental Power BI refresh and lightweight queries.
|
||
*
|
||
* @apiParam {Number} jobId Job ID
|
||
* @apiParam {String} fileId Session/file ID
|
||
* @apiParam {String} [after] Alias for `startingAfter`
|
||
* @apiParam {String} [startingAfter] Cursor for next page
|
||
* @apiParam {Number} [limit=500] Records per page (max configured by PUBLIC_API_RECORDS_MAX_LIMIT)
|
||
* @apiParam {Number} [interval] GPS thinning interval (seconds, float). Use `interval=0` (or omit) for no thinning.
|
||
* @apiParam {Boolean} [fm=false] Include Flight Master / AgDisp fields
|
||
*
|
||
* @apiHeader {String} X-API-Key API key
|
||
*
|
||
* @apiSuccess (200) {Object[]} data Array of GPS records
|
||
* @apiSuccess (200) {String} data.timeUtc ISO 8601 timestamp
|
||
* @apiSuccess (200) {Number} data.gpsTime Raw GPS time value from AppDetail
|
||
* @apiSuccess (200) {Number} data.lat Latitude (decimal degrees)
|
||
* @apiSuccess (200) {Number} data.lon Longitude (decimal degrees)
|
||
* @apiSuccess (200) {Number} data.utmX UTM X coordinate
|
||
* @apiSuccess (200) {Number} data.utmY UTM Y coordinate
|
||
* @apiSuccess (200) {Number} data.alt Altitude (meters)
|
||
* @apiSuccess (200) {Number} data.grSpeed Ground speed (m/s)
|
||
* @apiSuccess (200) {Number} data.heading Heading (degrees)
|
||
* @apiSuccess (200) {Number} data.xTrack Cross-track error
|
||
* @apiSuccess (200) {Number} data.lockedLine Locked guidance line number
|
||
* @apiSuccess (200) {Number} data.hdop HDOP value
|
||
* @apiSuccess (200) {Number} data.satsIn Raw satellite/inside-area value.
|
||
* AgNav native NT binary encoding: value is satellite count, plus 100 when inside spray area.
|
||
* If value < 100: outside area, number of satellites in view.
|
||
* If value >= 100: inside area, satellites = value - 100.
|
||
* @apiSuccess (200) {Number} data.tslu Raw time since last GPS differential correction update (seconds)
|
||
* @apiSuccess (200) {Number} data.calcodeFreq Raw calibration/frequency field.
|
||
* 30000-60000 indicates frequency/RPM => true RPM = calcodeFreq - 30000.
|
||
* Also used for spray offset in decimeter: <20000 positive offset; >60000 negative offset (stored as 65536 - abs(offset)).
|
||
* @apiSuccess (200) {Number} data.sprayStat Spray state value from source data (for example 0=off, 1/2=application, 3=segment-start marker)
|
||
* @apiSuccess (200) {Number} data.flowRateApplied Flow rate applied (L/min)
|
||
* @apiSuccess (200) {Number} data.flowRateRequired Flow rate required (L/min)
|
||
* @apiSuccess (200) {Number} data.appRateRequired Required application rate
|
||
* @apiSuccess (200) {Number} data.appRateApplied Application rate applied (L/ha)
|
||
* @apiSuccess (200) {Number} data.swathWidth Swath width (meters)
|
||
* @apiSuccess (200) {Number} data.boomPressure_psi Boom pressure (PSI)
|
||
* @apiSuccess (200) {String} [data.flowController] Flow controller display name
|
||
* @apiSuccess (200) {Number} [data.sprayOnLag_s] Spray-on lag in seconds
|
||
* @apiSuccess (200) {Number} [data.sprayOffLag_s] Spray-off lag in seconds
|
||
* @apiSuccess (200) {Number} [data.pulsesPerLiter] Pulses per liter from session metadata
|
||
* @apiSuccess (200) {Array} [data.rpm] RPM array/value from source data
|
||
* @apiSuccess (200) {Number} data.windSpeed_kt Wind speed in knots
|
||
* @apiSuccess (200) {Number} data.windDir_deg Wind direction (0-360°)
|
||
* @apiSuccess (200) {Number} data.temp_c Temperature (°C)
|
||
* @apiSuccess (200) {Number} data.humidity_pct Humidity (%)
|
||
* @apiSuccess (200) {Number} [data.sprayHeight_m] Flight Master spray height (when `fm=true`)
|
||
* @apiSuccess (200) {Number} [data.driftX_m] Flight Master drift X offset (when `fm=true`)
|
||
* @apiSuccess (200) {Number} [data.driftY_m] Flight Master drift Y offset (when `fm=true`)
|
||
* @apiSuccess (200) {Number} [data.depositX_m] Flight Master deposit X offset (when `fm=true`)
|
||
* @apiSuccess (200) {Number} [data.depositY_m] Flight Master deposit Y offset (when `fm=true`)
|
||
* @apiSuccess (200) {Number} [data.radarAlt_m] Radar altitude in meters (when `fm=true`)
|
||
* @apiSuccess (200) {Number} [data.laserAlt_m] Laser altitude in meters (when `fm=true`)
|
||
* @apiSuccess (200) {Boolean} hasMore True if more records available
|
||
* @apiSuccess (200) {String} [nextCursor] Cursor for next page
|
||
*
|
||
* @apiError (401) {Object} error Not authorized
|
||
* @apiError (404) {Object} error Session/file not found
|
||
*
|
||
* @apiExample {curl} Fetch 500 records, every 5 seconds:
|
||
* curl "https://api.agmission.com/api/v1/jobs/12345/sessions/507f1f77.../records?limit=500&interval=5" \
|
||
* -H "X-API-Key: ak_test_..."
|
||
*
|
||
* @apiExample {curl} Fetch next page:
|
||
* curl "https://api.agmission.com/api/v1/jobs/12345/sessions/507f1f77.../records?startingAfter=507f191e810c19729de8605f" \
|
||
* -H "X-API-Key: ak_test_..."
|
||
*/
|
||
router.get('/jobs/:jobId/sessions/:fileId/records', pubCtl.getSessionRecords);
|
||
|
||
// ── Spray-area GeoJSON polygons ──────────────────────────────────────────
|
||
/**
|
||
* @api {get} /api/v1/jobs/:jobId/areas Get Spray Areas (GeoJSON)
|
||
* @apiVersion 1.0.0
|
||
* @apiName GetAreas
|
||
* @apiGroup Areas
|
||
* @apiDescription Returns GeoJSON FeatureCollection of planned spray zones and exclusion boundaries.
|
||
* Features include spray areas (`type: "area"`) and no-spray zones (`type: "xcl"`).
|
||
*
|
||
* @apiParam {Number} jobId Job ID
|
||
*
|
||
* @apiHeader {String} X-API-Key API key
|
||
*
|
||
* @apiSuccess (200) {String} type GeoJSON type ("FeatureCollection")
|
||
* @apiSuccess (200) {Number} jobId Associated job ID
|
||
* @apiSuccess (200) {Object[]} features Array of GeoJSON features
|
||
* @apiSuccess (200) {String} features.type GeoJSON type ("Feature")
|
||
* @apiSuccess (200) {Object} features.properties Feature properties
|
||
* @apiSuccess (200) {String} features.properties.name Feature name
|
||
* @apiSuccess (200) {String} features.properties.type Feature type ("area" or "xcl")
|
||
* @apiSuccess (200) {Number} [features.properties.area_ha] Area in hectares (for type="area")
|
||
* @apiSuccess (200) {Number} [features.properties.appRate] Application rate (for type="area")
|
||
* @apiSuccess (200) {String} [features.properties.appRateUnit] Rate unit (for type="area", e.g., 'lit/ha')
|
||
* @apiSuccess (200) {Number} [features.properties.appRateUnitCode] Raw application-rate unit code
|
||
* @apiSuccess (200) {Object} features.geometry GeoJSON geometry (Polygon)
|
||
* @apiSuccess (200) {String} features.geometry.type Geometry type ("Polygon")
|
||
* @apiSuccess (200) {Number[][][]} features.geometry.coordinates Polygon coordinates
|
||
*
|
||
* @apiError (401) {Object} error Not authorized
|
||
* @apiError (404) {Object} error Job not found
|
||
*
|
||
* @apiExample {curl} Example Usage:
|
||
* curl -X GET https://api.agmission.com/api/v1/jobs/12345/areas \
|
||
* -H "X-API-Key: ak_test_..."
|
||
*
|
||
* @apiSeeAlso GET /api/v1/jobs/:jobId/sessions
|
||
*/
|
||
router.get('/jobs/:jobId/areas', pubCtl.getAreas);
|
||
|
||
// ── Async export ─────────────────────────────────────────────────────────
|
||
/**
|
||
* @api {post} /api/v1/jobs/:jobId/export Trigger Async Export
|
||
* @apiVersion 1.0.0
|
||
* @apiName TriggerExport
|
||
* @apiGroup Exports
|
||
* @apiDescription Initiates async generation of a bulk CSV or JSON export.
|
||
* Returns immediately with exportId; use GET /exports/:exportId to poll for status.
|
||
*
|
||
* Request deduplication: Identical requests within 5 minutes reuse existing export (no rate limit consumed).
|
||
* Per-account rate limit: 20 exports per 60 minutes (configurable).
|
||
*
|
||
* @apiParam {Number} jobId Job ID
|
||
*
|
||
* @apiHeader {String} X-API-Key API key
|
||
* @apiHeader {String} Content-Type application/json
|
||
*
|
||
* @apiBody {String} format Export format: "csv" or "json"
|
||
* @apiBody {String} [units="metric"] Unit system: "metric" (default) or "us"
|
||
* @apiBody {Number} [interval] GPS thinning interval in seconds (float, optional)
|
||
*
|
||
* @apiSuccess (202) {String} exportId Export job ID
|
||
* @apiSuccess (202) {String} status Export status ("pending")
|
||
* @apiSuccess (202) {String} format Export format
|
||
* @apiSuccess (202) {String} units Unit system
|
||
* @apiSuccess (202) {String} createdAt ISO 8601 creation timestamp
|
||
*
|
||
* @apiSuccess (200) {String} exportId Export job ID (reused from cache)
|
||
* @apiSuccess (200) {String} status Export status ("ready" or "pending")
|
||
* @apiSuccess (200) {Boolean} reused=true Indicates request was deduplicated
|
||
* @apiSuccess (200) {String} [downloadUrl] Download URL (if status="ready")
|
||
*
|
||
* @apiError (401) {Object} error Not authorized
|
||
* @apiError (404) {Object} error Job not found
|
||
* @apiError (409) {Object} error Invalid parameters
|
||
* @apiError (429) {Object} error Rate limit exceeded
|
||
*
|
||
* @apiHeader {Number} RateLimit-Limit Maximum requests per account per window
|
||
* @apiHeader {Number} RateLimit-Remaining Requests remaining in current window
|
||
* @apiHeader {Number} RateLimit-Reset Unix timestamp of window reset
|
||
* @apiHeader {Number} Retry-After Seconds to wait before retrying (on 429 only)
|
||
*
|
||
* @apiExample {curl} Trigger CSV export:
|
||
* curl -X POST https://api.agmission.com/api/v1/jobs/12345/export \
|
||
* -H "X-API-Key: ak_test_..." \
|
||
* -H "Content-Type: application/json" \
|
||
* -d '{"format":"csv","units":"metric"}'
|
||
*
|
||
* @apiSeeAlso GET /api/v1/exports/:exportId, GET /api/v1/exports/:exportId/download
|
||
*/
|
||
router.post('/jobs/:jobId/export', exportAccountLimiter, exportCtl.triggerExport);
|
||
|
||
/**
|
||
* @api {get} /api/v1/exports/:exportId Get Export Status
|
||
* @apiVersion 1.0.0
|
||
* @apiName GetExportStatus
|
||
* @apiGroup Exports
|
||
* @apiDescription Polls the status of an async export job.
|
||
* Keep polling until status is "ready", then download the file.
|
||
*
|
||
* @apiParam {String} exportId Export job ID (returned by POST /export)
|
||
*
|
||
* @apiHeader {String} X-API-Key API key
|
||
*
|
||
* @apiSuccess (200) {String} exportId Export job ID
|
||
* @apiSuccess (200) {String} status Export status: "pending", "processing", "ready", or "error"
|
||
* @apiSuccess (200) {String} format Export format ("csv" or "json")
|
||
* @apiSuccess (200) {String} units Unit system
|
||
* @apiSuccess (200) {String} createdAt ISO 8601 creation timestamp
|
||
* @apiSuccess (200) {String} [expiresAt] ISO 8601 expiry timestamp (file available until this time)
|
||
* @apiSuccess (200) {String} [downloadUrl] Download endpoint URL (when status="ready")
|
||
* @apiSuccess (200) {String} [error] Error message (when status="error")
|
||
*
|
||
* @apiError (401) {Object} error Not authorized
|
||
* @apiError (404) {Object} error Export not found
|
||
*
|
||
* @apiExample {curl} Poll export status:
|
||
* curl -X GET https://api.agmission.com/api/v1/exports/66f4a8c1.../status \
|
||
* -H "X-API-Key: ak_test_..."
|
||
*
|
||
* @apiSeeAlso POST /api/v1/jobs/:jobId/export, GET /api/v1/exports/:exportId/download
|
||
*/
|
||
router.get('/exports/:exportId', exportCtl.getExportStatus);
|
||
|
||
/**
|
||
* @api {get} /api/v1/exports/:exportId/download Download Export File
|
||
* @apiVersion 1.0.0
|
||
* @apiName DownloadExport
|
||
* @apiGroup Exports
|
||
* @apiDescription Streams the ready export file (CSV or JSON).
|
||
* Must call GET /exports/:exportId first and wait for status="ready".
|
||
*
|
||
* Files remain available for download until expiresAt (default 24 hours after ready).
|
||
* Can be downloaded multiple times before expiry.
|
||
*
|
||
* @apiParam {String} exportId Export job ID
|
||
*
|
||
* @apiHeader {String} X-API-Key API key
|
||
*
|
||
* @apiSuccess (200) {Binary} file File stream (CSV or JSON)
|
||
* @apiSuccessExample {curl} Response Headers:
|
||
* HTTP/1.1 200 OK
|
||
* Content-Type: text/csv
|
||
* Content-Disposition: attachment; filename="export_job12345_66f4a8c1.csv"
|
||
* Content-Length: 1048576
|
||
*
|
||
* @apiError (401) {Object} error Not authorized
|
||
* @apiError (404) {Object} error Export not found or expired
|
||
*
|
||
* @apiExample {curl} Download export:
|
||
* curl -X GET https://api.agmission.com/api/v1/exports/66f4a8c1.../download \
|
||
* -H "X-API-Key: ak_test_..." \
|
||
* -o export_job12345.csv
|
||
*
|
||
* @apiSeeAlso GET /api/v1/exports/:exportId
|
||
*/
|
||
router.get('/exports/:exportId/download', exportCtl.downloadExport);
|
||
|
||
app.use('/api/v1', router);
|
||
};
|