diff --git a/cloudformation/ecs-service.yaml b/cloudformation/ecs-service.yaml index 3099f95..47536fb 100644 --- a/cloudformation/ecs-service.yaml +++ b/cloudformation/ecs-service.yaml @@ -91,7 +91,7 @@ Resources: Cluster: Type: AWS::ECS::Cluster Properties: - ClusterName: !Join ["", [!Ref "AWS::StackName", Cluster]] + ClusterName: !Join ['', [!Ref 'AWS::StackName', Cluster]] TaskDefinition: Type: AWS::ECS::TaskDefinition @@ -99,7 +99,7 @@ Resources: DependsOn: LogGroup Properties: # Name of the task definition. Subsequent versions of the task definition are grouped together under this name. - Family: !Join ["", [!Ref "AWS::StackName", TaskDefinition]] + Family: !Join ['', [!Ref 'AWS::StackName', TaskDefinition]] # awsvpc is required for Fargate NetworkMode: awsvpc RequiresCompatibilities: @@ -119,10 +119,14 @@ Resources: - ContainerPort: !Ref ContainerPort # Define which parameters should be fetched Secrets: - - Name: "DATABASE_URL" - ValueFrom: !Sub "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${EnvironmentName}/graphql-api/DATABASE_URL" - - Name: "COGNITO_POOL_ID" - ValueFrom: !Sub "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${EnvironmentName}/cognito/ADMIN_POOL_ID" + - Name: 'DATABASE_URL' + ValueFrom: !Sub 'arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${EnvironmentName}/graphql-api/DATABASE_URL' + - Name: 'COGNITO_POOL_ID' + ValueFrom: !Sub 'arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${EnvironmentName}/cognito/ADMIN_POOL_ID' + - Name: 'INTERIORS_URL' + ValueFrom: !Sub 'arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${EnvironmentName}/INTERIORS_URL' + - Name: 'INTERIORS_TOKEN' + ValueFrom: !Sub 'arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${EnvironmentName}/INTERIORS_TOKEN' # Send logs to CloudWatch Logs LogConfiguration: LogDriver: awslogs @@ -135,47 +139,47 @@ Resources: ExecutionRole: Type: AWS::IAM::Role Properties: - RoleName: !Join ["", [!Ref "AWS::StackName", ExecutionRole]] + RoleName: !Join ['', [!Ref 'AWS::StackName', ExecutionRole]] AssumeRolePolicyDocument: Statement: - Effect: Allow Principal: Service: ecs-tasks.amazonaws.com - Action: "sts:AssumeRole" + Action: 'sts:AssumeRole' ManagedPolicyArns: - - "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" + - 'arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy' - arn:aws:iam::aws:policy/AmazonSSMReadOnlyAccess # A role for the containers TaskRole: Type: AWS::IAM::Role Properties: - RoleName: !Join ["", [!Ref "AWS::StackName", TaskRole]] + RoleName: !Join ['', [!Ref 'AWS::StackName', TaskRole]] AssumeRolePolicyDocument: Statement: - Effect: Allow Principal: Service: ecs-tasks.amazonaws.com - Action: "sts:AssumeRole" + Action: 'sts:AssumeRole' # A role needed for auto scaling AutoScalingRole: Type: AWS::IAM::Role Properties: - RoleName: !Join ["", [!Ref "AWS::StackName", AutoScalingRole]] + RoleName: !Join ['', [!Ref 'AWS::StackName', AutoScalingRole]] AssumeRolePolicyDocument: Statement: - Effect: Allow Principal: Service: ecs-tasks.amazonaws.com - Action: "sts:AssumeRole" + Action: 'sts:AssumeRole' ManagedPolicyArns: - - "arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceAutoscaleRole" + - 'arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceAutoscaleRole' ContainerSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: - !Join ["", [!Ref "AWS::StackName", ContainerSecurityGroup]] + !Join ['', [!Ref 'AWS::StackName', ContainerSecurityGroup]] VpcId: !Ref VPC SecurityGroupIngress: - IpProtocol: tcp @@ -187,7 +191,7 @@ Resources: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: - !Join ["", [!Ref "AWS::StackName", LoadBalancerSecurityGroup]] + !Join ['', [!Ref 'AWS::StackName', LoadBalancerSecurityGroup]] VpcId: !Ref VPC SecurityGroupIngress: - IpProtocol: tcp @@ -235,7 +239,7 @@ Resources: HealthCheckTimeoutSeconds: 10 UnhealthyThresholdCount: 2 HealthyThresholdCount: 2 - Name: !Join ["", [!Ref "AWS::StackName", Group]] + Name: !Join ['', [!Ref 'AWS::StackName', Group]] Port: !Ref ContainerPort Protocol: HTTP TargetGroupAttributes: @@ -262,7 +266,7 @@ Resources: LoadBalancerAttributes: - Key: idle_timeout.timeout_seconds Value: 1200 - Name: !Join ["", [!Ref "AWS::StackName", LB]] + Name: !Join ['', [!Ref 'AWS::StackName', LB]] # "internal" is also an option Scheme: internet-facing SecurityGroups: @@ -274,8 +278,8 @@ Resources: DNSRecord: Type: AWS::Route53::RecordSet Properties: - HostedZoneName: !Join ["", [!Ref HostedZoneName, .]] - Name: !Join ["", [!Ref Subdomain, ., !Ref HostedZoneName, .]] + HostedZoneName: !Join ['', [!Ref HostedZoneName, .]] + Name: !Join ['', [!Ref Subdomain, ., !Ref HostedZoneName, .]] Type: A AliasTarget: DNSName: !GetAtt LoadBalancer.DNSName @@ -284,14 +288,14 @@ Resources: LogGroup: Type: AWS::Logs::LogGroup Properties: - LogGroupName: !Join ["", [/ecs/, !Ref "AWS::StackName", TaskDefinition]] + LogGroupName: !Join ['', [/ecs/, !Ref 'AWS::StackName', TaskDefinition]] AutoScalingTarget: Type: AWS::ApplicationAutoScaling::ScalableTarget Properties: MinCapacity: !Ref MinContainers MaxCapacity: !Ref MaxContainers - ResourceId: !Join ["/", [service, !Ref Cluster, !GetAtt Service.Name]] + ResourceId: !Join ['/', [service, !Ref Cluster, !GetAtt Service.Name]] ScalableDimension: ecs:service:DesiredCount ServiceNamespace: ecs # "The Amazon Resource Name (ARN) of an AWS Identity and Access Management (IAM) role that allows Application Auto Scaling to modify your scalable target." @@ -299,7 +303,7 @@ Resources: AutoScalingPolicy: Type: AWS::ApplicationAutoScaling::ScalingPolicy Properties: - PolicyName: !Join ["", [!Ref "AWS::StackName", AutoScalingPolicy]] + PolicyName: !Join ['', [!Ref 'AWS::StackName', AutoScalingPolicy]] PolicyType: TargetTrackingScaling ScalingTargetId: !Ref AutoScalingTarget TargetTrackingScalingPolicyConfiguration: @@ -327,7 +331,7 @@ Resources: PolicyDocument: Version: 2012-10-17 Statement: - - Resource: "*" + - Resource: '*' Effect: Allow Action: - logs:CreateLogGroup @@ -380,7 +384,7 @@ Resources: - s3:GetObject - s3:GetObjectVersion - s3:GetBucketVersioning - - Resource: "*" + - Resource: '*' Effect: Allow Action: - ecs:List* @@ -455,7 +459,7 @@ Resources: Type: AWS::CodePipeline::Pipeline Properties: RoleArn: !GetAtt CodePipelineServiceRole.Arn - Name: !Join ["", [!Ref "AWS::StackName", Pipeline]] + Name: !Join ['', [!Ref 'AWS::StackName', Pipeline]] ArtifactStore: Type: S3 Location: !Ref ArtifactBucket @@ -511,7 +515,7 @@ Resources: Outputs: Endpoint: Description: Endpoint - Value: !Join ["", ["https://", !Ref DNSRecord]] + Value: !Join ['', ['https://', !Ref DNSRecord]] Service: Value: Service PipelineUrl: diff --git a/docker-compose.yml b/docker-compose.yml index 57012ed..c82ffeb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,6 +14,8 @@ services: environment: - COGNITO_POOL_ID=eu-west-1_3O4VfvPn7 - DATABASE_URL=${DATABASE_URL} + - INTERIOR_TOKEN=${INTERIOR_TOKEN} + - INTERIOR_URL=${INTERIOR_URL} networks: default: diff --git a/src/datasources/interior-api.ts b/src/datasources/interior-api.ts index 51f8b8d..b6b313d 100644 --- a/src/datasources/interior-api.ts +++ b/src/datasources/interior-api.ts @@ -47,7 +47,7 @@ export class InteriorAPI extends BaseSQLDataSource { } getInteriorRoomNumber(roomNumberPart: string): number { - return convertToPossibleType(roomNumberPart.slice(4)); + return parseInt(roomNumberPart.slice(4), 10); } decorateRow(row: any): Interior { diff --git a/src/datasources/interiors-lambda-api.ts b/src/datasources/interiors-lambda-api.ts new file mode 100644 index 0000000..810d477 --- /dev/null +++ b/src/datasources/interiors-lambda-api.ts @@ -0,0 +1,41 @@ +import { RESTDataSource } from 'apollo-datasource-rest'; +import { Product, ProductGroup } from '../types/product-types'; + +export class InteriorsLambdaAPI extends RESTDataSource { + constructor() { + super(); + this.baseURL = process.env.INTERIOR_URL; + } + + /** + * + * @param product Product with printProducts and fields + * Example usage in a resolver + * + * const product = await (dataSources.productApi).getProduct(productId); + * await (dataSources.interiorsLambdaAPI).generateNewInteriors(product); + * + */ + async generateNewInteriors(product: Product): Promise { + const found = product.printProducts.find( + (pr) => pr.groupId === ProductGroup.WALLPAPER, + ); + const isRepeating = found !== undefined; + + const body = { + token: process.env.INTERIOR_TOKEN, + image: `products/${product.id}.jpg`, + primary_image_area: [ + product.fields.focusXpoint2, + product.fields.focusYpoint2, + ], + autodetect: true, + }; + + if (isRepeating) { + body['print_file_height'] = product.fields.printFileHeight; + body['print_file_dpi'] = product.fields.printFileDpi; + } + return this.post('/batch', body); + } +} diff --git a/src/datasources/order-api.ts b/src/datasources/order-api.ts index eb61602..a701053 100644 --- a/src/datasources/order-api.ts +++ b/src/datasources/order-api.ts @@ -66,7 +66,7 @@ export class OrderAPI extends BaseSQLDataSource { .count() .first() .cache(MINUTE * 60); - return convertToPossibleType(res['count']); // Optimize later to return Promise + return res['count'] as number; // Optimize later to return Promise } async getOrders(input: Maybe): Promise> { diff --git a/src/datasources/product-api.ts b/src/datasources/product-api.ts index 5392a40..84506c2 100644 --- a/src/datasources/product-api.ts +++ b/src/datasources/product-api.ts @@ -17,7 +17,7 @@ import { ProductSizeTypes, } from '../types/product-types'; import { Maybe } from '../types/types'; -import { convertToPossibleType } from './utils'; +import { convertToPossibleType, nullOrNumber } from './utils'; const MINUTE = 60; @@ -75,11 +75,40 @@ export class ProductAPI extends BaseSQLDataSource { if (!row.fields) { return null; // 53460 for example } + // Convert to camelCase first const fields = Object.keys(row.fields).reduce((mem, key) => { - mem[camelcase(key)] = convertToPossibleType(row.fields[key]); + mem[camelcase(key)] = row.fields[key]; return mem; }, {}); + fields['artNo'] = fields['artNo']; + fields['name'] = fields['name']; + fields['copyright'] = fields['copyright'] ?? ''; + fields['batch'] = fields['batch'] ?? ''; + fields['height'] = nullOrNumber(fields['height'] ?? null); + fields['width'] = nullOrNumber(fields['width'] ?? null); + fields['printFileWidth'] = nullOrNumber(fields['printFileWidth'] ?? null); + fields['printFileHeight'] = nullOrNumber(fields['printFileHeight'] ?? null); + fields['printFileDpi'] = nullOrNumber(fields['printFileDpi'] ?? null); + fields['marginWidthMax'] = nullOrNumber(fields['marginWidthMax'] ?? null); + fields['marginWidthMin'] = nullOrNumber(fields['marginWidthMin'] ?? null); + fields['marginHeightMax'] = nullOrNumber(fields['marginHeightMax'] ?? null); + fields['marginHeightMin'] = nullOrNumber(fields['marginHeightMin'] ?? null); + fields['focusXpoint2'] = nullOrNumber(fields['focusXpoint2'] ?? null); + fields['focusYpoint2'] = nullOrNumber(fields['focusYpoint2'] ?? null); + fields['focusXpoint'] = nullOrNumber(fields['focusXpoint'] ?? null); + fields['focusYpoint'] = nullOrNumber(fields['focusYpoint'] ?? null); + fields['photowallResolution'] = nullOrNumber( + fields['photowallResolution'] ?? null, + ); + fields['canvasResolution'] = nullOrNumber( + fields['canvasResolution'] ?? null, + ); + fields['wallpaperResolution'] = nullOrNumber( + fields['wallpaperResolution'] ?? null, + ); + fields['proportionsWarning?'] = fields['proportionsWarning?'] ?? ''; + fields['imageResolution'] = nullOrNumber(fields['imageResolution'] ?? null); return fields; } @@ -104,8 +133,6 @@ export class ProductAPI extends BaseSQLDataSource { row.fields = this.getProductFields(row); row.designerId = row.designerid; row.orientation = this.getOrientation(row.fields); - row.copyright = row.copyright ?? ''; - row.batch = row.batch ?? ''; return row; } @@ -114,7 +141,7 @@ export class ProductAPI extends BaseSQLDataSource { const res = await this.cachedRaw(query) .cache(MINUTE * 60) .then((data) => data.rows); - return convertToPossibleType(res[0]['count']); // Optimize later to return Promise + return res[0]['count'] as number; // Optimize later to return Promise } async getProducts(input: ProductsFilterInput): Promise> { diff --git a/src/datasources/utils.ts b/src/datasources/utils.ts index b6844ee..373aa4c 100644 --- a/src/datasources/utils.ts +++ b/src/datasources/utils.ts @@ -1,8 +1,10 @@ -function isNumeric(n: any) { - return !isNaN(n); +type InputType = string | undefined | null; + +function isNumeric(n: InputType) { + return !isNaN(parseFloat(n)); } -function isBoolean(n: any) { +function isBoolean(n: InputType | boolean) { if (typeof n === 'boolean') { return true; } @@ -13,9 +15,13 @@ function isBoolean(n: any) { return false; } -export function convertToPossibleType(n: any) { +export function convertToPossibleType(n: InputType) { if ('' === n) return ''; if (isBoolean(n)) return Boolean(n); if (isNumeric(n)) return Number(n); return n; } + +export function nullOrNumber(n: InputType) { + return n === null || '' === n ? null : Number(n); +} diff --git a/src/index.ts b/src/index.ts index 3c6ef47..357187f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -17,6 +17,7 @@ import { InteriorAPI } from './datasources/interior-api'; import { verifyToken } from './cognito/cognito-client'; import { readFileSync } from 'fs'; +import { InteriorsLambdaAPI } from './datasources/interiors-lambda-api'; const path = require('path'); const typeDefs = readFileSync( path.join(__dirname, './schema.graphql'), @@ -29,6 +30,7 @@ const knexConfig = knexStringcase(dbConfig); const categoryApi = new CategoryAPI(knexConfig); const designerApi = new DesignerAPI(knexConfig); const imageServerApi = new ImageServerApi(); +const interiorsLambdaApi = new InteriorsLambdaAPI(); const interiorApi = new InteriorAPI(knexConfig, imageServerApi); const keywordApi = new KeywordAPI(knexConfig); const marketLocaleApi = new MarketLocaleAPI(knexConfig); @@ -39,6 +41,7 @@ const dataSources = () => ({ categoryApi, designerApi, interiorApi, + interiorsLambdaApi, imageServerApi, keywordApi, marketLocaleApi, diff --git a/src/resolvers/keywords-resolver.ts b/src/resolvers/keywords-resolver.ts index 0c36b2d..82f2e69 100644 --- a/src/resolvers/keywords-resolver.ts +++ b/src/resolvers/keywords-resolver.ts @@ -30,7 +30,6 @@ export const keywordsMutationTypeDefs = { name, type, ); - console.log(id); return (dataSources.keywordApi).getKeywordById(id); }, }; diff --git a/src/resolvers/products-resolver.ts b/src/resolvers/products-resolver.ts index 34b5562..320563b 100644 --- a/src/resolvers/products-resolver.ts +++ b/src/resolvers/products-resolver.ts @@ -10,6 +10,7 @@ import { KeywordAPI } from '../datasources/keyword-api'; import { Keyword } from '../types/keyword-types'; import { MarketLocaleAPI } from '../datasources/market-locale-api'; import { InteriorAPI } from '../datasources/interior-api'; +import { InteriorsLambdaAPI } from '../datasources/interiors-lambda-api'; const ProductBlacklist = { async market(parent, _args, { dataSources }) { @@ -111,7 +112,13 @@ export const productMutationTypeDefs = { productId, groupIds, ); - return (dataSources.productApi).getProduct(productId); + const product = await (dataSources.productApi).getProduct( + productId, + ); + await (( + dataSources.interiorsLambdaApi + )).generateNewInteriors(product); + return product; }, async productKeywords(_, { productId, keywordIds }, { dataSources }) { await (dataSources.keywordApi).setProductKeywords( diff --git a/src/schema.graphql b/src/schema.graphql index 13a84ad..1944340 100644 --- a/src/schema.graphql +++ b/src/schema.graphql @@ -267,7 +267,7 @@ type ProductFields { focusYpoint: Float focusXpoint2: Float focusYpoint2: Float - batch: String! + batch: String imageResolution: Int printFileWidth: Int printFileHeight: Int