📦 EqualifyEverything / equalify

📄 lambda.ts · 138 lines
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138import type { SQSEvent, SQSRecord } from "aws-lambda";
import middy from "@middy/core";
import {
  BatchProcessor,
  EventType,
  processPartialResponse,
} from "@aws-lambda-powertools/batch";
import type { PartialItemFailureResponse } from "@aws-lambda-powertools/batch/types";
import { logMetrics } from "@aws-lambda-powertools/metrics/middleware";
import { MetricUnit } from "@aws-lambda-powertools/metrics";
import convertVeraToEqualifyV2, {
  ReportData,
} from "../../../shared/convertors/VeraToEqualify2.ts";

import { logger, metrics } from "./telemetry.ts";
import scan from "./scan.ts";
import { SqsScanJob } from "../../../shared/types/sqsScanJob.ts";
//import convertToEqualifyV2 from "../../../shared/convertors/VeraToEqualify2.ts"

const processor = new BatchProcessor(EventType.SQS);
const RESULTS_ENDPOINT_PROD = "https://api.equalifyapp.com/public/scanWebhook";
const RESULTS_ENDPOINT_STAGING = "https://api-staging.equalifyapp.com/public/scanWebhook";
const getResultsEndpoint = (isStaging?: boolean) => isStaging ? RESULTS_ENDPOINT_STAGING : RESULTS_ENDPOINT_PROD;

// {"data":{"auditId":"51a5077e-f8e6-4f75-939e-9c91b00a1f2e","urlId":"ea350f8f-5e56-4361-8cd5-570fcea0025d","url":"http://decubing.com/wp-content/uploads/2025/05/zombieplan.pdf","type":"pdf"}}
interface sqsPayload {
  data: SqsScanJob
}

// Process a single SQS Record
const recordHandler = async (record: SQSRecord): Promise<void> => {
  metrics.captureColdStartMetric();
  const startTime = performance.now();
  const payload = record.body;
  
  logger.info("PDF to scan payload:", payload);
  const payloadParsed = JSON.parse(payload) as sqsPayload;

  const job = payloadParsed.data;
  
  // Validate job has required fields
  if (!job.scanId) {
    logger.warn("Job is missing scanId!", { auditId: job.auditId, urlId: job.urlId });
  }
  
  if (payload) {
    try {
      metrics.addMetric("scansStarted", MetricUnit.Count, 1);
      const results = await scan(job).then((result) => {
        const endTime = performance.now();
        const executionDuration = endTime - startTime;
        metrics.addMetric(
          "ScanDuration",
          MetricUnit.Milliseconds,
          executionDuration
        );
        return result;
      });
      if (results) {
        logger.info("Results from PDF scan:", results);
        const parsedResult = JSON.parse(results);
        const equalifiedResults = convertVeraToEqualifyV2(
          parsedResult as ReportData,
          job
        );
        logger.info(`Audit [${job.auditId}]:[${job.urlId}] Scan Complete!`);

        logger.info("Sending to API...", JSON.stringify(equalifiedResults));

        const sendResultsResponse = await fetch(getResultsEndpoint(job.isStaging), {
          method: "post",
          body: JSON.stringify(equalifiedResults),
          headers: { "Content-Type": "application/json" },
        });

        if(sendResultsResponse.ok){
          const responseData = await sendResultsResponse.json();
          logger.info(
            "PDF-scan Results sent to API results webhook!",
            JSON.stringify(responseData)
          );
        }else{
          const errorData = await sendResultsResponse.text();
          logger.error(
            "Error sending results to API results webhook!",
            sendResultsResponse.statusText
          );
          logger.error(
            "Failed to send. Response body:",
            errorData
          )
        }
        
      } else {
        logger.error("Error with PDF scan!", results);
        await sendFailedStatusToResultsEndpoint(job);
      }
    } catch (error) {
      logger.error("Error with payload!", error as string);
      await sendFailedStatusToResultsEndpoint(job);
      throw error;
    }
  }
  metrics.publishStoredMetrics();
  return;
};

const sendFailedStatusToResultsEndpoint = async (job: SqsScanJob) => {
  const failurePayload = {
    auditId: job.auditId,
    scanId: job.scanId,
    urlId: job.urlId,
    status: "failed",
    error: "PDF Scan encountered an error.",
    blockers: [],
  };

  const sendResultsResponse = await fetch(getResultsEndpoint(job.isStaging), {
    method: "post",
    body: JSON.stringify(failurePayload),
    headers: { "Content-Type": "application/json" },
  });
  return sendResultsResponse;
};

// handle batch
const batchHandler = async (event: SQSEvent, context: any) =>
  processPartialResponse(event, recordHandler, processor, {
    context,
    throwOnFullBatchFailure: false,
    processInParallel: false,
  });

// finally, export the handler
export const handler = middy<SQSEvent, PartialItemFailureResponse>(
  batchHandler
).use(logMetrics(metrics, { captureColdStartMetric: true }));