diff --git a/.circleci/config.yml b/.circleci/config.yml index 10d2ce3b1f0..b0610449790 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1243,6 +1243,14 @@ jobs: environment: TEST_SUITE: src/__tests__/frontend_config_drift.test.ts CLI_REGION: eu-west-2 + container-hosting-amplify_e2e_tests: + working_directory: ~/repo + docker: *ref_1 + resource_class: large + steps: *ref_5 + environment: + TEST_SUITE: src/__tests__/container-hosting.test.ts + CLI_REGION: eu-central-1 configure-project-amplify_e2e_tests: working_directory: ~/repo docker: *ref_1 @@ -1250,7 +1258,7 @@ jobs: steps: *ref_5 environment: TEST_SUITE: src/__tests__/configure-project.test.ts - CLI_REGION: eu-central-1 + CLI_REGION: ap-northeast-1 auth_6-amplify_e2e_tests: working_directory: ~/repo docker: *ref_1 @@ -1258,7 +1266,7 @@ jobs: steps: *ref_5 environment: TEST_SUITE: src/__tests__/auth_6.test.ts - CLI_REGION: ap-northeast-1 + CLI_REGION: ap-southeast-1 api_4-amplify_e2e_tests: working_directory: ~/repo docker: *ref_1 @@ -1266,7 +1274,7 @@ jobs: steps: *ref_5 environment: TEST_SUITE: src/__tests__/api_4.test.ts - CLI_REGION: ap-southeast-1 + CLI_REGION: ap-southeast-2 schema-iterative-update-4-amplify_e2e_tests_pkg_linux: working_directory: ~/repo docker: *ref_1 @@ -1997,6 +2005,16 @@ jobs: TEST_SUITE: src/__tests__/frontend_config_drift.test.ts CLI_REGION: eu-west-2 steps: *ref_6 + container-hosting-amplify_e2e_tests_pkg_linux: + working_directory: ~/repo + docker: *ref_1 + resource_class: large + environment: + AMPLIFY_DIR: /home/circleci/repo/out + AMPLIFY_PATH: /home/circleci/repo/out/amplify-pkg-linux + TEST_SUITE: src/__tests__/container-hosting.test.ts + CLI_REGION: eu-central-1 + steps: *ref_6 configure-project-amplify_e2e_tests_pkg_linux: working_directory: ~/repo docker: *ref_1 @@ -2005,7 +2023,7 @@ jobs: AMPLIFY_DIR: /home/circleci/repo/out AMPLIFY_PATH: /home/circleci/repo/out/amplify-pkg-linux TEST_SUITE: src/__tests__/configure-project.test.ts - CLI_REGION: eu-central-1 + CLI_REGION: ap-northeast-1 steps: *ref_6 auth_6-amplify_e2e_tests_pkg_linux: working_directory: ~/repo @@ -2015,7 +2033,7 @@ jobs: AMPLIFY_DIR: /home/circleci/repo/out AMPLIFY_PATH: /home/circleci/repo/out/amplify-pkg-linux TEST_SUITE: src/__tests__/auth_6.test.ts - CLI_REGION: ap-northeast-1 + CLI_REGION: ap-southeast-1 steps: *ref_6 api_4-amplify_e2e_tests_pkg_linux: working_directory: ~/repo @@ -2025,7 +2043,7 @@ jobs: AMPLIFY_DIR: /home/circleci/repo/out AMPLIFY_PATH: /home/circleci/repo/out/amplify-pkg-linux TEST_SUITE: src/__tests__/api_4.test.ts - CLI_REGION: ap-southeast-1 + CLI_REGION: ap-southeast-2 steps: *ref_6 workflows: version: 2 @@ -2144,19 +2162,19 @@ workflows: - schema-predictions-amplify_e2e_tests - amplify-configure-amplify_e2e_tests - migration-node-function-amplify_e2e_tests - - configure-project-amplify_e2e_tests + - container-hosting-amplify_e2e_tests - interactions-amplify_e2e_tests - datastore-modelgen-amplify_e2e_tests - layer-2-amplify_e2e_tests - - auth_6-amplify_e2e_tests + - configure-project-amplify_e2e_tests - schema-data-access-patterns-amplify_e2e_tests - init-special-case-amplify_e2e_tests - iam-permissions-boundary-amplify_e2e_tests - - api_4-amplify_e2e_tests - - feature-flags-amplify_e2e_tests + - auth_6-amplify_e2e_tests - schema-versioned-amplify_e2e_tests - plugin-amplify_e2e_tests - function_7-amplify_e2e_tests + - api_4-amplify_e2e_tests - done_with_pkg_linux_e2e_tests: requires: - analytics-amplify_e2e_tests_pkg_linux @@ -2174,19 +2192,19 @@ workflows: - schema-predictions-amplify_e2e_tests_pkg_linux - amplify-configure-amplify_e2e_tests_pkg_linux - migration-node-function-amplify_e2e_tests_pkg_linux - - configure-project-amplify_e2e_tests_pkg_linux + - container-hosting-amplify_e2e_tests_pkg_linux - interactions-amplify_e2e_tests_pkg_linux - datastore-modelgen-amplify_e2e_tests_pkg_linux - layer-2-amplify_e2e_tests_pkg_linux - - auth_6-amplify_e2e_tests_pkg_linux + - configure-project-amplify_e2e_tests_pkg_linux - schema-data-access-patterns-amplify_e2e_tests_pkg_linux - init-special-case-amplify_e2e_tests_pkg_linux - iam-permissions-boundary-amplify_e2e_tests_pkg_linux - - api_4-amplify_e2e_tests_pkg_linux - - feature-flags-amplify_e2e_tests_pkg_linux + - auth_6-amplify_e2e_tests_pkg_linux - schema-versioned-amplify_e2e_tests_pkg_linux - plugin-amplify_e2e_tests_pkg_linux - function_7-amplify_e2e_tests_pkg_linux + - api_4-amplify_e2e_tests_pkg_linux - amplify_migration_tests_latest: context: - amplify-ecr-image-pull @@ -2600,7 +2618,7 @@ workflows: filters: *ref_10 requires: - schema-iterative-update-1-amplify_e2e_tests - - configure-project-amplify_e2e_tests: + - container-hosting-amplify_e2e_tests: context: *ref_8 post-steps: *ref_9 filters: *ref_10 @@ -2666,7 +2684,7 @@ workflows: filters: *ref_10 requires: - function_3-amplify_e2e_tests - - auth_6-amplify_e2e_tests: + - configure-project-amplify_e2e_tests: context: *ref_8 post-steps: *ref_9 filters: *ref_10 @@ -2732,7 +2750,7 @@ workflows: filters: *ref_10 requires: - auth_5-amplify_e2e_tests - - api_4-amplify_e2e_tests: + - auth_6-amplify_e2e_tests: context: *ref_8 post-steps: *ref_9 filters: *ref_10 @@ -2798,6 +2816,12 @@ workflows: filters: *ref_10 requires: - auth_1-amplify_e2e_tests + - api_4-amplify_e2e_tests: + context: *ref_8 + post-steps: *ref_9 + filters: *ref_10 + requires: + - feature-flags-amplify_e2e_tests - schema-iterative-update-4-amplify_e2e_tests_pkg_linux: context: &ref_11 - amplify-ecr-image-pull @@ -3082,7 +3106,7 @@ workflows: filters: *ref_13 requires: - schema-iterative-update-1-amplify_e2e_tests_pkg_linux - - configure-project-amplify_e2e_tests_pkg_linux: + - container-hosting-amplify_e2e_tests_pkg_linux: context: *ref_11 post-steps: *ref_12 filters: *ref_13 @@ -3152,7 +3176,7 @@ workflows: filters: *ref_13 requires: - function_3-amplify_e2e_tests_pkg_linux - - auth_6-amplify_e2e_tests_pkg_linux: + - configure-project-amplify_e2e_tests_pkg_linux: context: *ref_11 post-steps: *ref_12 filters: *ref_13 @@ -3222,7 +3246,7 @@ workflows: filters: *ref_13 requires: - auth_5-amplify_e2e_tests_pkg_linux - - api_4-amplify_e2e_tests_pkg_linux: + - auth_6-amplify_e2e_tests_pkg_linux: context: *ref_11 post-steps: *ref_12 filters: *ref_13 @@ -3292,3 +3316,9 @@ workflows: filters: *ref_13 requires: - auth_1-amplify_e2e_tests_pkg_linux + - api_4-amplify_e2e_tests_pkg_linux: + context: *ref_11 + post-steps: *ref_12 + filters: *ref_13 + requires: + - feature-flags-amplify_e2e_tests_pkg_linux diff --git a/packages/amplify-category-api/src/provider-utils/awscloudformation/base-api-stack.ts b/packages/amplify-category-api/src/provider-utils/awscloudformation/base-api-stack.ts index 4b2bc6df8f2..61030462f85 100644 --- a/packages/amplify-category-api/src/provider-utils/awscloudformation/base-api-stack.ts +++ b/packages/amplify-category-api/src/provider-utils/awscloudformation/base-api-stack.ts @@ -10,498 +10,494 @@ import * as cdk from '@aws-cdk/core'; import { prepareApp } from '@aws-cdk/core/lib/private/prepare-app'; import { NETWORK_STACK_LOGICAL_ID } from '../../category-constants'; import Container from './docker-compose/ecs-objects/container'; -import { GitHubSourceActionInfo, PipelineWithAwaiter } from "./pipeline-with-awaiter"; +import { GitHubSourceActionInfo, PipelineWithAwaiter } from './pipeline-with-awaiter'; const PIPELINE_AWAITER_ZIP = 'custom-resource-pipeline-awaiter.zip'; export enum DEPLOYMENT_MECHANISM { - /** - * on every amplify push - */ - FULLY_MANAGED = 'FULLY_MANAGED', - /** - * on every github push - */ - INDENPENDENTLY_MANAGED = 'INDENPENDENTLY_MANAGED', - /** - * manually push by the customer to ECR - */ - SELF_MANAGED = 'SELF_MANAGED', -}; + /** + * on every amplify push + */ + FULLY_MANAGED = 'FULLY_MANAGED', + /** + * on every github push + */ + INDENPENDENTLY_MANAGED = 'INDENPENDENTLY_MANAGED', + /** + * manually push by the customer to ECR + */ + SELF_MANAGED = 'SELF_MANAGED', +} export type ContainersStackProps = Readonly<{ - skipWait?: boolean; - envName: string; - categoryName: string; - apiName: string; - deploymentBucketName: string, - dependsOn: ReadonlyArray<{ - category: string; - resourceName: string; - attributes: string[]; - }>; - taskEnvironmentVariables?: Record; - deploymentMechanism: DEPLOYMENT_MECHANISM; - restrictAccess: boolean; - policies?: ReadonlyArray>; - containers: ReadonlyArray; - secretsArns?: ReadonlyMap; - exposedContainer: { name: string; port: number }; - taskPorts: number[]; - isInitialDeploy: boolean; - desiredCount: number; - createCloudMapService?: boolean; - gitHubSourceActionInfo?: GitHubSourceActionInfo; - existingEcrRepositories: Set; + skipWait?: boolean; + envName: string; + categoryName: string; + apiName: string; + deploymentBucketName: string; + dependsOn: ReadonlyArray<{ + category: string; + resourceName: string; + attributes: string[]; + }>; + taskEnvironmentVariables?: Record; + deploymentMechanism: DEPLOYMENT_MECHANISM; + restrictAccess: boolean; + policies?: ReadonlyArray>; + containers: ReadonlyArray; + secretsArns?: ReadonlyMap; + exposedContainer: { name: string; port: number }; + taskPorts: number[]; + isInitialDeploy: boolean; + desiredCount: number; + createCloudMapService?: boolean; + gitHubSourceActionInfo?: GitHubSourceActionInfo; + existingEcrRepositories: Set; }>; export abstract class ContainersStack extends cdk.Stack { - protected readonly vpcId: string; - private readonly vpcCidrBlock: string; - protected readonly subnets: ReadonlyArray; - private readonly clusterName: string; - private readonly zipPath: string; - private readonly cloudMapNamespaceId: string; - protected readonly vpcLinkId: string; - private readonly pipelineWithAwaiter: PipelineWithAwaiter; - protected readonly cloudMapService: cloudmap.CfnService | undefined; - protected readonly ecsService: ecs.CfnService; - protected readonly isAuthCondition: cdk.CfnCondition; - protected readonly appClientId: string | undefined; - protected readonly userPoolId: string | undefined; - protected readonly ecsServiceSecurityGroup: ec2.CfnSecurityGroup; - protected readonly parameters: ReadonlyMap; - - constructor(scope: cdk.Construct, id: string, private readonly props: ContainersStackProps) { - super(scope, id); - - const { - parameters, - vpcId, - vpcCidrBlock, - subnets, - clusterName, - zipPath, - cloudMapNamespaceId, - vpcLinkId, - isAuthCondition, - appClientId, - userPoolId, - } = this.init(); - - this.parameters = parameters; - - this.vpcId = vpcId; - this.vpcCidrBlock = vpcCidrBlock; - this.subnets = subnets; - this.clusterName = clusterName; - this.zipPath = zipPath; - this.cloudMapNamespaceId = cloudMapNamespaceId; - this.vpcLinkId = vpcLinkId; - this.isAuthCondition = isAuthCondition; - this.appClientId = appClientId; - this.userPoolId = userPoolId; - - const { service, serviceSecurityGroup, containersInfo, cloudMapService } = this.ecs(); - - this.cloudMapService = cloudMapService; - this.ecsService = service; - this.ecsServiceSecurityGroup = serviceSecurityGroup; - - const { gitHubSourceActionInfo, skipWait } = this.props; - - const { pipelineWithAwaiter } = this.pipeline({ - skipWait, - service, - containersInfo, - gitHubSourceActionInfo, - }); - - this.pipelineWithAwaiter = pipelineWithAwaiter; + protected readonly vpcId: string; + private readonly vpcCidrBlock: string; + protected readonly subnets: ReadonlyArray; + private readonly clusterName: string; + private readonly zipPath: string; + private readonly cloudMapNamespaceId: string; + protected readonly vpcLinkId: string; + private readonly pipelineWithAwaiter: PipelineWithAwaiter; + protected readonly cloudMapService: cloudmap.CfnService | undefined; + protected readonly ecsService: ecs.CfnService; + protected readonly isAuthCondition: cdk.CfnCondition; + protected readonly appClientId: string | undefined; + protected readonly userPoolId: string | undefined; + protected readonly ecsServiceSecurityGroup: ec2.CfnSecurityGroup; + protected readonly parameters: ReadonlyMap; + + constructor(scope: cdk.Construct, id: string, private readonly props: ContainersStackProps) { + super(scope, id); + + const { + parameters, + vpcId, + vpcCidrBlock, + subnets, + clusterName, + zipPath, + cloudMapNamespaceId, + vpcLinkId, + isAuthCondition, + appClientId, + userPoolId, + } = this.init(); + + this.parameters = parameters; + + this.vpcId = vpcId; + this.vpcCidrBlock = vpcCidrBlock; + this.subnets = subnets; + this.clusterName = clusterName; + this.zipPath = zipPath; + this.cloudMapNamespaceId = cloudMapNamespaceId; + this.vpcLinkId = vpcLinkId; + this.isAuthCondition = isAuthCondition; + this.appClientId = appClientId; + this.userPoolId = userPoolId; + + const { service, serviceSecurityGroup, containersInfo, cloudMapService } = this.ecs(); + + this.cloudMapService = cloudMapService; + this.ecsService = service; + this.ecsServiceSecurityGroup = serviceSecurityGroup; + + const { gitHubSourceActionInfo, skipWait } = this.props; + + const { pipelineWithAwaiter } = this.pipeline({ + skipWait, + service, + containersInfo, + gitHubSourceActionInfo, + }); + + this.pipelineWithAwaiter = pipelineWithAwaiter; + + new cdk.CfnOutput(this, 'ContainerNames', { + value: cdk.Fn.join( + ',', + containersInfo.map(({ container: { containerName } }) => containerName), + ), + }); + } + + private init() { + const { restrictAccess, dependsOn, deploymentMechanism } = this.props; + + // Unused in this stack, but required by the root stack + new cdk.CfnParameter(this, 'env', { type: 'String' }); + + const paramDomain = new cdk.CfnParameter(this, 'domain', { type: 'String' }); + const paramRestrictAccess = new cdk.CfnParameter(this, 'restrictAccess', { + type: 'String', + allowedValues: ['true', 'false'], + }); + + const paramZipPath = new cdk.CfnParameter(this, 'ParamZipPath', { + type: 'String', + // Required only for FULLY_MANAGED + default: deploymentMechanism === DEPLOYMENT_MECHANISM.FULLY_MANAGED ? undefined : '', + }); + + const parameters: Map = new Map(); + + parameters.set('ParamZipPath', paramZipPath); + parameters.set('domain', paramDomain); + parameters.set('restrictAccess', paramRestrictAccess); + + const authParams: { + UserPoolId?: cdk.CfnParameter; + AppClientIDWeb?: cdk.CfnParameter; + } = {}; + + const paramTypes: Record = { + NetworkStackSubnetIds: 'CommaDelimitedList', + }; + + dependsOn.forEach(({ category, resourceName, attributes }) => { + attributes.forEach(attrib => { + const paramName = [category, resourceName, attrib].join(''); + + const type = paramTypes[paramName] ?? 'String'; + const param = new cdk.CfnParameter(this, paramName, { type }); + + parameters.set(paramName, param); + + if (category === 'auth') { + authParams[attrib as keyof typeof authParams] = param; + } + }); + }); + + const paramVpcId = parameters.get(`${NETWORK_STACK_LOGICAL_ID}VpcId`); + const paramVpcCidrBlock = parameters.get(`${NETWORK_STACK_LOGICAL_ID}VpcCidrBlock`); + const paramSubnetIds = parameters.get(`${NETWORK_STACK_LOGICAL_ID}SubnetIds`); + const paramClusterName = parameters.get(`${NETWORK_STACK_LOGICAL_ID}ClusterName`); + const paramCloudMapNamespaceId = parameters.get(`${NETWORK_STACK_LOGICAL_ID}CloudMapNamespaceId`); + const paramVpcLinkId = parameters.get(`${NETWORK_STACK_LOGICAL_ID}VpcLinkId`); + + const { UserPoolId: paramUserPoolId, AppClientIDWeb: paramAppClientIdWeb } = authParams; + + const isAuthCondition = new cdk.CfnCondition(this, 'isAuthCondition', { + expression: cdk.Fn.conditionAnd( + cdk.Fn.conditionEquals(restrictAccess, true), + cdk.Fn.conditionNot(cdk.Fn.conditionEquals(paramUserPoolId ?? '', '')), + cdk.Fn.conditionNot(cdk.Fn.conditionEquals(paramAppClientIdWeb ?? '', '')), + ), + }); - new cdk.CfnOutput(this, 'ContainerNames', { - value: cdk.Fn.join(',', containersInfo.map(({ container: { containerName } }) => containerName)) - }); + return { + parameters, + vpcId: paramVpcId.valueAsString, + vpcCidrBlock: paramVpcCidrBlock.valueAsString, + subnets: paramSubnetIds.valueAsList, + clusterName: paramClusterName.valueAsString, + zipPath: paramZipPath.valueAsString, + cloudMapNamespaceId: paramCloudMapNamespaceId.valueAsString, + vpcLinkId: paramVpcLinkId.valueAsString, + isAuthCondition, + userPoolId: paramUserPoolId && paramUserPoolId.valueAsString, + appClientId: paramAppClientIdWeb && paramAppClientIdWeb.valueAsString, + }; + } + + private ecs() { + const { + envName, + categoryName, + apiName, + policies, + containers, + secretsArns, + taskEnvironmentVariables, + exposedContainer, + taskPorts, + isInitialDeploy, + desiredCount, + createCloudMapService, + } = this.props; + + let cloudMapService: cloudmap.CfnService = undefined; + + if (createCloudMapService) { + cloudMapService = new cloudmap.CfnService(this, 'CloudmapService', { + name: apiName, + dnsConfig: { + dnsRecords: [ + { + ttl: 60, + type: cloudmap.DnsRecordType.SRV, + }, + ], + namespaceId: this.cloudMapNamespaceId, + routingPolicy: cloudmap.RoutingPolicy.MULTIVALUE, + }, + }); } - private init() { - const { - restrictAccess, - dependsOn, - deploymentMechanism, - } = this.props; - - // Unused in this stack, but required by the root stack - new cdk.CfnParameter(this, 'env', { type: 'String' }); - - const paramDomain = new cdk.CfnParameter(this, 'domain', { type: 'String' }); - const paramRestrictAccess = new cdk.CfnParameter(this, 'restrictAccess', { - type: 'String', - allowedValues: ['true', 'false'], + const task = new ecs.TaskDefinition(this, 'TaskDefinition', { + compatibility: ecs.Compatibility.FARGATE, + memoryMiB: '1024', + cpu: '512', + family: `${envName}-${apiName}`, + }); + (task.node.defaultChild as ecs.CfnTaskDefinition).overrideLogicalId('TaskDefinition'); + policies.forEach(policy => { + const statement = isPolicyStatement(policy) ? policy : wrapJsonPoliciesInCdkPolicies(policy); + + task.addToTaskRolePolicy(statement); + }); + + const containersInfo: { + container: ecs.ContainerDefinition; + repository: ecr.IRepository; + }[] = []; + + containers.forEach( + ({ + name, + image, + build, + portMappings, + logConfiguration, + environment, + entrypoint: entryPoint, + command, + working_dir: workingDirectory, + healthcheck: healthCheck, + secrets: containerSecrets, + }) => { + const logGroup = new logs.LogGroup(this, `${name}ContainerLogGroup`, { + logGroupName: `/ecs/${envName}-${apiName}-${name}`, + retention: logs.RetentionDays.ONE_MONTH, + removalPolicy: cdk.RemovalPolicy.DESTROY, }); - const paramZipPath = new cdk.CfnParameter(this, 'ParamZipPath', { - type: 'String', - // Required only for FULLY_MANAGED - default: deploymentMechanism === DEPLOYMENT_MECHANISM.FULLY_MANAGED ? undefined : '', - }); - - const parameters: Map = new Map(); - - parameters.set('ParamZipPath', paramZipPath); - parameters.set('domain', paramDomain); - parameters.set('restrictAccess', paramRestrictAccess); - - const authParams: { - UserPoolId?: cdk.CfnParameter; - AppClientIDWeb?: cdk.CfnParameter; - } = {}; - - const paramTypes: Record = { - NetworkStackSubnetIds: 'CommaDelimitedList', - }; - - dependsOn.forEach(({ category, resourceName, attributes }) => { - attributes.forEach(attrib => { - const paramName = [category, resourceName, attrib].join(''); - - const type = paramTypes[paramName] ?? 'String'; - const param = new cdk.CfnParameter(this, paramName, { type }); - - parameters.set(paramName, param); - - if (category === 'auth') { - authParams[attrib as keyof typeof authParams] = param; - } + const { logDriver, options: { 'awslogs-stream-prefix': streamPrefix } = {} } = logConfiguration; + + const logging: ecs.LogDriver = + logDriver === 'awslogs' + ? ecs.LogDriver.awsLogs({ + streamPrefix, + logGroup: logs.LogGroup.fromLogGroupName(this, `${name}logGroup`, logGroup.logGroupName), + }) + : undefined; + + let repository: ecr.IRepository; + if (build) { + const logicalId = `${name}Repository`; + + const repositoryName = `${envName}-${categoryName}-${apiName}-${name}`; + + if (this.props.existingEcrRepositories.has(repositoryName)) { + repository = ecr.Repository.fromRepositoryName(this, logicalId, repositoryName); + } else { + repository = new ecr.Repository(this, logicalId, { + repositoryName: `${envName}-${categoryName}-${apiName}-${name}`, + removalPolicy: cdk.RemovalPolicy.RETAIN, + lifecycleRules: [ + { + rulePriority: 10, + maxImageCount: 1, + tagPrefixList: ['latest'], + tagStatus: ecr.TagStatus.TAGGED, + }, + { + rulePriority: 100, + maxImageAge: cdk.Duration.days(7), + tagStatus: ecr.TagStatus.ANY, + }, + ], }); - }); + (repository.node.defaultChild as ecr.CfnRepository).overrideLogicalId(logicalId); + } - const paramVpcId = parameters.get(`${NETWORK_STACK_LOGICAL_ID}VpcId`); - const paramVpcCidrBlock = parameters.get(`${NETWORK_STACK_LOGICAL_ID}VpcCidrBlock`); - const paramSubnetIds = parameters.get(`${NETWORK_STACK_LOGICAL_ID}SubnetIds`); - const paramClusterName = parameters.get(`${NETWORK_STACK_LOGICAL_ID}ClusterName`); - const paramCloudMapNamespaceId = parameters.get(`${NETWORK_STACK_LOGICAL_ID}CloudMapNamespaceId`); - const paramVpcLinkId = parameters.get(`${NETWORK_STACK_LOGICAL_ID}VpcLinkId`); - - const { UserPoolId: paramUserPoolId, AppClientIDWeb: paramAppClientIdWeb } = authParams; - - const isAuthCondition = new cdk.CfnCondition(this, 'isAuthCondition', { - expression: cdk.Fn.conditionAnd( - cdk.Fn.conditionEquals(restrictAccess, true), - cdk.Fn.conditionNot(cdk.Fn.conditionEquals(paramUserPoolId ?? '', '')), - cdk.Fn.conditionNot(cdk.Fn.conditionEquals(paramAppClientIdWeb ?? '', '')), - ), - }); + // Needed because the image will be pulled from ecr repository later + repository.grantPull(task.obtainExecutionRole()); + } - return { - parameters, - vpcId: paramVpcId.valueAsString, - vpcCidrBlock: paramVpcCidrBlock.valueAsString, - subnets: paramSubnetIds.valueAsList, - clusterName: paramClusterName.valueAsString, - zipPath: paramZipPath.valueAsString, - cloudMapNamespaceId: paramCloudMapNamespaceId.valueAsString, - vpcLinkId: paramVpcLinkId.valueAsString, - isAuthCondition, - userPoolId: paramUserPoolId && paramUserPoolId.valueAsString, - appClientId: paramAppClientIdWeb && paramAppClientIdWeb.valueAsString, - }; - } + const secrets: ecs.ContainerDefinitionOptions['secrets'] = {}; + const environmentWithoutSecrets = environment || {}; - private ecs() { - const { - envName, - categoryName, - apiName, - policies, - containers, - secretsArns, - taskEnvironmentVariables, - exposedContainer, - taskPorts, - isInitialDeploy, - desiredCount, - createCloudMapService, - } = this.props; - - let cloudMapService: cloudmap.CfnService = undefined; - - if (createCloudMapService) { - cloudMapService = new cloudmap.CfnService(this, 'CloudmapService', { - name: apiName, - dnsConfig: { - dnsRecords: [ - { - ttl: 60, - type: cloudmap.DnsRecordType.SRV, - }, - ], - namespaceId: this.cloudMapNamespaceId, - routingPolicy: cloudmap.RoutingPolicy.MULTIVALUE, - }, - }); - } + containerSecrets.forEach((s, i) => { + if (secretsArns.has(s)) { + secrets[s] = ecs.Secret.fromSecretsManager(ssm.Secret.fromSecretCompleteArn(this, `${name}secret${i + 1}`, secretsArns.get(s))); + } - const task = new ecs.TaskDefinition(this, 'TaskDefinition', { - compatibility: ecs.Compatibility.FARGATE, - memoryMiB: '1024', - cpu: '512', - family: `${envName}-${apiName}`, + delete environmentWithoutSecrets[s]; }); - (task.node.defaultChild as ecs.CfnTaskDefinition).overrideLogicalId('TaskDefinition'); - policies.forEach(policy => { - const statement = isPolicyStatement(policy) ? policy : wrapJsonPoliciesInCdkPolicies(policy); - task.addToTaskRolePolicy(statement); + const container = task.addContainer(name, { + image: repository ? ecs.ContainerImage.fromEcrRepository(repository) : ecs.ContainerImage.fromRegistry(image), + logging, + environment: { + ...taskEnvironmentVariables, + ...environmentWithoutSecrets, + }, + entryPoint, + command, + workingDirectory, + healthCheck: healthCheck && { + command: healthCheck.command, + interval: cdk.Duration.seconds(healthCheck.interval ?? 30), + retries: healthCheck.retries, + timeout: cdk.Duration.seconds(healthCheck.timeout ?? 5), + startPeriod: cdk.Duration.seconds(healthCheck.start_period ?? 0), + }, + secrets, }); - const containersInfo: { - container: ecs.ContainerDefinition; - repository: ecr.IRepository; - }[] = []; - - containers.forEach( - ({ - name, - image, - build, - portMappings, - logConfiguration, - environment, - entrypoint: entryPoint, - command, - working_dir: workingDirectory, - healthcheck: healthCheck, - secrets: containerSecrets, - }) => { - const logGroup = new logs.LogGroup(this, `${name}ContainerLogGroup`, { - logGroupName: `/ecs/${envName}-${apiName}-${name}`, - retention: logs.RetentionDays.ONE_MONTH, - removalPolicy: cdk.RemovalPolicy.DESTROY, - }); - - const { logDriver, options: { 'awslogs-stream-prefix': streamPrefix } = {} } = logConfiguration; - - const logging: ecs.LogDriver = - logDriver === 'awslogs' - ? ecs.LogDriver.awsLogs({ - streamPrefix, - logGroup: logs.LogGroup.fromLogGroupName(this, `${name}logGroup`, logGroup.logGroupName), - }) - : undefined; - - let repository: ecr.IRepository; - if (build) { - const logicalId = `${name}Repository`; - - const repositoryName = `${envName}-${categoryName}-${apiName}-${name}`; - - if (this.props.existingEcrRepositories.has(repositoryName)) { - repository = ecr.Repository.fromRepositoryName(this, logicalId, repositoryName); - } else { - repository = new ecr.Repository(this, logicalId, { - repositoryName: `${envName}-${categoryName}-${apiName}-${name}`, - removalPolicy: cdk.RemovalPolicy.RETAIN, - lifecycleRules: [ - { - rulePriority: 10, - maxImageCount: 1, - tagPrefixList: ['latest'], - tagStatus: ecr.TagStatus.TAGGED, - }, - { - rulePriority: 100, - maxImageAge: cdk.Duration.days(7), - tagStatus: ecr.TagStatus.ANY, - }, - ], - }); - (repository.node.defaultChild as ecr.CfnRepository).overrideLogicalId(logicalId); - } - - // Needed because the image will be pulled from ecr repository later - repository.grantPull(task.obtainExecutionRole()); - } - - const secrets: ecs.ContainerDefinitionOptions['secrets'] = {}; - const environmentWithoutSecrets = environment || {}; - - containerSecrets.forEach((s, i) => { - if (secretsArns.has(s)) { - secrets[s] = ecs.Secret.fromSecretsManager(ssm.Secret.fromSecretCompleteArn(this, `${name}secret${i + 1}`, secretsArns.get(s))); - } - - delete environmentWithoutSecrets[s]; - }); - - const container = task.addContainer(name, { - image: repository ? ecs.ContainerImage.fromEcrRepository(repository) : ecs.ContainerImage.fromRegistry(image), - logging, - environment: { - ...taskEnvironmentVariables, - ...environmentWithoutSecrets, - }, - entryPoint, - command, - workingDirectory, - healthCheck: healthCheck && { - command: healthCheck.command, - interval: cdk.Duration.seconds(healthCheck.interval ?? 30), - retries: healthCheck.retries, - timeout: cdk.Duration.seconds(healthCheck.timeout ?? 5), - startPeriod: cdk.Duration.seconds(healthCheck.start_period ?? 0), - }, - secrets, - }); - - if (build) { - containersInfo.push({ - container, - repository, - }); - } - - // TODO: should we use hostPort too? check network mode - portMappings?.forEach(({ containerPort, protocol, hostPort }) => { - container.addPortMappings({ - containerPort, - protocol: ecs.Protocol.TCP, - }); - }); - }, - ); + if (build) { + containersInfo.push({ + container, + repository, + }); + } - const serviceSecurityGroup = new ec2.CfnSecurityGroup(this, 'ServiceSG', { - vpcId: this.vpcId, - groupDescription: 'Service SecurityGroup', - securityGroupEgress: [ - { - description: 'Allow all outbound traffic by default', - cidrIp: '0.0.0.0/0', - ipProtocol: '-1', - }, - ], - securityGroupIngress: taskPorts.map(servicePort => ({ - ipProtocol: 'tcp', - fromPort: servicePort, - toPort: servicePort, - cidrIp: this.vpcCidrBlock, - })), + // TODO: should we use hostPort too? check network mode + portMappings?.forEach(({ containerPort, protocol, hostPort }) => { + container.addPortMappings({ + containerPort, + protocol: ecs.Protocol.TCP, + }); }); + }, + ); + + const serviceSecurityGroup = new ec2.CfnSecurityGroup(this, 'ServiceSG', { + vpcId: this.vpcId, + groupDescription: 'Service SecurityGroup', + securityGroupEgress: [ + { + description: 'Allow all outbound traffic by default', + cidrIp: '0.0.0.0/0', + ipProtocol: '-1', + }, + ], + securityGroupIngress: taskPorts.map(servicePort => ({ + ipProtocol: 'tcp', + fromPort: servicePort, + toPort: servicePort, + cidrIp: this.vpcCidrBlock, + })), + }); + + let serviceRegistries: ecs.CfnService.ServiceRegistryProperty[] = undefined; + + if (cloudMapService) { + serviceRegistries = [ + { + containerName: exposedContainer.name, + containerPort: exposedContainer.port, + registryArn: cloudMapService.attrArn, + }, + ]; + } - let serviceRegistries: ecs.CfnService.ServiceRegistryProperty[] = undefined; + const service = new ecs.CfnService(this, 'Service', { + serviceName: `${apiName}-service-${exposedContainer.name}-${exposedContainer.port}`, + cluster: this.clusterName, + launchType: 'FARGATE', + desiredCount: isInitialDeploy ? 0 : desiredCount, // This is later adjusted by the Predeploy action in the codepipeline + networkConfiguration: { + awsvpcConfiguration: { + assignPublicIp: 'ENABLED', + securityGroups: [serviceSecurityGroup.attrGroupId], + subnets: this.subnets, + }, + }, + taskDefinition: task.taskDefinitionArn, + serviceRegistries, + }); - if (cloudMapService) { - serviceRegistries = [{ - containerName: exposedContainer.name, - containerPort: exposedContainer.port, - registryArn: cloudMapService.attrArn, - }]; - } + new cdk.CfnOutput(this, 'ServiceName', { + value: service.serviceName, + }); - const service = new ecs.CfnService(this, 'Service', { - serviceName: `${apiName}-service-${exposedContainer.name}-${exposedContainer.port}`, - cluster: this.clusterName, - launchType: 'FARGATE', - desiredCount: isInitialDeploy ? 0 : desiredCount, // This is later adjusted by the Predeploy action in the codepipeline - networkConfiguration: { - awsvpcConfiguration: { - assignPublicIp: 'ENABLED', - securityGroups: [serviceSecurityGroup.attrGroupId], - subnets: this.subnets, - }, - }, - taskDefinition: task.taskDefinitionArn, - serviceRegistries, - }); + new cdk.CfnOutput(this, 'ClusterName', { + value: this.clusterName, + }); - new cdk.CfnOutput(this, 'ServiceName', { - value: service.serviceName - }); + return { + service, + serviceSecurityGroup, + containersInfo, + cloudMapService, + }; + } + + private pipeline({ + skipWait = false, + service, + containersInfo, + gitHubSourceActionInfo, + }: { + skipWait?: boolean; + service: ecs.CfnService; + containersInfo: { + container: ecs.ContainerDefinition; + repository: ecr.IRepository; + }[]; + gitHubSourceActionInfo?: GitHubSourceActionInfo; + }) { + const { envName, deploymentBucketName, deploymentMechanism, desiredCount } = this.props; - new cdk.CfnOutput(this, 'ClusterName', { - value: this.clusterName - }); + const s3SourceActionKey = this.zipPath; - return { - service, - serviceSecurityGroup, - containersInfo, - cloudMapService, - }; - } + const bucket = s3.Bucket.fromBucketName(this, 'Bucket', deploymentBucketName); - private pipeline({ - skipWait = false, - service, - containersInfo, - gitHubSourceActionInfo, - }: { - skipWait?: boolean; - service: ecs.CfnService, - containersInfo: { - container: ecs.ContainerDefinition; - repository: ecr.IRepository; - }[], - gitHubSourceActionInfo?: GitHubSourceActionInfo - }) { - const { - envName, - deploymentBucketName, - deploymentMechanism, - desiredCount, - } = this.props; - - const s3SourceActionKey = this.zipPath; - - const bucket = s3.Bucket.fromBucketName(this, 'Bucket', deploymentBucketName); - - const pipelineWithAwaiter = new PipelineWithAwaiter(this, 'ApiPipeline', { - skipWait, - envName, - containersInfo, - service, - bucket, - s3SourceActionKey, - deploymentMechanism, - gitHubSourceActionInfo, - desiredCount, - }); + const pipelineWithAwaiter = new PipelineWithAwaiter(this, 'ApiPipeline', { + skipWait, + envName, + containersInfo, + service, + bucket, + s3SourceActionKey, + deploymentMechanism, + gitHubSourceActionInfo, + desiredCount, + }); - pipelineWithAwaiter.node.addDependency(service); + pipelineWithAwaiter.node.addDependency(service); - return { pipelineWithAwaiter }; - } + return { pipelineWithAwaiter }; + } - protected getPipelineName() { - return this.pipelineWithAwaiter.getPipelineName(); - } + protected getPipelineName() { + return this.pipelineWithAwaiter.getPipelineName(); + } - getPipelineConsoleUrl(region: string) { - const pipelineName = this.getPipelineName(); - return `https://${region}.console.aws.amazon.com/codesuite/codepipeline/pipelines/${pipelineName}/view`; - } + getPipelineConsoleUrl(region: string) { + const pipelineName = this.getPipelineName(); + return `https://${region}.console.aws.amazon.com/codesuite/codepipeline/pipelines/${pipelineName}/view`; + } - toCloudFormation() { - prepareApp(this); + toCloudFormation() { + prepareApp(this); - const cfn = this._toCloudFormation(); + const cfn = this._toCloudFormation(); - Object.keys(cfn.Parameters).forEach(k => { - if (k.startsWith('AssetParameters')) { - let value = ''; + Object.keys(cfn.Parameters).forEach(k => { + if (k.startsWith('AssetParameters')) { + let value = ''; - if (k.includes('Bucket')) { - value = this.props.deploymentBucketName; - } else if (k.includes('VersionKey')) { - value = `${PIPELINE_AWAITER_ZIP}||`; - } + if (k.includes('Bucket')) { + value = this.props.deploymentBucketName; + } else if (k.includes('VersionKey')) { + value = `${PIPELINE_AWAITER_ZIP}||`; + } - cfn.Parameters[k].Default = value; - } - }); + cfn.Parameters[k].Default = value; + } + }); - return cfn; - } + return cfn; + } } /** @@ -514,17 +510,17 @@ export abstract class ContainersStack extends cdk.Stack { * @returns {iam.PolicyStatement} CDK compatible policy statement */ function wrapJsonPoliciesInCdkPolicies(policy: Record): iam.PolicyStatement { - return { - toStatementJson() { - return policy; - }, - } as iam.PolicyStatement; + return { + toStatementJson() { + return policy; + }, + } as iam.PolicyStatement; } function isPolicyStatement(obj: any): obj is iam.PolicyStatement { - if (obj && typeof (obj).toStatementJson === 'function') { - return true; - } + if (obj && typeof (obj).toStatementJson === 'function') { + return true; + } - return false; -} \ No newline at end of file + return false; +} diff --git a/packages/amplify-cli/src/initialize-env.ts b/packages/amplify-cli/src/initialize-env.ts index 06461bce019..45f8cbdf3ec 100644 --- a/packages/amplify-cli/src/initialize-env.ts +++ b/packages/amplify-cli/src/initialize-env.ts @@ -122,8 +122,7 @@ function populateCategoriesMeta( category: string, serviceName: string, ) { - if (amplifyMeta[category]?.[serviceName] && - teamProviderInfo[CATEGORIES]?.[category]?.[serviceName]) { + if (amplifyMeta[category]?.[serviceName] && teamProviderInfo[CATEGORIES]?.[category]?.[serviceName]) { Object.assign(amplifyMeta[category][serviceName], teamProviderInfo[CATEGORIES][category][serviceName]); stateManager.setMeta(projectPath, amplifyMeta); } diff --git a/packages/amplify-e2e-tests/src/__tests__/container-hosting.test.ts b/packages/amplify-e2e-tests/src/__tests__/container-hosting.test.ts index 470bdb60d1c..5d2b80804a2 100644 --- a/packages/amplify-e2e-tests/src/__tests__/container-hosting.test.ts +++ b/packages/amplify-e2e-tests/src/__tests__/container-hosting.test.ts @@ -1,4 +1,4 @@ -import { +import { addDevContainerHosting, createNewProjectDir, deleteProject, @@ -6,13 +6,12 @@ import { enableContainerHosting, getBackendAmplifyMeta, initJSProjectWithProfile, - removeHosting + removeHosting, } from 'amplify-e2e-core'; import * as fs from 'fs-extra'; import * as path from 'path'; - describe('amplify add hosting - container', () => { let projRoot: string; diff --git a/packages/amplify-graphql-model-transformer/src/__tests__/model-transformer.test.ts b/packages/amplify-graphql-model-transformer/src/__tests__/model-transformer.test.ts index 7ea324e85de..4b20ee9cdc6 100644 --- a/packages/amplify-graphql-model-transformer/src/__tests__/model-transformer.test.ts +++ b/packages/amplify-graphql-model-transformer/src/__tests__/model-transformer.test.ts @@ -339,24 +339,33 @@ describe('ModelTransformer: ', () => { expectFields(mutationType!, ['createPost', 'updatePost', 'deletePost']); }); - it('should not validate reserved type names when validateTypeNameReservedWords is off', () => { - const schema = ` - type Subscription @model{ - id: Int - str: String - } + it('should support non model objects contain id as a type for fields', () => { + const validSchema = ` + type Post @model { + id: ID! + comments: [Comment] + } + type Comment { + id: String! + text: String! + } `; const transformer = new GraphQLTransform({ transformers: [new ModelTransformer()], - featureFlags: ({ - getBoolean: jest.fn().mockImplementation(name => (name === 'validateTypeNameReservedWords' ? false : undefined)), - } as unknown) as FeatureFlagProvider, + featureFlags, }); - const out = transformer.transform(schema); + const out = transformer.transform(validSchema); expect(out).toBeDefined(); - const parsed = parse(out.schema); + const definition = out.schema; + expect(definition).toBeDefined(); + const parsed = parse(definition); validateModelSchema(parsed); - const subscriptionType = getObjectType(parsed, 'Subscription'); - expect(subscriptionType).toBeDefined(); + const commentInput = getInputType(parsed, 'CommentInput'); + expectFieldsOnInputType(commentInput!, ['id', 'text']); + const commentObject = getObjectType(parsed, 'Comment'); + const commentInputObject = getInputType(parsed, 'CommentInput'); + const commentObjectIDField = getFieldOnObjectType(commentObject!, 'id'); + const commentInputIDField = getFieldOnInputType(commentInputObject!, 'id'); + verifyMatchingTypes(commentObjectIDField.type, commentInputIDField.type); }); }); diff --git a/packages/amplify-graphql-model-transformer/src/graphql-model-transformer.ts b/packages/amplify-graphql-model-transformer/src/graphql-model-transformer.ts index 4c76aba32ac..f7106e58fe7 100644 --- a/packages/amplify-graphql-model-transformer/src/graphql-model-transformer.ts +++ b/packages/amplify-graphql-model-transformer/src/graphql-model-transformer.ts @@ -56,7 +56,7 @@ import { DirectiveWrapper, FieldWrapper, InputObjectDefinitionWrapper, - ObjectDefinationWrapper, + ObjectDefinitionWrapper, } from './wrappers/object-definition-wrapper'; export type Nullable = T | null; @@ -223,7 +223,7 @@ export class ModelTransformer extends TransformerModelBase implements Transforme if (mutation) { return mutation.fieldName; } - throw new Error('Unknow Subscription type'); + throw new Error('Unknown Subscription type'); }; const subscriptionsFields = this.getSubscriptionFieldNames(ctx, def!); @@ -865,7 +865,7 @@ export class ModelTransformer extends TransformerModelBase implements Transforme if (!typeObj) { throw new Error(`Type ${name} is missing in outputs`); } - const typeWrapper = new ObjectDefinationWrapper(typeObj); + const typeWrapper = new ObjectDefinitionWrapper(typeObj); if (!typeWrapper.hasField('id')) { const idField = FieldWrapper.create('id', 'ID'); typeWrapper.addField(idField); @@ -876,7 +876,7 @@ export class ModelTransformer extends TransformerModelBase implements Transforme if (typeWrapper.hasField(fieldName)) { const createdAtField = typeWrapper.getField(fieldName); if (!['String', 'AWSDateTime'].includes(createdAtField.getTypeName())) { - console.warn(`type ${name}.${fieldName} is not of String or AWSDateTime. Autopoupulation is not supported`); + console.warn(`type ${name}.${fieldName} is not of String or AWSDateTime. Auto population is not supported`); } } else { const createdAtField = FieldWrapper.create(fieldName, 'AWSDateTime'); diff --git a/packages/amplify-graphql-model-transformer/src/graphql-types/common.ts b/packages/amplify-graphql-model-transformer/src/graphql-types/common.ts index 499084e19f2..c1b3b910b58 100644 --- a/packages/amplify-graphql-model-transformer/src/graphql-types/common.ts +++ b/packages/amplify-graphql-model-transformer/src/graphql-types/common.ts @@ -31,9 +31,9 @@ import { } from '../definitions'; import { EnumWrapper, - InputFieldWraper, + InputFieldWrapper, InputObjectDefinitionWrapper, - ObjectDefinationWrapper, + ObjectDefinitionWrapper, } from '../wrappers/object-definition-wrapper'; /** @@ -48,7 +48,7 @@ export const makeConditionFilterInput = ( object: ObjectTypeDefinitionNode, ): InputObjectDefinitionWrapper => { const input = InputObjectDefinitionWrapper.create(name); - const wrappedObject = new ObjectDefinationWrapper(object); + const wrappedObject = new ObjectDefinitionWrapper(object); for (let field of wrappedObject.fields) { const fieldType = ctx.output.getType(field.getTypeName()); const isEnumType = fieldType && fieldType.kind === 'EnumTypeDefinition'; @@ -56,19 +56,19 @@ export const makeConditionFilterInput = ( const fieldTypeName = field.getTypeName(); const nameOverride = DEFAULT_SCALARS[fieldTypeName] || fieldTypeName; const conditionTypeName = isEnumType && field.isList() ? `Model${nameOverride}ListInput` : `Model${nameOverride}Input`; - const inputField = InputFieldWraper.create(field.name, conditionTypeName, true); + const inputField = InputFieldWrapper.create(field.name, conditionTypeName, true); input.addField(inputField); } } // additional conditions of list type for (let additionalField of ['and', 'or']) { - const inputField = InputFieldWraper.create(additionalField, name, true, true); + const inputField = InputFieldWrapper.create(additionalField, name, true, true); input.addField(inputField); } // additional conditions of non-list type for (let additionalField of ['not']) { - const inputField = InputFieldWraper.create(additionalField, name, true, false); + const inputField = InputFieldWrapper.create(additionalField, name, true, false); input.addField(inputField); } return input; @@ -105,7 +105,7 @@ export const createEnumModelFilters = ( type: ObjectTypeDefinitionNode, ): InputObjectTypeDefinitionNode[] => { // add enum type if present - const typeWrapper = new ObjectDefinationWrapper(type); + const typeWrapper = new ObjectDefinitionWrapper(type); const enumFields = typeWrapper.fields.filter(field => { const typeName = field.getTypeName(); const typeObj = ctx.output.getType(typeName); @@ -133,7 +133,7 @@ export function makeModelScalarFilterInputObject(type: string, supportsCondition default: typeName = type; } - const field = InputFieldWraper.create(condition, typeName, true); + const field = InputFieldWrapper.create(condition, typeName, true); if (condition === 'between') { field.wrapListType(); } @@ -177,20 +177,20 @@ function getFunctionListForType(typeName: string): Set { } } -function makeFunctionInputFields(typeName: string): InputFieldWraper[] { +function makeFunctionInputFields(typeName: string): InputFieldWrapper[] { const functions = getFunctionListForType(typeName); - const fields = new Array(); + const fields = new Array(); if (functions.has('attributeExists')) { - fields.push(InputFieldWraper.create('attributeExists', 'Boolean', true)); + fields.push(InputFieldWrapper.create('attributeExists', 'Boolean', true)); } if (functions.has('attributeType')) { - fields.push(InputFieldWraper.create('attributeType', 'ModelAttributeTypes', true)); + fields.push(InputFieldWrapper.create('attributeType', 'ModelAttributeTypes', true)); } if (functions.has('size')) { - fields.push(InputFieldWraper.create('size', 'ModelSizeInput', true)); + fields.push(InputFieldWrapper.create('size', 'ModelSizeInput', true)); } return fields; @@ -211,7 +211,7 @@ export function makeSizeInputType(): InputObjectTypeDefinitionNode { const input = InputObjectDefinitionWrapper.create(name); for (let condition of SIZE_CONDITIONS) { - const field = InputFieldWraper.create(condition, 'Int', true); + const field = InputFieldWrapper.create(condition, 'Int', true); if (condition === 'between') field.wrapListType(); input.addField(field); } @@ -222,7 +222,7 @@ export function makeEnumFilterInput(name: string): InputObjectTypeDefinitionNode const inputName = toPascalCase(['Model', name, 'Input']); const input = InputObjectDefinitionWrapper.create(inputName); ['eq', 'ne'].forEach(fieldName => { - const field = InputFieldWraper.create(fieldName, name, true); + const field = InputFieldWrapper.create(fieldName, name, true); input.addField(field); }); return input.serialize(); diff --git a/packages/amplify-graphql-model-transformer/src/graphql-types/mutation.ts b/packages/amplify-graphql-model-transformer/src/graphql-types/mutation.ts index e2ec3d7c475..04a4a780214 100644 --- a/packages/amplify-graphql-model-transformer/src/graphql-types/mutation.ts +++ b/packages/amplify-graphql-model-transformer/src/graphql-types/mutation.ts @@ -1,8 +1,8 @@ import { TransformerTransformSchemaStepContextProvider } from '@aws-amplify/graphql-transformer-interfaces'; import { ObjectTypeDefinitionNode, InputObjectTypeDefinitionNode } from 'graphql'; -import { toPascalCase } from 'graphql-transformer-common'; +import { ModelResourceIDs, toPascalCase } from 'graphql-transformer-common'; import { ModelDirectiveConfiguration } from '../graphql-model-transformer'; -import { ObjectDefinationWrapper, InputObjectDefinitionWrapper, InputFieldWraper } from '../wrappers/object-definition-wrapper'; +import { ObjectDefinitionWrapper, InputObjectDefinitionWrapper, InputFieldWrapper } from '../wrappers/object-definition-wrapper'; import { makeConditionFilterInput } from './common'; /** @@ -17,7 +17,7 @@ export const makeUpdateInputField = ( knownModelTypes: Set, ): InputObjectTypeDefinitionNode => { // sync related things - const objectWrapped = new ObjectDefinationWrapper(obj); + const objectWrapped = new ObjectDefinitionWrapper(obj); const typeName = objectWrapped.name; const name = toPascalCase([`Update`, typeName, 'Input']); const hasIdField = objectWrapped.hasField('id'); @@ -42,7 +42,7 @@ export const makeUpdateInputField = ( // Add id field and make it optional if (!hasIdField) { - input.addField(InputFieldWraper.create('id', 'ID', false)); + input.addField(InputFieldWrapper.create('id', 'ID', false)); } else { const idField = input.fields.find(f => f.name === 'id'); if (idField) { @@ -69,7 +69,7 @@ export const makeUpdateInputField = ( export const makeDeleteInputField = (type: ObjectTypeDefinitionNode): InputObjectTypeDefinitionNode => { const name = toPascalCase(['Delete', type.name.value, 'input']); const inputField = InputObjectDefinitionWrapper.create(name); - const idField = InputFieldWraper.create('id', 'ID', false, false); + const idField = InputFieldWrapper.create('id', 'ID', false, false); inputField.addField(idField); return inputField.serialize(); }; @@ -86,9 +86,10 @@ export const makeCreateInputField = ( knownModelTypes: Set, ): InputObjectTypeDefinitionNode => { // sync related things - const objectWrapped = new ObjectDefinationWrapper(obj); + const objectWrapped = new ObjectDefinitionWrapper(obj); const typeName = objectWrapped.name; - const name = toPascalCase([`Create`, typeName, 'Input']); + const name = ModelResourceIDs.ModelCreateInputObjectName(typeName); + const hasIdField = objectWrapped.hasField('id'); const fieldsToRemove = objectWrapped .fields!.filter(field => { @@ -108,7 +109,7 @@ export const makeCreateInputField = ( // Add id field and make it optional if (!hasIdField) { - input.addField(InputFieldWraper.create('id', 'ID')); + input.addField(InputFieldWrapper.create('id', 'ID')); } else { const idField = input.fields.find(f => f.name === 'id'); if (idField) { diff --git a/packages/amplify-graphql-model-transformer/src/graphql-types/query.ts b/packages/amplify-graphql-model-transformer/src/graphql-types/query.ts index 767053e58de..dfe6c7ee1ce 100644 --- a/packages/amplify-graphql-model-transformer/src/graphql-types/query.ts +++ b/packages/amplify-graphql-model-transformer/src/graphql-types/query.ts @@ -1,6 +1,6 @@ import { TransformerTransformSchemaStepContextProvider } from '@aws-amplify/graphql-transformer-interfaces'; import { InputObjectTypeDefinitionNode, ObjectTypeDefinitionNode } from 'graphql'; -import { FieldWrapper, ObjectDefinationWrapper } from '../wrappers/object-definition-wrapper'; +import { FieldWrapper, ObjectDefinitionWrapper } from '../wrappers/object-definition-wrapper'; import { makeConditionFilterInput } from './common'; export const makeListQueryFilterInput = ( ctx: TransformerTransformSchemaStepContextProvider, @@ -11,7 +11,7 @@ export const makeListQueryFilterInput = ( }; export const makeListQueryModel = (type: ObjectTypeDefinitionNode, modelName: string): ObjectTypeDefinitionNode => { - const outputType = ObjectDefinationWrapper.create(modelName); + const outputType = ObjectDefinitionWrapper.create(modelName); outputType.addField(FieldWrapper.create('items', type.name.value, true, true)); outputType.addField(FieldWrapper.create('nextToken', 'String', true, false)); diff --git a/packages/amplify-graphql-model-transformer/src/wrappers/object-definition-wrapper.ts b/packages/amplify-graphql-model-transformer/src/wrappers/object-definition-wrapper.ts index e2ad533af5d..b8077e85ee6 100644 --- a/packages/amplify-graphql-model-transformer/src/wrappers/object-definition-wrapper.ts +++ b/packages/amplify-graphql-model-transformer/src/wrappers/object-definition-wrapper.ts @@ -16,8 +16,9 @@ import { InputObjectTypeDefinitionNode, NamedTypeNode, EnumTypeDefinitionNode, + Kind, } from 'graphql'; -import { DEFAULT_SCALARS } from 'graphql-transformer-common'; +import { DEFAULT_SCALARS, getBaseType, isScalar, ModelResourceIDs, unwrapNonNull, withNamedNodeNamed } from 'graphql-transformer-common'; import { merge } from 'lodash'; // Todo: to be moved to core later. context.output.getObject would return wrapper type so its easier to manipulate @@ -121,7 +122,7 @@ export class GenericFieldWrapper { }; public getBaseType = (): NamedTypeNode => { let node = this.type; - while (node.kind === 'ListType' || node.kind === 'NonNullType') { + while (node.kind === Kind.LIST_TYPE || node.kind === Kind.NON_NULL_TYPE) { node = node.type; } return node; @@ -156,7 +157,7 @@ export class GenericFieldWrapper { // }; // } -export class InputFieldWraper extends GenericFieldWrapper { +export class InputFieldWrapper extends GenericFieldWrapper { public readonly argumenets?: InputValueDefinitionNode[]; public readonly description?: StringValueNode; public type: TypeNode; @@ -179,15 +180,39 @@ export class InputFieldWraper extends GenericFieldWrapper { directives: this.directives?.map(d => d.serialize()), }; }; - static fromField = (name: string, field: FieldDefinitionNode): InputFieldWraper => { - return new InputFieldWraper({ + + static fromField = (name: string, field: FieldDefinitionNode): InputFieldWrapper => { + const autoGeneratableFieldsWithType: Record = { + id: ['ID'], + createdAt: ['AWSDateTime', 'String'], + updatedAt: ['AWSDateTime', 'String'], + }; + + let type: TypeNode; + + if ( + Object.keys(autoGeneratableFieldsWithType).indexOf(name) !== -1 && + autoGeneratableFieldsWithType[name].indexOf(unwrapNonNull(field.type).name.value) !== -1 + ) { + // ids are always optional. when provided the value is used. + // when not provided the value is not used. + type = unwrapNonNull(field.type); + } else { + type = + isScalar(field.type) || getBaseType(field.type) === Kind.ENUM_TYPE_DEFINITION + ? field.type + : withNamedNodeNamed(field.type, ModelResourceIDs.NonModelInputObjectName(getBaseType(field.type))); + } + + return new InputFieldWrapper({ kind: 'InputValueDefinition', name: { kind: 'Name', value: name }, - type: field.type, + type, }); }; - static create = (name: string, type: string, isNullable = false, isList = false): InputFieldWraper => { - const field = new InputFieldWraper({ + + static create = (name: string, type: string, isNullable = false, isList = false): InputFieldWrapper => { + const field = new InputFieldWrapper({ kind: 'InputValueDefinition', name: { kind: 'Name', @@ -259,7 +284,7 @@ export class FieldWrapper extends GenericFieldWrapper { }; } -export class ObjectDefinationWrapper { +export class ObjectDefinitionWrapper { public readonly directives?: DirectiveWrapper[]; public readonly fields: FieldWrapper[]; public readonly name: string; @@ -310,8 +335,8 @@ export class ObjectDefinationWrapper { this.fields.splice(index, 1); }; - static create = (name: string, fields: FieldDefinitionNode[] = [], directives: DirectiveNode[] = []): ObjectDefinationWrapper => { - return new ObjectDefinationWrapper({ + static create = (name: string, fields: FieldDefinitionNode[] = [], directives: DirectiveNode[] = []): ObjectDefinitionWrapper => { + return new ObjectDefinitionWrapper({ kind: 'ObjectTypeDefinition', name: { kind: 'Name', @@ -325,11 +350,11 @@ export class ObjectDefinationWrapper { export class InputObjectDefinitionWrapper { public readonly directives?: DirectiveWrapper[]; - public readonly fields: InputFieldWraper[]; + public readonly fields: InputFieldWrapper[]; public readonly name: string; constructor(private node: InputObjectTypeDefinitionNode) { this.directives = (node.directives || []).map(d => new DirectiveWrapper(d)); - this.fields = (node.fields || []).map(f => new InputFieldWraper(f)); + this.fields = (node.fields || []).map(f => new InputFieldWrapper(f)); this.name = node.name.value; } @@ -345,7 +370,7 @@ export class InputObjectDefinitionWrapper { return field ? true : false; }; - getField = (name: string): InputFieldWraper => { + getField = (name: string): InputFieldWrapper => { const field = this.fields.find(f => f.name === name); if (!field) { throw new Error(`Field ${name} missing in type ${this.name}`); @@ -353,14 +378,14 @@ export class InputObjectDefinitionWrapper { return field; }; - addField = (field: InputFieldWraper): void => { + addField = (field: InputFieldWrapper): void => { if (this.hasField(field.name)) { throw new Error(`type ${this.name} has already a field with name ${field.name}`); } this.fields.push(field); }; - removeField = (field: InputFieldWraper): void => { + removeField = (field: InputFieldWrapper): void => { if (this.hasField(field.name)) { throw new Error(`type ${this.name} does not have the field with name ${field.name}`); } @@ -384,7 +409,7 @@ export class InputObjectDefinitionWrapper { directives: directives, }); for (let field of fields) { - const fieldWrapper = new InputFieldWraper(field); + const fieldWrapper = new InputFieldWrapper(field); wrappedObj.addField(fieldWrapper); } return wrappedObj; @@ -397,14 +422,10 @@ export class InputObjectDefinitionWrapper { fields: [], directives: [], }; + const wrappedInput = new InputObjectDefinitionWrapper(inputObj); for (let f of def.fields || []) { - const wrappedField = new InputFieldWraper({ - kind: 'InputValueDefinition', - name: f.name, - type: f.type, - directives: [], - }); + const wrappedField = InputFieldWrapper.fromField(f.name.value, f); wrappedInput.fields.push(wrappedField); } return wrappedInput; diff --git a/packages/amplify-graphql-searchable-transformer/src/__tests__/__snapshots__/amplify-graphql-searchable-transformer.tests.ts.snap b/packages/amplify-graphql-searchable-transformer/src/__tests__/__snapshots__/amplify-graphql-searchable-transformer.tests.ts.snap index 40828bcb609..976a98ba7a6 100644 --- a/packages/amplify-graphql-searchable-transformer/src/__tests__/__snapshots__/amplify-graphql-searchable-transformer.tests.ts.snap +++ b/packages/amplify-graphql-searchable-transformer/src/__tests__/__snapshots__/amplify-graphql-searchable-transformer.tests.ts.snap @@ -268,7 +268,7 @@ input CreateEmployeeInput { id: ID firstName: String! lastName: String! - type: EmploymentType! + type: EmploymentTypeInput! } type Mutation { @@ -281,7 +281,7 @@ input UpdateEmployeeInput { id: ID! firstName: String lastName: String - type: EmploymentType + type: EmploymentTypeInput } input DeleteEmployeeInput { diff --git a/packages/amplify-graphql-searchable-transformer/src/definitions.ts b/packages/amplify-graphql-searchable-transformer/src/definitions.ts index 3fba67a2e1b..2aa3c3c5897 100644 --- a/packages/amplify-graphql-searchable-transformer/src/definitions.ts +++ b/packages/amplify-graphql-searchable-transformer/src/definitions.ts @@ -9,7 +9,16 @@ import { EnumTypeDefinitionNode, EnumValueDefinitionNode, } from 'graphql'; -import { graphqlName, makeNamedType, isScalar, isEnum, makeListType, makeNonNullType, getBaseType, SearchableResourceIDs } from 'graphql-transformer-common'; +import { + graphqlName, + makeNamedType, + isScalar, + isEnum, + makeListType, + makeNonNullType, + getBaseType, + SearchableResourceIDs, +} from 'graphql-transformer-common'; const ID_CONDITIONS = [ 'ne', @@ -228,12 +237,11 @@ export function makeSearchableXSortInputObject(obj: ObjectTypeDefinitionNode): I export function makeSearchableAggregateTypeEnumObject(): EnumTypeDefinitionNode { const name = graphqlName('SearchableAggregateType'); - const values: EnumValueDefinitionNode[] = ['terms', 'avg', 'min', 'max', 'sum'] - .map((type: string) => ({ - kind: Kind.ENUM_VALUE_DEFINITION, - name: { kind: 'Name', value: type }, - directives: [], - })); + const values: EnumValueDefinitionNode[] = ['terms', 'avg', 'min', 'max', 'sum'].map((type: string) => ({ + kind: Kind.ENUM_VALUE_DEFINITION, + name: { kind: 'Name', value: type }, + directives: [], + })); return { kind: Kind.ENUM_TYPE_DEFINITION, diff --git a/packages/amplify-graphql-searchable-transformer/src/generate-resolver-vtl.ts b/packages/amplify-graphql-searchable-transformer/src/generate-resolver-vtl.ts index 8fbeabc2336..fb075997cf2 100644 --- a/packages/amplify-graphql-searchable-transformer/src/generate-resolver-vtl.ts +++ b/packages/amplify-graphql-searchable-transformer/src/generate-resolver-vtl.ts @@ -39,39 +39,31 @@ export function requestTemplate(primaryKey: string, nonKeywordFields: Expression set(ref('sortDirection'), ref('util.toJson({"order": "desc"})')), qref('$sortValues.add("{$sortField: $sortDirection}")'), ]), - forEach( - ref('sortItem'), - ref('context.args.sort'), - [ + forEach(ref('sortItem'), ref('context.args.sort'), [ + ifElse( + ref('util.isNullOrEmpty($sortItem.field)'), ifElse( - ref('util.isNullOrEmpty($sortItem.field)'), - ifElse( - ref('nonKeywordFields.contains($primaryKey)'), - set(ref('sortField'), ref('util.toJson($primaryKey)')), - set(ref('sortField'), ref('util.toJson("${primaryKey}.keyword")')), - ), - ifElse( - ref('nonKeywordFields.contains($sortItem.field)'), - set(ref('sortField'), ref('util.toJson($sortItem.field)')), - set(ref('sortField'), ref('util.toJson("${sortItem.field}.keyword")')), - ), + ref('nonKeywordFields.contains($primaryKey)'), + set(ref('sortField'), ref('util.toJson($primaryKey)')), + set(ref('sortField'), ref('util.toJson("${primaryKey}.keyword")')), + ), + ifElse( + ref('nonKeywordFields.contains($sortItem.field)'), + set(ref('sortField'), ref('util.toJson($sortItem.field)')), + set(ref('sortField'), ref('util.toJson("${sortItem.field}.keyword")')), ), - set(ref('sortDirection'), ref('util.toJson({"order": $sortItem.direction})')), - qref('$sortValues.add("{$sortField: $sortDirection}")'), - ], - ), - ), - forEach( - ref('aggItem'), - ref('context.args.aggregates'), - [ - ifElse( - ref('nonKeywordFields.contains($aggItem.field)'), - qref('$aggregateValues.put("$aggItem.name", {"$aggItem.type": {"field": "$aggItem.field"}})'), - qref('$aggregateValues.put("$aggItem.name", {"$aggItem.type": {"field": "${aggItem.field}.keyword"}})'), ), - ], + set(ref('sortDirection'), ref('util.toJson({"order": $sortItem.direction})')), + qref('$sortValues.add("{$sortField: $sortDirection}")'), + ]), ), + forEach(ref('aggItem'), ref('context.args.aggregates'), [ + ifElse( + ref('nonKeywordFields.contains($aggItem.field)'), + qref('$aggregateValues.put("$aggItem.name", {"$aggItem.type": {"field": "$aggItem.field"}})'), + qref('$aggregateValues.put("$aggItem.name", {"$aggItem.type": {"field": "${aggItem.field}.keyword"}})'), + ), + ]), ElasticsearchMappingTemplate.searchTemplate({ path: str('$indexPath'), size: ifElse(ref('context.args.limit'), ref('context.args.limit'), int(ResourceConstants.DEFAULT_SEARCHABLE_PAGE_LIMIT), true), diff --git a/packages/graphql-mapping-template/src/elasticsearch.ts b/packages/graphql-mapping-template/src/elasticsearch.ts index 7fbf7a57a0f..f1a08dd2719 100644 --- a/packages/graphql-mapping-template/src/elasticsearch.ts +++ b/packages/graphql-mapping-template/src/elasticsearch.ts @@ -74,7 +74,7 @@ export class ElasticsearchMappingTemplate { * @param query the query * @param aggs aggregate the query results */ - public static searchTemplate({ + public static searchTemplate({ query, size, search_after, diff --git a/packages/graphql-transformer-common/src/definition.ts b/packages/graphql-transformer-common/src/definition.ts index f867a0f610f..e8b1e107e2b 100644 --- a/packages/graphql-transformer-common/src/definition.ts +++ b/packages/graphql-transformer-common/src/definition.ts @@ -266,7 +266,7 @@ export function defineUnionType(name: string, types: NamedTypeNode[] = []): Unio kind: Kind.UNION_TYPE_DEFINITION, name: { kind: 'Name', - value: name + value: name, }, types: types, }; diff --git a/packages/graphql-transformers-e2e-tests/src/__tests__/SearchableModelTransformerV2.e2e.test.ts b/packages/graphql-transformers-e2e-tests/src/__tests__/SearchableModelTransformerV2.e2e.test.ts index b975777327c..af34dd1ec1a 100644 --- a/packages/graphql-transformers-e2e-tests/src/__tests__/SearchableModelTransformerV2.e2e.test.ts +++ b/packages/graphql-transformers-e2e-tests/src/__tests__/SearchableModelTransformerV2.e2e.test.ts @@ -84,9 +84,7 @@ beforeAll(async () => { `; const transformer = new GraphQLTransform({ featureFlags, - transformers: [ - new ModelTransformer(), new SearchableModelTransformer(), - ], + transformers: [new ModelTransformer(), new SearchableModelTransformer()], }); try { await awsS3Client.createBucket({ Bucket: BUCKET_NAME }).promise(); @@ -121,7 +119,7 @@ beforeAll(async () => { await createEntries(); } catch (e) { console.error(e); - throw(e); + throw e; } });