From 7e87472cfae2600d3c2f9f476096f5ab0b5fd155 Mon Sep 17 00:00:00 2001 From: Theresa Kamerman Date: Tue, 18 Jun 2024 16:36:24 -0700 Subject: [PATCH] Add `/modelExtraction` endpoint --- src/main.ts | 2 + src/packages/hasura/hasura-events.ts | 124 +++++++++++++++++++++++++++ 2 files changed, 126 insertions(+) create mode 100644 src/packages/hasura/hasura-events.ts diff --git a/src/main.ts b/src/main.ts index 14bf8e5..0284a1c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -7,6 +7,7 @@ import initApiPlaygroundRoutes from './packages/api-playground/api-playground.js import initAuthRoutes from './packages/auth/routes.js'; import { DbMerlin } from './packages/db/db.js'; import initFileRoutes from './packages/files/files.js'; +import initHasuraRoutes from './packages/hasura/hasura-events.js'; import initHealthRoutes from './packages/health/health.js'; import initSwaggerRoutes from './packages/swagger/swagger.js'; import cookieParser from 'cookie-parser'; @@ -44,6 +45,7 @@ async function main(): Promise { initAuthRoutes(app, authHandler); initFileRoutes(app); initHealthRoutes(app); + initHasuraRoutes(app); initSwaggerRoutes(app); app.listen(PORT, () => { diff --git a/src/packages/hasura/hasura-events.ts b/src/packages/hasura/hasura-events.ts new file mode 100644 index 0000000..a83f0d0 --- /dev/null +++ b/src/packages/hasura/hasura-events.ts @@ -0,0 +1,124 @@ +import type { Express } from 'express'; +import { adminOnlyAuth } from '../auth/middleware.js'; +import rateLimit from 'express-rate-limit'; +import getLogger from '../../logger.js'; +import { getEnv } from '../../env.js'; +import { generateJwt, decodeJwt } from '../auth/functions.js'; + +export default (app: Express) => { + const logger = getLogger('packages/hasura/hasura-events'); + const { RATE_LIMITER_LOGIN_MAX } = getEnv(); + + const refreshLimiter = rateLimit({ + legacyHeaders: false, + max: RATE_LIMITER_LOGIN_MAX, + standardHeaders: true, + windowMs: 15 * 60 * 1000, // 15 minutes + }); + + /** + * @swagger + * /modelExtraction: + * post: + * security: + * - bearerAuth: [] + * consumes: + * - application/json + * produces: + * - application/json + * parameters: + * - in: header + * name: x-hasura-role + * schema: + * type: string + * required: false + * requestBody: + * content: + * application/json: + * schema: + * type: object + * properties: + * missionModelId: + * type: integer + * responses: + * 200: + * description: ExtractionResponse + * 403: + * description: Unauthorized error + * 401: + * description: Unauthenticated error + * summary: Request extraction of a Mission Model's JAR + * tags: + * - Hasura + */ + app.post('/modelExtraction', refreshLimiter, adminOnlyAuth, async (req, res) => { + const { jwtPayload } = decodeJwt(req.get('authorization')); + const username = jwtPayload?.username as string; + + const { body } = req; + const { missionModelId } = body; + + // Invoke endpoints using the Hasura Metadata API + const { HASURA_METADATA_API_URL: metadataURL } = getEnv(); + + // Generate a temporary token that has Hasura Admin access + const tempToken = generateJwt(username, 'admin', ['admin'], '10s'); + + const headers = { + Authorization: `Bearer ${tempToken}`, + 'Content-Type': 'application/json', + 'x-hasura-role': 'admin', + 'x-hasura-user-id': username, + }; + + const generateBody = (name: string) => + JSON.stringify({ + args: { + name: `refresh${name}`, + payload: { id: missionModelId }, + source: 'Aerie', + }, + type: 'pg_invoke_event_trigger', + }); + + const extract = async (name: string) => { + return await fetch(metadataURL, { + body: generateBody(name), + headers, + method: 'POST', + }) + .then(response => { + if (!response.ok) { + logger.error(`Bad status received when extracting ${name}: [${response.status}] ${response.statusText}`); + return { + error: `Bad status received when extracting ${name}: [${response.status}] ${response.statusText}`, + status: response.status, + statusText: response.statusText, + }; + } + return response.json(); + }) + .catch(error => { + logger.error(`Error connecting to Hasura metadata API at ${metadataURL}. Full error below:\n${error}`); + return { error: `Error connecting to metadata API at ${metadataURL}` }; + }); + }; + + const [activityTypeResp, modelParameterResp, resourceTypeResp] = await Promise.all([ + extract('ActivityTypes'), + extract('ModelParameters'), + extract('ResourceTypes'), + ]); + + logger.info(`POST /modelExtraction: Extraction triggered for model: ${missionModelId}`); + + res.json({ + message: `Extraction triggered for model: ${missionModelId}`, + response: { + activity_types: activityTypeResp, + model_parameters: modelParameterResp, + resource_types: resourceTypeResp, + }, + }); + }); +};