0

Please note: although this question mentions AWS SAM, it is 100% a DynamoDB JavaScript SDK question at heart and can be answered by anyone with experience writing JavaScript Lambdas (or any client-side apps) against DynamoDB using the AWS DynamoDB client/SDK.


So I used AWS SAM to provision a new DynamoDB table with the following attributes:

FeedbackDynamoDB:
  Type: AWS::DynamoDB::Table
  Properties:
    TableName: commentary
    AttributeDefinitions:
      - AttributeName: id
        AttributeType: S
    KeySchema:
      - AttributeName: id
        KeyType: HASH
    ProvisionedThroughput:
      ReadCapacityUnits: 5
      WriteCapacityUnits: 5
    StreamSpecification:
      StreamViewType: NEW_IMAGE

This configuration successfully creates a DynamoDB table called commentary. However, when I view this table in the DynamoDB web console, I noticed a few things:

  • it has a partition key of id (type S)
  • it has no sort key
  • it has no (0) indexes
  • it has a read/write capacity mode of "5"

I'm not sure if this raises any red flags with anyone but I figured I would include those details, in case I've configured anything incorrectly.

Now then, I have a JavaScript (TypeScript) Lambda that instantiates a DynamoDB client (using the JavaScript SDK) and attempts to add a record/item to this table:

// this code is in a file named app.ts:
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import { User, allUsers } from './users';
import { Commentary } from './commentary';
import { PutItemCommand } from "@aws-sdk/client-dynamodb";
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";

export const lambdaHandler = async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
    try {

        const ddbClient = new DynamoDBClient({ region: "us-east-1" });

        let status: number = 200;
        let responseBody: string = "\"message\": \"hello world\"";

        const { id, content, createdAt, providerId, receiverId } = JSON.parse(event.body);
        const commentary = new Commentary(id, content, createdAt, providerId, receiverId);
        console.log("deserialized this into commentary");
        console.log("and the deserialized commentary has content of: " + commentary.getContent());
        await provideCommentary(ddbClient, commentary);
        responseBody = "\"message\": \"received commentary -- check dynamoDb!\"";

        return {
            statusCode: status,
            body: responseBody
        };

    } catch (err) {
        console.log(err);
        return {
            statusCode: 500,
            body: JSON.stringify({
                message: err.stack,
            }),
        };
    }
};

const provideCommentary = async (ddbClient: DynamoDBClient, commentary: Commentary) => {

  const params = {
    TableName: "commentary",

    Item: {
      id: {
        S: commentary.getId()
      },
      content: {
        S: commentary.getContent()
      },
      createdAt: {
        S: commentary.getCreatedAt()
      },
      providerId: {
        N: commentary.getProviderId()
      },
      receiverId: {
        N: commentary.getReceiverId()
      }
    }

  };

  console.log("about to try to insert commentary into dynamo...");

  try {
    console.log("wait for it...")
    const rc = await ddbClient.send(new PutItemCommand(params));
    console.log("DDB response:", rc);
  } catch (err) {
    console.log("hmmm something awry. something....in the mist");
    console.log("Error", err.stack);
    throw err;
  }
};

Where commentary.ts is:

class Commentary {
  private id: string;
  private content: string;
  private createdAt: Date;
  private providerId: number;
  private receiverId: number;

  constructor(id: string, content: string, createdAt: Date, providerId: number, receiverId: number) {
    this.id = id;
    this.content = content;
    this.createdAt = createdAt;
    this.providerId = providerId;
    this.receiverId = receiverId;
  }

  public getId(): string {
    return this.id;
  }

  public getContent(): string {
    return this.content;
  }

  public getCreatedAt(): Date {
    return this.createdAt;
  }

  public getProviderId(): number {
    return this.providerId;
  }

  public getReceiverId(): number {
    return this.receiverId;
  }

}

export { Commentary };

When I update the Lambda with this handler code, and hit the Lambda with the following curl (the Lambda is invoked by an API Gateway URL that I can hit via curl/http):

curl -i --request POST 'https://<my-api-gateway>.execute-api.us-east-1.amazonaws.com/Stage/feedback' \
--header 'Content-Type: application/json' -d '{"id":"123","content":"test feedback","createdAt":"2022-12-02T08:45:26.261-05:00","providerId":457,"receiverId":789}'

I get the following HTTP 500 response:

{"message":"SerializationException: NUMBER_VALUE cannot be converted to String\n

Am I passing it a bad request body (in the curl) or do I need to tweak something in app.ts and/or commentary.ts?

hotmeatballsoup
  • 385
  • 6
  • 58
  • 136
  • 1
    Does CloudWatch Logs indicate that the Lambda function timed out at 3 seconds (the default)? Is the Lambda function configured in VPC public subnet with no DynamoDB VPC endpoint or private subnet with no route to NAT or DynamoDB endpoint? – jarmod Dec 03 '22 at 02:08
  • Great thought @jarmod but the Lambda is executing in less than 500ms and **no**, I'm not seeing indication in the CW logs that its timing out. I also upped the timeout to 60s just for good measure. And I think I'm OK as far as routing/networking/subnets go, but will re-evaluate if I start seeing network errors. I'm just not seeing _any_ errors which is the frustrating part. – hotmeatballsoup Dec 05 '22 at 15:27
  • 1
    What does your (presumably async) Lambda function return? It would typically either return a promise from the DDB send call (e..g `return ddbClient.send(new PutItemCommand(params))` or await that promise and return the promise's resulting data (e.g. `const res = await ddbClient.send(new PutItemCommand(params)); // code here; return res;`). Make sure the function is async and that you await the DDB call or return its promise. – jarmod Dec 05 '22 at 15:38
  • Hi @jarmod -- my intention of the Lambda is to write to DynamoDB and return an empty 200 OK response (this Lambda is sitting behind an API Gateway that exposes it via HTTP/S). Which I'm getting (my test curl returns 200 OK). – hotmeatballsoup Dec 05 '22 at 15:40
  • 1
    OK, so add try/catch around an await of the DDB send, and return 200 OK after the await, return 5xx (or whatever) in the catch. – jarmod Dec 05 '22 at 15:49
  • OK thanks again @jarmod please see my updates again, which reflect the full, actual code I'm using. I think I have the try-catch handling any-and-all possible errors correctly, but please let me know if you see otherwise! – hotmeatballsoup Dec 05 '22 at 15:55
  • 1
    Your code is not awaiting the outcome of the DDB send call. Discard the old callback mechanism and switch to `const rc = await ddbClient.send(new PutItemCommand(params)); console.log("DDB response:", rc);` – jarmod Dec 05 '22 at 16:33
  • Thanks again @jarmod -- your last comment helped significantly but I am still running into new (but similar) errors. I have update the question and started a bounty. Thanks again for your help here today! – hotmeatballsoup Dec 05 '22 at 19:01
  • Use the [DynamoDBDocumentClient](https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/dynamodb-example-dynamodb-utilities.html) to auto-marshall native JavaScript types. See [here](https://stackoverflow.com/questions/71488712/number-value-cannot-be-converted-to-string-when-updating-item) for why, even though that answer related to JavaScript SDK v2. PS it would have been better to raise this new question as a new question, not a complete rewrite of the original. – jarmod Dec 05 '22 at 19:52

2 Answers2

1

Interestingly the DynamoDB API expects numerical fields of items as strings. For example:

"N": "123.45"

The doc says;

Numbers are sent across the network to DynamoDB as strings, to maximize compatibility across languages and libraries. However, DynamoDB treats them as number type attributes for mathematical operations.

Have you tried sending your input with the numerical parameters as strings as shown below? (See providerId and receiverId)

{
   "id":"123",
   "content":"test feedback",
   "createdAt":"2022-12-02T08:45:26.261-05:00",
   "providerId":"457",
   "receiverId":"789"
}

You can convert these IDs into string when you're populating your input Item:

  providerId: {
    N: String(commentary.getProviderId())
  },
  receiverId: {
    N: String(commentary.getReceiverId())
  }

You could also use .toString() but then you'd get errors if the field is not set (null or undefined).

Rafet
  • 858
  • 9
  • 24
  • Thanks @Rafet (+1) I will give the suggested curl a try, but is there anything on the Lambda-side (JavaScript code) I can change to accomodate `id` being a `string` and then both `providerId` and `receiverId` being `numbers`? – hotmeatballsoup Dec 06 '22 at 00:07
  • No worries. It should be as simple as a string conversion. I've updated my answer to include this. – Rafet Dec 06 '22 at 08:18
  • 1
    This was it! Thanks @Rafet I will award you the green check and the full bounty in 5 hrs once StackOverflow allows me to! Thank you so much! I've never seen such a thing: you specify `N` for number but then cast the **number** to...a string! Only the JS devs at Amazon could whiteboard something like that! – hotmeatballsoup Dec 06 '22 at 13:29
  • No problem, glad it worked! To be fair it's the same for all languages' SDKs for DynamoDB. It's to implement a more straightforward compatibility. – Rafet Dec 07 '22 at 08:15
  • @hotmeatballsoup you've marked the wrong answer! :( – Rafet Dec 08 '22 at 09:18
0

Try using a promise to see the outcome:

client.send(command).then(
  (data) => {
    // process data.
  },
  (error) => {
    // error handling.
  }
);

Everything seems alright with your table setup, I believe it's Lambda async issue with the JS sdk. I'm guessing Lambda is not waiting on your code and exiting early. Can you include your full lambda code.

Leeroy Hannigan
  • 11,409
  • 3
  • 14
  • 31
  • Thanks for the suggestion @Lee (+1) -- please see my updates above, I tried this suggestion and it had _no affect (!!!)_ on the Lambda execution! The Lambda shuts down shortly after sending the request off to DynamoDB. – hotmeatballsoup Dec 05 '22 at 15:30
  • @hotmeatballsoup must've marked this as the answer by accident. Please see the thread under my answer. – Rafet Dec 08 '22 at 09:20