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

fix: Lambda client env var name issue #2324

Merged
merged 13 commits into from
Dec 17, 2024
7 changes: 7 additions & 0 deletions .changeset/warm-sloths-tickle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@aws-amplify/backend-function': minor
'@aws-amplify/backend-data': patch
'@aws-amplify/platform-core': minor
---

Update getAmplifyDataClientConfig to work with named data backend
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

33 changes: 26 additions & 7 deletions packages/backend-data/src/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import { Bucket } from 'aws-cdk-lib/aws-s3';
import { BucketDeployment, Source } from 'aws-cdk-lib/aws-s3-deployment';

const modelIntrospectionSchemaKey = 'modelIntrospectionSchema.json';
const defaultName = 'amplifyData';

/**
* Singleton factory for AmplifyGraphqlApi constructs that can be used in Amplify project files.
Expand Down Expand Up @@ -127,7 +128,7 @@ class DataGenerator implements ConstructContainerEntryGenerator {
private readonly getInstanceProps: ConstructFactoryGetInstanceProps,
private readonly outputStorageStrategy: BackendOutputStorageStrategy<GraphqlOutput>
) {
this.name = props.name ?? 'amplifyData';
this.name = props.name ?? defaultName;
}

generateContainerEntry = ({
Expand Down Expand Up @@ -307,14 +308,32 @@ class DataGenerator implements ConstructContainerEntryGenerator {

convertJsResolverDefinition(scope, amplifyApi, schemasJsFunctions);

const namePrefix = this.name === defaultName ? '' : defaultName;

const ssmEnvironmentScopeContext = {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const ssmEnvironmentScopeContext = {
const ssmEnvironmentEntries = {

perhaps. this is a nit, feel free to ignore.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This naming came from the function that receives these entries/vars ssmEnvironmentEntriesGenerator.generateSsmEnvironmentEntries(scopeContext). I'm going to proceed with this naming and can revisit it in another change if we decide its clearer with different naming.

[`${namePrefix}${this.name}_GRAPHQL_ENDPOINT`]:
amplifyApi.resources.cfnResources.cfnGraphqlApi.attrGraphQlUrl,
[`${namePrefix}${this.name}_MODEL_INTROSPECTION_SCHEMA_BUCKET_NAME`]:
modelIntrospectionSchemaBucket.bucketName,
[`${namePrefix}${this.name}_MODEL_INTROSPECTION_SCHEMA_KEY`]:
modelIntrospectionSchemaKey,
['AMPLIFY_DATA_DEFAULT_NAME']: `${namePrefix}${this.name}`,
};

const backwardsCompatibleScopeContext =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

update this plz if you take above nit.

`${this.name}_GRAPHQL_ENDPOINT` !==
`${namePrefix}${this.name}_GRAPHQL_ENDPOINT`
? {
// @deprecated
[`${this.name}_GRAPHQL_ENDPOINT`]:
amplifyApi.resources.cfnResources.cfnGraphqlApi.attrGraphQlUrl,
}
: {};

const ssmEnvironmentEntries =
ssmEnvironmentEntriesGenerator.generateSsmEnvironmentEntries({
[`${this.name}_GRAPHQL_ENDPOINT`]:
amplifyApi.resources.cfnResources.cfnGraphqlApi.attrGraphQlUrl,
[`${this.name}_MODEL_INTROSPECTION_SCHEMA_BUCKET_NAME`]:
modelIntrospectionSchemaBucket.bucketName,
[`${this.name}_MODEL_INTROSPECTION_SCHEMA_KEY`]:
modelIntrospectionSchemaKey,
...ssmEnvironmentScopeContext,
...backwardsCompatibleScopeContext,
});

const policyGenerator = new AppSyncPolicyGenerator(
Expand Down
8 changes: 3 additions & 5 deletions packages/backend-function/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,12 @@ type DataClientConfig = {

// @public (undocumented)
type DataClientEnv = {
AMPLIFY_DATA_GRAPHQL_ENDPOINT: string;
AMPLIFY_DATA_MODEL_INTROSPECTION_SCHEMA_BUCKET_NAME: string;
AMPLIFY_DATA_MODEL_INTROSPECTION_SCHEMA_KEY: string;
AWS_ACCESS_KEY_ID: string;
AWS_SECRET_ACCESS_KEY: string;
AWS_SESSION_TOKEN: string;
AWS_REGION: string;
};
AMPLIFY_DATA_DEFAULT_NAME: string;
} & Record<string, unknown>;

// @public (undocumented)
type DataClientError = {
Expand Down Expand Up @@ -111,7 +109,7 @@ const getAmplifyDataClientConfig: <T>(env: T, s3Client?: S3Client) => Promise<Da

// @public (undocumented)
type InvalidConfig = unknown & {
invalidType: 'This function needs to be granted `authorization((allow) => [allow.resource(fcn)])` on the data schema.';
invalidType: 'Some of the AWS environment variables needed to configure Amplify are missing.';
};

// @public (undocumented)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { NoSuchKey, S3, S3ServiceException } from '@aws-sdk/client-s3';

import { getAmplifyDataClientConfig } from './get_amplify_clients_configuration.js';

const validEnv = {
const validDefaultEnv = {
AMPLIFY_DATA_MODEL_INTROSPECTION_SCHEMA_BUCKET_NAME:
'TEST_VALUE for AMPLIFY_DATA_MODEL_INTROSPECTION_SCHEMA_BUCKET_NAME',
AMPLIFY_DATA_MODEL_INTROSPECTION_SCHEMA_KEY:
Expand All @@ -14,6 +14,21 @@ const validEnv = {
AWS_SESSION_TOKEN: 'TEST_VALUE for AWS_SESSION_TOKEN',
AWS_REGION: 'TEST_VALUE for AWS_REGION',
AMPLIFY_DATA_GRAPHQL_ENDPOINT: 'TEST_VALUE for AMPLIFY_DATA_GRAPHQL_ENDPOINT',
AMPLIFY_DATA_DEFAULT_NAME: 'AmplifyData',
};

const validNamedEnv = {
AMPLIFY_DATA_TEST_NAME_MODEL_INTROSPECTION_SCHEMA_BUCKET_NAME:
'TEST_VALUE for AMPLIFY_DATA_MODEL_INTROSPECTION_SCHEMA_BUCKET_NAME',
AMPLIFY_DATA_TEST_NAME_MODEL_INTROSPECTION_SCHEMA_KEY:
'TEST_VALUE for AMPLIFY_DATA_MODEL_INTROSPECTION_SCHEMA_KEY',
AWS_ACCESS_KEY_ID: 'TEST_VALUE for AWS_ACCESS_KEY_ID',
AWS_SECRET_ACCESS_KEY: 'TEST_VALUE for AWS_SECRET_ACCESS_KEY',
AWS_SESSION_TOKEN: 'TEST_VALUE for AWS_SESSION_TOKEN',
AWS_REGION: 'TEST_VALUE for AWS_REGION',
AMPLIFY_DATA_TEST_NAME_GRAPHQL_ENDPOINT:
'TEST_VALUE for AMPLIFY_DATA_GRAPHQL_ENDPOINT',
AMPLIFY_DATA_DEFAULT_NAME: 'AmplifyDataTestName',
};

let mockS3Client: S3;
Expand All @@ -23,110 +38,146 @@ void describe('getAmplifyDataClientConfig', () => {
mockS3Client = new S3();
});

Object.keys(validEnv).forEach((envFieldToExclude) => {
void it(`returns empty config objects when ${envFieldToExclude} is not included`, async () => {
const env = { ...validEnv } as Record<string, string>;
delete env[envFieldToExclude];
assert.deepEqual(await getAmplifyDataClientConfig(env), {
resourceConfig: {},
libraryOptions: {},
[
{
name: 'no set name',
dataBackendName: 'AMPLIFY_DATA',
validEnv: validDefaultEnv,
},
{
name: 'an explicit name',
dataBackendName: 'AMPLIFY_DATA_TEST_NAME',
validEnv: validNamedEnv,
},
].forEach(({ name, dataBackendName, validEnv }) => {
void describe(`env variable with ${name} for the data backend`, () => {
Object.keys(validEnv)
.filter((k) => k !== 'AMPLIFY_DATA_DEFAULT_NAME')
.forEach((envFieldToExclude) => {
if (envFieldToExclude.includes(dataBackendName)) {
void it(`throws error when ${envFieldToExclude} is not included`, async () => {
const env = { ...validEnv } as Record<string, string>;
delete env[envFieldToExclude];
await assert.rejects(
async () => await getAmplifyDataClientConfig(env),
/The data environment variables are malformed/
);
});

void it(`throws error when ${envFieldToExclude} is not a string`, async () => {
const env = { ...validEnv } as Record<string, unknown>;
env[envFieldToExclude] = 123;
await assert.rejects(
async () => await getAmplifyDataClientConfig(env),
/The data environment variables are malformed/
);
});
} else {
void it(`returns empty config objects when ${envFieldToExclude} is not included`, async () => {
const env = { ...validEnv } as Record<string, string>;
delete env[envFieldToExclude];
assert.deepEqual(await getAmplifyDataClientConfig(env), {
resourceConfig: {},
libraryOptions: {},
});
});

void it(`returns empty config objects when ${envFieldToExclude} is not a string`, async () => {
const env = { ...validEnv } as Record<string, unknown>;
env[envFieldToExclude] = 123;
assert.deepEqual(await getAmplifyDataClientConfig(env), {
resourceConfig: {},
libraryOptions: {},
});
});
}
});

void it('raises a custom error message when the model introspection schema is missing from the s3 bucket', async () => {
const s3ClientSendMock = mock.method(mockS3Client, 'send', async () => {
throw new NoSuchKey({ message: 'TEST_ERROR', $metadata: {} });
});
mock.method(mockS3Client, 'send', s3ClientSendMock);

await assert.rejects(
async () => await getAmplifyDataClientConfig(validEnv, mockS3Client),
new Error(
'Error retrieving the schema from S3. Please confirm that your project has a `defineData` included in the `defineBackend` definition.'
)
);
});
});

void it(`returns empty config objects when ${envFieldToExclude} is not a string`, async () => {
const env = { ...validEnv } as Record<string, unknown>;
env[envFieldToExclude] = 123;
assert.deepEqual(await getAmplifyDataClientConfig(env), {
resourceConfig: {},
libraryOptions: {},
void it('raises a custom error message when there is a S3ServiceException error retrieving the model introspection schema from the s3 bucket', async () => {
const s3ClientSendMock = mock.method(mockS3Client, 'send', async () => {
throw new S3ServiceException({
name: 'TEST_ERROR',
message: 'TEST_MESSAGE',
$fault: 'server',
$metadata: {},
});
});
mock.method(mockS3Client, 'send', s3ClientSendMock);

await assert.rejects(
async () => await getAmplifyDataClientConfig(validEnv, mockS3Client),
new Error(
'Error retrieving the schema from S3. You may need to grant this function authorization on the schema. TEST_ERROR: TEST_MESSAGE.'
)
);
});
});
});

void it('raises a custom error message when the model introspection schema is missing from the s3 bucket', async () => {
const s3ClientSendMock = mock.method(mockS3Client, 'send', async () => {
throw new NoSuchKey({ message: 'TEST_ERROR', $metadata: {} });
});
mock.method(mockS3Client, 'send', s3ClientSendMock);

await assert.rejects(
async () => await getAmplifyDataClientConfig(validEnv, mockS3Client),
new Error(
'Error retrieving the schema from S3. Please confirm that your project has a `defineData` included in the `defineBackend` definition.'
)
);
});
void it('re-raises a non-S3 error received when retrieving the model introspection schema from the s3 bucket', async () => {
const s3ClientSendMock = mock.method(mockS3Client, 'send', async () => {
throw new Error('Test Error');
});
mock.method(mockS3Client, 'send', s3ClientSendMock);

void it('raises a custom error message when there is a S3ServiceException error retrieving the model introspection schema from the s3 bucket', async () => {
const s3ClientSendMock = mock.method(mockS3Client, 'send', async () => {
throw new S3ServiceException({
name: 'TEST_ERROR',
message: 'TEST_MESSAGE',
$fault: 'server',
$metadata: {},
await assert.rejects(
async () => await getAmplifyDataClientConfig(validEnv, mockS3Client),
new Error('Test Error')
);
});
});
mock.method(mockS3Client, 'send', s3ClientSendMock);

await assert.rejects(
async () => await getAmplifyDataClientConfig(validEnv, mockS3Client),
new Error(
'Error retrieving the schema from S3. You may need to grant this function authorization on the schema. TEST_ERROR: TEST_MESSAGE.'
)
);
});

void it('re-raises a non-S3 error received when retrieving the model introspection schema from the s3 bucket', async () => {
const s3ClientSendMock = mock.method(mockS3Client, 'send', async () => {
throw new Error('Test Error');
});
mock.method(mockS3Client, 'send', s3ClientSendMock);

await assert.rejects(
async () => await getAmplifyDataClientConfig(validEnv, mockS3Client),
new Error('Test Error')
);
});

void it('returns the expected libraryOptions and resourceConfig values in the happy case', async () => {
const s3ClientSendMock = mock.method(mockS3Client, 'send', () => {
return Promise.resolve({
Body: {
transformToString: () => JSON.stringify({ testSchema: 'TESTING' }),
},
void it('returns the expected libraryOptions and resourceConfig values in the happy case', async () => {
const s3ClientSendMock = mock.method(mockS3Client, 'send', () => {
return Promise.resolve({
Body: {
transformToString: () =>
JSON.stringify({ testSchema: 'TESTING' }),
},
});
});
mock.method(mockS3Client, 'send', s3ClientSendMock);

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

assert.deepEqual(
await libraryOptions.Auth.credentialsProvider.getCredentialsAndIdentityId?.(),
{
credentials: {
accessKeyId: 'TEST_VALUE for AWS_ACCESS_KEY_ID',
secretAccessKey: 'TEST_VALUE for AWS_SECRET_ACCESS_KEY',
sessionToken: 'TEST_VALUE for AWS_SESSION_TOKEN',
},
}
);
assert.deepEqual(
await libraryOptions.Auth.credentialsProvider.clearCredentialsAndIdentityId?.(),
undefined
);

assert.deepEqual(resourceConfig, {
API: {
GraphQL: {
endpoint: 'TEST_VALUE for AMPLIFY_DATA_GRAPHQL_ENDPOINT',
region: 'TEST_VALUE for AWS_REGION',
defaultAuthMode: 'iam',
modelIntrospection: { testSchema: 'TESTING' },
},
},
});
});
});
mock.method(mockS3Client, 'send', s3ClientSendMock);

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

assert.deepEqual(
await libraryOptions.Auth.credentialsProvider.getCredentialsAndIdentityId?.(),
{
credentials: {
accessKeyId: 'TEST_VALUE for AWS_ACCESS_KEY_ID',
secretAccessKey: 'TEST_VALUE for AWS_SECRET_ACCESS_KEY',
sessionToken: 'TEST_VALUE for AWS_SESSION_TOKEN',
},
}
);
assert.deepEqual(
await libraryOptions.Auth.credentialsProvider.clearCredentialsAndIdentityId?.(),
undefined
);

assert.deepEqual(resourceConfig, {
API: {
GraphQL: {
endpoint: 'TEST_VALUE for AMPLIFY_DATA_GRAPHQL_ENDPOINT',
region: 'TEST_VALUE for AWS_REGION',
defaultAuthMode: 'iam',
modelIntrospection: { testSchema: 'TESTING' },
},
},
});
});
});
Loading
Loading