File upload functionality for own interiors (#39)

This commit is contained in:
Niklas Fondberg
2021-09-13 13:38:53 +02:00
committed by GitHub
parent 0a4be2b081
commit 84bed9a7ce
15 changed files with 2275 additions and 4 deletions
+4
View File
@@ -2,3 +2,7 @@ DATABASE_URL=postgresql://fondberg:@docker.for.mac.localhost/photowall
COGNITO_POOL_ID=eu-west-1_3O4VfvPn7 COGNITO_POOL_ID=eu-west-1_3O4VfvPn7
INTERIORS_TOKEN= INTERIORS_TOKEN=
INTERIORS_URL=https://0hrq0zel2h.execute-api.eu-west-1.amazonaws.com/Prod INTERIORS_URL=https://0hrq0zel2h.execute-api.eu-west-1.amazonaws.com/Prod
AWS_SECRET_ACCESS_KEY=
AWS_ACCESS_KEY_ID=
AWS_DEFAULT_REGION=eu-west-1
+10 -2
View File
@@ -31,12 +31,16 @@ ifeq ($(STACK_ENVIRONMENT_NAME),production)
DB_SECURITY_GROUP_ID=$(DB_SECURITY_GROUP_ID_PRODUCTION) DB_SECURITY_GROUP_ID=$(DB_SECURITY_GROUP_ID_PRODUCTION)
CPU_SIZE=1024 CPU_SIZE=1024
MEMORY_SIZE=2GB MEMORY_SIZE=2GB
IMAGES_BUCKET=photowall-images
UPLOAD_BUCKET=photowall-inquiries
else else
SUBDOMAIN=$(STACK_NAME)-$(STACK_ENVIRONMENT_NAME) SUBDOMAIN=$(STACK_NAME)-$(STACK_ENVIRONMENT_NAME)
GITHUB_BRANCH=$(STACK_ENVIRONMENT_NAME) GITHUB_BRANCH=$(STACK_ENVIRONMENT_NAME)
DB_SECURITY_GROUP_ID=$(DB_SECURITY_GROUP_ID_STAGING) DB_SECURITY_GROUP_ID=$(DB_SECURITY_GROUP_ID_STAGING)
CPU_SIZE=512 CPU_SIZE=512
MEMORY_SIZE=1GB MEMORY_SIZE=1GB
IMAGES_BUCKET=photowall-images-dev
UPLOAD_BUCKET=photowall-inquiries-dev
endif endif
check-create-initial-ecr-image: check-create-initial-ecr-image:
@@ -99,7 +103,9 @@ create-stack: check-create-variables-set check-create-initial-ecr-image
ParameterKey="GitHubBranch",ParameterValue="$(GITHUB_BRANCH)" \ ParameterKey="GitHubBranch",ParameterValue="$(GITHUB_BRANCH)" \
ParameterKey="GitHubToken",ParameterValue="$(GITHUB_TOKEN)" \ ParameterKey="GitHubToken",ParameterValue="$(GITHUB_TOKEN)" \
ParameterKey="CpuSize",ParameterValue="$(CPU_SIZE)" \ ParameterKey="CpuSize",ParameterValue="$(CPU_SIZE)" \
ParameterKey="MemorySize",ParameterValue="$(MEMORY_SIZE)" ParameterKey="MemorySize",ParameterValue="$(MEMORY_SIZE)" \
ParameterKey="ImagesBucket",ParameterValue="$(IMAGES_BUCKET)" \
ParameterKey="UploadBucket",ParameterValue="$(UPLOAD_BUCKET)"
update-stack: set-repository-lifecycle-policy update-stack: set-repository-lifecycle-policy
$(call checkdef,STACK_NAME) $(call checkdef,STACK_NAME)
@@ -117,4 +123,6 @@ update-stack: set-repository-lifecycle-policy
ParameterKey="GitHubBranch",UsePreviousValue=true \ ParameterKey="GitHubBranch",UsePreviousValue=true \
ParameterKey="GitHubToken",UsePreviousValue=true \ ParameterKey="GitHubToken",UsePreviousValue=true \
ParameterKey="CpuSize",ParameterValue="$(CPU_SIZE)" \ ParameterKey="CpuSize",ParameterValue="$(CPU_SIZE)" \
ParameterKey="MemorySize",ParameterValue="$(MEMORY_SIZE)" ParameterKey="MemorySize",ParameterValue="$(MEMORY_SIZE)" \
ParameterKey="ImagesBucket",ParameterValue="$(IMAGES_BUCKET)" \
ParameterKey="UploadBucket",ParameterValue="$(UPLOAD_BUCKET)"
+34
View File
@@ -87,6 +87,12 @@ Parameters:
GitHubUser: GitHubUser:
Type: String Type: String
# Buckets
ImagesBucket:
Type: String
UploadBucket:
Type: String
Resources: Resources:
Cluster: Cluster:
Type: AWS::ECS::Cluster Type: AWS::ECS::Cluster
@@ -149,6 +155,32 @@ Resources:
ManagedPolicyArns: ManagedPolicyArns:
- 'arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy' - 'arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy'
- arn:aws:iam::aws:policy/AmazonSSMReadOnlyAccess - arn:aws:iam::aws:policy/AmazonSSMReadOnlyAccess
TaskRoleAccessPolicy:
Type: 'AWS::IAM::ManagedPolicy'
Properties:
Description: Create access policies for the container task
Path: /
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- s3:*
Resource: !Sub arn:aws:s3:::${ImagesBucket}/*
- Effect: Allow
Action:
- s3:*
Resource: !Sub arn:aws:s3:::${ImagesBucket}
- Effect: Allow
Action:
- s3:*
Resource: !Sub arn:aws:s3:::${UploadBucket}/*
- Effect: Allow
Action:
- s3:*
Resource: !Sub arn:aws:s3:::${UploadBucket}
# A role for the containers # A role for the containers
TaskRole: TaskRole:
Type: AWS::IAM::Role Type: AWS::IAM::Role
@@ -160,6 +192,8 @@ Resources:
Principal: Principal:
Service: ecs-tasks.amazonaws.com Service: ecs-tasks.amazonaws.com
Action: 'sts:AssumeRole' Action: 'sts:AssumeRole'
ManagedPolicyArns:
- !Ref TaskRoleAccessPolicy
# A role needed for auto scaling # A role needed for auto scaling
AutoScalingRole: AutoScalingRole:
+3
View File
@@ -16,6 +16,9 @@ services:
- DATABASE_URL=${DATABASE_URL} - DATABASE_URL=${DATABASE_URL}
- INTERIORS_TOKEN=${INTERIORS_TOKEN} - INTERIORS_TOKEN=${INTERIORS_TOKEN}
- INTERIORS_URL=${INTERIORS_URL} - INTERIORS_URL=${INTERIORS_URL}
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}
- AWS_DEFAULT_REGION=${AWS_DEFAULT_REGION}
networks: networks:
default: default:
+2033
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -11,6 +11,8 @@
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "^3.31.0",
"@aws-sdk/s3-request-presigner": "^3.31.0",
"apollo-datasource": "^3.1.0", "apollo-datasource": "^3.1.0",
"apollo-datasource-rest": "^3.2.0", "apollo-datasource-rest": "^3.2.0",
"apollo-server": "^3.3.0", "apollo-server": "^3.3.0",
+10
View File
@@ -17,3 +17,13 @@ export const dbConfig = {
}, },
], ],
}; };
export const uploadBucket =
process.env.ENVIRONMENT_NAME === 'production'
? 'photowall-inquiries-dev'
: 'photowall-inquiries-dev';
export const imageBucket =
process.env.ENVIRONMENT_NAME === 'production'
? 'photowall-images'
: 'photowall-images-dev';
+34 -2
View File
@@ -4,10 +4,11 @@ import {
InteriorType, InteriorType,
} from '../types/interior-types'; } from '../types/interior-types';
import { Orientation } from '../types/product-types'; import { Orientation } from '../types/product-types';
import { Maybe } from '../types/types'; import { IdNumberResult, Maybe } from '../types/types';
import { BaseSQLDataSource } from './BaseSQLDataSource'; import { BaseSQLDataSource } from './BaseSQLDataSource';
import { ImageServerApi } from './imageserver-api'; import { ImageServerApi } from './imageserver-api';
import { convertToPossibleType } from './utils'; import * as CONFIG from '../config';
import { moveS3File } from '../s3';
const MINUTE = 60; const MINUTE = 60;
@@ -120,4 +121,35 @@ export class InteriorAPI extends BaseSQLDataSource {
}); });
}); });
} }
// Mutations
async addOwnUploadToPrintId(
printId: number,
uploadedS3Key: string,
): Promise<IdNumberResult> {
// Get the last position entered in interiors for this printId
const positionRes: any = await this.knex
.queryBuilder()
.from('interiors')
.where({ print_id: printId })
.max('position')
.first();
const position = positionRes.max === null ? 0 : positionRes.max + 1;
// Insert the printId in interiors returning the id (important as it is used as image key name)
const res = await this.knex('interiors')
.insert({ print_id: printId, position: position })
.returning('id');
const interiorId = res[0];
// Move file
moveS3File(
CONFIG.uploadBucket,
uploadedS3Key,
CONFIG.imageBucket,
`interior-images/${interiorId}.jpg`,
);
return {
id: interiorId,
};
}
} }
+31
View File
@@ -0,0 +1,31 @@
import { EXPIRES_DEFAULT_SECS, getSignedPutObjectUrl } from '../s3';
import {
SignedUploadURL,
SignedUploadURLInput,
} from '../types/file-upload-types';
import * as CONFIG from '../config';
async function getSignedFileUploadURL(
_,
args: SignedUploadURLInput,
__,
): Promise<SignedUploadURL> {
const config = args.config;
const bucket = CONFIG.uploadBucket;
const url = await getSignedPutObjectUrl(
bucket,
config.key,
config.contentType,
);
return {
url: url,
key: config.key,
bucket: bucket,
expires: EXPIRES_DEFAULT_SECS,
};
}
export const imagesQueryTypeDefs = {
signedFileUploadURL: getSignedFileUploadURL,
};
+2
View File
@@ -18,6 +18,7 @@ import {
import { DateTimeResolver, DateResolver, JSONResolver } from 'graphql-scalars'; import { DateTimeResolver, DateResolver, JSONResolver } from 'graphql-scalars';
import { interiorsQueryTypeDefs, interiorTypeDefs } from './interiors-resolver'; import { interiorsQueryTypeDefs, interiorTypeDefs } from './interiors-resolver';
import { imagesQueryTypeDefs } from './file-upload-resolvers';
const resolvers = { const resolvers = {
Query: { Query: {
...orderQueryTypeDefs, ...orderQueryTypeDefs,
@@ -27,6 +28,7 @@ const resolvers = {
...keywordsQueryTypeDefs, ...keywordsQueryTypeDefs,
...marketsQueryTypeDefs, ...marketsQueryTypeDefs,
...interiorsQueryTypeDefs, ...interiorsQueryTypeDefs,
...imagesQueryTypeDefs,
}, },
Mutation: { Mutation: {
...productMutationTypeDefs, ...productMutationTypeDefs,
+12
View File
@@ -4,6 +4,7 @@ import {
ProductListResult, ProductListResult,
ProductsFilterInput, ProductsFilterInput,
} from '../types/product-types'; } from '../types/product-types';
import * as CONFIG from '../config';
import { CategoryAPI } from '../datasources/category-api'; import { CategoryAPI } from '../datasources/category-api';
import { Category } from '../types/category-types'; import { Category } from '../types/category-types';
import { KeywordAPI } from '../datasources/keyword-api'; import { KeywordAPI } from '../datasources/keyword-api';
@@ -11,6 +12,7 @@ import { Keyword } from '../types/keyword-types';
import { MarketLocaleAPI } from '../datasources/market-locale-api'; import { MarketLocaleAPI } from '../datasources/market-locale-api';
import { InteriorAPI } from '../datasources/interior-api'; import { InteriorAPI } from '../datasources/interior-api';
import { InteriorsLambdaAPI } from '../datasources/interiors-lambda-api'; import { InteriorsLambdaAPI } from '../datasources/interiors-lambda-api';
import { IdNumberResult } from '../types/types';
const ProductBlacklist = { const ProductBlacklist = {
async market(parent, _args, { dataSources }) { async market(parent, _args, { dataSources }) {
@@ -127,6 +129,16 @@ export const productMutationTypeDefs = {
); );
return (<ProductAPI>dataSources.productApi).getProduct(productId); return (<ProductAPI>dataSources.productApi).getProduct(productId);
}, },
async addOwnInteriorToPrintProduct(
_,
{ printId, uploadedS3Key },
{ dataSources },
): Promise<IdNumberResult> {
return (<InteriorAPI>dataSources.interiorApi).addOwnUploadToPrintId(
printId,
uploadedS3Key,
);
},
async productWallpaperType( async productWallpaperType(
_, _,
{ productId, wallpaperTypes }, { productId, wallpaperTypes },
+48
View File
@@ -0,0 +1,48 @@
import {
S3Client,
CopyObjectCommand,
DeleteObjectCommand,
PutObjectCommand,
} from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
export const EXPIRES_DEFAULT_SECS = 300;
export const getSignedPutObjectUrl = (
bucket: string,
key: string,
contentType: string,
): Promise<string> => {
const s3 = new S3Client({ region: 'eu-west-1' });
return getSignedUrl(
s3,
new PutObjectCommand({
Bucket: bucket,
Key: key,
ContentType: contentType,
}),
{ expiresIn: EXPIRES_DEFAULT_SECS },
);
};
export const moveS3File = async (
fromBucket: string,
fromKey: string,
toBucket: string,
toKey: string,
): Promise<any> => {
const s3 = new S3Client({ region: 'eu-west-1' });
const copyCmd = new CopyObjectCommand({
Bucket: toBucket,
CopySource: `${fromBucket}/${fromKey}`,
Key: toKey,
});
await s3.send(copyCmd);
const deleteCmd = new DeleteObjectCommand({
Bucket: fromBucket,
Key: fromKey,
});
return s3.send(deleteCmd);
};
+33
View File
@@ -5,6 +5,17 @@ scalar DateTime
scalar Date scalar Date
scalar JSON scalar JSON
enum CacheControlScope {
PUBLIC
PRIVATE
}
directive @cacheControl(
maxAge: Int
scope: CacheControlScope
inheritMaxAge: Boolean
) on FIELD_DEFINITION | OBJECT | INTERFACE | UNION
type Query { type Query {
orders(pagination: PaginationInput!, filter: DateFilterInput!): OrdersResult orders(pagination: PaginationInput!, filter: DateFilterInput!): OrdersResult
order(id: ID!): Order order(id: ID!): Order
@@ -24,6 +35,13 @@ type Query {
locales: [Locale] locales: [Locale]
locale(value: String!): Locale locale(value: String!): Locale
interiors(filter: InteriorsFilterInput): [Interior] interiors(filter: InteriorsFilterInput): [Interior]
signedFileUploadURL(config: SignedUploadURLInput): SignedUploadURL
@cacheControl(maxAge: 0)
}
input SignedUploadURLInput {
contentType: String!
key: String!
} }
input PaginationInput { input PaginationInput {
@@ -68,6 +86,13 @@ input InteriorsFilterInput {
productId: Int! productId: Int!
} }
type SignedUploadURL {
url: String!
key: String!
bucket: String!
expires: Int!
}
type Market { type Market {
id: ID! id: ID!
name: String! name: String!
@@ -394,6 +419,10 @@ input ProductBlacklistingInput {
groupIds: [Int] groupIds: [Int]
} }
type IdNumberResult {
id: Int!
}
type Mutation { type Mutation {
productInfo(productId: Int!, info: ProductInfoInput!): Product productInfo(productId: Int!, info: ProductInfoInput!): Product
productFocusPoint( productFocusPoint(
@@ -407,6 +436,10 @@ type Mutation {
): Product ): Product
productGroup(productId: Int!, groupIds: [Int]): Product productGroup(productId: Int!, groupIds: [Int]): Product
productKeywords(productId: Int!, keywordIds: [Int]): Product productKeywords(productId: Int!, keywordIds: [Int]): Product
addOwnInteriorToPrintProduct(
printId: Int!
uploadedS3Key: String!
): IdNumberResult
productWallpaperType( productWallpaperType(
productId: Int! productId: Int!
wallpaperTypes: [ProductWallpaperType]! wallpaperTypes: [ProductWallpaperType]!
+15
View File
@@ -0,0 +1,15 @@
export interface SignedUploadURLInput {
config: SignedUploadURLInputConfig;
}
export interface SignedUploadURLInputConfig {
contentType: string;
key: string;
}
export interface SignedUploadURL {
url: string;
key: string;
bucket: string;
expires: number;
}
+4
View File
@@ -24,3 +24,7 @@ export interface PaginationResult {
offset: number; offset: number;
limit: number; limit: number;
} }
export interface IdNumberResult {
id: number;
}