diff --git a/.changeset/warm-sloths-tickle.md b/.changeset/warm-sloths-tickle.md new file mode 100644 index 0000000000..d880694339 --- /dev/null +++ b/.changeset/warm-sloths-tickle.md @@ -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 diff --git a/package-lock.json b/package-lock.json index c3fe532467..503a3ce89c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31661,8 +31661,7 @@ "@aws-amplify/data-schema": "^1.13.4", "@aws-amplify/platform-core": "^1.3.0", "@aws-amplify/plugin-types": "^1.6.0", - "@aws-sdk/client-amplify": "^3.624.0", - "lodash.snakecase": "^4.1.1" + "@aws-sdk/client-amplify": "^3.624.0" }, "devDependencies": { "@types/aws-lambda": "^8.10.119", @@ -33002,6 +33001,7 @@ "@aws-sdk/client-sts": "^3.624.0", "is-ci": "^3.0.1", "lodash.mergewith": "^4.6.2", + "lodash.snakecase": "^4.1.1", "semver": "^7.6.3", "uuid": "^9.0.1", "zod": "^3.22.2" diff --git a/packages/backend-data/src/factory.ts b/packages/backend-data/src/factory.ts index 98d48b0667..af2cfd9789 100644 --- a/packages/backend-data/src/factory.ts +++ b/packages/backend-data/src/factory.ts @@ -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. @@ -127,7 +128,7 @@ class DataGenerator implements ConstructContainerEntryGenerator { private readonly getInstanceProps: ConstructFactoryGetInstanceProps, private readonly outputStorageStrategy: BackendOutputStorageStrategy ) { - this.name = props.name ?? 'amplifyData'; + this.name = props.name ?? defaultName; } generateContainerEntry = ({ @@ -307,14 +308,32 @@ class DataGenerator implements ConstructContainerEntryGenerator { convertJsResolverDefinition(scope, amplifyApi, schemasJsFunctions); + const namePrefix = this.name === defaultName ? '' : defaultName; + + const ssmEnvironmentScopeContext = { + [`${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 = + `${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( diff --git a/packages/backend-function/API.md b/packages/backend-function/API.md index 4c44cd115d..d5453c7eb3 100644 --- a/packages/backend-function/API.md +++ b/packages/backend-function/API.md @@ -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; // @public (undocumented) type DataClientError = { @@ -111,7 +109,7 @@ const getAmplifyDataClientConfig: (env: T, s3Client?: S3Client) => Promise [allow.resource(fcn)])` on the data schema.'; + invalidType: 'Some of the AWS environment variables needed to configure Amplify are missing.'; }; // @public (undocumented) diff --git a/packages/backend-function/src/runtime/get_amplify_clients_configuration.test.ts b/packages/backend-function/src/runtime/get_amplify_clients_configuration.test.ts index e2e5d45407..1287b11922 100644 --- a/packages/backend-function/src/runtime/get_amplify_clients_configuration.test.ts +++ b/packages/backend-function/src/runtime/get_amplify_clients_configuration.test.ts @@ -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: @@ -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; @@ -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; - 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; + 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; + 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; + 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; + 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; - 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' }, - }, - }, - }); }); }); diff --git a/packages/backend-function/src/runtime/get_amplify_clients_configuration.ts b/packages/backend-function/src/runtime/get_amplify_clients_configuration.ts index 2f377fe349..8c179fdf57 100644 --- a/packages/backend-function/src/runtime/get_amplify_clients_configuration.ts +++ b/packages/backend-function/src/runtime/get_amplify_clients_configuration.ts @@ -1,3 +1,4 @@ +import { NamingConverter } from '@aws-amplify/platform-core'; import { GetObjectCommand, NoSuchKey, @@ -5,37 +6,40 @@ import { S3ServiceException, } from '@aws-sdk/client-s3'; +const dataKeyNameContent = '_MODEL_INTROSPECTION_SCHEMA_KEY'; +const dataBucketNameContent = '_MODEL_INTROSPECTION_SCHEMA_BUCKET_NAME'; +const dataEndpointNameContent = '_GRAPHQL_ENDPOINT'; + export type DataClientEnv = { /* eslint-disable @typescript-eslint/naming-convention */ - 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; /* eslint-enable @typescript-eslint/naming-convention */ +} & Record; + +type DataEnvExtension = { + dataBucket: string; + dataKey: string; + dataEndpoint: string; }; -const isDataClientEnv = (env: unknown): env is DataClientEnv => { +type ExtendedAmplifyClientEnv = DataClientEnv & DataEnvExtension; + +const isAmplifyClientEnv = (env: object): env is DataClientEnv => { return ( - env !== null && - typeof env === 'object' && - 'AMPLIFY_DATA_MODEL_INTROSPECTION_SCHEMA_BUCKET_NAME' in env && - 'AMPLIFY_DATA_MODEL_INTROSPECTION_SCHEMA_KEY' in env && 'AWS_ACCESS_KEY_ID' in env && - 'AWS_SECRET_ACCESS_KEY' in env && - 'AWS_SESSION_TOKEN' in env && - 'AWS_REGION' in env && - 'AMPLIFY_DATA_GRAPHQL_ENDPOINT' in env && - typeof env.AMPLIFY_DATA_MODEL_INTROSPECTION_SCHEMA_BUCKET_NAME === - 'string' && - typeof env.AMPLIFY_DATA_MODEL_INTROSPECTION_SCHEMA_KEY === 'string' && typeof env.AWS_ACCESS_KEY_ID === 'string' && + 'AWS_SECRET_ACCESS_KEY' in env && typeof env.AWS_SECRET_ACCESS_KEY === 'string' && + 'AWS_SESSION_TOKEN' in env && typeof env.AWS_SESSION_TOKEN === 'string' && + 'AWS_REGION' in env && typeof env.AWS_REGION === 'string' && - typeof env.AMPLIFY_DATA_GRAPHQL_ENDPOINT === 'string' + 'AMPLIFY_DATA_DEFAULT_NAME' in env && + typeof env.AMPLIFY_DATA_DEFAULT_NAME === 'string' ); }; @@ -56,16 +60,15 @@ export type ResourceConfig = { /* eslint-enable @typescript-eslint/naming-convention */ const getResourceConfig = ( - env: DataClientEnv, + env: ExtendedAmplifyClientEnv, modelIntrospectionSchema: object ): ResourceConfig => { return { API: { GraphQL: { - endpoint: env.AMPLIFY_DATA_GRAPHQL_ENDPOINT, + endpoint: env.dataEndpoint, region: env.AWS_REGION, defaultAuthMode: 'iam' as const, - modelIntrospection: modelIntrospectionSchema, }, }, @@ -108,7 +111,7 @@ const getLibraryOptions = (env: DataClientEnv): LibraryOptions => { }; export 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.'; }; export type DataClientError = { @@ -125,6 +128,40 @@ export type DataClientReturn = T extends DataClientEnv ? DataClientConfig : DataClientError; +const extendEnv = ( + env: DataClientEnv & Record, + dataName: string +): ExtendedAmplifyClientEnv => { + const bucketName = `${dataName}${dataBucketNameContent}`; + const keyName = `${dataName}${dataKeyNameContent}`; + const endpointName = `${dataName}${dataEndpointNameContent}`; + if ( + !( + bucketName in env && + keyName in env && + endpointName in env && + typeof env[bucketName] === 'string' && + typeof env[keyName] === 'string' && + typeof env[endpointName] === 'string' + ) + ) { + throw new Error( + `The data environment variables are malformed. env=${JSON.stringify(env)}` + ); + } + + const dataBucket = env[bucketName] as string; + const dataKey = env[keyName] as string; + const dataEndpoint = env[endpointName] as string; + + return { + ...env, + dataBucket, + dataKey, + dataEndpoint, + }; +}; + /** * Generate the `resourceConfig` and `libraryOptions` need to configure * Amplify for the data client in a lambda. @@ -141,17 +178,26 @@ export const getAmplifyDataClientConfig = async ( if (!s3Client) { s3Client = new S3Client(); } + if (env === null || typeof env !== 'object') { + throw new Error(`Invalid environment variables: ${JSON.stringify(env)}`); + } - if (!isDataClientEnv(env)) { + if (!isAmplifyClientEnv(env)) { return { resourceConfig: {}, libraryOptions: {} } as DataClientReturn; } + + const dataName = new NamingConverter().toScreamingSnakeCase( + env.AMPLIFY_DATA_DEFAULT_NAME + ); + const extendedEnv = extendEnv(env, dataName); + let modelIntrospectionSchema: object; try { const response = await s3Client.send( new GetObjectCommand({ - Bucket: env.AMPLIFY_DATA_MODEL_INTROSPECTION_SCHEMA_BUCKET_NAME, - Key: env.AMPLIFY_DATA_MODEL_INTROSPECTION_SCHEMA_KEY, + Bucket: extendedEnv.dataBucket, + Key: extendedEnv.dataKey, }) ); const modelIntrospectionSchemaJson = @@ -173,7 +219,10 @@ export const getAmplifyDataClientConfig = async ( const libraryOptions = getLibraryOptions(env); - const resourceConfig = getResourceConfig(env, modelIntrospectionSchema); + const resourceConfig = getResourceConfig( + extendedEnv, + modelIntrospectionSchema + ); return { resourceConfig, libraryOptions } as DataClientReturn; }; diff --git a/packages/backend/package.json b/packages/backend/package.json index 7ef401f5aa..6ff0d2c384 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -41,8 +41,7 @@ "@aws-amplify/client-config": "^1.5.3", "@aws-amplify/platform-core": "^1.3.0", "@aws-amplify/plugin-types": "^1.6.0", - "@aws-sdk/client-amplify": "^3.624.0", - "lodash.snakecase": "^4.1.1" + "@aws-sdk/client-amplify": "^3.624.0" }, "peerDependencies": { "aws-cdk-lib": "^2.168.0", diff --git a/packages/backend/src/engine/backend_id_scoped_ssm_environment_entries_generator.ts b/packages/backend/src/engine/backend_id_scoped_ssm_environment_entries_generator.ts index 067ccf32ba..c104f96b02 100644 --- a/packages/backend/src/engine/backend_id_scoped_ssm_environment_entries_generator.ts +++ b/packages/backend/src/engine/backend_id_scoped_ssm_environment_entries_generator.ts @@ -1,11 +1,13 @@ -import { ParameterPathConversions } from '@aws-amplify/platform-core'; +import { + NamingConverter, + ParameterPathConversions, +} from '@aws-amplify/platform-core'; import { BackendIdentifier, SsmEnvironmentEntriesGenerator, } from '@aws-amplify/plugin-types'; import { StringParameter } from 'aws-cdk-lib/aws-ssm'; import { Construct } from 'constructs'; -import { toScreamingSnakeCase } from './naming_convention_conversions.js'; /** * Generates SsmEnvironmentEntry[] with SSM parameters that are scoped to a specific backend identifier @@ -50,7 +52,9 @@ export class BackendIdScopedSsmEnvironmentEntriesGenerator */ generateSsmEnvironmentEntries = (scopeContext: Record) => Object.entries(scopeContext).map(([contextKey, contextValue]) => { - const sanitizedContextKey = toScreamingSnakeCase(contextKey); + const sanitizedContextKey = new NamingConverter().toScreamingSnakeCase( + contextKey + ); const parameterPath = ParameterPathConversions.toResourceReferenceFullPath( this.backendId, diff --git a/packages/backend/src/engine/naming_convention_conversions.ts b/packages/backend/src/engine/naming_convention_conversions.ts deleted file mode 100644 index 272601da6f..0000000000 --- a/packages/backend/src/engine/naming_convention_conversions.ts +++ /dev/null @@ -1,8 +0,0 @@ -import snakeCase from 'lodash.snakecase'; - -/** - * Converts input string to SCREAMING_SNAKE_CASE - */ -export const toScreamingSnakeCase = (input: string): string => { - return snakeCase(input).toUpperCase(); -}; diff --git a/packages/integration-tests/src/test-projects/data_access_from_function/amplify/data/resource.ts b/packages/integration-tests/src/test-projects/data_access_from_function/amplify/data/resource.ts index 4c3442bdc7..2bb4831790 100644 --- a/packages/integration-tests/src/test-projects/data_access_from_function/amplify/data/resource.ts +++ b/packages/integration-tests/src/test-projects/data_access_from_function/amplify/data/resource.ts @@ -31,6 +31,7 @@ const schema = a export type Schema = ClientSchema; export const data = defineData({ + name: 'DATATEST', schema, authorizationModes: { defaultAuthorizationMode: 'apiKey', diff --git a/packages/platform-core/API.md b/packages/platform-core/API.md index f40d1c5364..50d40bce8e 100644 --- a/packages/platform-core/API.md +++ b/packages/platform-core/API.md @@ -145,6 +145,11 @@ class LogRetentionConverter { toCDKRetentionDays: (retention: LogRetention | undefined) => RetentionDays | undefined; } +// @public +export class NamingConverter { + toScreamingSnakeCase(input: string): string; +} + // @public export class ObjectAccumulator { constructor(accumulator: DeepPartialAmplifyGeneratedConfigs, versionKey?: string); diff --git a/packages/platform-core/package.json b/packages/platform-core/package.json index 6ea68ea197..bce16548a8 100644 --- a/packages/platform-core/package.json +++ b/packages/platform-core/package.json @@ -34,6 +34,7 @@ "is-ci": "^3.0.1", "lodash.mergewith": "^4.6.2", "semver": "^7.6.3", + "lodash.snakecase": "^4.1.1", "uuid": "^9.0.1", "zod": "^3.22.2" }, diff --git a/packages/platform-core/src/index.ts b/packages/platform-core/src/index.ts index 7e05a510ea..97987eade8 100644 --- a/packages/platform-core/src/index.ts +++ b/packages/platform-core/src/index.ts @@ -11,3 +11,4 @@ export { CDKContextKey } from './cdk_context_key.js'; export * from './parameter_path_conversions.js'; export * from './object_accumulator.js'; export { TagName } from './tag_name.js'; +export * from './naming_convention_conversions.js'; diff --git a/packages/backend/src/engine/naming_convention_conversions.test.ts b/packages/platform-core/src/naming_convention_conversions.test.ts similarity index 80% rename from packages/backend/src/engine/naming_convention_conversions.test.ts rename to packages/platform-core/src/naming_convention_conversions.test.ts index d6e9e2d496..f2092bc4f3 100644 --- a/packages/backend/src/engine/naming_convention_conversions.test.ts +++ b/packages/platform-core/src/naming_convention_conversions.test.ts @@ -1,5 +1,5 @@ import { describe, it } from 'node:test'; -import { toScreamingSnakeCase } from './naming_convention_conversions.js'; +import { NamingConverter } from './naming_convention_conversions.js'; import assert from 'node:assert'; void describe('screaming snake conversions', () => { @@ -16,7 +16,10 @@ void describe('screaming snake conversions', () => { ]; testCases.forEach((testCase) => { void it(`should successfully convert ${testCase.input} to ${testCase.expected}`, () => { - assert.equal(toScreamingSnakeCase(testCase.input), testCase.expected); + assert.equal( + new NamingConverter().toScreamingSnakeCase(testCase.input), + testCase.expected + ); }); }); }); diff --git a/packages/platform-core/src/naming_convention_conversions.ts b/packages/platform-core/src/naming_convention_conversions.ts new file mode 100644 index 0000000000..7829db9496 --- /dev/null +++ b/packages/platform-core/src/naming_convention_conversions.ts @@ -0,0 +1,16 @@ +import snakeCase from 'lodash.snakecase'; + +/** + * Naming Converter + * @example + * new NamingConverter().toScreamingSnakeCase('myInputString') + */ +export class NamingConverter { + /** + * Converts input string to SCREAMING_SNAKE_CASE + * @param input Input string to convert + */ + public toScreamingSnakeCase(input: string): string { + return snakeCase(input).toUpperCase(); + } +}