@seasketch/geoprocessing
Version:
Geoprocessing and reporting framework for SeaSketch 2.0
210 lines (183 loc) • 7.27 kB
text/typescript
import { Stack, StackProps } from "aws-cdk-lib";
import { Construct } from "constructs";
import { CloudFrontWebDistribution } from "aws-cdk-lib/aws-cloudfront";
import {
Manifest,
GeoprocessingFunctionMetadata,
ProcessingFunctionMetadata,
getSyncFunctionMetadata,
getAsyncFunctionMetadata,
hasClients,
isSyncFunctionMetadata,
isAsyncFunctionMetadata,
} from "../manifest.js";
import { createPublicBuckets, setupBucketFunctionAccess } from "./buckets.js";
import {
createClientResources,
setupClientFunctionAccess,
} from "./clientResources.js";
import { createProjectFunctions } from "./functionResources.js";
import { createTables, setupTableFunctionAccess } from "./dynamodb.js";
import { createRestApi } from "./restApiGateway.js";
import {
createWebSocketApi,
setupWebSocketFunctionAccess,
} from "./socketApiGateway.js";
import { RestApi } from "aws-cdk-lib/aws-apigateway";
import { WebSocketApi } from "aws-cdk-lib/aws-apigatewayv2";
import {
GpPublicBuckets,
GpProjectFunctions,
GpDynamoTables,
SyncFunctionWithMeta,
AsyncFunctionWithMeta,
ProcessingFunctions,
} from "./types.js";
import { genOutputMeta } from "./outputMeta.js";
import { Bucket } from "aws-cdk-lib/aws-s3";
import { LambdaStack } from "./LambdaStack.js";
import { createLambdaStacks } from "./lambdaResources.js";
import { Function } from "aws-cdk-lib/aws-lambda";
import { hasOwnProperty } from "../../client-core.js";
/** StackProps extended with geoprocessing project metadata */
export interface GeoprocessingStackProps extends StackProps {
/** Name of the geoprocessing project */
projectName: string;
/** Path to top-level of geoprocessing project */
projectPath: string;
/** Manifest used to figure out what resources should be created for this stack */
manifest: Manifest;
/** maximum number of functions to allow per LambdaStack */
functionsPerStack?: number;
/** State of function stacks if already deployed (by function title) */
existingFunctionStacks?: string[][];
/** State of worker stacks if already deployed (by function title) */
existingWorkerStacks?: string[][];
}
/**
* A geoprocessing project is deployed as a single monolithic stack using CloudFormation.
* The stack inspects the manifest and creates stack resources
* Supports functions being sync or async in executionMode and preprocessor or geoprocessor in purpose
* Async + preprocessor combination is not supported
* Each project gets one API gateway, s3 bucket, and db table for tracking tasks and function run timeestimates
* If async functions also get a socket subscriptions db table and web socket machinery
* Each function gets a lambda and rest endpoint for sync mode, or a set of lambdas (start + run) for async mode
*/
export class GeoprocessingStack extends Stack {
props: GeoprocessingStackProps;
// Refer to key stack resources using class properties for easy reference, just pass the stack instance around
// See types for more info on these resources
publicBuckets: GpPublicBuckets;
tables: GpDynamoTables;
projectFunctions: GpProjectFunctions;
restApi: RestApi;
socketApi?: WebSocketApi;
clientBucket?: Bucket;
clientDistribution?: CloudFrontWebDistribution;
lambdaStacks: LambdaStack[];
constructor(scope: Construct, id: string, props: GeoprocessingStackProps) {
super(scope, id, props);
this.props = props;
this.lambdaStacks = createLambdaStacks(this, this.props);
this.projectFunctions = createProjectFunctions(this);
this.publicBuckets = createPublicBuckets(this);
// Create client bundle with bucket deploymentand and Cloudfront distribution
const { clientBucket, clientDistribution } = createClientResources(this);
this.clientBucket = clientBucket;
this.clientDistribution = clientDistribution;
this.tables = createTables(this);
// Create rest endpoints for gp lambdas
this.restApi = createRestApi(this);
this.socketApi = createWebSocketApi(this);
// Provide gp lambdas access to other services
setupTableFunctionAccess(this);
setupBucketFunctionAccess(this);
setupWebSocketFunctionAccess(this);
setupClientFunctionAccess(this);
genOutputMeta(this);
}
hasClients(): boolean {
return hasClients(this.props.manifest);
}
/** Return metadata for all PreprocessingHandlers and sync GeoprocessingHandlers in manifest */
getSyncFunctionMetas(): ProcessingFunctionMetadata[] {
return getSyncFunctionMetadata(this.props.manifest);
}
/** Return metadata for all async GeoprocessingHandlers in manifest */
getAsyncFunctionMetas(): GeoprocessingFunctionMetadata[] {
return getAsyncFunctionMetadata(this.props.manifest);
}
/** Returns true if manifest has sync functions metadata for all PreprocessingHandlers and GeoprocessingHandlers in manifest */
hasSyncFunctions(): boolean {
return this.getSyncFunctionMetas().length > 0;
}
hasAsyncFunctions(): boolean {
return this.getAsyncFunctionMetas().length > 0;
}
/** aggregate and return sync lambda function meta from lambda stacks */
getSyncFunctionsWithMeta(): SyncFunctionWithMeta[] {
return this.lambdaStacks.reduce<SyncFunctionWithMeta[]>((acc, curStack) => {
const syncFunctions = curStack
.getProcessingFunctions()
.filter<SyncFunctionWithMeta>(this.isSyncFunctionWithMeta);
return [...acc, ...syncFunctions];
}, []);
}
/**
* @returns run functions across all lambda stacks
*/
getAsyncRunLambdas(): Function[] {
return this.lambdaStacks.reduce<Function[]>(
(acc, curStack) => [...acc, ...curStack.getAsyncRunLambdas()],
[],
);
}
/**
* @returns async lambda function meta from all lambda stacks
*/
getAsyncFunctionsWithMeta(): AsyncFunctionWithMeta[] {
return this.lambdaStacks.reduce<AsyncFunctionWithMeta[]>(
(acc, curStack) => {
const asyncFunctions = curStack
.getProcessingFunctions()
.filter<AsyncFunctionWithMeta>(this.isAsyncFunctionWithMeta);
return [...acc, ...asyncFunctions];
},
[],
);
}
/** Returns true if sync function with meta and narrows type */
isSyncFunctionWithMeta(
funcWithMeta: any,
): funcWithMeta is SyncFunctionWithMeta {
return (
hasOwnProperty(funcWithMeta, "func") &&
hasOwnProperty(funcWithMeta, "meta") &&
isSyncFunctionMetadata(funcWithMeta.meta)
);
}
/** Returns true if async function with meta and narrows type */
isAsyncFunctionWithMeta(
funcWithMeta: any,
): funcWithMeta is AsyncFunctionWithMeta {
return (
hasOwnProperty(funcWithMeta, "startFunc") &&
hasOwnProperty(funcWithMeta, "runFunc") &&
hasOwnProperty(funcWithMeta, "meta") &&
isAsyncFunctionMetadata(funcWithMeta.meta)
);
}
getProcessingFunctions() {
return this.lambdaStacks.reduce<ProcessingFunctions>((acc, curStack) => {
return [...acc, ...curStack.getProcessingFunctions()];
}, []);
}
}
/**
* Returns root lambda handler method pointer in module.function dot notation
*/
export function getHandlerPointer(funcMeta: ProcessingFunctionMetadata) {
return `${funcMeta.handlerFilename
.replace(/\.js$/, "")
.replace(/\.ts$/, "")}Handler.handler`;
}