diff --git a/packages/integration-tests/src/test-e2e/deployment/data_and_function_project.deployment.test.ts b/packages/integration-tests/src/test-e2e/deployment/data_and_function_project.deployment.test.ts new file mode 100644 index 00000000000..ba756dc6d22 --- /dev/null +++ b/packages/integration-tests/src/test-e2e/deployment/data_and_function_project.deployment.test.ts @@ -0,0 +1,4 @@ +import { defineDeploymentTest } from './deployment.test.template.js'; +import { DataAndFunctionTestProjectCreator } from '../../test-project-setup/data_and_function_project.js'; + +defineDeploymentTest(new DataAndFunctionTestProjectCreator()); diff --git a/packages/integration-tests/src/test-e2e/sandbox/data_and_function_project.sandbox.test.ts b/packages/integration-tests/src/test-e2e/sandbox/data_and_function_project.sandbox.test.ts new file mode 100644 index 00000000000..b37217f777d --- /dev/null +++ b/packages/integration-tests/src/test-e2e/sandbox/data_and_function_project.sandbox.test.ts @@ -0,0 +1,4 @@ +import { defineSandboxTest } from './sandbox.test.template.js'; +import { DataAndFunctionTestProjectCreator } from '../../test-project-setup/data_and_function_project.js'; + +defineSandboxTest(new DataAndFunctionTestProjectCreator()); diff --git a/packages/integration-tests/src/test-project-setup/data_and_function_project.ts b/packages/integration-tests/src/test-project-setup/data_and_function_project.ts new file mode 100644 index 00000000000..2f89fdd6a6c --- /dev/null +++ b/packages/integration-tests/src/test-project-setup/data_and_function_project.ts @@ -0,0 +1,192 @@ +import { TestProjectBase } from './test_project_base.js'; +import fs from 'fs/promises'; +import { createEmptyAmplifyProject } from './create_empty_amplify_project.js'; +import { CloudFormationClient } from '@aws-sdk/client-cloudformation'; +import { TestProjectCreator } from './test_project_creator.js'; +import { AmplifyClient } from '@aws-sdk/client-amplify'; +import { BackendIdentifier } from '@aws-amplify/plugin-types'; +import { LambdaClient } from '@aws-sdk/client-lambda'; +import { DeployedResourcesFinder } from '../find_deployed_resource.js'; +import { generateClientConfig } from '@aws-amplify/client-config'; +import { CognitoIdentityProviderClient } from '@aws-sdk/client-cognito-identity-provider'; +import { SemVer } from 'semver'; +import crypto from 'node:crypto'; +import { + ApolloClient, + ApolloLink, + HttpLink, + InMemoryCache, +} from '@apollo/client/core'; +import { AUTH_TYPE, createAuthLink } from 'aws-appsync-auth-link'; +import { gql } from 'graphql-tag'; +import assert from 'assert'; +import { NormalizedCacheObject } from '@apollo/client'; +import { e2eToolingClientConfig } from '../e2e_tooling_client_config.js'; + +// TODO: this is a work around +// it seems like as of amplify v6 , some of the code only runs in the browser ... +// see https://github.com/aws-amplify/amplify-js/issues/12751 +if (process.versions.node) { + // node >= 20 now exposes crypto by default. This workaround is not needed: https://github.com/nodejs/node/pull/42083 + if (new SemVer(process.versions.node).major < 20) { + // @ts-expect-error altering typing for global to make compiler happy is not worth the effort assuming this is temporary workaround + globalThis.crypto = crypto; + } +} + +/** + * Creates the data and function test project. + */ +export class DataAndFunctionTestProjectCreator implements TestProjectCreator { + readonly name = 'data-and-function'; + + /** + * Creates project creator. + */ + constructor( + private readonly cfnClient: CloudFormationClient = new CloudFormationClient( + e2eToolingClientConfig + ), + private readonly amplifyClient: AmplifyClient = new AmplifyClient( + e2eToolingClientConfig + ), + private readonly lambdaClient: LambdaClient = new LambdaClient( + e2eToolingClientConfig + ), + private readonly cognitoIdentityProviderClient: CognitoIdentityProviderClient = new CognitoIdentityProviderClient( + e2eToolingClientConfig + ), + private readonly resourceFinder: DeployedResourcesFinder = new DeployedResourcesFinder() + ) {} + + createProject = async (e2eProjectDir: string): Promise => { + const { projectName, projectRoot, projectAmplifyDir } = + await createEmptyAmplifyProject(this.name, e2eProjectDir); + + const project = new DataAndFunctionTestProject( + projectName, + projectRoot, + projectAmplifyDir, + this.cfnClient, + this.amplifyClient, + this.lambdaClient, + this.cognitoIdentityProviderClient, + this.resourceFinder + ); + await fs.cp( + project.sourceProjectAmplifyDirURL, + project.projectAmplifyDirPath, + { + recursive: true, + } + ); + return project; + }; +} + +/** + * The data and function test project. + */ +class DataAndFunctionTestProject extends TestProjectBase { + readonly sourceProjectDirPath = '../../src/test-projects/data-and-function'; + + readonly sourceProjectAmplifyDirSuffix = `${this.sourceProjectDirPath}/amplify`; + + readonly sourceProjectAmplifyDirURL: URL = new URL( + this.sourceProjectAmplifyDirSuffix, + import.meta.url + ); + + /** + * Create a test project instance. + */ + constructor( + name: string, + projectDirPath: string, + projectAmplifyDirPath: string, + cfnClient: CloudFormationClient, + amplifyClient: AmplifyClient, + private readonly lambdaClient: LambdaClient = new LambdaClient( + e2eToolingClientConfig + ), + private readonly cognitoIdentityProviderClient: CognitoIdentityProviderClient = new CognitoIdentityProviderClient( + e2eToolingClientConfig + ), + private readonly resourceFinder: DeployedResourcesFinder = new DeployedResourcesFinder() + ) { + super( + name, + projectDirPath, + projectAmplifyDirPath, + cfnClient, + amplifyClient + ); + } + + override async assertPostDeployment( + backendId: BackendIdentifier + ): Promise { + await super.assertPostDeployment(backendId); + + const clientConfig = await generateClientConfig(backendId, '1.1'); + if (!clientConfig.data?.url) { + throw new Error('Data and function project must include data'); + } + if (!clientConfig.data.api_key) { + throw new Error('Data and function project must include api_key'); + } + + // const dataUrl = clientConfig.data?.url; + + const httpLink = new HttpLink({ uri: clientConfig.data.url }); + const link = ApolloLink.from([ + createAuthLink({ + url: clientConfig.data.url, + region: clientConfig.data.aws_region, + auth: { + type: AUTH_TYPE.API_KEY, + apiKey: clientConfig.data.api_key, + }, + }), + // see https://github.com/awslabs/aws-mobile-appsync-sdk-js/issues/473#issuecomment-543029072 + httpLink, + ]); + const apolloClient = new ApolloClient({ + link, + cache: new InMemoryCache(), + }); + + await this.assertDataFunctionCallSucceeds(apolloClient); + await this.assertNoopWithImportCallSucceeds(apolloClient); + } + + private assertDataFunctionCallSucceeds = async ( + apolloClient: ApolloClient + ): Promise => { + const response = await apolloClient.query({ + query: gql` + query todoCount { + todoCount + } + `, + variables: {}, + }); + + assert.deepEqual(response.data, { todoCount: 0 }); + }; + + private assertNoopWithImportCallSucceeds = async ( + apolloClient: ApolloClient + ): Promise => { + const response = await apolloClient.query({ + query: gql` + query noopImport { + noopImport + } + `, + variables: {}, + }); + + assert.deepEqual(response.data, { noopImport: 'STATIC TEST RESPONSE' }); + }; +} diff --git a/packages/integration-tests/src/test-projects/data-and-function/amplify/backend.ts b/packages/integration-tests/src/test-projects/data-and-function/amplify/backend.ts new file mode 100644 index 00000000000..f84a66738da --- /dev/null +++ b/packages/integration-tests/src/test-projects/data-and-function/amplify/backend.ts @@ -0,0 +1,6 @@ +import { defineBackend } from '@aws-amplify/backend'; +import { data } from './data/resource.js'; +import { todoCount } from './functions/todo-count/resource.js'; +import { noopImport } from './functions/noop-import/resource.js'; + +const backend = defineBackend({ data, todoCount, noopImport }); diff --git a/packages/integration-tests/src/test-projects/data-and-function/amplify/data/resource.ts b/packages/integration-tests/src/test-projects/data-and-function/amplify/data/resource.ts new file mode 100644 index 00000000000..45b7f9302ef --- /dev/null +++ b/packages/integration-tests/src/test-projects/data-and-function/amplify/data/resource.ts @@ -0,0 +1,42 @@ +import { a, ClientSchema, defineData } from '@aws-amplify/backend'; +import { todoCount } from '../functions/todo-count/resource.js'; +import { noopImport } from '../functions/noop-import/resource.js'; + +const schema = a + .schema({ + Todo: a + .model({ + title: a.string().required(), + done: a.boolean().default(false), // default value is false + }) + .authorization((allow) => [allow.publicApiKey()]), + todoCount: a + .query() + .arguments({}) + .returns(a.integer()) + .handler(a.handler.function(todoCount)) + .authorization((allow) => [allow.publicApiKey()]), + noopImport: a + .query() + .arguments({}) + .returns(a.string()) + .handler(a.handler.function(noopImport)) + .authorization((allow) => [allow.publicApiKey()]), + }) + .authorization((allow) => [ + allow.resource(todoCount), + allow.resource(noopImport), + ]); + +export type Schema = ClientSchema; + +export const data = defineData({ + schema, + authorizationModes: { + defaultAuthorizationMode: 'apiKey', + // API Key is used for a.allow.public() rules + apiKeyAuthorizationMode: { + expiresInDays: 30, + }, + }, +}); diff --git a/packages/integration-tests/src/test-projects/data-and-function/amplify/functions/noop-import/handler.ts b/packages/integration-tests/src/test-projects/data-and-function/amplify/functions/noop-import/handler.ts new file mode 100644 index 00000000000..39cb79009dd --- /dev/null +++ b/packages/integration-tests/src/test-projects/data-and-function/amplify/functions/noop-import/handler.ts @@ -0,0 +1,22 @@ +import type { Handler } from 'aws-lambda'; +import { Amplify } from 'aws-amplify'; +import { generateClient } from 'aws-amplify/data'; +import type { Schema } from '../../data/resource.js'; +import { getAmplifyDataClientConfig } from '@aws-amplify/backend/function/runtime'; +// @ts-ignore +import { env } from '$amplify/env/noop-import.js'; +import { S3Client } from '@aws-sdk/client-s3'; + +const { resourceConfig, libraryOptions } = await getAmplifyDataClientConfig( + env +); + +Amplify.configure(resourceConfig, libraryOptions); + +const client = generateClient(); + +export const handler: Handler = async () => { + const _s3Client = new S3Client(); + const _todos = await client.models.Todo.list(); + return 'STATIC TEST RESPONSE'; +}; diff --git a/packages/integration-tests/src/test-projects/data-and-function/amplify/functions/noop-import/resource.ts b/packages/integration-tests/src/test-projects/data-and-function/amplify/functions/noop-import/resource.ts new file mode 100644 index 00000000000..914c93f8c4a --- /dev/null +++ b/packages/integration-tests/src/test-projects/data-and-function/amplify/functions/noop-import/resource.ts @@ -0,0 +1,7 @@ +import { defineFunction } from '@aws-amplify/backend'; + +export const noopImport = defineFunction({ + name: 'noop-import', + entry: './handler.ts', + timeoutSeconds: 30, +}); diff --git a/packages/integration-tests/src/test-projects/data-and-function/amplify/functions/todo-count/handler.ts b/packages/integration-tests/src/test-projects/data-and-function/amplify/functions/todo-count/handler.ts new file mode 100644 index 00000000000..0383ebf092d --- /dev/null +++ b/packages/integration-tests/src/test-projects/data-and-function/amplify/functions/todo-count/handler.ts @@ -0,0 +1,20 @@ +import type { Handler } from 'aws-lambda'; +import { Amplify } from 'aws-amplify'; +import { generateClient } from 'aws-amplify/data'; +import type { Schema } from '../../data/resource.js'; +import { getAmplifyDataClientConfig } from '@aws-amplify/backend/function/runtime'; +// @ts-ignore +import { env } from '$amplify/env/todo-count.js'; + +const { resourceConfig, libraryOptions } = await getAmplifyDataClientConfig( + env +); + +Amplify.configure(resourceConfig, libraryOptions); + +const client = generateClient(); + +export const handler: Handler = async () => { + const todos = await client.models.Todo.list(); + return todos.data.length; +}; diff --git a/packages/integration-tests/src/test-projects/data-and-function/amplify/functions/todo-count/resource.ts b/packages/integration-tests/src/test-projects/data-and-function/amplify/functions/todo-count/resource.ts new file mode 100644 index 00000000000..6fa45f4b81f --- /dev/null +++ b/packages/integration-tests/src/test-projects/data-and-function/amplify/functions/todo-count/resource.ts @@ -0,0 +1,7 @@ +import { defineFunction } from '@aws-amplify/backend'; + +export const todoCount = defineFunction({ + name: 'todo-count', + entry: './handler.ts', + timeoutSeconds: 30, +});