aws-amplify / amplify-backend

Home to all tools related to Amplify's code-first DX (Gen 2) for building fullstack apps on AWS
Apache License 2.0
167 stars 56 forks source link

Make it easier to drop levels of abstraction and access CloudFormation outputs. Allow a.handler.function to have a datasource. #1658

Closed sisygoboom closed 1 month ago

sisygoboom commented 3 months ago

Environment information

N/A

Description

Amplify's backend currently makes anything outside of the expected patterns extremely difficult. This is because of a combination of two things: 1) Automated resource creation 2) Inability to access the low-level CDK constructs from said resource creation in a typesafe or object-oriented way with the ARNs attached

Example/steps to reproduce:

I have a custom resolver in my schema, it needs a role with access to write to DynamoDB.

I can see the problem from the console and its an easy fix:

Failed to process request: User: arn:aws:sts::592261843145:assumed-role/amplify-paritaeapp-chris--getMessagesByConversation-iAvA6ZVYEWJM/amplify-paritaeapp-chris--getMessagesByConversatio-esQehfeuATUS is not authorized to perform: dynamodb:Query on resource: arn:aws:dynamodb:eu-west-2:592261843145:table/ChatTable because no identity-based policy allows the dynamodb:Query action

but defineFunction does not allow me to modify any of the constructs. Next logical step would be to add a dynamodbdatasource to the handler but that is also not an option because a.handler.function only has the property brandSymbol and no additional parameters besides the function. Next bet is to try a.query()...authorization(allow => ['dynamodb:Query']) or something similar but that allow only includes IAM options. Event the defineData factory returns nothing of use.

After exhausting options in data/resource.ts I looked for possibilities in backend.ts but unfortunately this turned out to be impossible without running into a circular dependency. It also doesn't help that while the cloudformation outputs are present somewhere in backend its not easy to navigate as the typing system here is very loose and the random hash applied to resources by amplify make it impossible to guess/hardcode the arn.

// backend.ts

import { defineBackend } from '@aws-amplify/backend';
import { auth } from './auth/resource';
import { data } from './data/resource';
import { AttributeType, BillingMode, Table } from 'aws-cdk-lib/aws-dynamodb';
import { Stack } from 'aws-cdk-lib';

const backend = defineBackend({
  auth,
  data,
});

// Define the DynamoDB table
const chatTable = new Table(Stack.of(backend.data), "ChatTable", {
  tableName: "ChatTable",
  partitionKey: {
    name: "pk",
    type: AttributeType.STRING,
  },
  sortKey: {
    name: "sk",
    type: AttributeType.STRING,
  },
  billingMode: BillingMode.PAY_PER_REQUEST,
});

// Add the DynamoDB data source to the backend
backend.data.addDynamoDbDataSource("ChatTableDataSource", chatTable);

export default backend;

// data/resource.ts

import { type ClientSchema, a, defineData, defineFunction } from '@aws-amplify/backend';

export const getMessagesByConversationId = defineFunction({
  name: 'getMessagesByConversationIdFunction',
  entry: './getMessagesByConversationId.ts',
});

const schema = a.schema({
  ChatTable: a.customType({
    pk: a.string().required(),
    sk: a.string().required(),
    type: a.string().required(),
    message: a.string(),
    userId: a.string(),
    conversationId: a.string(),
    timestamp: a.string(),
  }),
  getMessagesByConversationId: a
    .query()
    .arguments({
      conversationId: a.string().required(),
      lastKey: a.string(),
    })
    .returns(a.ref("ChatTable").array())
    .handler(a.handler.function(getMessagesByConversationId))
    .authorization(allow => [allow.publicApiKey()]),
})
.authorization(allow => [
  allow.resource(getMessagesByConversationId).to(['query']),
]);

export type Schema = ClientSchema<typeof schema>;

export const data = defineData({
  schema,
  authorizationModes: {
    defaultAuthorizationMode: 'apiKey',
    apiKeyAuthorizationMode: {
      expiresInDays: 30
    },
  }
});
ykethan commented 3 months ago

Hey @sisygoboom, thank you for reaching out. From the error message and behavior observed on the backend.ts, the issue appears to similar to https://github.com/aws-amplify/amplify-backend/issues/1375 https://github.com/aws-amplify/amplify-backend/issues/1552

Could you try the suggestions on comments https://github.com/aws-amplify/amplify-backend/issues/1375#issuecomment-2079388509 https://github.com/aws-amplify/amplify-backend/issues/1552#issuecomment-2138816880

You should be able to access the table name on the backend.ts for example backend.data.resources.tables["Todo"].tableArn which should return a token and resolved by CloudFormation. Do let us know if i may have misunderstood anything.

But i do agree, a prop to add datasources on the function would be great. Marking this as feature request.