#✅ - Amplify Gen 2 Lambda function that creates an Amplify Gen 2 Data record

41 messages · Page 1 of 1 (latest)

wide ether
#

I am basically trying to find an simple example of a Gen 2 function that creates a record in the amplify database. So basically a combination of this:

(from https://docs.amplify.aws/react/build-a-backend/functions/set-up-function/)

`import type { Schema } from "../../data/resource"

export const handler: Schema["sayHello"]["functionHandler"] = async (event) => {
// arguments typed from .arguments()
const { name } = event.arguments
// return typed from .returns()
return Hello, ${name}!
}
`
and this: (from https://docs.amplify.aws/react/build-a-backend/data/mutate-data/)

`import { generateClient } from 'aws-amplify/data';
import { type Schema } from '../amplify/data/resource'

const client = generateClient<Schema>();

const { errors, data: newTodo } = await client.models.Todo.create({
content: "My new todo",
isDone: true,
})`

This seems like a very common use case (I need it because I need to add a trusted timestamp from the server). There is a page that seems to slightly touch on this (https://docs.amplify.aws/react/build-a-backend/data/customize-authz/grant-lambda-function-access-to-api/) but the file layout is very different from the layout described in the functions section of the doc (https://docs.amplify.aws/react/build-a-backend/functions/) so I am wondering if it is out of date or referring to gen 1 (it looks like from the server I have to access it through the GraphQL API?).

I think a clear simple example of server side data access would be very useful for the docs.

Thanks!

mystic willow
wide ether
#

So, as a long shot I tried just putting in the exact same code that works on the client side to see if it would work, and it did.

So the code looks like:

`import type { Schema } from "../../data/resource"
import { generateClient } from "aws-amplify/data";
import { Amplify } from 'aws-amplify';
import outputs from '../../../amplify_outputs.json';

Amplify.configure(outputs);

const client = generateClient<Schema>();

export const handler: Schema["clientAnswerFn"]["functionHandler"] = async (event) => {
// arguments typed from .arguments()
const { name } = event.arguments

let curTime = new Date();
let curTimeString = curTime.toISOString();

const { errors, data: newQuiz } = await client.models.Quiz.create({
title: "Test quiz " + curTime,
});

if (errors != null) {
return "Error " + errors[0].message;
}
if (newQuiz == null) {
return "No quiz items";
}
// return typed from .returns()
return Hello, ${name}! + " " + curTimeString + " " + newQuiz.title;
}`

Getting that outputs from "amplify_outputs.json" seems a little dicey to me, but it works. I'm nervous that it is only working because I am in a sandbox. I'd love some verification from the AWS guys that this is okay to have in a lambda function. This is my handler.ts file (copied from the example in https://docs.amplify.aws/react/build-a-backend/functions/set-up-function/) and I have a quiz object that contains a string title (just like th ToDo).

midnight elkBOT
#

✅ - Amplify Gen 2 Lambda function that creates an Amplify Gen 2 Data record

midnight elkBOT
rotund schooner
#

Hi @wide ether . I am also tring to do the same thing. It's surprising to know that the client code also works on the backend. Have you verified if it's work in production? Thanks.

teal basin
#

You can use npx ampx generate graphql-client-code --app-id [app-id] --branch main within the function directory to generate the graphql code

wide ether
scenic bear
wide ether
wide ether
teal basin
# wide ether This tuitorial is for a PostConfirmationTriggerEvent and does not work for defin...

You should be able to follow the same principles from the tutorial, ex:

import { Amplify } from "aws-amplify";
import { generateClient } from "aws-amplify/data";
import { env } from "$amplify/env/run-project";
import { getProject } from "../../graphql/queries";
import { Schema } from "../../data/resource";

Amplify.configure(
  {
    API: {
      GraphQL: {
        endpoint: env.AMPLIFY_DATA_GRAPHQL_ENDPOINT,
        region: env.AWS_REGION,
        defaultAuthMode: "iam",
      },
    },
  },
  {
    Auth: {
      credentialsProvider: {
        getCredentialsAndIdentityId: async () => ({
          credentials: {
            accessKeyId: env.AWS_ACCESS_KEY_ID,
            secretAccessKey: env.AWS_SECRET_ACCESS_KEY,
            sessionToken: env.AWS_SESSION_TOKEN,
          },
        }),
        clearCredentialsAndIdentityId: () => {
        },
      },
    },
  }
);

const client = generateClient<Schema>({
  authMode: "iam",
});

export const handler: Schema["runProject"]["functionHandler"] = async (event) => {
  console.log(`Run Project: ${JSON.stringify(event)}`);

  const projectResult = await client.graphql({
    query: getProject,
    variables: {
      id: event.arguments.projectId,
    },
  });

  const project = projectResult.data.getProject;
  console.log(`Retrieved project: ${JSON.stringify(project)}`);
  if (!project || !project.id || !project.name || !project.owner) {
    console.error(`Invalid project: ${JSON.stringify(project)}`);
    return {success: false, error: "invalid project"};
  }

  return {success: true, project: project};
};
#

Handler.ts:

import {defineFunction, secret} from "@aws-amplify/backend"

export const runProject = defineFunction({
  name: "run-project",
  entry: "./handler.ts",
  environment: {
    ...
  },
  timeoutSeconds: 30,
})
#

Make sure to grant it access within your data/resource.ts

#

& make sure to generate the graphql code via npx ampx generate graphql-client-code --app-id [app-id] --branch main somewhere accessible by the handler

wide ether
#

ok, so, since this is a function, do I build the graphql folder inside the amplify/functions/my-function folder?

(The tutorial has you build it in amplify/auth/client-response)

#

(Which is why I am guessing it is expecting the handler to be a TriggerHandler instead of a Remote Function Handler)

wide ether
#

ah. That seems to work. Thanks! My only issue is now is that env.AMPLIFY_DATA_GRAPHQL_ENDPOINT doesn't seem to exist

"Property 'AMPLIFY_DATA_GRAPHQL_ENDPOINT' does not exist on type 'LambdaProvidedEnvVars'.ts(2339)"

wide ether
#

Ah, got it. I needed to add authorization for my function to my schema. Thanks for all your help! Seems to work now!

serene robin
#

Can you share what the auth you added to you function was for the schema?

#

I am using Cognito for user auth and trying to get this postConfirmation lambda working too

#

With "iam" I get this error

2024-12-06T23:16:21.729Z    0efbff11-e186-4e3d-9e47-cc947477d0bb    INFO    Error saving API key: {
  data: {},
  errors: [
    UnauthorizedException: Unknown error
        at Xde (file:///var/task/index.mjs:198:11794)
        at Fv (file:///var/task/index.mjs:198:11443)
        at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
        at async s (file:///var/task/index.mjs:198:12090)
        at async cO._graphql (file:///var/task/index.mjs:198:41384)
        at async Runtime.mYe [as handler] (file:///var/task/index.mjs:221:805) {
      recoverySuggestion: `If you're calling an Amplify-generated API, make sure to set the "authMode" in generateClient({ authMode: '...' }) to the backend authorization rule's auth provider ('apiKey', 'userPool', 'iam', 'oidc', 'lambda')`
    }
  ]
}
wide ether
#

I can share what I did, but I'm not doing this for PostConfirmation. I wanted to write a callable lambda function that made some changes to a data table.

So in my data/resource.ts my schema looks like:

const schema = a.schema({
clientAnswerFn: a
.query()
.arguments({
matchId: a.id(),
matchPlayInstanceId: a.id(),
questionIndex: a.integer(),
answer: a.integer(),
})
.returns(a.string())
.handler(a.handler.function(clientAnswerFn))
.authorization(allow => [allow.publicApiKey()]), // NOT HERE
Question: a
.model({
quizId: a.id(),
orderInd: a.integer(),
prompt: a.string(),
ans1: a.string(),
ans2: a.string(),
ans3: a.string(),
ans4: a.string(),
correct: a.integer(),
})
.authorization(allow => [allow.publicApiKey()]),

...

}).authorization(allow => [allow.resource(clientAnswerFn)]); // HERE

The top entry "clientAnswerFn" is my function, and below that are my data tables "Question", ...

The trick is that the functio authorization goes AFTER the a.schema(), NOT after the function. I think what this is saying is "Give this function authorization over the whole database" Maybe there is a way to authorize a specific table, but I couldn't get that to work, and the one line in the docs that did it, did it for the entire schema like I am doing.

serene robin
#

hmm I cant use a public api key tho


const functionWithDataAccess = defineFunction({
  entry: '../lambdas/generate-api-key/handler.ts',
});

const schema = a.schema({
  generateApiKey: a
    .query()
    .arguments({
    })
    .returns(a.json())
    .authorization(allow => [allow.authenticated(),])
    .handler(a.handler.function(GenerateApiKey)),

  ApiKey: a
    .model({
      key: a.string().required(), // The API key itself
      userId: a.string().required(), // Cognito User ID
      expirationDate: a.string(), // ISO format expiration date
    })
    .authorization((allow) => [allow.owner(), allow.authenticated()]), // Restrict access to the owner

})
.authorization(allow => [allow.authenticated(), allow.resource(functionWithDataAccess)]);
scenic bear
#

userPool

serene robin
#
defaultAuthorizationMode: "userPool",
#

Maybe I am going about this all the wrong way tho

I want AWS Cognito for user sign up etc
I want users to be able to hit the API with an API key
I want to be able to query the users data from an external server using an auth token or API key so each request can only requests the users data.

To do this I created a new schema for storing API keys and want to add a new API key when a user signs up using the postConfirmation trigger

#

Is there some way to achieve this without managing the keys in my own schema?

mild tundra
#

if your lambda function is importing amplify_outputs.json, that file doesn't exist before the backend is built/deployed in the default amplify.yml. You are going to end up in a race condition. You are only working locally in sandbox because you implemented the Lambda after the amplify_outputs.json already exists locally. If you blow away your sandbox and amplify_outputs.json and attempt to rebuild, you'll hit an import error when it tries to bundle the lambda.

#

Backend code shouldn't depend on anything in amplify_outputs.json as the file doesn't exist before the backend is deployed for the first time, so that pattern will never work in a net new deployment.

#

(comment was mainly for @wide ether 's first reply regarding accessing the amplify_outputs.json from Lambda)

earnest olive
#

I have a question / problem related to this topic!
I am trying to do what the docs say here https://docs.amplify.aws/nextjs/build-a-backend/data/customize-authz/grant-lambda-function-access-to-api/
It's not working to configure Amplify in the suggested way:

import { getAmplifyDataClientConfig } from '@aws-amplify/backend-function/runtime';
import { env } from '$amplify/env/<function-name>'; // replace with your function name

const { resourceConfig, libraryOptions } = await getAmplifyDataClientConfig(env);

Amplify.configure(resourceConfig, libraryOptions);

Error in "resourceConfig": Argument of type '{ invalidType: "This function needs to be granted authorization((allow) => [allow.resource(fcn)]) on the data schema."; }' is not assignable to parameter of type 'ResourcesConfig | LegacyConfig | AmplifyOutputs'.

handler.ts

import { Amplify } from "aws-amplify";
import type { EventBridgeHandler } from "aws-lambda";
import { refreshMoviesData } from '@amplify/services/movieService';

import { getAmplifyDataClientConfig } from '@aws-amplify/backend-function/runtime';
import { env } from '$amplify/env/refreshMoviesData'; // replace with your function name

const { resourceConfig, libraryOptions } = await getAmplifyDataClientConfig(env);

Amplify.configure(resourceConfig, libraryOptions);

export const handler: EventBridgeHandler<"Scheduled Event", null, void> = async (event) => {
  try {
    await refreshMoviesData();
  } catch (error) {
    ...
  }
};

data\resource.ts

import { type ClientSchema, a, defineData } from "@aws-amplify/backend";
import { refreshMoviesData } from "../jobs/refreshMoviesData/resource";

const schema = a.schema({
  Movie: a
    .model({...}),
}).authorization(allow => [allow.authenticated(), allow.resource(refreshMoviesData)]);

export type Schema = ClientSchema<typeof schema>;

export const data = defineData({
  schema,
  authorizationModes: {
    defaultAuthorizationMode: 'userPool',
  },
});
coral hull
mystic willow
#

@wide ether sorry for the confusion and @teal basin thanks for clarifying.

#

@coral hull Thanks for pointing that out. Does the information provided by @teal basin not resolve your issue?

scenic bear