Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NestJS & Unable to determine event source based on event #658

Open
mits87 opened this issue Oct 5, 2023 · 5 comments
Open

NestJS & Unable to determine event source based on event #658

mits87 opened this issue Oct 5, 2023 · 5 comments

Comments

@mits87
Copy link

mits87 commented Oct 5, 2023

Hi guys,

First of al thanks for the great plugin - helps a lot!
Unfortunately I have a small problem when I'm trying to use it together with NestJS and GraphQL.

When I'm executing sls invoke local -f api I'm getting:

{
    "errorMessage": "Unable to determine event source based on event.",
    "errorType": "Error",
    "stackTrace": [
        "Error: Unable to determine event source based on event.",
        "    at getEventSourceNameBasedOnEvent (/Users/lolo/Sites/app/node_modules/@vendia/serverless-express/src/event-sources/utils.js:127:9)",
        "    at proxy (/Users/lolo/Sites/app/node_modules/@vendia/serverless-express/src/configure.js:38:51)",
        "    at handler (/Users/lolo/Sites/app/node_modules/@vendia/serverless-express/src/configure.js:99:12)",
        "    at handler (/Users/lolo/Sites/app/src/events/api.ts:35:10)"
    ]
}

My setup is similar to the example but of course much more advanced (I use additional GraphQL).

Below my setup:

serverless.yml

service: tmp-app

frameworkVersion: '3'
useDotenv: true

plugins:
  - serverless-offline

package:
  excludeDevDependencies: true
  individually: true

provider:
  name: aws
  runtime: nodejs18.x
  architecture: arm64
  region: ${opt:region, 'eu-central-1'}
  stage: ${opt:stage, 'dev'}
  memorySize: 2048
  versionFunctions: false
  logRetentionInDays: 1

functions:
  api:
    handler: ./src/events/api.handler
    events:
      - httpApi:
          path: /{proxy+}
          method: ANY

./src/events/api.ts

import { ExpressAdapter } from '@nestjs/platform-express';
import serverlessExpress from '@vendia/serverless-express';
import { NestFactory } from '@nestjs/core';
import { APIGatewayProxyEventV2, Callback, Context, Handler } from 'aws-lambda';
import express from 'express';

import { AppModule } from './app.module';

let server: Handler;

export const handler: Handler = async (
  event: APIGatewayProxyEventV2,
  context: Context,
  callback: Callback,
): Promise<Handler> => {
  if (!server) {
    const expressApp = express();

    const app = await NestFactory.create(AppModule, new ExpressAdapter(expressApp))
    await app.init();

    server = serverlessExpress({ app: expressApp });
  }

  return server(event, context, callback);
};

./app.module.ts

import { ApolloDriverConfig } from '@nestjs/apollo';
import { Module, NestModule } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';

const IS_PROD = process.env.NODE_ENV === 'production';

@Module({
  imports: [
    GraphQLModule.forRoot<ApolloDriverConfig>({
      driver: ApolloDriver,
      autoSchemaFile: IS_PROD ? undefined : `${process.cwd()}/src/schema.graphql`,
      typePaths: IS_PROD ? ['./**/*.graphql'] : undefined,
      autoTransformHttpErrors: true,
      buildSchemaOptions: {
        // Refs. https://docs.nestjs.com/graphql/scalars#code-first
        dateScalarMode: 'timestamp',
      },
      introspection: true,
      installSubscriptionHandlers: true,
      sortSchema: true,
    }),
  ],
})
export class AppModule implements NestModule {}

./nest-cli.json

{
  "$schema": "https://json.schemastore.org/nest-cli",
  "collection": "@nestjs/schematics",
  "sourceRoot": "src",
  "compilerOptions": {
    "deleteOutDir": true,
    "assets": [
      { "include": "**/*.graphql", "watchAssets": true }
    ],
    "plugins": [
      {
        "name": "@nestjs/graphql",
        "options": {
          "introspectComments": true
        }
      }
    ]
  }
}

Any ideas what is wrong?

@mits87
Copy link
Author

mits87 commented Nov 2, 2023

Any news here?

@Raphael0010
Copy link

Raphael0010 commented Nov 14, 2023

Same problem here, any news ? 🥲
@mits87 Did you find a solution ?

@hernandemonteiro
Copy link

Temporary Solution

The error is from the file utils, i will work to fix this problem, now i just work in this, and on the node_modules i commented a function call.

serverless.yml

service: test-lambda
useDotenv: true

plugins:
  - "serverless-plugin-typescript"
  - serverless-plugin-optimize
  - serverless-offline

provider:
  name: aws
  runtime: nodejs20.x

functions:
  main:
    handler: src/lambda.handler
    events:
      - http:
          method: ANY
          path: /
          async: true
      - http:
          method: ANY
          path: "{any+}"
          async: true

lambda.ts

import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import cookieParser from 'cookie-parser';
import serverlessExpress from '@vendia/serverless-express';
import { Callback, Context, Handler } from 'aws-lambda';

import { AppModule } from './app.module';

let server: Handler;

async function bootstrap() {
  if (!server) {
    const nestApp = await NestFactory.create(AppModule);
    nestApp.useGlobalPipes(new ValidationPipe());
    nestApp.use(cookieParser());
    await nestApp.init();

    const app = nestApp.getHttpAdapter().getInstance();
    server = serverlessExpress({ app });
  }
  return server;
}

export const handler: Handler = async (
  event,
  context: Context,
  callback: Callback,
) => {
  event = {
    ...event,
    path: event.requestPath,
    httpMethod: event.method,
    requestContext: {},
  };
  server = server ?? (await bootstrap());
  return await server(event, context, callback);
};

utils.js on node_modules commented

const url = require('url')

function getPathWithQueryStringParams ({
  event,
  query = event.multiValueQueryStringParameters,
  // NOTE: Use `event.pathParameters.proxy` if available ({proxy+}); fall back to `event.path`
  path = (event.pathParameters && event.pathParameters.proxy && `/${event.pathParameters.proxy}`) || event.path,
  // NOTE: Strip base path for custom domains
  stripBasePath = '',
  replaceRegex = new RegExp(`^${stripBasePath}`)
}) {
  return url.format({
    pathname: path.replace(replaceRegex, ''),
    query
  })
}

function getEventBody ({
  event,
  body = event.body,
  isBase64Encoded = event.isBase64Encoded
}) {
  return Buffer.from(body, isBase64Encoded ? 'base64' : 'utf8')
}

function getRequestValuesFromEvent ({
  event,
  method = event.httpMethod,
  path = getPathWithQueryStringParams({ event })
}) {
  let headers = {}

  if (event.multiValueHeaders) {
    headers = getCommaDelimitedHeaders({ headersMap: event.multiValueHeaders, lowerCaseKey: true })
  } else if (event.headers) {
    headers = event.headers
  }

  let body = event.body

  // if (event.body) {
  //   body = getEventBody({ event })
  //   const { isBase64Encoded } = event
  //   headers['content-length'] = Buffer.byteLength(body, isBase64Encoded ? 'base64' : 'utf8')
  // }

  const remoteAddress = (event && event.requestContext && event.requestContext.identity && event.requestContext.identity.sourceIp) || ''

  return {
    method,
    headers,
    body,
    remoteAddress,
    path
  }
}

function getMultiValueHeaders ({ headers }) {
  const multiValueHeaders = {}

  Object.entries(headers).forEach(([headerKey, headerValue]) => {
    const headerArray = Array.isArray(headerValue) ? headerValue.map(String) : [String(headerValue)]

    multiValueHeaders[headerKey.toLowerCase()] = headerArray
  })

  return multiValueHeaders
}

function getEventSourceNameBasedOnEvent ({
  event
}) {
  if (event.requestContext && event.requestContext.elb) return 'AWS_ALB'
  if (event.Records) {
    const eventSource = event.Records[0] ? event.Records[0].EventSource || event.Records[0].eventSource : undefined
    if (eventSource === 'aws:sns') {
      return 'AWS_SNS'
    }
    if (eventSource === 'aws:dynamodb') {
      return 'AWS_DYNAMODB'
    }
    if (eventSource === 'aws:sqs') {
      return 'AWS_SQS'
    }
    if (eventSource === 'aws:kinesis') {
      return 'AWS_KINESIS_DATA_STREAM'
    }
    return 'AWS_LAMBDA_EDGE'
  }
  if (event.requestContext) {
    return event.version === '2.0' ? 'AWS_API_GATEWAY_V2' : 'AWS_API_GATEWAY_V1'
  }
  if (event.traceContext) {
    const functionsExtensionVersion = process.env.FUNCTIONS_EXTENSION_VERSION

    if (!functionsExtensionVersion) {
      console.warn('The environment variable \'FUNCTIONS_EXTENSION_VERSION\' is not set. Only the function runtime \'~3\' is supported.')
    } else if (functionsExtensionVersion === '~3') {
      return 'AZURE_HTTP_FUNCTION_V3'
    } else if (functionsExtensionVersion === '~4') {
      return 'AZURE_HTTP_FUNCTION_V4'
    } else {
      console.warn('The function runtime \'' + functionsExtensionVersion + '\' is not supported. Only \'~3\' and \'~4\' are supported.')
    }
  }
  if (
    event.version &&
    event.version === '0' &&
    event.id &&
    event['detail-type'] &&
    event.source &&
    event.source.startsWith('aws.') && // Might need to adjust this for "Partner Sources", e.g. Auth0, Datadog, etc
    event.account &&
    event.time &&
    event.region &&
    event.resources &&
    Array.isArray(event.resources) &&
    event.detail &&
    typeof event.detail === 'object' &&
    !Array.isArray(event.detail)
  ) {
    // AWS doesn't have a defining Event Source here, so we're being incredibly selective on the structure
    // Ref: https://docs.aws.amazon.com/lambda/latest/dg/services-cloudwatchevents.html
    return 'AWS_EVENTBRIDGE'
  }

  throw new Error('Unable to determine event source based on event.')
}

function getCommaDelimitedHeaders ({ headersMap, separator = ',', lowerCaseKey = false }) {
  const commaDelimitedHeaders = {}

  Object.entries(headersMap)
    .forEach(([headerKey, headerValue]) => {
      const newKey = lowerCaseKey ? headerKey.toLowerCase() : headerKey
      if (Array.isArray(headerValue)) {
        commaDelimitedHeaders[newKey] = headerValue.join(separator)
      } else {
        commaDelimitedHeaders[newKey] = headerValue
      }
    })

  return commaDelimitedHeaders
}

const emptyResponseMapper = () => {}

const parseCookie = (str) =>
  str.split(';')
    .map((v) => v.split('='))
    .reduce((acc, v) => {
      if (!v[1]) {
        return acc
      }
      acc[decodeURIComponent(v[0].trim().toLowerCase())] = decodeURIComponent(v[1].trim())
      return acc
    }, {})

module.exports = {
  getPathWithQueryStringParams,
  getRequestValuesFromEvent,
  getMultiValueHeaders,
  getEventSourceNameBasedOnEvent,
  getEventBody,
  getCommaDelimitedHeaders,
  emptyResponseMapper,
  parseCookie
}

@mits87
Copy link
Author

mits87 commented Jan 5, 2024

@hernandemonteiro thanks for that, waiting for a proper fix :)

@klutzer
Copy link

klutzer commented May 10, 2024

AFAIK there is a fix but just when using {+proxy} mapping on AWS API Gateway.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants