UNPKG

@seasketch/geoprocessing

Version:

Geoprocessing and reporting framework for SeaSketch 2.0

150 lines (144 loc) 4.36 kB
import { Context, APIGatewayProxyResult, APIGatewayProxyEvent, } from "aws-lambda"; import { PreprocessingHandlerOptions, PreprocessingRequest, PreprocessingResponse, Geometry, Feature, Polygon, LineString, Point, Sketch, ValidationError, JSONValue, } from "../types/index.js"; const commonHeaders = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Credentials": true, "Access-Control-Allow-Headers": "*", }; /** * Lambda handler for a preprocessing function * @template G the geometry type of the feature for the geoprocessing function, automatically set from func feature type */ export class PreprocessingHandler< G extends Geometry = Polygon | LineString | Point, P = Record<string, JSONValue>, > { func: ( feature: Feature<G> | Sketch<G>, extraParams: P, ) => Promise<Feature<G> | Sketch<G>>; options: PreprocessingHandlerOptions; // Store last request id to avoid retries on a failure of the lambda // aws runs several retries and there appears to be no setting to avoid this lastRequestId?: string; /** * @param func the preprocessing function, overloaded to allow caller to pass Feature *or* Sketch * @param options prerocessing function deployment options * @template G the geometry type of features for the preprocessing function, automatically set from func feature type */ constructor( func: (feature: Feature<G>, extraParams: P) => Promise<Feature<G>>, options: PreprocessingHandlerOptions, ); constructor( func: (feature: Sketch<G>, extraParams: P) => Promise<Sketch<G>>, options: PreprocessingHandlerOptions, ); constructor( func: (feature, extraParams) => Promise<any>, options: PreprocessingHandlerOptions, ) { this.func = func; this.options = Object.assign({ memory: 1024 }, options); } async lambdaHandler( event: APIGatewayProxyEvent, context: Context, ): Promise<APIGatewayProxyResult> { // TODO: Rate limiting (probably in api gateway?) let request; try { request = this.parseRequest(event); } catch (error: unknown) { return { statusCode: 500, headers: commonHeaders, body: JSON.stringify({ error: error instanceof Error ? error.message : "Internal server error", status: "error", }), }; } // Bail out if replaying previous task if (context.awsRequestId && context.awsRequestId === this.lastRequestId) { // don't replay if (process.env.NODE_ENV !== "test") { console.log("cancelling since event is being replayed"); } return { statusCode: 200, body: "", }; } else { this.lastRequestId = context.awsRequestId; } try { if (process.env.NODE_ENV !== "test") console.log("request", JSON.stringify(request)); const feature = await this.func(request.feature, request.extraParams); return { statusCode: 200, headers: commonHeaders, body: JSON.stringify({ data: feature, status: "ok", } as PreprocessingResponse<Feature<G> | Sketch<G>>), }; } catch (error: unknown) { if (error instanceof ValidationError) { return { statusCode: 200, headers: commonHeaders, body: JSON.stringify({ error: error.message, status: "validationError", } as PreprocessingResponse), }; } else { return { statusCode: 500, headers: commonHeaders, body: JSON.stringify({ error: error instanceof Error ? error.message : "Internal server error", status: "error", } as PreprocessingResponse), }; } } } parseRequest(event: APIGatewayProxyEvent): PreprocessingRequest { if (!event.body) { throw new Error("Invalid request. No request body"); } const json = JSON.parse(event.body); if (!json.feature || !json.feature.type) { throw new Error( "Invalid request. body.feature must be specified as valid GeoJSON", ); } return { feature: json.feature, // TODO: support more response types in seasketch/next extraParams: json.extraParams || {}, responseFormat: "application/json", }; } }